diff --git a/src/runtime/cli/Arguments.rs b/src/runtime/cli/Arguments.rs index 14e20c0e7b5a..4413fde27476 100644 --- a/src/runtime/cli/Arguments.rs +++ b/src/runtime/cli/Arguments.rs @@ -317,9 +317,74 @@ 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!("--security-revert ..."), + parse_param!("--security-reverts ..."), + 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] = &[ + // 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 871a1809d2fb..bd4829c32660 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,123 @@ 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(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); + }); + + 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", + "--security-revert", + "--security-reverts", + "--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", + "--test-name-pattern", + ]; + + 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); + }); +});