Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions docs/runtime/nodejs-compat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion src/resolver/package_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
14 changes: 14 additions & 0 deletions src/resolver/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/runtime/cli/bunx_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/runtime/cli/filter_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/runtime/cli/multi_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,7 @@ pub(crate) fn run(ctx: &mut Command::ContextData) -> Result<core::convert::Infal
ConfigureEnvOptions {
log_errors: true,
store_root_fd: false,
defer_package_errors: false,
},
)?;
// SAFETY: `configure_env_for_run` fully writes the slot on the success path.
Expand Down
1 change: 1 addition & 0 deletions src/runtime/cli/pack_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2121,6 +2121,7 @@ pub(crate) fn pack<const FOR_PUBLISH: bool>(
ConfigureEnvOptions {
log_errors: ctx.manager.options.log_level != LogLevel::Silent,
store_root_fd: false,
defer_package_errors: false,
},
) {
if matches!(err, crate::Error::Alloc(_)) {
Expand Down
4 changes: 4 additions & 0 deletions src/runtime/cli/run_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -610,6 +612,7 @@ Full documentation is available at <magenta>https://bun.com/docs/cli/run<r>
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
Expand Down Expand Up @@ -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
Expand Down
71 changes: 71 additions & 0 deletions test/cli/run/run-eval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: "" },
Expand Down
Loading