Files

643 lines
25 KiB
TypeScript
Raw Permalink Normal View History

2026-08-27 21:09:14 +00:00
import { spawn } from "bun";
import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
import { chmodSync, existsSync, readFileSync, realpathSync, statSync, symlinkSync } from "fs";
import { rm, writeFile } from "fs/promises";
import { bunEnv, bunExe, isWindows, tempDir, VerdaccioRegistry } from "harness";
import { join, sep } from "path";
let verdaccio: VerdaccioRegistry;
setDefaultTimeout(1000 * 60 * 5);
beforeAll(async () => {
verdaccio = new VerdaccioRegistry();
await verdaccio.start();
});
afterAll(() => {
verdaccio.stop();
});
// POSIX puts a symlink at `.bin/<name>`. Windows writes `<name>.exe` (a copy
// of the shim PE) plus `<name>.bunx` whose first UTF-16LE field is the target
// path relative to node_modules.
function binEntry(binDir: string, name: string) {
return join(binDir, isWindows ? `${name}.exe` : name);
}
function readBinTarget(binDir: string, name: string) {
if (isWindows) {
const raw = readFileSync(join(binDir, `${name}.bunx`)).toString("utf16le");
// bin_path is terminated by `"` then NUL; keep only the path and normalize
// separators so callers can compare against join() output.
const end = raw.indexOf('"');
return (end === -1 ? raw : raw.slice(0, end)).replaceAll("/", sep);
}
return realpathSync(join(binDir, name));
}
describe.concurrent("native binlink optimization", () => {
for (const linker of ["hoisted", "isolated"]) {
test(`uses platform-specific bin instead of main package bin with linker ${linker}`, async () => {
let env = { ...bunEnv };
const { packageDir, packageJson } = await verdaccio.createTestDir();
env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache");
env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp");
// Create bunfig
await writeFile(
join(packageDir, "bunfig.toml"),
Bun.TOML.stringify({
install: {
cache: join(packageDir, ".bun-cache"),
registry: verdaccio.registryUrl(),
linker,
},
}),
);
// Install the main package
await writeFile(
packageJson,
JSON.stringify({
name: "test-app",
version: "1.0.0",
dependencies: {
"test-native-binlink": "1.0.0",
},
nativeDependencies: ["test-native-binlink"],
trustedDependencies: ["test-native-binlink"],
}),
);
const binDir = join(packageDir, "node_modules", ".bin");
const binPath = binEntry(binDir, "test-binlink-cmd");
async function runInstall() {
const proc = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "inherit",
stdin: "ignore",
stderr: "inherit",
env,
});
expect(await proc.exited).toBe(0);
}
async function expectPlatformBin() {
const proc = spawn({
cmd: [binPath],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "inherit",
env,
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(stdout).toContain("SUCCESS: Using platform-specific bin");
expect(exitCode).toBe(0);
}
await runInstall();
expect(readBinTarget(binDir, "test-binlink-cmd")).toContain(join("test-native-binlink-target", "bin", "main.js"));
await expectPlatformBin();
// Now delete the node_modules folder, keep the bun.lock, re-install
await rm(join(packageDir, "node_modules"), { recursive: true, force: true });
await runInstall();
await expectPlatformBin();
// Now do a no-op re-install.
await runInstall();
await expectPlatformBin();
// Now do an install with the .bin folder gone
await rm(binDir, { recursive: true, force: true });
await runInstall();
await expectPlatformBin();
});
// Regression: a package on the nativeDependencies list whose platform-specific
// optionalDependency does NOT contain the bin file at the expected path must
// fall back to linking the original package's bin. Previously the `seen` map
// was poisoned by the failed redirect attempt, so the retry silently no-op'd
// and `.bin/<cmd>` was never created (broke `bunx @anthropic-ai/claude-code`).
test(`falls back to main package bin when platform dep has no matching bin file with linker ${linker}`, async () => {
let env = { ...bunEnv };
const { packageDir, packageJson } = await verdaccio.createTestDir();
env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache");
env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp");
await writeFile(
join(packageDir, "bunfig.toml"),
Bun.TOML.stringify({
install: {
cache: join(packageDir, ".bun-cache"),
registry: verdaccio.registryUrl(),
linker,
},
}),
);
await writeFile(
packageJson,
JSON.stringify({
name: "test-app",
version: "1.0.0",
dependencies: {
"test-native-binlink-fallback": "1.0.0",
},
nativeDependencies: ["test-native-binlink-fallback"],
trustedDependencies: ["test-native-binlink-fallback"],
}),
);
const installProc = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
});
const [, installStderr, installExit] = await Promise.all([
installProc.stdout.text(),
installProc.stderr.text(),
installProc.exited,
]);
expect(installStderr).not.toContain("error:");
expect(installExit).toBe(0);
const binDir = join(packageDir, "node_modules", ".bin");
const binPath = binEntry(binDir, "fallback-cmd");
expect(readBinTarget(binDir, "fallback-cmd")).toContain(join("test-native-binlink-fallback", "cli.js"));
const binProc = spawn({
cmd: [binPath],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "inherit",
env,
});
const [binStdout, binExitCode] = await Promise.all([binProc.stdout.text(), binProc.exited]);
expect(binStdout).toContain("SUCCESS: Using main package bin");
expect(binExitCode).toBe(0);
// Re-install with node_modules removed (lockfile-only path)
await rm(join(packageDir, "node_modules"), { recursive: true, force: true });
const installProc2 = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "inherit",
stdin: "ignore",
stderr: "inherit",
env,
});
expect(await installProc2.exited).toBe(0);
const binProc2 = spawn({
cmd: [binPath],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "inherit",
env,
});
const [binStdout2, binExitCode2] = await Promise.all([binProc2.stdout.text(), binProc2.exited]);
expect(binStdout2).toContain("SUCCESS: Using main package bin");
expect(binExitCode2).toBe(0);
});
}
// The postinstall skip must apply to every copy of a nativeDependencies
// package in the tree, not just the hoisted one. Previously a second,
// differently-versioned esbuild nested under a transitive dependent would
// still run `node install.js`.
describe("nested nativeDependencies", () => {
async function setup(opts: { linker: "hoisted" | "isolated"; deps: Record<string, string>; extraEnv?: object }) {
let env: Record<string, string> = { ...bunEnv, ...(opts.extraEnv ?? {}) };
const { packageDir, packageJson } = await verdaccio.createTestDir();
env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache");
env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp");
await writeFile(
join(packageDir, "bunfig.toml"),
Bun.TOML.stringify({
install: {
cache: join(packageDir, ".bun-cache"),
registry: verdaccio.registryUrl(),
linker: opts.linker,
},
}),
);
await writeFile(
packageJson,
JSON.stringify({
name: "test-app",
version: "1.0.0",
dependencies: opts.deps,
nativeDependencies: ["test-postinstall-skip"],
trustedDependencies: ["test-postinstall-skip"],
}),
);
async function install() {
const proc = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
});
const [, stderr, exit] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).not.toContain("error:");
expect(exit).toBe(0);
}
async function runBin(binDir: string, name: string) {
const proc = spawn({
cmd: [binEntry(binDir, name)],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
});
const [out, err, code] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
return { out: out.trim(), err, code };
}
return { packageDir, env, install, runBin };
}
function postinstallRanMarkers(dirs: Record<string, string>) {
return Object.fromEntries(Object.entries(dirs).map(([k, d]) => [k, existsSync(join(d, "postinstall-ran"))]));
}
for (const linker of ["hoisted", "isolated"] as const) {
test(`skips postinstall for nested copies (${linker}, platform dep in child tree)`, async () => {
const { packageDir, install, runBin } = await setup({
linker,
deps: { "test-postinstall-skip": "2.0.0", "test-postinstall-skip-parent": "1.0.0" },
});
await install();
const nm = join(packageDir, "node_modules");
const nestedScope =
linker === "hoisted"
? join(nm, "test-postinstall-skip-parent", "node_modules")
: join(nm, ".bun", "[email protected]", "node_modules");
const hoisted = realpathSync(join(nm, "test-postinstall-skip"));
const nested = realpathSync(join(nestedScope, "test-postinstall-skip"));
expect({
hoisted: JSON.parse(readFileSync(join(hoisted, "package.json"), "utf8")).version,
nested: JSON.parse(readFileSync(join(nested, "package.json"), "utf8")).version,
}).toEqual({ hoisted: "2.0.0", nested: "1.0.0" });
expect(postinstallRanMarkers({ hoisted, nested })).toEqual({ hoisted: false, nested: false });
const hoistedBinDir = join(nm, ".bin");
const nestedBinDir = join(nestedScope, ".bin");
expect(readBinTarget(hoistedBinDir, "skip-test-cmd")).toContain("test-postinstall-skip-native");
expect(readBinTarget(nestedBinDir, "skip-test-cmd")).toContain("test-postinstall-skip-native");
if (linker === "hoisted") {
// The platform dep lands in the child tree under
// `test-postinstall-skip/node_modules/`, so the redirect has to cross
// trees (covers the `target_tree_id != tree_id` defer-and-relink path).
expect(readBinTarget(nestedBinDir, "skip-test-cmd")).toContain(
join("test-postinstall-skip", "node_modules", "test-postinstall-skip-native"),
);
}
expect(await runBin(hoistedBinDir, "skip-test-cmd")).toEqual({ out: "native v2.0.0", err: "", code: 0 });
expect(await runBin(nestedBinDir, "skip-test-cmd")).toEqual({ out: "native v1.0.0", err: "", code: 0 });
// Lockfile-only reinstall.
await rm(nm, { recursive: true, force: true });
await install();
expect(postinstallRanMarkers({ hoisted, nested })).toEqual({ hoisted: false, nested: false });
expect(readBinTarget(nestedBinDir, "skip-test-cmd")).toContain("test-postinstall-skip-native");
expect(await runBin(hoistedBinDir, "skip-test-cmd")).toEqual({ out: "native v2.0.0", err: "", code: 0 });
expect(await runBin(nestedBinDir, "skip-test-cmd")).toEqual({ out: "native v1.0.0", err: "", code: 0 });
// No-op reinstall keeps the redirect intact.
await install();
expect(postinstallRanMarkers({ hoisted, nested })).toEqual({ hoisted: false, nested: false });
expect(await runBin(nestedBinDir, "skip-test-cmd")).toEqual({ out: "native v1.0.0", err: "", code: 0 });
if (linker === "hoisted") {
// Removing only the nested `.bin` folder and reinstalling recreates the redirect.
await rm(nestedBinDir, { recursive: true, force: true });
await install();
expect(readBinTarget(nestedBinDir, "skip-test-cmd")).toContain("test-postinstall-skip-native");
expect(await runBin(nestedBinDir, "skip-test-cmd")).toEqual({ out: "native v1.0.0", err: "", code: 0 });
}
});
}
test("skips postinstall for nested copies (hoisted, platform dep as sibling)", async () => {
// [email protected] also depends on [email protected] directly,
// so the platform package lands in the same tree as the nested main package.
const { packageDir, install, runBin } = await setup({
linker: "hoisted",
deps: { "test-postinstall-skip": "2.0.0", "test-postinstall-skip-parent": "2.0.0" },
});
await install();
const nested = join(packageDir, "node_modules", "test-postinstall-skip-parent", "node_modules");
expect(existsSync(join(nested, "test-postinstall-skip-native"))).toBeTrue();
expect(existsSync(join(nested, "test-postinstall-skip", "node_modules"))).toBeFalse();
expect(
postinstallRanMarkers({
hoisted: join(packageDir, "node_modules", "test-postinstall-skip"),
nested: join(nested, "test-postinstall-skip"),
}),
).toEqual({ hoisted: false, nested: false });
const nestedBinDir = join(nested, ".bin");
if (isWindows) {
expect(readBinTarget(nestedBinDir, "skip-test-cmd")).toBe(
join("test-postinstall-skip-native", "bin", "cmd.js"),
);
} else {
expect(readBinTarget(nestedBinDir, "skip-test-cmd")).toBe(
realpathSync(join(nested, "test-postinstall-skip-native", "bin", "cmd.js")),
);
}
expect(await runBin(nestedBinDir, "skip-test-cmd")).toEqual({ out: "native v1.0.0", err: "", code: 0 });
});
test("skips postinstall for nested copies (hoisted, platform dep in ancestor tree)", async () => {
// Root depends on [email protected] directly and on [email protected]. The nested
// [email protected]'s optional [email protected] dedupes into root, so the redirect
// crosses upward into tree 0.
const { packageDir, install, runBin } = await setup({
linker: "hoisted",
deps: {
"test-postinstall-skip": "2.0.0",
"test-postinstall-skip-native": "1.0.0",
"test-postinstall-skip-parent": "1.0.0",
},
});
await install();
const nested = join(packageDir, "node_modules", "test-postinstall-skip-parent", "node_modules");
// [email protected] is at root; nothing under the nested tree.
expect(
JSON.parse(
readFileSync(join(packageDir, "node_modules", "test-postinstall-skip-native", "package.json"), "utf8"),
).version,
).toBe("1.0.0");
expect(existsSync(join(nested, "test-postinstall-skip-native"))).toBeFalse();
expect(existsSync(join(nested, "test-postinstall-skip", "node_modules"))).toBeFalse();
expect(
postinstallRanMarkers({
nested: join(nested, "test-postinstall-skip"),
}),
).toEqual({ nested: false });
const nestedBinDir = join(nested, ".bin");
if (isWindows) {
expect(readBinTarget(nestedBinDir, "skip-test-cmd")).toBe(
join("..", "..", "test-postinstall-skip-native", "bin", "cmd.js"),
);
} else {
expect(readBinTarget(nestedBinDir, "skip-test-cmd")).toBe(
realpathSync(join(packageDir, "node_modules", "test-postinstall-skip-native", "bin", "cmd.js")),
);
}
expect(await runBin(nestedBinDir, "skip-test-cmd")).toEqual({ out: "native v1.0.0", err: "", code: 0 });
});
test("BUN_FEATURE_FLAG_DISABLE_IGNORE_SCRIPTS runs nested postinstall", async () => {
const { packageDir, install } = await setup({
linker: "hoisted",
deps: { "test-postinstall-skip": "2.0.0", "test-postinstall-skip-parent": "1.0.0" },
extraEnv: { BUN_FEATURE_FLAG_DISABLE_IGNORE_SCRIPTS: "1" },
});
await install();
expect(
postinstallRanMarkers({
hoisted: join(packageDir, "node_modules", "test-postinstall-skip"),
nested: join(
packageDir,
"node_modules",
"test-postinstall-skip-parent",
"node_modules",
"test-postinstall-skip",
),
}),
).toEqual({ hoisted: true, nested: true });
});
});
});
// Regression for `bunx @anthropic-ai/claude-code` silently exiting / the
// Windows "Unsupported 16-Bit Application" dialog: the parent package's `bin`
// points at a stub that postinstall is meant to replace, while the platform
// optionalDependency ships the real binary at a different path. Each version
// of the fixture exercises one of the alternate-path probes in
// `bin::Linker::resolve_bin_target`.
describe.concurrent("native binlink altpath", () => {
const shapes = [
{
version: "1.0.0",
targetFile: "altpath-cmd",
description: "<pkg>/<bin_name> (claude-code-linux shape)",
},
{
version: "2.0.0",
targetFile: "launcher.exe",
description: "<pkg>/<basename(target)> (bin key differs from target stem)",
},
{
version: "3.0.0",
targetFile: "altpath-cmd.exe",
description: "<pkg>/<bin_name>.exe (@esbuild/win32 shape)",
},
] as const;
for (const linker of ["hoisted", "isolated"]) {
for (const { version, targetFile, description } of shapes) {
test(`finds native bin via ${description} with linker ${linker}`, async () => {
let env = { ...bunEnv };
const { packageDir, packageJson } = await verdaccio.createTestDir();
env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache");
env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp");
await writeFile(
join(packageDir, "bunfig.toml"),
Bun.TOML.stringify({
install: {
cache: join(packageDir, ".bun-cache"),
registry: verdaccio.registryUrl(),
linker,
},
}),
);
await writeFile(
packageJson,
JSON.stringify({
name: "test-app",
version: "1.0.0",
dependencies: {
"test-native-binlink-altpath": version,
},
nativeDependencies: ["test-native-binlink-altpath"],
trustedDependencies: ["test-native-binlink-altpath"],
}),
);
const installProc = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
});
const [, installStderr, installExit] = await Promise.all([
installProc.stdout.text(),
installProc.stderr.text(),
installProc.exited,
]);
expect(installStderr).not.toContain("error:");
expect(installExit).toBe(0);
const binDir = join(packageDir, "node_modules", ".bin");
const binPath = binEntry(binDir, "altpath-cmd");
// The bin must resolve into the platform-specific package, not back into
// the parent package's placeholder stub.
expect(readBinTarget(binDir, "altpath-cmd")).toContain(join("test-native-binlink-altpath-target", targetFile));
const binProc = spawn({
cmd: [binPath],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
});
const [binStdout, binStderr, binExitCode] = await Promise.all([
binProc.stdout.text(),
binProc.stderr.text(),
binProc.exited,
]);
expect({ stdout: binStdout, stderr: binStderr }).toEqual({
stdout: expect.stringContaining("SUCCESS: Using platform-specific bin at package root"),
stderr: "",
});
expect(binExitCode).toBe(0);
// Because the redirect succeeded, the postinstall should have been
// skipped entirely (that's the point of the optimization).
expect(
existsSync(join(packageDir, "node_modules", "test-native-binlink-altpath", "postinstall-ran")),
).toBeFalse();
// Re-install with node_modules removed (lockfile-only path)
await rm(join(packageDir, "node_modules"), { recursive: true, force: true });
const installProc2 = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "inherit",
stdin: "ignore",
stderr: "inherit",
env,
});
expect(await installProc2.exited).toBe(0);
const binProc2 = spawn({
cmd: [binPath],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "inherit",
env,
});
const [binStdout2, binExitCode2] = await Promise.all([binProc2.stdout.text(), binProc2.exited]);
expect(binStdout2).toContain("SUCCESS: Using platform-specific bin at package root");
expect(binExitCode2).toBe(0);
});
}
}
});
// The bin linker must not create a `node_modules/.bin` entry (nor chmod or rewrite the
// target) when a package's bin path resolves through an in-package symlink to a location
// outside the package directory. Bins that resolve inside the package must still link,
// including for workspace packages whose node_modules entry is itself a symlink.
//
// POSIX-only: the Windows bin linker writes a shim+metadata pair without
// touching the target file (no chmod, no shebang rewrite), so the attack this
// guards against can't occur there, and the resolved-containment check in
// `link_bin_or_create_shim` is `#[cfg(not(windows))]` accordingly.
test.skipIf(isWindows)(
"skips bin entries whose target resolves outside the package directory and keeps outside files untouched",
async () => {
const secretContents = "#!/usr/bin/env node\r\nconsole.log('outside file');\n";
using dir = tempDir("binlink-resolved-target", {
"package.json": JSON.stringify({
name: "binlink-containment-app",
version: "1.0.0",
workspaces: ["packages/*"],
dependencies: {
"resolved-escape-pkg": "workspace:*",
"contained-bin-pkg": "workspace:*",
},
}),
"outside/secret.txt": secretContents,
"packages/resolved-escape-pkg/package.json": JSON.stringify({
name: "resolved-escape-pkg",
version: "1.0.0",
bin: { "escape-cmd": "./payload/secret.txt" },
}),
"packages/contained-bin-pkg/package.json": JSON.stringify({
name: "contained-bin-pkg",
version: "1.0.0",
bin: { "contained-cmd": "bin/run.js" },
}),
"packages/contained-bin-pkg/bin/run.js": '#!/usr/bin/env node\nconsole.log("contained-cmd ok");\n',
});
const root = String(dir);
const secretPath = join(root, "outside", "secret.txt");
// A mode without any execute bits, so a chmod performed by bin linking is observable.
chmodSync(secretPath, 0o600);
// In-package path component that resolves to a directory outside the package.
symlinkSync(join(root, "outside"), join(root, "packages", "resolved-escape-pkg", "payload"));
await using install = spawn({
cmd: [bunExe(), "install"],
cwd: root,
stdout: "ignore",
stdin: "ignore",
stderr: "pipe",
env: bunEnv,
});
const [installStderr, installExit] = await Promise.all([install.stderr.text(), install.exited]);
expect(installStderr).not.toContain("error:");
expect(installExit).toBe(0);
// The file outside the package keeps its mode and contents.
expect(statSync(secretPath).mode & 0o777).toBe(0o600);
expect(readFileSync(secretPath, "utf8")).toBe(secretContents);
// No `.bin` entry was created for the bin whose resolved target leaves the package.
expect(existsSync(join(root, "node_modules", ".bin", "escape-cmd"))).toBeFalse();
// The workspace bin that resolves inside its package is still linked and made executable,
// even though node_modules/<name> is a symlink into packages/.
const containedBin = join(root, "node_modules", ".bin", "contained-cmd");
expect(existsSync(containedBin)).toBeTrue();
expect(realpathSync(containedBin)).toBe(realpathSync(join(root, "packages", "contained-bin-pkg", "bin", "run.js")));
expect(statSync(join(root, "packages", "contained-bin-pkg", "bin", "run.js")).mode & 0o111).not.toBe(0);
},
);