mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-24 08:02:14 +03:00
fix(build): spawn esbuild cross-platform in the postbuild hook (#11159)
Validated on the combined batch board over tip 0b41259f: static gates clean, typecheck:core clean, 430+ focused tests green across 5 groups.
postbuild's esbuild spawn now resolves cross-platform (no more ENOENT after a successful Next compile on Windows). build-tool-runner-win-shim suite green. Thank you @aliyosufi — first contribution, welcome!
This commit is contained in:
162
scripts/build/buildToolRunner.mjs
Normal file
162
scripts/build/buildToolRunner.mjs
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* OmniRoute — cross-platform spawning of locally installed build tools.
|
||||
*
|
||||
* WHY: `node_modules/.bin/<tool>` (no extension) is a POSIX shell script. On
|
||||
* Windows the executable shim is `<tool>.cmd`, so `execFileSync(join(ROOT,
|
||||
* "node_modules", ".bin", "esbuild"), …)` dies with
|
||||
*
|
||||
* Error: spawnSync C:\…\node_modules\.bin\esbuild ENOENT
|
||||
*
|
||||
* and — because the `postbuild` hook runs after a SUCCESSFUL `next build` — the
|
||||
* operator sees "✓ Compiled successfully" immediately followed by a failed
|
||||
* `npm run build`, with a complete `.build/next/standalone` tree on disk.
|
||||
*
|
||||
* Switching to `<tool>.cmd` alone is not enough: since the CVE-2024-27980
|
||||
* hardening, Node >= 20 refuses to spawn a `.cmd`/`.bat` without a shell
|
||||
* (EINVAL), and `shell: true` in turn disables argument escaping (DEP0190).
|
||||
*
|
||||
* So the preferred path avoids the shim entirely: read the tool's own `bin`
|
||||
* entry from its package.json and run THAT with this Node binary — no shim, no
|
||||
* shell, nothing to escape, identical behaviour on every platform. The `.bin`
|
||||
* shim stays only as a last resort for a tool that is not resolvable inside the
|
||||
* local dependency tree.
|
||||
*
|
||||
* These helpers were private to `scripts/build/prepublish.ts`, where the same
|
||||
* Windows failure was already fixed; they live here so plain-`node` build
|
||||
* scripts (`postbuild` → colocate-standalone.mjs) can share one implementation
|
||||
* instead of re-learning the same lesson. `planBuildToolSpawn()` takes the
|
||||
* platform as a parameter — like `resolveNextBuildEnv()` in
|
||||
* build-next-isolated.mjs — so the Windows behaviour is unit-testable from CI's
|
||||
* Linux runners.
|
||||
*/
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { closeSync, existsSync, openSync, readFileSync, readSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
||||
|
||||
/**
|
||||
* Absolute path of a tool's own `bin` entry inside the local dependency tree,
|
||||
* or `null` when the package (or the entry it advertises) is not there.
|
||||
*
|
||||
* @param {string} packageName Package that ships the tool, e.g. `"esbuild"`.
|
||||
* @param {string} binName Key in that package's `bin` map, e.g. `"esbuild"`.
|
||||
* @param {string} [root] Directory holding `node_modules` (defaults to repo root).
|
||||
* @returns {string | null}
|
||||
*/
|
||||
export function resolveLocalBinEntry(packageName, binName, root = ROOT) {
|
||||
try {
|
||||
const packageJsonPath = join(root, "node_modules", packageName, "package.json");
|
||||
if (!existsSync(packageJsonPath)) return null;
|
||||
const meta = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
||||
const relative = typeof meta.bin === "string" ? meta.bin : meta.bin?.[binName];
|
||||
if (!relative) return null;
|
||||
const absolute = join(root, "node_modules", packageName, relative);
|
||||
return existsSync(absolute) ? absolute : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this file start with an executable image's magic bytes?
|
||||
*
|
||||
* esbuild >= 0.25 ships `bin/esbuild` as the NATIVE platform executable on
|
||||
* Linux/macOS (ELF / Mach-O) instead of a JS shim — handing that to
|
||||
* `process.execPath` makes Node parse machine code as JavaScript and die with
|
||||
* "SyntaxError: Invalid or unexpected token". Native entries must be executed
|
||||
* directly; JS entries go through this Node binary.
|
||||
*
|
||||
* @param {string} entryPath
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isNativeExecutable(entryPath) {
|
||||
try {
|
||||
const fd = openSync(entryPath, "r");
|
||||
const head = Buffer.alloc(4);
|
||||
readSync(fd, head, 0, 4, 0);
|
||||
closeSync(fd);
|
||||
return (
|
||||
(head[0] === 0x7f && head[1] === 0x45 && head[2] === 0x4c && head[3] === 0x46) || // ELF
|
||||
head.readUInt32BE(0) === 0xfeedfacf || // Mach-O 64
|
||||
head.readUInt32BE(0) === 0xcffaedfe || // Mach-O 64 (LE on disk)
|
||||
(head[0] === 0x4d && head[1] === 0x5a) // PE (Windows MZ)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `cmd.exe` receives one flat command line, and Node does NOT escape arguments
|
||||
* when `shell` is set, so anything holding whitespace has to be quoted here.
|
||||
* Build arguments carry absolute paths, and `C:\Users\First Last\…` is an
|
||||
* ordinary Windows home directory.
|
||||
*
|
||||
* @param {string} value
|
||||
* @returns {string}
|
||||
*/
|
||||
function quoteForShell(value) {
|
||||
if (!/\s/.test(value) || value.startsWith('"')) return value;
|
||||
return `"${value}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide HOW to spawn a build tool. Pure: no filesystem access, no `process`
|
||||
* inspection beyond `execPath`, platform injected — so a Linux test can assert
|
||||
* the Windows plan.
|
||||
*
|
||||
* @param {object} input
|
||||
* @param {string} input.binName Tool name as it appears in `node_modules/.bin`.
|
||||
* @param {readonly string[]} input.args Arguments for the tool.
|
||||
* @param {string | null} [input.entryPath] Result of {@link resolveLocalBinEntry}.
|
||||
* @param {boolean} [input.entryIsNative] Result of {@link isNativeExecutable}.
|
||||
* @param {string} [input.root] Directory holding `node_modules`.
|
||||
* @param {string} [input.platform] `process.platform` value to plan for.
|
||||
* @returns {{ file: string, args: string[], shell: boolean }} `file`/`args` are
|
||||
* already shell-quoted when `shell` is true, and must be passed together.
|
||||
*/
|
||||
export function planBuildToolSpawn({
|
||||
binName,
|
||||
args,
|
||||
entryPath = null,
|
||||
entryIsNative = false,
|
||||
root = ROOT,
|
||||
platform = process.platform,
|
||||
}) {
|
||||
// Preferred: the tool's own entry point, spawned with no shim and no shell.
|
||||
if (entryPath) {
|
||||
return entryIsNative
|
||||
? { file: entryPath, args: [...args], shell: false }
|
||||
: { file: process.execPath, args: [entryPath, ...args], shell: false };
|
||||
}
|
||||
|
||||
// Last resort: the `node_modules/.bin` shim. On Windows that means the `.cmd`
|
||||
// variant, which Node only spawns through a shell (see the module header).
|
||||
const isWindows = platform === "win32";
|
||||
const shim = join(root, "node_modules", ".bin", isWindows ? `${binName}.cmd` : binName);
|
||||
return isWindows
|
||||
? { file: quoteForShell(shim), args: args.map(quoteForShell), shell: true }
|
||||
: { file: shim, args: [...args], shell: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a locally installed build tool, synchronously, on any platform.
|
||||
*
|
||||
* @param {string} packageName Package that ships the tool, e.g. `"esbuild"`.
|
||||
* @param {string} binName Key in that package's `bin` map, e.g. `"esbuild"`.
|
||||
* @param {readonly string[]} args Arguments for the tool.
|
||||
* @param {import("node:child_process").ExecFileSyncOptions} [options] Passed to `execFileSync`.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function runBuildTool(packageName, binName, args, options = {}) {
|
||||
const entryPath = resolveLocalBinEntry(packageName, binName);
|
||||
const plan = planBuildToolSpawn({
|
||||
binName,
|
||||
args,
|
||||
entryPath,
|
||||
entryIsNative: entryPath ? isNativeExecutable(entryPath) : false,
|
||||
});
|
||||
execFileSync(plan.file, plan.args, plan.shell ? { ...options, shell: true } : options);
|
||||
}
|
||||
@@ -18,8 +18,8 @@
|
||||
*/
|
||||
import { cpSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { runBuildTool } from "./buildToolRunner.mjs";
|
||||
import { computeDependencyClosure } from "./colocateOptionals.mjs";
|
||||
|
||||
const ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
||||
@@ -89,8 +89,12 @@ function main() {
|
||||
|
||||
const callLogWorkerDest = join(STANDALONE, CALL_LOG_WORKER_REL);
|
||||
mkdirSync(dirname(callLogWorkerDest), { recursive: true });
|
||||
execFileSync(
|
||||
join(ROOT, "node_modules", ".bin", "esbuild"),
|
||||
// Never spawn `node_modules/.bin/esbuild` directly: that extensionless path is
|
||||
// a POSIX shell script and does not exist on Windows (ENOENT), which failed
|
||||
// `npm run build` right after a successful `next build`. See buildToolRunner.mjs.
|
||||
runBuildTool(
|
||||
"esbuild",
|
||||
"esbuild",
|
||||
[
|
||||
CALL_LOG_WORKER_SRC,
|
||||
"--bundle",
|
||||
@@ -120,8 +124,9 @@ function main() {
|
||||
if (!existsSync(workerDest)) {
|
||||
mkdirSync(dirname(workerDest), { recursive: true });
|
||||
try {
|
||||
execFileSync(
|
||||
join(ROOT, "node_modules", ".bin", "esbuild"),
|
||||
runBuildTool(
|
||||
"esbuild",
|
||||
"esbuild",
|
||||
[
|
||||
join(
|
||||
ROOT,
|
||||
|
||||
@@ -22,14 +22,12 @@ import {
|
||||
readdirSync,
|
||||
statSync,
|
||||
chmodSync,
|
||||
openSync,
|
||||
readSync,
|
||||
closeSync,
|
||||
} from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { assembleStandalone } from "./assembleStandalone.mjs";
|
||||
import { isNativeExecutable, resolveLocalBinEntry } from "./buildToolRunner.mjs";
|
||||
import { resolveBundledNpmEntry } from "./resolveNpmEntry.ts";
|
||||
import {
|
||||
APP_STAGING_ALLOWED_EXACT_PATHS,
|
||||
@@ -51,52 +49,15 @@ const NPX_BIN = process.platform === "win32" ? "npx.cmd" : "npx";
|
||||
//
|
||||
// `shell: true` would fix the spawn but disables argument escaping (DEP0190), so it
|
||||
// is only the last resort. Preferred order: run the tool's own JS entry point with
|
||||
// this Node binary — no shim, no shell, nothing to escape.
|
||||
function resolveLocalBinEntry(packageName: string, binName: string): string | null {
|
||||
try {
|
||||
const packageJsonPath = join(ROOT, "node_modules", packageName, "package.json");
|
||||
if (!existsSync(packageJsonPath)) return null;
|
||||
const meta = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
|
||||
bin?: string | Record<string, string>;
|
||||
};
|
||||
const relative = typeof meta.bin === "string" ? meta.bin : meta.bin?.[binName];
|
||||
if (!relative) return null;
|
||||
const absolute = join(ROOT, "node_modules", packageName, relative);
|
||||
return existsSync(absolute) ? absolute : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// this Node binary — no shim, no shell, nothing to escape. `resolveLocalBinEntry()`
|
||||
// and `isNativeExecutable()` implement that resolution and now live in
|
||||
// buildToolRunner.mjs, shared with the plain-`node` build scripts.
|
||||
|
||||
/**
|
||||
* Runs a build tool without ever touching a `.cmd` shim. `packageName` is where the
|
||||
* tool lives in the local dependency tree; when it is not installed there the call
|
||||
* falls back to the Node-resolved `npx` entry point, and only then to the shim.
|
||||
*/
|
||||
/**
|
||||
* esbuild ≥0.25 ships its `bin/esbuild` as the NATIVE platform executable on
|
||||
* Linux/macOS (ELF / Mach-O) instead of a JS shim — running it through
|
||||
* `process.execPath` makes Node parse machine code as JavaScript and crash with
|
||||
* "SyntaxError: Invalid or unexpected token". Sniff the magic bytes and exec
|
||||
* native entries directly; JS entries keep going through this Node binary.
|
||||
*/
|
||||
function isNativeExecutable(entryPath: string): boolean {
|
||||
try {
|
||||
const fd = openSync(entryPath, "r");
|
||||
const head = Buffer.alloc(4);
|
||||
readSync(fd, head, 0, 4, 0);
|
||||
closeSync(fd);
|
||||
return (
|
||||
(head[0] === 0x7f && head[1] === 0x45 && head[2] === 0x4c && head[3] === 0x46) || // ELF
|
||||
head.readUInt32BE(0) === 0xfeedfacf || // Mach-O 64
|
||||
head.readUInt32BE(0) === 0xcffaedfe || // Mach-O 64 (LE on disk)
|
||||
(head[0] === 0x4d && head[1] === 0x5a) // PE (Windows MZ)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function runBuildTool(
|
||||
packageName: string,
|
||||
binName: string,
|
||||
|
||||
208
tests/unit/build/build-tool-runner-win-shim.test.ts
Normal file
208
tests/unit/build/build-tool-runner-win-shim.test.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
|
||||
import { join, sep } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
import {
|
||||
isNativeExecutable,
|
||||
planBuildToolSpawn,
|
||||
resolveLocalBinEntry,
|
||||
runBuildTool,
|
||||
} from "../../../scripts/build/buildToolRunner.mjs";
|
||||
|
||||
/**
|
||||
* Regression coverage for the Windows `postbuild` crash.
|
||||
*
|
||||
* `colocate-standalone.mjs` spawned `node_modules/.bin/esbuild` — an
|
||||
* extensionless POSIX shell script that does not exist on Windows. `npm run
|
||||
* build` therefore died with
|
||||
*
|
||||
* Error: spawnSync C:\…\node_modules\.bin\esbuild ENOENT
|
||||
*
|
||||
* immediately AFTER `next build` reported "✓ Compiled successfully", leaving a
|
||||
* complete `.build/next/standalone` tree next to a failed build.
|
||||
*
|
||||
* The platform is injected into `planBuildToolSpawn()` (same seam as
|
||||
* `resolveNextBuildEnv()` in build-next-isolated.mjs) so the Windows decisions
|
||||
* are asserted from CI's Linux runners.
|
||||
*/
|
||||
|
||||
test("planBuildToolSpawn prefers the tool's own JS entry over any .bin shim", () => {
|
||||
const plan = planBuildToolSpawn({
|
||||
binName: "esbuild",
|
||||
args: ["in.ts", "--outfile=out.js"],
|
||||
entryPath: "/repo/node_modules/esbuild/bin/esbuild",
|
||||
entryIsNative: false,
|
||||
platform: "win32",
|
||||
});
|
||||
|
||||
assert.equal(plan.file, process.execPath, "a JS entry runs on this Node binary");
|
||||
assert.deepEqual(plan.args, [
|
||||
"/repo/node_modules/esbuild/bin/esbuild",
|
||||
"in.ts",
|
||||
"--outfile=out.js",
|
||||
]);
|
||||
assert.equal(plan.shell, false, "no shell means no argument-escaping hazard (DEP0190)");
|
||||
});
|
||||
|
||||
test("planBuildToolSpawn execs a NATIVE entry directly instead of feeding it to Node", () => {
|
||||
// esbuild >= 0.25 ships bin/esbuild as an ELF/Mach-O binary on Linux/macOS;
|
||||
// handing that to process.execPath crashes with "Invalid or unexpected token".
|
||||
const plan = planBuildToolSpawn({
|
||||
binName: "esbuild",
|
||||
args: ["in.ts"],
|
||||
entryPath: "/repo/node_modules/esbuild/bin/esbuild",
|
||||
entryIsNative: true,
|
||||
platform: "linux",
|
||||
});
|
||||
|
||||
assert.equal(plan.file, "/repo/node_modules/esbuild/bin/esbuild");
|
||||
assert.deepEqual(plan.args, ["in.ts"]);
|
||||
assert.equal(plan.shell, false);
|
||||
});
|
||||
|
||||
test("planBuildToolSpawn falls back to the .cmd shim (with a shell) on win32", () => {
|
||||
const plan = planBuildToolSpawn({
|
||||
binName: "esbuild",
|
||||
args: ["in.ts"],
|
||||
entryPath: null,
|
||||
root: "C:\\repo",
|
||||
platform: "win32",
|
||||
});
|
||||
|
||||
assert.ok(plan.file.endsWith("esbuild.cmd"), `expected a .cmd shim, got ${plan.file}`);
|
||||
// Node >= 20 refuses to spawn a .cmd without a shell (CVE-2024-27980 hardening).
|
||||
assert.equal(plan.shell, true, "a .cmd only spawns through a shell");
|
||||
});
|
||||
|
||||
test("planBuildToolSpawn falls back to the extensionless shim (no shell) elsewhere", () => {
|
||||
const plan = planBuildToolSpawn({
|
||||
binName: "esbuild",
|
||||
args: ["in.ts"],
|
||||
entryPath: null,
|
||||
root: "/repo",
|
||||
platform: "linux",
|
||||
});
|
||||
|
||||
assert.equal(plan.file, join("/repo", "node_modules", ".bin", "esbuild"));
|
||||
assert.ok(!plan.file.endsWith(".cmd"), "no .cmd suffix off Windows");
|
||||
assert.equal(plan.shell, false);
|
||||
});
|
||||
|
||||
test("planBuildToolSpawn quotes whitespace paths when it has to use a shell", () => {
|
||||
// `C:\Users\First Last\…` is an ordinary Windows home directory, and Node does
|
||||
// not escape arguments once `shell` is set.
|
||||
const plan = planBuildToolSpawn({
|
||||
binName: "esbuild",
|
||||
args: ["--outfile=C:\\Users\\First Last\\out.js", "--bundle"],
|
||||
entryPath: null,
|
||||
root: "C:\\Users\\First Last\\repo",
|
||||
platform: "win32",
|
||||
});
|
||||
|
||||
assert.ok(plan.file.startsWith('"') && plan.file.endsWith('"'), "shim path is quoted");
|
||||
assert.equal(plan.args[0], '"--outfile=C:\\Users\\First Last\\out.js"');
|
||||
assert.equal(plan.args[1], "--bundle", "arguments without whitespace are left alone");
|
||||
});
|
||||
|
||||
test("resolveLocalBinEntry reads the package's own bin map, never node_modules/.bin", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "bin-entry-"));
|
||||
try {
|
||||
const pkgDir = join(root, "node_modules", "esbuild");
|
||||
mkdirSync(join(pkgDir, "bin"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(pkgDir, "package.json"),
|
||||
JSON.stringify({ bin: { esbuild: "bin/esbuild" } })
|
||||
);
|
||||
writeFileSync(join(pkgDir, "bin", "esbuild"), "#!/usr/bin/env node\n");
|
||||
|
||||
const entry = resolveLocalBinEntry("esbuild", "esbuild", root);
|
||||
assert.equal(entry, join(pkgDir, "bin", "esbuild"));
|
||||
assert.ok(
|
||||
!entry.includes(`${sep}.bin${sep}`),
|
||||
"the resolved entry must bypass the platform-specific .bin shim"
|
||||
);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveLocalBinEntry returns null for a missing package or a missing entry", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "bin-entry-missing-"));
|
||||
try {
|
||||
assert.equal(resolveLocalBinEntry("nope", "nope", root), null);
|
||||
|
||||
const pkgDir = join(root, "node_modules", "esbuild");
|
||||
mkdirSync(pkgDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(pkgDir, "package.json"),
|
||||
JSON.stringify({ bin: { esbuild: "bin/esbuild" } })
|
||||
);
|
||||
assert.equal(
|
||||
resolveLocalBinEntry("esbuild", "esbuild", root),
|
||||
null,
|
||||
"an advertised entry that is not on disk must not be spawned"
|
||||
);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("isNativeExecutable distinguishes an executable image from a JS shim", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "native-sniff-"));
|
||||
try {
|
||||
const shim = join(root, "shim.js");
|
||||
const elf = join(root, "elf.bin");
|
||||
const pe = join(root, "pe.exe");
|
||||
writeFileSync(shim, "#!/usr/bin/env node\nconsole.log(1);\n");
|
||||
writeFileSync(elf, Buffer.from([0x7f, 0x45, 0x4c, 0x46, 0x02]));
|
||||
writeFileSync(pe, Buffer.from([0x4d, 0x5a, 0x90, 0x00]));
|
||||
|
||||
assert.equal(isNativeExecutable(shim), false);
|
||||
assert.equal(isNativeExecutable(elf), true);
|
||||
assert.equal(isNativeExecutable(pe), true);
|
||||
assert.equal(isNativeExecutable(join(root, "absent")), false, "a missing file is not native");
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("runBuildTool actually runs esbuild from this repo's dependency tree", () => {
|
||||
// End-to-end on whatever platform the suite runs on: the bug was a spawn
|
||||
// failure, so the only conclusive assertion is a real spawn.
|
||||
const out = mkdtempSync(join(tmpdir(), "esbuild-spawn-"));
|
||||
try {
|
||||
const src = join(out, "worker.ts");
|
||||
const dest = join(out, "worker.js");
|
||||
writeFileSync(src, "export const answer: number = 42;\n");
|
||||
|
||||
runBuildTool(
|
||||
"esbuild",
|
||||
"esbuild",
|
||||
[src, "--bundle", "--platform=node", "--format=esm", `--outfile=${dest}`],
|
||||
{ stdio: "pipe" }
|
||||
);
|
||||
|
||||
assert.match(readFileSync(dest, "utf8"), /42/, "esbuild produced the bundle");
|
||||
} finally {
|
||||
rmSync(out, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("colocate-standalone.mjs never spawns the node_modules/.bin shim again", () => {
|
||||
const source = readFileSync(
|
||||
new URL("../../../scripts/build/colocate-standalone.mjs", import.meta.url),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
!/\.bin["'\s,]+["']esbuild/.test(source),
|
||||
"the postbuild hook must not reference node_modules/.bin/esbuild — that path is Windows-fatal"
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/runBuildTool\(/,
|
||||
"esbuild is spawned through the shared cross-platform runner"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user