Conversation
An invalid package.json (malformed JSON, non-object root, or a "type" field that is not a string) was silently ignored by the runtime module loader: the resolver cached package_json = None for that directory and enclosing_package_json fell through to the parent scope, so every .js file under it loaded with whatever module format content-sniffing or the nearest valid ancestor gave. Node.js instead refuses the whole package scope with ERR_INVALID_PACKAGE_CONFIG on every load path (static import, dynamic import(), require(), entrypoint). A corrupted or half-written package.json must not silently change the module format of every file beneath it. PackageJSON::parse now returns a poisoned entry (invalid = true) instead of None on parse failure / non-object root, and marks non-string "type" as invalid too. dir_info_uncached treats a poisoned entry as the enclosing scope boundary so child directories do not inherit past it. transpile_file throws ERR_INVALID_PACKAGE_CONFIG when deriving the module format of a .js/.ts file from a poisoned package.json; .mjs/.cjs still load (format comes from the extension), matching Node.
WalkthroughChangesThe resolver now preserves invalid Package configuration resolution
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Updated 1:16 AM PT - Jul 10th, 2026
❌ @robobun, your commit e3af988 has 2 failures in
🧪 To try this PR locally: bunx bun-pr 33890That installs a local version of the PR into your bun-33890 --bun |
…lter PackageJSON::parse now returns Some(invalid) instead of None for malformed JSON, so the let-else in filter_run / multi_run no longer hit their skip path and the "Failed to read ... skipping this workspace package" warning was lost. Filter the poisoned entry back to None at these call sites so the existing behavior is preserved.
enclosing_package_json skips nameless package.json files (the #229 name-gate), so routing the invalid flag through it let a poisoned grandparent leak past a valid nameless intermediate (e.g. {} or {"type":"commonjs"}) and wrongly reject files Node accepts. DirInfo now carries nearest_package_json: the first package.json walking up, regardless of name or validity, which is Node's actual package-scope boundary for "type" resolution. The runtime module-type lookups in jsc_hooks use this field instead. enclosing_package_json keeps its original semantics (named, valid) so bun run / bin resolution are unchanged. Also treat a 0-byte package.json as invalid: the JSON parser short-circuits empty input to a synthetic {} before parsing, but Node rejects it as ERR_INVALID_PACKAGE_CONFIG.
A non-string "type" sets invalid = true but parsing continues, so a
package like {"name":"foo","type":42,"scripts":{...}} is fully usable
for script execution (npm/pnpm/yarn all run it). The blanket
.filter(|p| !p.invalid) skipped it with a misleading "Failed to read".
Only the poisoned placeholder entries (malformed JSON / empty / non-object
root) have empty source_contents; filter on that instead so fully-parsed
entries with a bad "type" pass through unchanged.
The gate blocked a fully-parsed package.json with a non-string "type"
from becoming enclosing_package_json, so plain `bun run <script>` on
{"name":"foo","type":42,"scripts":{...}} stopped finding the script
(npm/pnpm/yarn all run it).
enclosing_package_json is now back to its pre-PR condition; the
name-gate already excludes truly-poisoned entries (empty name) when
care_about_bin_folder is false, and when it is true (bun run startup)
a poisoned entry just has scripts=None so the lookup falls through.
module-type scoping uses the separate nearest_package_json field.
There was a problem hiding this comment.
All prior review feedback has been addressed and I found nothing new, but this touches core resolver/module-loading semantics (new nearest_package_json scope boundary, poisoned PackageJSON entries, an intentional bun run fallthrough change) and uses source_contents.is_empty() as the discriminator between "unparseable" and "invalid-type-only" — worth a human sign-off on the design.
Extended reasoning...
Overview
This PR changes how Bun's resolver and runtime module loader handle malformed package.json files, adding Node-compatible ERR_INVALID_PACKAGE_CONFIG errors. It touches:
src/resolver/package_json.rs— addsinvalid: bool, returns poisoned entries instead ofNonefor malformed JSON / empty file / non-object root / non-string"type"src/resolver/dir_info.rs+resolver.rs— addsnearest_package_json(Node's package-scope boundary, distinct from the name-gatedenclosing_package_json)src/runtime/jsc_hooks.rs— 4 module-type lookup sites switched tonearest_package_json;transpile_filenow throwsERR_INVALID_PACKAGE_CONFIGfor.js/.tsunder a poisoned scopesrc/runtime/cli/{filter_run,multi_run}.rs— filter poisoned entries so the existing "skipping this workspace package" warning still fires- 3 test files with a 28-case matrix plus regression guards
Security risks
None identified. No auth/crypto/permissions surface. The change tightens error handling (more failures surfaced, not fewer). The only new user-facing string is the error message, formatted via the standard ErrCode machinery.
Level of scrutiny
High. The resolver's DirInfo cache and module-type determination are hot-path, process-lifetime-cached, and every .js/.ts load goes through transpile_file. This PR went through four rounds of review corrections (poison leaking through nameless intermediates, 0-byte files missed, --filter warning lost, non-string-"type" breaking bun run scripts), each of which was a real regression. The final shape looks correct, but the iteration count itself signals this is subtle territory.
Other factors
- Design smell:
filter_run.rs/multi_run.rsdistinguish "truly poisoned" from "invalid-type-only" via!p.source_contents.is_empty()— a content heuristic rather than a dedicated flag. It works because theinvalid()closure returnssource: init_path_string(path, b""), but a maintainer might reasonably prefer splittinginvalidinto two flags as suggested in earlier review. - Intentional behavior change:
bun run <script>in a directory with a truly-malformedpackage.jsonnow stops with "Script not found" instead of falling through to a parent's scripts. The PR description justifies this against npm/pnpm/yarn, but it's a user-visible change a maintainer should ack. - Test coverage is thorough (4 invalid shapes × 4 load paths + negative cases + the two
bun runregression guards), and all my prior inline comments are resolved with dedicated test cases. - The
enclosing_package_jsonassignment condition is now byte-identical tomain(per df3fd20), so bundler/bin resolution should be unaffected — but I have not exhaustively audited every other consumer ofPackageJSON::parsereturningSome(invalid)where it previously returnedNone.
The empty-file-is-invalid check broke test/js/bun/resolve/jsonc.test.ts
which asserts the existing Bun convention that an empty package.json (and
tsconfig.json) parses as {} via the JSONC loader. Running any .ts file in
a directory with an empty package.json became impossible.
Node rejects a 0-byte package.json as ERR_INVALID_PACKAGE_CONFIG, but
Bun already deliberately diverges here (empty JSONC -> {}), so keep that
and drop the 0-byte case from the ERR_INVALID_PACKAGE_CONFIG matrix.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/runtime/jsc_hooks.rs (1)
1347-1359: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winKeep
read_dir_info_package_jsononenclosing_package_json
LoaderResult.package_jsonfeedsParseTask::init, which turns it intopackage_namefor barrel optimization. Switching this bundler-facing hook tonearest_package_jsoncan stop at a nameless boundary package.json and clear the inherited package name, changing bundler output for nested packages.src/runtime/jsc_hooks.rs:1347🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/runtime/jsc_hooks.rs` around lines 1347 - 1359, The read_dir_info_package_json hook must preserve the enclosing package metadata used by ParseTask::init and barrel optimization. In the read_dir_info_package_json match within the resolver hook, return dir_info.package_json() only and remove the fallback to dir_info.nearest_package_json, while retaining the existing pointer conversion and None handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/resolver/package_json.rs`:
- Around line 519-529: Introduce a public `PackageJSON::is_poisoned` predicate
that returns true when `invalid` is set and `source_contents` is empty,
documenting the distinction from parseable objects with invalid `"type"` values.
Replace the duplicated `!p.invalid || !p.source_contents.is_empty()` checks in
the `filter_run` and `multi_run` consumers with `.filter(|p| !p.is_poisoned())`.
In `@src/runtime/jsc_hooks.rs`:
- Around line 4158-4180: Update the invalid package configuration error in the
module-type resolution block to quote the package.json path using
bun_core::fmt::quote, matching the convention used by the CLI equivalent.
Replace the raw BStr path interpolation in the ERR_INVALID_PACKAGE_CONFIG
message while preserving the existing error flow and message wording.
---
Outside diff comments:
In `@src/runtime/jsc_hooks.rs`:
- Around line 1347-1359: The read_dir_info_package_json hook must preserve the
enclosing package metadata used by ParseTask::init and barrel optimization. In
the read_dir_info_package_json match within the resolver hook, return
dir_info.package_json() only and remove the fallback to
dir_info.nearest_package_json, while retaining the existing pointer conversion
and None handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: a10ccacf-30cd-433f-894a-0ae4f329dd8b
📒 Files selected for processing (9)
src/resolver/dir_info.rssrc/resolver/package_json.rssrc/resolver/resolver.rssrc/runtime/cli/filter_run.rssrc/runtime/cli/multi_run.rssrc/runtime/jsc_hooks.rstest/cli/run/filter-workspace.test.tstest/cli/run/run_command.test.tstest/js/bun/resolve/resolve-error.test.ts
|
Re the outside-diff comment on Traced the data flow and I don't think that's where it goes.
So keeping |
|
Diff is ready for review. The two most recent CI runs each failed on a single test unrelated to this change:
Neither touches the resolver or package.json handling. All review threads are resolved; the new tests (resolve-error.test.ts, filter-workspace.test.ts, run_command.test.ts) and the previously-failing jsonc.test.ts pass on every lane where they ran. Holding off on further retriggers. |
…ed or not (#41232) ### Problem - Regression on main from #41150. No release has it. `bun build` reads `"type"` from the wrong package.json, so it misses a `{ "type": "module" }` that has no `"name"`. Two common places: a project root, and a dual package's `dist/esm/package.json`. - A `.js` or `.ts` file there gets `exports.default` for the default import of a CommonJS module with `__esModule`. Node, esbuild and bun 1.4.0 give the whole `module.exports`. The bundle has `__toESM(require_tsdep())`, without `, 1`. - Cause: `finalize_result` (`src/resolver/resolver.rs:1669`) read `"type"` from the package root, or else from `enclosing_package_json`. `dir_info_uncached` (`src/resolver/resolver.rs:6357`) sets that field only for a named package.json (#229). ### Fix - `DirInfo` gets `package_json_for_module_type`: the nearest package.json in the directory or above it, named or not. `finalize_result` reads `"type"` from it for the primary path. The extension still wins. - Correct because esbuild uses this rule, and Node ignores `"name"` too. - Only the bundler reads `Result.module_type`. `enclosing_package_json` does not change. The four runtime lookups in `src/runtime/jsc_hooks.rs` are out of scope. - Verified: `test/bundler/bundler_cjs.test.ts`, 10 new cases, 9 fail on main. Self-reviewed: 3 concerns raised, 3 addressed. Other suites in Notes. ### Background - `__toESM(mod, isNodeMode)` builds the ESM view of a CommonJS module. With `, 1` (Node mode), `default` is the whole `module.exports`. Without it, `default` is `mod.default` when `__esModule` is set. - `DirInfo` is the resolver's cached record for one directory. Its "enclosing" fields come from the parent. - Open PRs in this area: #33883, #33807, #33890, #40940. This PR supersedes none. Notes cover #40940. <details><summary>Notes</summary> Found by comparing `bun build` on main with bun 1.4.0, Node 26 and esbuild 0.25. No issue is open for it. Repro for the dual-package face: ```sh D=$(mktemp -d); cd $D; mkdir -p node_modules/pkg/dist/esm node_modules/tsdep echo '{"name":"tsdep","version":"1.0.0","main":"index.js"}' > node_modules/tsdep/package.json echo 'Object.defineProperty(exports,"__esModule",{value:true}); exports.default=function styled(){}; exports.css="css";' > node_modules/tsdep/index.js echo '{"name":"pkg","version":"1.0.0","main":"./dist/esm/index.js"}' > node_modules/pkg/package.json echo '{"type":"module"}' > node_modules/pkg/dist/esm/package.json echo 'import styled from "tsdep"; export const seen = typeof styled + "/" + typeof styled.default;' > node_modules/pkg/dist/esm/index.js echo 'import { seen } from "pkg"; console.log(seen);' > app.mjs node app.mjs # object/function bun build ./app.mjs --target=node --outfile=out.mjs && node out.mjs # main: function/undefined, this PR: object/function ``` For the project-root face, put `{ "type": "module" }` (no `"name"`) in the project's package.json and bundle a `.js` file that imports `tsdep`. Faces of the bug on main. Each has a test: - A project package.json with `"type"` and no `"name"` (case 58). - The nested marker reached through `"main"`, `"module"` or a relative path (cases 53, 55, 56). Through an exports map it worked, because `handle_esm_resolution` reads the file's own directory. - A nested package.json with a `"name"`, reached through `"main"` (case 54). `result.package_json` was the package root, so the nested file was not read at all. - A file in a subdirectory of the marker (case 57). Why a new field instead of widening `enclosing_package_json`: that field also names the package for `sideEffects`, the auto-install version gate and `bun run` script discovery. #33883 widens it for every consumer and had to rework the `sideEffects` loop in `finalize_result` to keep the DCE tests passing. Four cases pin the lookup rule. Each result matches esbuild 0.25.1: - Case 59: a nameless `{ "type": "commonjs" }` below a `"type": "module"` package wins, because it is the nearest. - Case 60: a nearest package.json without `"type"` is the scope. The lookup does not continue to a typed package root, so the importer is not ESM by type. Main read the root's `"type"` here. Node prints `object/function` for this shape, but only because it detects ESM syntax in a file with no `"type"`. #41150 chose the esbuild rule for such files. - Case 61: the lookup does not stop at a `node_modules` directory. A package without a package.json of its own takes the `"type"` above it. Node prints the same result. - Case 62: only the primary path decides. With the default target, `"module"` is the primary path and `"main"` is the fallback for `require()`. The fallback's package.json does not count. esbuild has the same check. The case fails when the check is removed. Overlap with #40940: it adds a field with the same name, but its lookup stops at `node_modules`, and the runtime reads it too. If #40940 lands after this PR, it must choose one rule for the field. Case 61 pins the crossing for the bundler. Node's stop can still apply at the runtime read sites. #40940 also calls the lookup for the fallback path, which case 62 rejects. Out of scope: the runtime's four lookups (`src/runtime/jsc_hooks.rs` lines 1474, 2972, 3220 and 4071) keep `package_json().or(enclosing_package_json)`. So `bun run` still skips a nameless package.json above the file's own directory. That gap predates #41150. For this import, `bun run` 1.4.1 gives `exports.default` for every importer, even `.mjs`. Self-review, the three concerns and what changed: - Document the `node_modules` rule on the field. Done in `src/resolver/dir_info.rs`. - Add a default-target case with both `"main"` and `"module"`. That is case 62. - Say in this body that no release has the bug, lead with the project-root face, and name the runtime lookups that stay. Suites run with the fix on a debug ASAN build, after a rebase on main: `bundler_cjs` (62), `esbuild/packagejson`, `esbuild/dce`, `esbuild/default`, `bundler_cjs2esm`, `bundler_npm`, `bundler_edgecase`, `bundler_regressions`, `bundler_splitting`, `bundler_barrel`, `cli/run/run-cjs`, `test/js/bun/resolve`. All pass except the second case of `test/js/bun/resolve/load-same-js-file-a-lot.test.ts`. It times out at 5 s on this build with and without this change (back-to-back runs on the same machine). </details> <!-- robobun:evidence:begin --> --- **[human-review]** gate passed · iteration 0 · 4 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 9 FAILED $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/bundler_cjs.test.ts bun test v1.4.1 (a6c4cc2) test/bundler/bundler_cjs.test.ts: (pass) bundler > cjs/__toESM_import_syntax_with_esModule [945.38ms] (pass) bundler > cjs/__toESM_import_syntax_without_esModule [445.45ms] (pass) bundler > cjs/__toESM_import_syntax_function [371.94ms] (pass) bundler > cjs/__toESM_import_syntax_primitive [387.72ms] (pass) bundler > cjs/__toESM_import_syntax_named_and_default [361.78ms] (pass) bundler > cjs/__toESM_import_syntax_namespace [367.76ms] (pass) bundler > cjs/__toESM_target_node [455.64ms] (pass) bundler > cjs/__toESM_target_browser [413.38ms] (pass) bundler > cjs/__toESM_target_bun [455.22ms] (pass) bundler > cjs/__toESM_format_esm [462.40ms] (pass) bundler > cjs/__toESM_format_cjs_with_import [377.93ms] (pass) bundler > cjs/__toESM_mjs_reexport [431.60ms] (pass) bundler > cjs/__toESM_mjs_reexport_with_esModule [432.63ms] (pass) bundler > cjs/__toESM_deep_reexport_chain [368.70ms] (pass) bundler > cjs/__toESM_reexport_with_rename [443.84ms] (pass) bundler > cjs/__toESM_default_prop ... (truncated) release without fix: 20 FAILED bun test v1.4.1-canary.1 (a6c4cc2) test/bundler/bundler_cjs.test.ts: runtime failed file: /tmp/bun-build-tests/bun-4t1lr0/cjs/__toESM_import_syntax_with_esModule/out.js stdout output: {"__esModule":true,"default":{"value":"default export"},"named":"named export"} --- expected stdout: {"value":"default export"} --- 1913 | console.log(`---`); 1914 | console.log(`expected ${name}:`); 1915 | console.log(expected); 1916 | console.log(`---`); 1917 | } 1918 | expect(result).toBe(expected); ^ error: expect(received).toBe(expected) Expected: "{"value":"default export"}" Received: "{"__esModule":true,"default":{"value":"default export"},"named":"named export"}" at <anonymous> (/workspace/bun/test/bundler/expectBundled.ts:1918:28) (fail) bundler > cjs/__toESM_import_syntax_with_esModule [29.17ms] (pass) bundler > cjs/__toESM_import_syntax_without_esModule [11.71ms] (pass) bundler > cjs/__toESM_import_syntax_function [9.90ms] (pass) bundler > cjs/__toESM_import_syntax_primitive [9.43ms] (pass) bundler > cjs/__toESM_import_syntax_named_and_default [9.97ms] ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console ASAN with fix: all passed $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/bundler_cjs.test.ts bun test v1.4.1 (a6c4cc2) test/bundler/bundler_cjs.test.ts: (pass) bundler > cjs/__toESM_import_syntax_with_esModule [1024.06ms] (pass) bundler > cjs/__toESM_import_syntax_without_esModule [473.45ms] (pass) bundler > cjs/__toESM_import_syntax_function [489.71ms] (pass) bundler > cjs/__toESM_import_syntax_primitive [494.25ms] (pass) bundler > cjs/__toESM_import_syntax_named_and_default [388.89ms] (pass) bundler > cjs/__toESM_import_syntax_namespace [381.97ms] (pass) bundler > cjs/__toESM_target_node [389.92ms] (pass) bundler > cjs/__toESM_target_browser [453.17ms] (pass) bundler > cjs/__toESM_target_bun [481.14ms] (pass) bundler > cjs/__toESM_format_esm [415.53ms] (pass) bundler > cjs/__toESM_format_cjs_with_import [376.77ms] (pass) bundler > cjs/__toESM_mjs_reexport [451.67ms] (pass) bundler > cjs/__toESM_mjs_reexport_with_esModule [380.06ms] (pass) bundler > cjs/__toESM_deep_reexport_chain [453.02ms] (pass) bundler > cjs/__toESM_reexport_with_rename [430.08ms] (pass) bundler > cjs/__toESM_default_pro ... (truncated) release with fix: all passed $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) target linux-x64-gnu build type Release build dir ./build/release revision 822e3b2 features baseline 23 deps, 131 codegen, 1172 objects in 647ms ninja: Entering directory `/workspace/bun/build/release' [1/1244] install /workspace/bun bun install v1.4.1-canary.1 (a6c4cc2) Checked 25 installs across 62 packages (no changes) [10.00ms] [2/1244] gen bindgenv2 [3/1244] gen ErrorCode+*.h [4/1244] install /workspace/bun/packages/bun-error bun install v1.4.1-canary.1 (a6c4cc2) Checked 1 install across 2 packages (no changes) [3.00ms] [5/1244] fetch libjpeg-turbo [libjpeg-turbo] up to date [6/1217] gen ProcessBindingConstants.lut.h Generating /workspace/bun/build/release/codegen/ProcessBindingConstants.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingConstants.cpp [7/1217] gen bake.{client,server,error}.js -> bake.client.js, bake.server.js, bake.error.js [8/1217] fetch tinycc [tinycc] up to date [9/1216] install /workspace/bun/src/node-fallbacks bun install v1.4.1-canary.1 (a6c4cc2) Checked 111 installs across 104 packages (no changes) [15.00 ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` src/resolver/dir_info.rs | 9 ++ src/resolver/resolver.rs | 20 +++-- src/resolver/result.rs | 15 +--- test/bundler/bundler_cjs.test.ts | 182 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 207 insertions(+), 19 deletions(-) ``` </details> **gate history** · 1 passed · 0 rejected · iteration 0 <details><summary>evidence per changed file</summary> ``` file reads edits tests src/resolver/dir_info.rs 3 4 33 src/resolver/resolver.rs 8 5 34 src/resolver/result.rs 1 1 33 test/bundler/bundler_cjs.test.ts 3 11 33 ``` </details> <!-- robobun:evidence:end -->
|
Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-10, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main. |
What
An invalid
package.json(malformed JSON, non-object root, or a"type"field that is not a string) was silently ignored by the runtime module loader. Every.jsfile under it loaded with whatever module format content-sniffing or the nearest valid ancestor produced, with no error and no warning.Node.js refuses the whole package scope with
ERR_INVALID_PACKAGE_CONFIGon every load path (static import,import(),require(), entrypoint). A corrupted, half-written, or merge-conflictedpackage.jsonmust not silently change the module semantics of every file beneath it.Repro
Before:
After:
Same divergence held for
{"type":42}(schema-invalid),42(non-object root), across all four load paths.Cause
Three layers swallowed the failure:
JsonCache::parse_rowsconverts a JSON parseErrintoOk(None), pushing diagnostics into the resolver log only.PackageJSON::parsereturnedNoneonOk(None)/ non-object root, and only warned on non-string"type". (A 0-byte file is intentionally left as{}to preserve Bun's existing JSONC-empty convention; see jsonc.test.ts.)Resolver::dir_info_uncacheddid.ok().flatten()and cachedpackage_json = None, so the parent scope was inherited for module-type resolution.The process-lifetime
DirInfocache then treated the invalid file exactly like "nopackage.jsonhere" for the rest of the process.Fix
PackageJSONgains aninvalid: boolfield.PackageJSON::parsereturns a poisoned entry (invalid = true, path set, everything else default) instead ofNoneon parse failure / non-object root, and marks non-string"type"as invalid.DirInfogainsnearest_package_json: the firstpackage.jsonwalking up, regardless of name or validity. This is Node's actual package-scope boundary for"type"resolution, distinct fromenclosing_package_json(which skips nameless entries per Support running package.json scripts from package.json without a name #229 forbun run/bin purposes). A valid nameless{}or{"type":...}between a poisoned ancestor and a file correctly terminates the scope.jsc_hooks.rsusenearest_package_json.transpile_filethrowsERR_INVALID_PACKAGE_CONFIG(with the offending path) when it needs thepackage.jsonto determine the module format of a.js/.tsfile..mjs/.cjsstill load fine since their format comes from the extension, matching Node.enclosing_package_jsonkeeps its original assignment condition. Apackage.jsonwith a non-string"type"but validscriptsstill becomes enclosing, sobun run <script>finds it (matches npm/pnpm/yarn).filter_run/multi_runfilter out only the unparseable poisoned entries (emptysource_contents) so the existing "skipping this workspace package" warning still fires for those while scripts from a fully-parsed entry with a bad"type"continue to run.Unknown string
"type"values ("type":"nonsense") continue to be treated as unset, also matching Node.One side effect for
bun run <script>: when the cwd's ownpackage.jsonis truly unparseable, bun now stops there ("Script not found") instead of silently falling through to a parent directory's scripts. npm/pnpm/yarn all refuse to fall through in that case (they error on the parse), so this bringsbun runin line with them.Tests
describe.concurrent("ERR_INVALID_PACKAGE_CONFIG")intest/js/bun/resolve/resolve-error.test.tscovers the 3 invalid shapes x 4 load paths, plus negative cases (.mjs/.cjsstill load, unknown string"type"does not error, parent scope unaffected, error is catchable with.code, valid nameless intermediate stops poison propagation). 14 of the 22 cases fail on main and pass with this change. Additional regression guards infilter-workspace.test.tsandrun_command.test.tscover the non-string-"type"+ scripts case.no test proof · iteration 3 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/run/filter-workspace.test.ts test/cli/run/run_command.test.ts test/js/bun/resolve/resolve-error.test.ts