mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
fix(cli): add node_modules/.bin to PATH for lifecycle scripts
The `prepack`, `postpack`, `prepublishOnly`, `publish`, `postpublish`, `preversion`, `version`, and `postversion` scripts run via `runPackageScriptForeground` were missing `node_modules/.bin` in their PATH, preventing them from finding locally installed binaries. This was inconsistent with: - `bun run <script>` which correctly adds node_modules/.bin to PATH - Install lifecycle hooks (preinstall, postinstall) which also have it The fix adds node_modules/.bin directories to PATH in `runPackageScriptForeground`, walking up from the cwd (same approach used in PackageManagerLifecycle.zig for install scripts). Fixes #17493 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -216,6 +216,23 @@ pub const RunCommand = struct {
|
||||
silent: bool,
|
||||
use_system_shell: bool,
|
||||
) !void {
|
||||
// Add node_modules/.bin directories to PATH (walking up from cwd)
|
||||
const original_path = env.get("PATH") orelse "";
|
||||
var PATH: bun.EnvPath(.{}) = try .initCapacity(allocator, original_path.len + 1 + "node_modules/.bin".len + cwd.len + 1);
|
||||
defer PATH.deinit();
|
||||
|
||||
var parent: ?string = cwd;
|
||||
while (parent) |dir| {
|
||||
var builder = PATH.pathComponentBuilder();
|
||||
builder.append(dir);
|
||||
builder.append("node_modules/.bin");
|
||||
try builder.apply();
|
||||
parent = std.fs.path.dirname(dir);
|
||||
}
|
||||
|
||||
try PATH.append(original_path);
|
||||
try env.map.put("PATH", PATH.slice());
|
||||
|
||||
const shell_bin = findShell(env.get("PATH") orelse "", cwd) orelse return error.MissingShell;
|
||||
env.map.put("npm_lifecycle_event", name) catch unreachable;
|
||||
env.map.put("npm_lifecycle_script", original_script) catch unreachable;
|
||||
|
||||
157
test/regression/issue/17493.test.ts
Normal file
157
test/regression/issue/17493.test.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDir } from "harness";
|
||||
|
||||
test("prepack script has node_modules/.bin in PATH", async () => {
|
||||
// Create a test package that verifies rimraf is accessible from prepack script
|
||||
using dir = tempDir("pack-bin-path", {
|
||||
"package.json": JSON.stringify(
|
||||
{
|
||||
name: "test-bin-path",
|
||||
version: "1.0.0",
|
||||
scripts: {
|
||||
// Use which to find rimraf, or check if it's in PATH
|
||||
prepack: "node check-path.js",
|
||||
},
|
||||
devDependencies: {
|
||||
rimraf: "^3.0.2",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"check-path.js": `
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Check if node_modules/.bin is in PATH
|
||||
const pathDirs = process.env.PATH.split(path.delimiter);
|
||||
const binDir = path.join(process.cwd(), 'node_modules', '.bin');
|
||||
const hasBinInPath = pathDirs.some(dir => {
|
||||
// Normalize paths for comparison
|
||||
const normalizedDir = path.normalize(dir);
|
||||
const normalizedBinDir = path.normalize(binDir);
|
||||
return normalizedDir === normalizedBinDir || normalizedDir.endsWith(path.join('node_modules', '.bin'));
|
||||
});
|
||||
|
||||
if (!hasBinInPath) {
|
||||
console.error('ERROR: node_modules/.bin is NOT in PATH');
|
||||
console.error('PATH directories:', pathDirs);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Also verify rimraf executable exists in node_modules/.bin
|
||||
const rimrafPath = path.join(binDir, process.platform === 'win32' ? 'rimraf.cmd' : 'rimraf');
|
||||
if (!fs.existsSync(rimrafPath)) {
|
||||
// Try the bunx shim path
|
||||
const bunxShimPath = path.join(binDir, 'rimraf');
|
||||
if (!fs.existsSync(bunxShimPath)) {
|
||||
console.error('ERROR: rimraf not found in node_modules/.bin');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('SUCCESS: node_modules/.bin is in PATH');
|
||||
`,
|
||||
});
|
||||
|
||||
// First install dependencies
|
||||
await using installProc = Bun.spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [, , installExitCode] = await Promise.all([
|
||||
installProc.stdout.text(),
|
||||
installProc.stderr.text(),
|
||||
installProc.exited,
|
||||
]);
|
||||
expect(installExitCode).toBe(0);
|
||||
|
||||
// Now run bun pm pack and verify prepack script can find binaries
|
||||
await using packProc = Bun.spawn({
|
||||
cmd: [bunExe(), "pm", "pack"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
packProc.stdout.text(),
|
||||
packProc.stderr.text(),
|
||||
packProc.exited,
|
||||
]);
|
||||
|
||||
// The check-path.js script should succeed, meaning node_modules/.bin was in PATH
|
||||
expect(stdout + stderr).toContain("SUCCESS: node_modules/.bin is in PATH");
|
||||
expect(stdout + stderr).not.toContain("ERROR:");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("bun run script has node_modules/.bin in PATH (control test)", async () => {
|
||||
// This verifies that `bun run` works correctly (as a control)
|
||||
using dir = tempDir("run-bin-path", {
|
||||
"package.json": JSON.stringify(
|
||||
{
|
||||
name: "test-run-bin-path",
|
||||
version: "1.0.0",
|
||||
scripts: {
|
||||
"check-path": "node check-path.js",
|
||||
},
|
||||
devDependencies: {
|
||||
rimraf: "^3.0.2",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"check-path.js": `
|
||||
const path = require('path');
|
||||
|
||||
// Check if node_modules/.bin is in PATH
|
||||
const pathDirs = process.env.PATH.split(path.delimiter);
|
||||
const hasBinInPath = pathDirs.some(dir => dir.endsWith(path.join('node_modules', '.bin')));
|
||||
|
||||
if (!hasBinInPath) {
|
||||
console.error('ERROR: node_modules/.bin is NOT in PATH');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('SUCCESS: node_modules/.bin is in PATH');
|
||||
`,
|
||||
});
|
||||
|
||||
// First install dependencies
|
||||
await using installProc = Bun.spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [, , installExitCode] = await Promise.all([
|
||||
installProc.stdout.text(),
|
||||
installProc.stderr.text(),
|
||||
installProc.exited,
|
||||
]);
|
||||
expect(installExitCode).toBe(0);
|
||||
|
||||
// Run the check-path script via bun run
|
||||
await using runProc = Bun.spawn({
|
||||
cmd: [bunExe(), "run", "check-path"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([runProc.stdout.text(), runProc.stderr.text(), runProc.exited]);
|
||||
|
||||
expect(stdout + stderr).toContain("SUCCESS: node_modules/.bin is in PATH");
|
||||
expect(stdout + stderr).not.toContain("ERROR:");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
Reference in New Issue
Block a user