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.
This commit is contained in:
Mohamed Boudra
2026-08-19 12:20:32 +02:00
parent e55dd5a212
commit ab619f982e
3 changed files with 20 additions and 5 deletions
@@ -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", () => {
+1
View File
@@ -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";
+17 -5
View File
@@ -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<string> {
const { build } = await import("esbuild");
const { build } = loadEsbuild();
const source = await readFile(entryPath, "utf8");
const filteredSource = filterEntrypoint(source, target);
const result = await build({