diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ac72bfb7049..053ffb16eeb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Defer stdin and eval package-scope validation until module resolution needs it, so inline scripts can diagnose malformed ancestor metadata themselves. + - Dispatch child IPC messages and disconnects through JavaScript `process.emit`, preserving wrappers, accessors, and inherited overrides. - Synchronize resolver entry-cache snapshots with symlink fills, fd updates, and re-stats, preventing torn path reads during concurrent worker resolution. diff --git a/docs/runtime/nodejs-compat.mdx b/docs/runtime/nodejs-compat.mdx index 0effbf84ff1f..3d7439b8d239 100644 --- a/docs/runtime/nodejs-compat.mdx +++ b/docs/runtime/nodejs-compat.mdx @@ -11,6 +11,8 @@ We update this page regularly. It reflects the latest version of Bun's compatibi ## OpenClaw fork: auto-install defaults off +Stdin and eval scripts report malformed ancestor package metadata only when an import or `require()` call needs that scope. + The OpenClaw fork reports missing runtime imports without downloading npm packages. This default covers scripts, workers, and child processes running the fork. You can opt in with `--install=auto|fallback|force` or an explicit bunfig `install.auto` setting. Package-manager commands (`bun install`, `bun add`, `bun x`, and `bunx`) are unchanged. See [Auto-install](/runtime/auto-install). ## Built-in Node.js modules diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index d2970b7250ca..a43aea4c94f7 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -5719,6 +5719,12 @@ impl VirtualMachine { && mode != ResolveMode::ResolvedEsm && jsc_vm.transpiler.resolver.node_module_error.is_none() && bun_paths::is_absolute(result.path) + // Inline entries do not infer their module kind from a package scope. + && !jsc_vm + .module_loader + .eval_source + .as_ref() + .is_some_and(|source| source.path.text == result.path) { jsc_vm.transpiler.resolver.node_module_error = jsc_vm .transpiler diff --git a/src/resolver/package_json.rs b/src/resolver/package_json.rs index 313c76de6a7a..981bb8d81839 100644 --- a/src/resolver/package_json.rs +++ b/src/resolver/package_json.rs @@ -593,7 +593,14 @@ impl PackageJSON { .map(|fields| fields.json_errors.clone()) .unwrap_or_default(); - let parsed_json = match r.caches.json.parse_package_json(r_log, &json_source) { + // Runtime resolution reports cached Node errors only when the scope is used. + let mut deferred_log = bun_ast::Log::default(); + let parse_log = if r.validate_package_config { + &mut deferred_log + } else { + &mut *r_log + }; + let parsed_json = match r.caches.json.parse_package_json(parse_log, &json_source) { Ok(Some(v)) => v, Ok(None) => { return Some(Self::from_node_fields( diff --git a/src/resolver/resolver.rs b/src/resolver/resolver.rs index eafebf8fe587..0ca028ac11f4 100644 --- a/src/resolver/resolver.rs +++ b/src/resolver/resolver.rs @@ -1725,6 +1725,20 @@ impl<'a> Resolver<'a> { ) -> ResultUnion { debug_assert!(bun_paths::is_absolute(source_dir)); + // Node's CJS self lookup reads the parent scope even for relative requests. + // https://github.com/nodejs/node/blob/v24.21.0/lib/internal/modules/cjs/loader.js#L659-L664 + if self.validate_package_config + && matches!( + kind, + ast::ImportKind::Require | ast::ImportKind::RequireResolve + ) + { + if let Some(error) = self.node_package_scope_error_for_directory(source_dir) { + self.capture_node_module_error(error); + return ResultUnion::NotFound; + } + } + let mut import_path = input_import_path; // This implements the module resolution algorithm from node.js, which is diff --git a/src/runtime/cli/bunx_command.rs b/src/runtime/cli/bunx_command.rs index 42af649bb36a..16b2c39bdc38 100644 --- a/src/runtime/cli/bunx_command.rs +++ b/src/runtime/cli/bunx_command.rs @@ -752,6 +752,7 @@ impl BunxCommand { ConfigureEnvOptions { log_errors: true, store_root_fd: true, + defer_package_errors: false, }, )?; // SAFETY: `configure_env_for_run` returned `Ok`, so the slot is fully diff --git a/src/runtime/cli/filter_run.rs b/src/runtime/cli/filter_run.rs index fcc10802e940..1dd5d40b9ad5 100644 --- a/src/runtime/cli/filter_run.rs +++ b/src/runtime/cli/filter_run.rs @@ -813,6 +813,7 @@ pub(crate) fn run_scripts_with_filter( ConfigureEnvOptions { log_errors: true, store_root_fd: false, + defer_package_errors: false, }, )?; // SAFETY: configure_env_for_run fully initializes the out-param on Ok. diff --git a/src/runtime/cli/multi_run.rs b/src/runtime/cli/multi_run.rs index 83faeab5d470..e307823ed9cb 100644 --- a/src/runtime/cli/multi_run.rs +++ b/src/runtime/cli/multi_run.rs @@ -884,6 +884,7 @@ pub(crate) fn run(ctx: &mut Command::ContextData) -> Result( ConfigureEnvOptions { log_errors: ctx.manager.options.log_level != LogLevel::Silent, store_root_fd: false, + defer_package_errors: false, }, ) { if matches!(err, crate::Error::Alloc(_)) { diff --git a/src/runtime/cli/run_command.rs b/src/runtime/cli/run_command.rs index 47ea96db02d0..547d79e02f09 100644 --- a/src/runtime/cli/run_command.rs +++ b/src/runtime/cli/run_command.rs @@ -93,6 +93,8 @@ pub(crate) struct ConfigureEnvOptions { /// for callers that go on to read files through it, like `bunx` resolving /// a package's `bin`. pub(crate) store_root_fd: bool, + /// Leave inline-source package errors for runtime resolution to report. + pub(crate) defer_package_errors: bool, } pub(crate) struct RunCommand; @@ -610,6 +612,7 @@ Full documentation is available at https://bun.com/docs/cli/run this_transpiler.resolver.care_about_bin_folder = true; this_transpiler.resolver.care_about_scripts = true; this_transpiler.resolver.store_fd = opts.store_root_fd; + this_transpiler.resolver.validate_package_config = opts.defer_package_errors; // Bundler-linker + JSX-runtime config: only callers that actually // transpile through this `Transpiler` need it. `configure_linker`'s @@ -2374,6 +2377,7 @@ impl RunCommand { ConfigureEnvOptions { log_errors, store_root_fd: false, + defer_package_errors: target_name == b"-", }, )?; // SAFETY: `configure_env_for_run_without_linker` returned `Ok`, so the diff --git a/test/cli/run/run-eval.test.ts b/test/cli/run/run-eval.test.ts index 5fcdfe2bb6b5..f5929c7e769d 100644 --- a/test/cli/run/run-eval.test.ts +++ b/test/cli/run/run-eval.test.ts @@ -5,6 +5,77 @@ import { bunEnv, bunExe, isWindows, tempDir, tmpdirSync } from "harness"; import { tmpdir } from "os"; import { join, sep } from "path"; +for (const asNode of [false, true]) { + for (const entry of ["stdin", "eval", asNode ? "implicit stdin" : "run stdin"]) { + test.concurrent.each([ + { name: "plain script", source: 'console.log("executed")', stdout: "executed\n" }, + { + name: "builtin require", + source: 'console.log(require("node:path").basename("/a/b"))', + stdout: "b\n", + }, + { + name: "builtin import", + source: 'import { basename } from "node:path"; console.log(basename("/a/b"))', + stdout: "b\n", + }, + { + name: "script-owned JSON rejection", + source: + 'try { JSON.parse(require("node:fs").readFileSync("../package.json", "utf8")) } catch { console.log("invalid JSON from script"); process.exitCode = 17 }', + stdout: "invalid JSON from script\n", + exitCode: 17, + }, + { + name: "require validates its scope", + source: 'console.log("executed"); try { require("./value.cjs") } catch (e) { console.log(e.code) }', + stdout: "executed\nERR_INVALID_PACKAGE_CONFIG\n", + }, + { + name: "dynamic import validates a js scope", + source: 'console.log("executed"); import("./value.js").catch(e => console.log(e.code))', + stdout: "executed\nERR_INVALID_PACKAGE_CONFIG\n", + }, + { + name: "dynamic import does not need a cjs scope", + source: 'console.log("executed"); import("./value.cjs").then(m => console.log(m.default))', + stdout: "executed\n7\n", + }, + { + name: "nearer valid scope shields the ancestor", + source: 'console.log("executed"); import("./scoped/value.js").then(m => console.log(m.default))', + stdout: "executed\n9\n", + }, + ])(`inline entry ${entry}, node alias = ${asNode}: $name`, async ({ source, stdout, exitCode = 0 }) => { + using dir = tempDir("inline-package-scope-", { + "package.json": "{", + "nested/value.cjs": "module.exports = 7", + "nested/value.js": "module.exports = 8", + "nested/scoped/package.json": "{}", + "nested/scoped/value.js": "module.exports = 9", + }); + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + ...(entry === "eval" ? ["-e", source] : entry === "stdin" ? ["-"] : asNode ? [] : ["run", "-"]), + ], + ...(asNode ? { argv0: "node" } : {}), + cwd: join(String(dir), "nested"), + env: bunEnv, + stdin: entry === "eval" ? "ignore" : Buffer.from(source), + stdout: "pipe", + stderr: "pipe", + }); + const [actualStdout, stderr, actualExitCode] = await Promise.all([ + proc.stdout.text(), + proc.stderr.text(), + proc.exited, + ]); + expect({ stdout: actualStdout, stderr, exitCode: actualExitCode }).toEqual({ stdout, stderr: "", exitCode }); + }); + } +} + test.concurrent.each([ { args: ["run", "-"], source: "", expected: "" }, { args: ["-"], source: "", expected: "" },