| #!/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); | |
| } | |
| // Let Vite finish the initial dependency crawl + optimizer commit before the | |
| // window loads (issue #4775). `warmupRequest` on the renderer entry kicks the | |
| // recursive pre-transform of the static import graph (which registers every | |
| // reachable dep with the optimizer), and `waitForRequestsIdle` resolves at | |
| // crawl end — the same signal the dep optimizer waits on before committing | |
| // node_modules/.vite/deps. Loading Electron before that commit let the page | |
| // execute chunks from a previous optimizer generation alongside fresh ones — | |
| // two React instances, a null hook dispatcher, and a renderer crash on the | |
| // first lazy component. warmupRequest swallows transform errors itself, so | |
| // this can never abort the launch; it only reorders the startup race away. | |
| log('vite', 'warming renderer entry and waiting for the dep crawl to settle...'); | |
| await server.environments.client.warmupRequest('/main.tsx'); | |
| await server.environments.client.waitForRequestsIdle(); | |
| 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)); | |
| } | |
| } |