From af4cdf5369cbf40b8ca99ffc88b1d2851ab1d85c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:04:19 +0000 Subject: [PATCH 1/3] cli: declare Node's value-taking flags so their value is not parsed as the entrypoint `bun --experimental-loader ./hooks.mjs app.mjs` silently ran hooks.mjs as the program and never executed app.mjs: the unknown-flag skip in the arg parser moved past --experimental-loader but treated its value as the first positional. The comment above the trace-event declarations already documents this hazard. Declare --experimental-loader and the rest of Node's value-taking CLI options (with no help text, so they stay hidden from --help) in RUNTIME_PARAMS_ so the value is consumed. This covers `bun`, `bun run`, and bun-as-node. --- src/runtime/cli/Arguments.rs | 59 +++++++++++++++++ test/cli/run/as-node.test.ts | 121 ++++++++++++++++++++++++++++++++++- 2 files changed, 179 insertions(+), 1 deletion(-) diff --git a/src/runtime/cli/Arguments.rs b/src/runtime/cli/Arguments.rs index 14e20c0e7b5a..21528a7ae1f0 100644 --- a/src/runtime/cli/Arguments.rs +++ b/src/runtime/cli/Arguments.rs @@ -317,6 +317,65 @@ pub(crate) const RUNTIME_PARAMS_: &[ParamType] = &[ parse_param!("--trace-exit"), parse_param!("--expose-internals"), parse_param!("--stack-trace-limit "), + // More Node.js flags Bun does not implement. Same rationale as above: the + // value-taking ones must be declared so the next arg (the value) is not + // parsed as the entrypoint. Keep this list in sync with the value-taking + // options in Node's `src/node_options.cc`. + parse_param!("--experimental-loader ..."), + parse_param!("--allow-fs-read ..."), + parse_param!("--allow-fs-write ..."), + parse_param!("--build-sea "), + parse_param!("--build-snapshot-config "), + parse_param!("--diagnostic-dir "), + parse_param!("--disable-proto "), + parse_param!("--disable-warning ..."), + parse_param!("--env-file-if-exists ..."), + parse_param!("--experimental-config-file "), + parse_param!("--experimental-sea-config "), + parse_param!("--heap-prof-interval "), + parse_param!("--heapsnapshot-near-heap-limit "), + parse_param!("--heapsnapshot-signal "), + parse_param!("--icu-data-dir "), + parse_param!("--input-type "), + parse_param!("--inspect-port "), + parse_param!("--debug-port "), + parse_param!("--inspect-publish-uid "), + parse_param!("--localstorage-file "), + parse_param!("--max-old-space-size-percentage "), + parse_param!("--network-family-autoselection-attempt-timeout "), + parse_param!("--openssl-config "), + parse_param!("--redirect-warnings "), + parse_param!("--report-dir "), + parse_param!("--report-directory "), + parse_param!("--report-filename "), + parse_param!("--report-signal "), + parse_param!("--secure-heap "), + parse_param!("--secure-heap-min "), + parse_param!("--snapshot-blob "), + parse_param!("--tls-cipher-list "), + parse_param!("--tls-keylog "), + parse_param!("--trace-require-module "), + parse_param!("--use-largepages "), + parse_param!("--v8-pool-size "), + parse_param!("--watch-path ..."), + parse_param!("--watch-kill-signal "), + parse_param!("--test-concurrency "), + parse_param!("--test-coverage-branches "), + parse_param!("--test-coverage-exclude ..."), + parse_param!("--test-coverage-functions "), + parse_param!("--test-coverage-include ..."), + parse_param!("--test-coverage-lines "), + parse_param!("--test-global-setup "), + parse_param!("--test-isolation "), + parse_param!("--experimental-test-isolation "), + parse_param!("--test-random-seed "), + parse_param!("--test-reporter ..."), + parse_param!("--test-reporter-destination ..."), + parse_param!("--test-rerun-failures "), + parse_param!("--test-shard "), + parse_param!("--test-skip-pattern ..."), + parse_param!("--experimental-test-tag-filter ..."), + parse_param!("--test-timeout "), ]; pub(crate) const AUTO_OR_RUN_PARAMS: &[ParamType] = &[ diff --git a/test/cli/run/as-node.test.ts b/test/cli/run/as-node.test.ts index 871a1809d2fb..c267aafe0bc1 100644 --- a/test/cli/run/as-node.test.ts +++ b/test/cli/run/as-node.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { join } from "path"; -import { fakeNodeRun, tempDirWithFiles } from "../../harness"; +import { bunEnv, bunExe, fakeNodeRun, tempDir, tempDirWithFiles } from "../../harness"; describe("fake node cli", () => { test("the node cli actually works", () => { @@ -102,3 +102,122 @@ describe("fake node cli", () => { expect(() => fakeNodeRun(temp, [])).toThrow(); }); }); + +describe("node value-taking CLI flags do not eat the entrypoint", () => { + // Node.js flags that take a value and which Bun does not otherwise implement + // must still consume their value argument so the *next* arg is parsed as the + // entrypoint. Otherwise `bun --experimental-loader ./hooks.mjs app.mjs` + // silently runs hooks.mjs as the program and app.mjs never executes. + const appBody = `console.log(JSON.stringify({ argv: process.argv.slice(2), execArgv: process.execArgv }));`; + const wrongBody = `throw new Error("flag value was run as the entrypoint");`; + + async function run(preArgs: string[], cwd: string) { + await using proc = Bun.spawn({ + cmd: [bunExe(), ...preArgs, "app.mjs", "scriptarg"], + env: bunEnv, + cwd, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout: stdout.trim(), stderr, exitCode }; + } + + test("--experimental-loader ./hooks.mjs app.mjs runs app.mjs, not hooks.mjs", async () => { + using dir = tempDir("node-value-flag", { + "hooks.mjs": wrongBody, + "app.mjs": appBody, + }); + const { stdout, stderr, exitCode } = await run(["--experimental-loader", "./hooks.mjs"], String(dir)); + expect({ stdout, stderr }).toEqual({ + stdout: JSON.stringify({ argv: ["scriptarg"], execArgv: ["--experimental-loader", "./hooks.mjs"] }), + stderr: "", + }); + expect(exitCode).toBe(0); + }); + + const valueFlags = [ + "--experimental-loader", + "--allow-fs-read", + "--allow-fs-write", + "--build-sea", + "--build-snapshot-config", + "--diagnostic-dir", + "--disable-proto", + "--disable-warning", + "--env-file-if-exists", + "--experimental-config-file", + "--experimental-sea-config", + "--heap-prof-interval", + "--heapsnapshot-near-heap-limit", + "--heapsnapshot-signal", + "--icu-data-dir", + "--input-type", + "--inspect-port", + "--debug-port", + "--inspect-publish-uid", + "--localstorage-file", + "--max-old-space-size-percentage", + "--network-family-autoselection-attempt-timeout", + "--openssl-config", + "--redirect-warnings", + "--report-dir", + "--report-directory", + "--report-filename", + "--report-signal", + "--secure-heap", + "--secure-heap-min", + "--snapshot-blob", + "--tls-cipher-list", + "--tls-keylog", + "--trace-require-module", + "--use-largepages", + "--v8-pool-size", + "--watch-path", + "--watch-kill-signal", + "--test-concurrency", + "--test-coverage-branches", + "--test-coverage-exclude", + "--test-coverage-functions", + "--test-coverage-include", + "--test-coverage-lines", + "--test-global-setup", + "--test-isolation", + "--experimental-test-isolation", + "--test-random-seed", + "--test-reporter", + "--test-reporter-destination", + "--test-rerun-failures", + "--test-shard", + "--test-skip-pattern", + "--experimental-test-tag-filter", + "--test-timeout", + ]; + + describe.each([[[]], [["run"]]])("bun %p", runArg => { + test.concurrent.each(valueFlags)("%s app.mjs runs app.mjs", async flag => { + using dir = tempDir("node-value-flag", { + "value.mjs": wrongBody, + "app.mjs": appBody, + }); + const { stdout, stderr, exitCode } = await run([...runArg, flag, "./value.mjs"], String(dir)); + expect(stderr).not.toContain("flag value was run as the entrypoint"); + expect(JSON.parse(stdout)).toEqual({ argv: ["scriptarg"], execArgv: [flag, "./value.mjs"] }); + expect(exitCode).toBe(0); + }); + }); + + test("hidden from --help", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "--help"], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout).not.toContain("--experimental-loader"); + expect(stdout).not.toContain("--openssl-config"); + expect(stdout).not.toContain("--v8-pool-size"); + expect(exitCode).toBe(0); + }); +}); From 130f04c72483efeae7e93c98d8ddf4c3faaae5e1 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:26:42 +0000 Subject: [PATCH 2/3] cli: also declare --test-name-pattern for auto/run so its value is consumed --test-name-pattern lives in TEST_ONLY_PARAMS (for `bun test`), which does not feed AUTO_PARAMS or RUN_PARAMS, so `bun --test-name-pattern foo app.mjs` still parsed foo as the entrypoint. Declare it in AUTO_OR_RUN_PARAMS (hidden from --help) rather than RUNTIME_PARAMS_ to avoid a duplicate in TEST_PARAMS. Also drop the exact stderr match in the primary test in favor of the same .not.toContain check the parameterized suite uses. --- src/runtime/cli/Arguments.rs | 4 ++++ test/cli/run/as-node.test.ts | 7 +++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/runtime/cli/Arguments.rs b/src/runtime/cli/Arguments.rs index 21528a7ae1f0..bfb31d98126c 100644 --- a/src/runtime/cli/Arguments.rs +++ b/src/runtime/cli/Arguments.rs @@ -379,6 +379,10 @@ pub(crate) const RUNTIME_PARAMS_: &[ParamType] = &[ ]; pub(crate) const AUTO_OR_RUN_PARAMS: &[ParamType] = &[ + // Node.js --test-name-pattern, here rather than in RUNTIME_PARAMS_ so it + // does not collide with `bun test`'s own -t/--test-name-pattern entry in + // TEST_ONLY_PARAMS. Hidden from --help (empty description). + parse_param!("--test-name-pattern ..."), parse_param!( "-F, --filter ... Run a script in all workspace packages matching the pattern" ), diff --git a/test/cli/run/as-node.test.ts b/test/cli/run/as-node.test.ts index c267aafe0bc1..e6fcacef11b0 100644 --- a/test/cli/run/as-node.test.ts +++ b/test/cli/run/as-node.test.ts @@ -129,10 +129,8 @@ describe("node value-taking CLI flags do not eat the entrypoint", () => { "app.mjs": appBody, }); const { stdout, stderr, exitCode } = await run(["--experimental-loader", "./hooks.mjs"], String(dir)); - expect({ stdout, stderr }).toEqual({ - stdout: JSON.stringify({ argv: ["scriptarg"], execArgv: ["--experimental-loader", "./hooks.mjs"] }), - stderr: "", - }); + expect(stderr).not.toContain("flag value was run as the entrypoint"); + expect(JSON.parse(stdout)).toEqual({ argv: ["scriptarg"], execArgv: ["--experimental-loader", "./hooks.mjs"] }); expect(exitCode).toBe(0); }); @@ -192,6 +190,7 @@ describe("node value-taking CLI flags do not eat the entrypoint", () => { "--test-skip-pattern", "--experimental-test-tag-filter", "--test-timeout", + "--test-name-pattern", ]; describe.each([[[]], [["run"]]])("bun %p", runArg => { From 3ddbe6019d7cb372a19363693b97f7a9ebd5ed4e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:52:46 +0000 Subject: [PATCH 3/3] cli: also declare --security-revert/--security-reverts so their value is consumed Node's --security-revert (and the --security-reverts alias) binds to PerProcessOptions::security_reverts (a string vector) and requires a value. Verified against Node v26.3.0. --experimental-default-config-file is boolean there and --experimental-package-map does not exist yet, so neither is added. --- src/runtime/cli/Arguments.rs | 2 ++ test/cli/run/as-node.test.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/runtime/cli/Arguments.rs b/src/runtime/cli/Arguments.rs index bfb31d98126c..4413fde27476 100644 --- a/src/runtime/cli/Arguments.rs +++ b/src/runtime/cli/Arguments.rs @@ -351,6 +351,8 @@ pub(crate) const RUNTIME_PARAMS_: &[ParamType] = &[ parse_param!("--report-signal "), parse_param!("--secure-heap "), parse_param!("--secure-heap-min "), + parse_param!("--security-revert ..."), + parse_param!("--security-reverts ..."), parse_param!("--snapshot-blob "), parse_param!("--tls-cipher-list "), parse_param!("--tls-keylog "), diff --git a/test/cli/run/as-node.test.ts b/test/cli/run/as-node.test.ts index e6fcacef11b0..bd4829c32660 100644 --- a/test/cli/run/as-node.test.ts +++ b/test/cli/run/as-node.test.ts @@ -165,6 +165,8 @@ describe("node value-taking CLI flags do not eat the entrypoint", () => { "--report-signal", "--secure-heap", "--secure-heap-min", + "--security-revert", + "--security-reverts", "--snapshot-blob", "--tls-cipher-list", "--tls-keylog",