#!/usr/bin/env node /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * 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 { createDevelopmentLaunchSession, handleDevelopmentLaunchOutcome, waitForDevelopmentLaunchVerdict, } 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 MODEL_METADATA_SYNC = join(REPO_ROOT, 'scripts', 'sync-model-metadata.mjs'); 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(); // A clean or ignore-scripts install has no generated model modules yet, and // `tsc --build` bypasses workspace prebuild hooks. Generate from the committed // snapshot before starting the incremental library graph. log('build', 'model metadata — generating from committed snapshot'); await runNodeTool(REPO_ROOT, MODEL_METADATA_SYNC, []); // 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)`); // Created before launch so signals during codesign/preparation are durable. // Closing the terminal still stops only launcher resources, not the // independently owned TCC app. const launchSession = createDevelopmentLaunchSession({ close: () => server.close(), }); let app = null; try { app = await launchSession.start({ argv: process.argv.slice(2), viteUrl: devUrl }); } catch (error) { console.error(`[dev] failed to start Electron: ${String(error)}`); await launchSession.stop(1); } if (app) { 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}`); launchSession.stop(1); }); if (app.isMacosBundle) { // `open` exits 0 at the handoff, so only a failure to hand off is news here. // The app's later lifetime is deliberately independent from this dev server. app.child.on('exit', (code) => { if (code) launchSession.stop(code); }); // All branching lives in handleDevelopmentLaunchOutcome; this line is the only // un-automated surface here (launcher scripts are non-exported, darwin-only). waitForDevelopmentLaunchVerdict({ stopped: launchSession.isStopping, resultFile: app.resultFile }).then((outcome) => handleDevelopmentLaunchOutcome(outcome, { log: (m) => console.error('[dev]', m), exit: (code) => launchSession.stop(code), }), ); } else { app.child.on('exit', (code) => launchSession.stop(code ?? 0)); } }