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:
@@ -19,6 +19,7 @@ describe("paseo env contract", () => {
|
|||||||
PASEO_DESKTOP_MANAGED: "1",
|
PASEO_DESKTOP_MANAGED: "1",
|
||||||
[PASEO_NODE_ENV]: "production",
|
[PASEO_NODE_ENV]: "production",
|
||||||
PASEO_SUPERVISED: "1",
|
PASEO_SUPERVISED: "1",
|
||||||
|
ESBUILD_BINARY_PATH: "/Applications/Paseo.app/Contents/Resources/app.asar.unpacked/esbuild",
|
||||||
};
|
};
|
||||||
const runtimeControlEnvKeys = [
|
const runtimeControlEnvKeys = [
|
||||||
"ELECTRON_RUN_AS_NODE",
|
"ELECTRON_RUN_AS_NODE",
|
||||||
@@ -26,6 +27,7 @@ describe("paseo env contract", () => {
|
|||||||
"PASEO_DESKTOP_MANAGED",
|
"PASEO_DESKTOP_MANAGED",
|
||||||
"PASEO_SUPERVISED",
|
"PASEO_SUPERVISED",
|
||||||
"ELECTRON_NO_ATTACH_CONSOLE",
|
"ELECTRON_NO_ATTACH_CONSOLE",
|
||||||
|
"ESBUILD_BINARY_PATH",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
test("builds internal daemon child env by preserving pass-through and control vars", () => {
|
test("builds internal daemon child env by preserving pass-through and control vars", () => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const RUNTIME_CONTROL_ENV_KEYS = [
|
|||||||
"PASEO_SUPERVISED",
|
"PASEO_SUPERVISED",
|
||||||
ELECTRON_RUN_AS_NODE,
|
ELECTRON_RUN_AS_NODE,
|
||||||
"ELECTRON_NO_ATTACH_CONSOLE",
|
"ELECTRON_NO_ATTACH_CONSOLE",
|
||||||
|
"ESBUILD_BINARY_PATH",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type PaseoNodeEnv = "development" | "production" | "test";
|
export type PaseoNodeEnv = "development" | "production" | "test";
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import path from "node:path";
|
|||||||
import { parse } from "@babel/parser";
|
import { parse } from "@babel/parser";
|
||||||
import type { Plugin } from "esbuild";
|
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
|
// 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
|
// 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.
|
// 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 {
|
function resolveAsarUnpackedEsbuildBinary(): string | null {
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
let esbuildDir: string;
|
let esbuildDir: string;
|
||||||
try {
|
try {
|
||||||
esbuildDir = path.dirname(require.resolve("esbuild/package.json"));
|
esbuildDir = path.dirname(nodeRequire.resolve("esbuild/package.json"));
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return resolveExistingAsarUnpackedEsbuildBinary(esbuildDir);
|
return resolveExistingAsarUnpackedEsbuildBinary(esbuildDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!process.env.ESBUILD_BINARY_PATH) {
|
function loadEsbuild(): typeof import("esbuild") {
|
||||||
|
const previousBinaryPath = process.env[ESBUILD_BINARY_PATH];
|
||||||
const unpackedBinary = resolveAsarUnpackedEsbuildBinary();
|
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";
|
type PluginBuildTarget = "client" | "server";
|
||||||
@@ -278,7 +290,7 @@ function createUnusedPlatformModulePlugin(target: PluginBuildTarget): Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function compileTarget(entryPath: string, target: PluginBuildTarget): Promise<string> {
|
async function compileTarget(entryPath: string, target: PluginBuildTarget): Promise<string> {
|
||||||
const { build } = await import("esbuild");
|
const { build } = loadEsbuild();
|
||||||
const source = await readFile(entryPath, "utf8");
|
const source = await readFile(entryPath, "utf8");
|
||||||
const filteredSource = filterEntrypoint(source, target);
|
const filteredSource = filterEntrypoint(source, target);
|
||||||
const result = await build({
|
const result = await build({
|
||||||
|
|||||||
Reference in New Issue
Block a user