Лицензия, редактор, пульт и сборка

- Main: license service, IPC, router; закрытие окон; yauzl закрытие zip (EMFILE), zipRead тест
- Editor: стабильный projectState без мигания, логотип и меню, строки UI, LayoutShell overlay
- Control: ластик для всех типов эффектов, затухание/нарастание музыки при смене сцены
- Сборка: vite, build/dev scripts, obfuscate-main и build-env скрипты с тестами; package.json

Made-with: Cursor
This commit is contained in:
Ivan Fontosh
2026-04-19 20:11:24 +08:00
parent 5e7dc5ea19
commit 2fa20da94d
40 changed files with 2629 additions and 211 deletions
+12
View File
@@ -0,0 +1,12 @@
/**
* @param {string[]} argv
* @param {NodeJS.ProcessEnv} env
*/
export function resolveIsProduction(argv = process.argv, env = process.env) {
return env.NODE_ENV === 'production' || argv.includes('--production');
}
/** @param {string[]} argv */
export function resolveObfuscateMain(argv = process.argv) {
return argv.includes('--obfuscate');
}
+21
View File
@@ -0,0 +1,21 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { resolveIsProduction, resolveObfuscateMain } from './build-env.mjs';
void test('resolveIsProduction: argv --production', () => {
assert.equal(resolveIsProduction(['node', 'scripts/build.mjs', '--production'], {}), true);
});
void test('resolveIsProduction: NODE_ENV=production', () => {
assert.equal(resolveIsProduction(['node', 'scripts/build.mjs'], { NODE_ENV: 'production' }), true);
});
void test('resolveIsProduction: dev по умолчанию', () => {
assert.equal(resolveIsProduction(['node', 'scripts/build.mjs'], { NODE_ENV: 'development' }), false);
});
void test('resolveObfuscateMain: --obfuscate', () => {
assert.equal(resolveObfuscateMain(['node', 'build.mjs', '--production', '--obfuscate']), true);
assert.equal(resolveObfuscateMain(['node', 'build.mjs', '--production']), false);
});
+44 -12
View File
@@ -5,39 +5,71 @@ import { fileURLToPath } from 'node:url';
import { build } from 'esbuild';
import { resolveIsProduction, resolveObfuscateMain } from './build-env.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const root = path.resolve(__dirname, '..');
const isProd = resolveIsProduction();
const obfuscateMain = resolveObfuscateMain();
/** Старые .map от dev-сборок не должны попадать в pack. */
function removeStaleNodeBundleMaps() {
for (const p of [
path.join(root, 'dist/main/index.cjs.map'),
path.join(root, 'dist/preload/index.cjs.map'),
]) {
if (fs.existsSync(p)) fs.unlinkSync(p);
}
}
function runViteBuild() {
execFileSync('npx vite build', {
const cmd = isProd ? 'npx vite build' : 'npx vite build --mode development';
execFileSync(cmd, {
cwd: root,
stdio: 'inherit',
shell: true,
env: {
...process.env,
NODE_ENV: isProd ? 'production' : 'development',
},
});
}
async function buildNodeTargets() {
await build({
entryPoints: [path.join(root, 'app/main/index.ts')],
outfile: path.join(root, 'dist/main/index.cjs'),
if (isProd) removeStaleNodeBundleMaps();
const nodeEnvLiteral = JSON.stringify(isProd ? 'production' : 'development');
const common = {
platform: 'node',
target: 'node22',
format: 'cjs',
bundle: true,
sourcemap: true,
minify: isProd,
sourcemap: !isProd,
external: ['electron'],
});
define: { 'process.env.NODE_ENV': nodeEnvLiteral },
drop: isProd ? ['console', 'debugger'] : [],
};
const mainOut = path.join(root, 'dist/main/index.cjs');
await build({
...common,
entryPoints: [path.join(root, 'app/main/index.ts')],
outfile: mainOut,
});
if (isProd && obfuscateMain) {
const { obfuscateMainBundleFile } = await import('./obfuscate-main.mjs');
obfuscateMainBundleFile(mainOut);
}
await build({
...common,
entryPoints: [path.join(root, 'app/preload/index.ts')],
outfile: path.join(root, 'dist/preload/index.cjs'),
platform: 'node',
target: 'node22',
format: 'cjs',
bundle: true,
sourcemap: true,
external: ['electron'],
});
}
+12 -2
View File
@@ -77,10 +77,20 @@ async function watchMainAndPreload() {
const dispose = await watchMainAndPreload();
const vite = spawnShell('npx vite dev --strictPort', {
env: { ...process.env, NODE_ENV: 'development', VITE_DEV_SERVER_URL: 'http://localhost:5173/' },
env: {
...process.env,
NODE_ENV: 'development',
DND_SKIP_LICENSE: process.env.DND_SKIP_LICENSE ?? '1',
VITE_DEV_SERVER_URL: 'http://localhost:5173/',
},
});
const electron = spawnShell('npx electron .', {
env: { ...process.env, NODE_ENV: 'development', VITE_DEV_SERVER_URL: 'http://localhost:5173/' },
env: {
...process.env,
NODE_ENV: 'development',
DND_SKIP_LICENSE: process.env.DND_SKIP_LICENSE ?? '1',
VITE_DEV_SERVER_URL: 'http://localhost:5173/',
},
});
let shuttingDown = false;
+27
View File
@@ -0,0 +1,27 @@
import fs from 'node:fs';
import JavaScriptObfuscator from 'javascript-obfuscator';
/**
* Лёгкие настройки: без selfDefending / controlFlowFlattening — меньше шансов сломать Electron main.
* @param {string} filePath
*/
export function obfuscateMainBundleFile(filePath) {
const code = fs.readFileSync(filePath, 'utf8');
const obfuscated = JavaScriptObfuscator.obfuscate(code, {
compact: true,
controlFlowFlattening: false,
deadCodeInjection: false,
debugProtection: false,
disableConsoleOutput: false,
identifierNamesGenerator: 'hexadecimal',
renameGlobals: false,
selfDefending: false,
simplify: true,
stringArray: true,
stringArrayEncoding: [],
stringArrayThreshold: 0.75,
transformObjectKeys: false,
}).getObfuscatedCode();
fs.writeFileSync(filePath, obfuscated, 'utf8');
}
+23
View File
@@ -0,0 +1,23 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { obfuscateMainBundleFile } from './obfuscate-main.mjs';
void test('obfuscateMainBundleFile: CJS после обфускации исполняется (кроссплатформенно)', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dnd-obf-'));
const filePath = path.join(dir, 'stub.cjs');
fs.writeFileSync(filePath, 'exports.answer = 40 + 2;\n', 'utf8');
obfuscateMainBundleFile(filePath);
const out = fs.readFileSync(filePath, 'utf8');
assert.ok(out.length > 0);
assert.ok(!out.includes('exports.answer = 42'), 'исходный вид строки не должен сохраняться дословно');
const requireFromTest = createRequire(import.meta.url);
assert.equal(requireFromTest(filePath).answer, 42);
});