From 704252e85fabd267b9a53110f6a661f4ceafc684 Mon Sep 17 00:00:00 2001 From: robobun Date: Mon, 19 Jan 2026 17:11:38 -0800 Subject: [PATCH] fix(npm): show helpful error when postinstall script hasn't run (#26259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Replaces empty placeholder executables with shell scripts that print helpful error messages - The scripts exit with code 1 instead of silently succeeding with code 0 - Helps users diagnose issues when installing with `--ignore-scripts` or using pnpm ## Problem When installing the `bun` npm package with `--ignore-scripts` or using pnpm (which skips postinstall by default), the placeholder `bun.exe` and `bunx.exe` files were empty, causing them to silently exit with code 0 and produce no output. This made it very difficult for users to understand why bun wasn't working. ## Solution The placeholder files are now shell scripts that: 1. Print a clear error message explaining the issue 2. Provide instructions on how to fix it (manually running postinstall or reinstalling without `--ignore-scripts`) 3. Exit with code 1 to indicate failure Example output when running the placeholder: ``` Error: Bun's postinstall script was not run. This occurs when using --ignore-scripts during installation, or when using a package manager like pnpm that does not run postinstall scripts by default. To fix this, run the postinstall script manually: cd node_modules/bun && node install.js Or reinstall bun without the --ignore-scripts flag. ``` ## Test plan - [x] Added regression test that verifies the placeholder script behavior - [x] Test passes with `bun bd test test/regression/issue/24329.test.ts` Fixes #24329 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude Opus 4.5 --- packages/bun-release/scripts/upload-npm.ts | 19 +++- test/regression/issue/24329.test.ts | 101 +++++++++++++++++++++ 2 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 test/regression/issue/24329.test.ts diff --git a/packages/bun-release/scripts/upload-npm.ts b/packages/bun-release/scripts/upload-npm.ts index 57d731b133..353673ebed 100644 --- a/packages/bun-release/scripts/upload-npm.ts +++ b/packages/bun-release/scripts/upload-npm.ts @@ -71,8 +71,23 @@ async function buildRootModule(dryRun?: boolean) { js: "// Source code: https://github.com/oven-sh/bun/blob/main/packages/bun-release/scripts/npm-postinstall.ts", }, }); - write(join(cwd, "bin", "bun.exe"), ""); - write(join(cwd, "bin", "bunx.exe"), ""); + // Create placeholder scripts that print an error message if postinstall hasn't run. + // On Unix, these are executed as shell scripts despite the .exe extension. + // On Windows, npm creates .cmd wrappers that would fail anyway if the binary isn't valid. + const placeholderScript = `#!/bin/sh +echo "Error: Bun's postinstall script was not run." >&2 +echo "" >&2 +echo "This occurs when using --ignore-scripts during installation, or when using a" >&2 +echo "package manager like pnpm that does not run postinstall scripts by default." >&2 +echo "" >&2 +echo "To fix this, run the postinstall script manually:" >&2 +echo " cd node_modules/bun && node install.js" >&2 +echo "" >&2 +echo "Or reinstall bun without the --ignore-scripts flag." >&2 +exit 1 +`; + write(join(cwd, "bin", "bun.exe"), placeholderScript); + write(join(cwd, "bin", "bunx.exe"), placeholderScript); write( join(cwd, "bin", "README.txt"), `The 'bun.exe' file is a placeholder for the binary file, which diff --git a/test/regression/issue/24329.test.ts b/test/regression/issue/24329.test.ts new file mode 100644 index 0000000000..f63e1c709a --- /dev/null +++ b/test/regression/issue/24329.test.ts @@ -0,0 +1,101 @@ +import { expect, test } from "bun:test"; +import { bunEnv, isWindows, tempDir } from "harness"; + +// This test verifies that the placeholder scripts created during npm package build +// print an error message and exit with code 1, rather than silently succeeding. +// See: https://github.com/oven-sh/bun/issues/24329 + +test("bun npm placeholder script should exit with error if postinstall hasn't run", async () => { + // Skip on Windows as the placeholder is a shell script + if (isWindows) { + return; + } + + // This is the placeholder script content that gets written to bin/bun.exe + // during npm package build (see packages/bun-release/scripts/upload-npm.ts) + const placeholderScript = `#!/bin/sh +echo "Error: Bun's postinstall script was not run." >&2 +echo "" >&2 +echo "This occurs when using --ignore-scripts during installation, or when using a" >&2 +echo "package manager like pnpm that does not run postinstall scripts by default." >&2 +echo "" >&2 +echo "To fix this, run the postinstall script manually:" >&2 +echo " cd node_modules/bun && node install.js" >&2 +echo "" >&2 +echo "Or reinstall bun without the --ignore-scripts flag." >&2 +exit 1 +`; + + using dir = tempDir("issue-24329", { + "bun-placeholder": placeholderScript, + }); + + // Make the placeholder executable + const { exitCode: chmodExitCode } = Bun.spawnSync({ + cmd: ["chmod", "+x", "bun-placeholder"], + cwd: String(dir), + env: bunEnv, + }); + expect(chmodExitCode).toBe(0); + + // Run the placeholder script + await using proc = Bun.spawn({ + cmd: ["./bun-placeholder", "--version"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // The placeholder should exit with code 1 + expect(exitCode).toBe(1); + + // stdout should be empty (all output goes to stderr) + expect(stdout).toBe(""); + + // stderr should contain the error message + expect(stderr).toContain("Error: Bun's postinstall script was not run."); + expect(stderr).toContain("--ignore-scripts"); + expect(stderr).toContain("cd node_modules/bun && node install.js"); +}); + +test("empty shell script exits with code 0 (demonstrating why the fix is needed)", async () => { + // Skip on Windows + if (isWindows) { + return; + } + + // This simulates the OLD behavior: an empty shell script (with shebang) + // Note: A completely empty file can't be executed by Bun.spawn (ENOEXEC), + // but an empty shell script with a shebang exits with code 0 + using dir = tempDir("issue-24329-old", { + "bun-placeholder": "#!/bin/sh\n", + }); + + // Make it executable + const { exitCode: chmodExitCode } = Bun.spawnSync({ + cmd: ["chmod", "+x", "bun-placeholder"], + cwd: String(dir), + env: bunEnv, + }); + expect(chmodExitCode).toBe(0); + + // Run the empty shell script + await using proc = Bun.spawn({ + cmd: ["./bun-placeholder", "--version"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // Empty shell script exits with code 0 silently - this is similar to the bug behavior + // Assert stdout/stderr before exitCode to get more useful error messages on failure + expect(stdout).toBe(""); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); +});