From 9607941446cc1d168fb6d066ac36adebb7207526 Mon Sep 17 00:00:00 2001 From: jackwener Date: Fri, 3 Jul 2026 20:02:44 +0800 Subject: [PATCH] =?UTF-8?q?fix(dev):=20run=20esbuild=20through=20its=20JS?= =?UTF-8?q?=20API=20=E2=80=94=20bin/esbuild=20is=20a=20native=20binary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npm run dev crashed on any machine where esbuild's postinstall had run: dev.mjs spawned `node node_modules/esbuild/bin/esbuild`, but postinstall replaces that file with a platform-native executable (Mach-O on darwin), so node threw SyntaxError on byte one and the preload/main bundles never built. Both esbuild invocations now use the JS API (esbuildBuild with equivalent options: bundle/platform/format/packages/external), which is binary-agnostic, cross-platform, and skips a child process. The dev-startup contract now asserts the API options instead of CLI flags, with the rationale inline. Verified: npm run dev boots end-to-end (incremental build 0.2s, vite on :5173, Electron launches). Desktop suite 1708/1708. --- apps/desktop/scripts/dev.mjs | 29 +++++++++++++++++-- .../__tests__/dev-startup-contract.test.ts | 9 ++++-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/apps/desktop/scripts/dev.mjs b/apps/desktop/scripts/dev.mjs index 4a66e43550..46b96ac55f 100644 --- a/apps/desktop/scripts/dev.mjs +++ b/apps/desktop/scripts/dev.mjs @@ -20,12 +20,12 @@ import { existsSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { createServer } from 'vite'; +import { build as esbuildBuild } from 'esbuild'; const DESKTOP_DIR = resolve(fileURLToPath(new URL('..', import.meta.url))); const REPO_ROOT = resolve(DESKTOP_DIR, '..', '..'); const ON_WINDOWS = process.platform === 'win32'; const TSC_CLI = join(REPO_ROOT, 'node_modules', 'typescript', 'bin', 'tsc'); -const ESBUILD_CLI = join(REPO_ROOT, 'node_modules', 'esbuild', 'bin', 'esbuild'); // ── helpers ────────────────────────────────────────────────────────────────── @@ -71,7 +71,20 @@ await Promise.all([ () => log('build', 'libraries (all) — done'), (e) => { log('build', `libraries — FAILED: ${e.message}`); throw e; }, ), - runNodeTool(DESKTOP_DIR, ESBUILD_CLI, ['src/preload/preload.ts', '--bundle', '--platform=node', '--format=cjs', '--outfile=dist/preload/preload.cjs', '--external:electron']).then( + // 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). + 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; }, ), @@ -81,7 +94,17 @@ await Promise.all([ // tsconfig.main.json still compiles tests for `npm test` and typechecks // main-process code in verification commands. log('build', 'main — starting'); -await runNodeTool(DESKTOP_DIR, ESBUILD_CLI, ['src/main/main.ts', '--bundle', '--platform=node', '--format=esm', '--packages=external', '--outfile=dist/main/main.js', '--external:electron']); +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; diff --git a/apps/desktop/src/main/__tests__/dev-startup-contract.test.ts b/apps/desktop/src/main/__tests__/dev-startup-contract.test.ts index b699d62156..ff3cbc85bf 100644 --- a/apps/desktop/src/main/__tests__/dev-startup-contract.test.ts +++ b/apps/desktop/src/main/__tests__/dev-startup-contract.test.ts @@ -7,10 +7,13 @@ test('dev launcher bundles app main sources without compiling main-process tests const cwd = process.cwd(); const desktopRoot = cwd.endsWith(join('apps', 'desktop')) ? cwd : join(cwd, 'apps', 'desktop'); const devScript = await readFile(join(desktopRoot, 'scripts', 'dev.mjs'), 'utf8'); - assert.match(devScript, /esbuild/); + // dev.mjs now calls esbuild's JS API instead of `node bin/esbuild` + // (postinstall replaces that file with a platform-native binary that + // node cannot execute) — assert on the API options instead of CLI flags. + assert.match(devScript, /esbuildBuild\(/); assert.match(devScript, /src\/main\/main\.ts/); - assert.match(devScript, /--bundle/); - assert.match(devScript, /--packages=external/); + assert.match(devScript, /bundle:\s*true/); + assert.match(devScript, /packages:\s*'external'/); assert.doesNotMatch(devScript, /\['tsc', '-p'/); assert.doesNotMatch(devScript, /tsconfig\.main\.app\.json/); });