blob: 34e89da44e23a6433ae164a2e44c39983cae422c [file]
#!/usr/bin/env node
/**
* Dev launcher with PARALLEL + INCREMENTAL builds.
*
* Uses `tsc --build` for library packages so the compiler skips
* unchanged sub-projects via .tsbuildinfo (incremental).
*
* Dependency graph (→ compiles after):
* core ─┬→ storage
* ├→ runtime
* └→ ui
*
* libs (tsc --build tsconfig.lib.json) ─── covers core+storage+runtime+ui
* ├─→ preload (esbuild)
* └─→ filesystem worker (esbuild)
* cursor overlay (esbuild) ─── independent
* main (esbuild) ─── fast app bundle for Electron
* Vite dev server + Electron ─── fork
*/
import { spawn } from 'node:child_process';
import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createServer } from 'vite';
import { build as esbuildBuild } from 'esbuild';
import { buildCursorOverlay } from '../../../scripts/build-cursor-overlay.mjs';
import { monitorDevelopmentApp, startDevelopmentApp } from './dev-app-runtime.mjs';
const DESKTOP_DIR = resolve(fileURLToPath(new URL('..', import.meta.url)));
const REPO_ROOT = resolve(DESKTOP_DIR, '..', '..');
const TSC_CLI = join(REPO_ROOT, 'node_modules', 'typescript', 'bin', 'tsc');
const RUNTIME_WORKER_BUILD = join(REPO_ROOT, 'packages', 'runtime', 'scripts', 'build-filesystem-worker.mjs');
// ── helpers ──────────────────────────────────────────────────────────────────
function log(label, msg) {
const ts = new Date().toLocaleTimeString('zh-CN', { hour12: false });
console.log(`[${ts}][${label}] ${msg}`);
}
function runNodeTool(dir, script, args) {
return new Promise((resolve_, reject_) => {
const child = spawn(process.execPath, [script, ...args], {
cwd: dir,
stdio: ['ignore', 'inherit', 'inherit'],
});
child.on('exit', (code) => {
if (code === 0) resolve_();
else reject_(new Error(`"${script} ${args.join(' ')}" exited with code ${code}`));
});
child.on('error', reject_);
});
}
// ── build phases ─────────────────────────────────────────────────────────────
const TIMER_START = Date.now();
// Phase 1: all library packages via `tsc --build` (single process, shared
// .tsbuildinfo, sub-project incremental detection). The preload bundle imports
// workspace package dist files, so it starts only after that build is ready.
log('build', 'libraries — starting (tsc --build)');
const librariesBuild = runNodeTool(REPO_ROOT, TSC_CLI, ['--build', 'tsconfig.lib.json']).then(
() => log('build', 'libraries (all) — done'),
(e) => {
log('build', `libraries — FAILED: ${e.message}`);
throw e;
},
);
await Promise.all([
librariesBuild,
librariesBuild.then(() => runNodeTool(REPO_ROOT, RUNTIME_WORKER_BUILD, [])).then(
() => log('build', 'filesystem worker bundle — done'),
(e) => { log('build', `filesystem worker bundle — FAILED: ${e.message}`); throw e; },
),
// esbuild via its JS API — NOT `node node_modules/esbuild/bin/esbuild`:
// esbuild's postinstall swaps that file for a platform-native binary,
// and executing a Mach-O file with node throws SyntaxError (broke
// `npm run dev` on any machine where postinstall ran).
librariesBuild.then(() => esbuildBuild({
absWorkingDir: DESKTOP_DIR,
entryPoints: ['src/preload/preload.ts'],
bundle: true,
platform: 'node',
format: 'cjs',
outfile: 'dist/preload/preload.cjs',
external: ['electron'],
logLevel: 'warning',
})).then(
() => log('build', 'preload — done'),
(e) => { log('build', `preload — FAILED: ${e.message}`); throw e; },
),
buildCursorOverlay({ logLevel: 'warning' }).then(
() => log('build', 'cursor overlay — done'),
(e) => { log('build', `cursor overlay — FAILED: ${e.message}`); throw e; },
),
]);
// Phase 2: main — esbuild bundle for dev startup. The full
// tsconfig.main.json still compiles tests for `npm test` and typechecks
// main-process code in verification commands.
log('build', 'main — starting');
await esbuildBuild({
absWorkingDir: DESKTOP_DIR,
entryPoints: ['src/main/main.ts'],
bundle: true,
platform: 'node',
format: 'esm',
packages: 'external',
outfile: 'dist/main/main.js',
external: ['electron'],
logLevel: 'warning',
});
log('build', 'main — done');
const BUILD_MS = Date.now() - TIMER_START;
log('build', `all builds finished in ${(BUILD_MS / 1000).toFixed(1)}s`);
// ── Vite dev server + Electron ───────────────────────────────────────────────
process.chdir(DESKTOP_DIR);
log('vite', 'starting dev server...');
const server = await createServer();
await server.listen();
server.printUrls();
const devUrl = server.resolvedUrls?.local?.[0]?.replace(/\/$/, '');
if (!devUrl) {
console.error('[dev] vite did not report a local URL; aborting.');
await server.close();
process.exit(1);
}
log('electron', `launching against ${devUrl} (renderer HMR live)`);
let app = null;
let shuttingDown = false;
async function shutdown(code) {
if (shuttingDown) return;
shuttingDown = true;
await app?.stop();
await server.close().catch(() => {});
process.exit(code);
}
// Registered before the launch await: preparing the bundle can take a codesign
// rebuild, and a signal arriving with no handler installed takes the default
// action, leaving the dev server and any app behind.
process.on('SIGINT', () => shutdown(0));
process.on('SIGTERM', () => shutdown(0));
// Closing the terminal window sends SIGHUP; without this the detached bundle
// survives as an orphan holding the single-instance lock.
process.on('SIGHUP', () => shutdown(0));
app = await startDevelopmentApp({ argv: process.argv.slice(2), viteUrl: devUrl });
if (app.isMacosBundle) log('electron', 'launched Maka Dev.app through LaunchServices');
app.child.on('error', (err) => {
console.error(`[dev] failed to start Electron: ${err.message}`);
shutdown(1);
});
if (app.isMacosBundle) {
// `open` exits 0 at the handoff, so only a failure to hand off is news here;
// the app's own lifetime is what the monitor reports.
app.child.on('exit', (code) => {
if (code) shutdown(code);
});
monitorDevelopmentApp({ stopped: () => shuttingDown }).then((outcome) => {
if (outcome === 'never-started') {
console.error('[dev] Maka Dev.app did not start (see the output above)');
shutdown(1);
} else if (outcome === 'exited') {
log('electron', 'Maka Dev.app quit');
shutdown(0);
}
});
} else {
app.child.on('exit', (code) => shutdown(code ?? 0));
}