From ab619f982ed8fcea858aa815380b116772759a0f Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 19 Aug 2026 12:20:32 +0200 Subject: [PATCH] fix(server): isolate esbuild binary override Esbuild reads its binary override during module evaluation, but the previous global environment mutation leaked that host-only path into desktop-launched agents. Scope the compatibility bridge to module loading and scrub the variable from external process environments. --- packages/server/src/server/paseo-env.test.ts | 2 ++ packages/server/src/server/paseo-env.ts | 1 + .../server/src/server/plugins/compiler.ts | 22 ++++++++++++++----- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/server/src/server/paseo-env.test.ts b/packages/server/src/server/paseo-env.test.ts index 9155e8d5a..fd789ae89 100644 --- a/packages/server/src/server/paseo-env.test.ts +++ b/packages/server/src/server/paseo-env.test.ts @@ -19,6 +19,7 @@ describe("paseo env contract", () => { PASEO_DESKTOP_MANAGED: "1", [PASEO_NODE_ENV]: "production", PASEO_SUPERVISED: "1", + ESBUILD_BINARY_PATH: "/Applications/Paseo.app/Contents/Resources/app.asar.unpacked/esbuild", }; const runtimeControlEnvKeys = [ "ELECTRON_RUN_AS_NODE", @@ -26,6 +27,7 @@ describe("paseo env contract", () => { "PASEO_DESKTOP_MANAGED", "PASEO_SUPERVISED", "ELECTRON_NO_ATTACH_CONSOLE", + "ESBUILD_BINARY_PATH", ] as const; test("builds internal daemon child env by preserving pass-through and control vars", () => { diff --git a/packages/server/src/server/paseo-env.ts b/packages/server/src/server/paseo-env.ts index b2268a3d6..4df58e11d 100644 --- a/packages/server/src/server/paseo-env.ts +++ b/packages/server/src/server/paseo-env.ts @@ -7,6 +7,7 @@ const RUNTIME_CONTROL_ENV_KEYS = [ "PASEO_SUPERVISED", ELECTRON_RUN_AS_NODE, "ELECTRON_NO_ATTACH_CONSOLE", + "ESBUILD_BINARY_PATH", ] as const; export type PaseoNodeEnv = "development" | "production" | "test"; diff --git a/packages/server/src/server/plugins/compiler.ts b/packages/server/src/server/plugins/compiler.ts index f63888d3b..e383d605d 100644 --- a/packages/server/src/server/plugins/compiler.ts +++ b/packages/server/src/server/plugins/compiler.ts @@ -5,6 +5,9 @@ import path from "node:path"; import { parse } from "@babel/parser"; import type { Plugin } from "esbuild"; +const nodeRequire = createRequire(import.meta.url); +const ESBUILD_BINARY_PATH = "ESBUILD_BINARY_PATH"; + // esbuild resolves its own platform binary via require.resolve() the first time its // module is evaluated. Inside the packaged desktop app that resolves to a path under // app.asar even though electron-builder unpacks the real binary to app.asar.unpacked. @@ -38,19 +41,28 @@ export function resolveExistingAsarUnpackedEsbuildBinary( } function resolveAsarUnpackedEsbuildBinary(): string | null { - const require = createRequire(import.meta.url); let esbuildDir: string; try { - esbuildDir = path.dirname(require.resolve("esbuild/package.json")); + esbuildDir = path.dirname(nodeRequire.resolve("esbuild/package.json")); } catch { return null; } return resolveExistingAsarUnpackedEsbuildBinary(esbuildDir); } -if (!process.env.ESBUILD_BINARY_PATH) { +function loadEsbuild(): typeof import("esbuild") { + const previousBinaryPath = process.env[ESBUILD_BINARY_PATH]; const unpackedBinary = resolveAsarUnpackedEsbuildBinary(); - if (unpackedBinary) process.env.ESBUILD_BINARY_PATH = unpackedBinary; + if (unpackedBinary) process.env[ESBUILD_BINARY_PATH] = unpackedBinary; + + try { + // esbuild reads this variable while its CommonJS module is evaluated. Keep + // the compatibility bridge local so it cannot become an agent's environment. + return nodeRequire("esbuild") as typeof import("esbuild"); + } finally { + if (previousBinaryPath === undefined) delete process.env[ESBUILD_BINARY_PATH]; + else process.env[ESBUILD_BINARY_PATH] = previousBinaryPath; + } } type PluginBuildTarget = "client" | "server"; @@ -278,7 +290,7 @@ function createUnusedPlatformModulePlugin(target: PluginBuildTarget): Plugin { } async function compileTarget(entryPath: string, target: PluginBuildTarget): Promise { - const { build } = await import("esbuild"); + const { build } = loadEsbuild(); const source = await readFile(entryPath, "utf8"); const filteredSource = filterEntrypoint(source, target); const result = await build({