Repository navigation
Conversation
…mmonJS unwrap list
In ESM output, require() of a package on the CommonJS unwrap list becomes
`import * as ns`. The decision looked at the package name only, so
require("react/package.json") was unwrapped too. With the package external
the import has no `with { type: "json" }` and Node rejects it. With the
package bundled the result is a module namespace object, not the parsed
value.
transpose_require now leaves a require() whose specifier ends in .json a
require(), as for every package that is not on the list.
Collaborator
Author
|
Updated 8:05 PM PT - Oct 21st, 2026
❌ @robobun, your commit 7b4a72f has some failures in 🧪 To try this PR locally: bunx bun-pr 46386That installs a local version of the PR into your bun-46386 --bun |
Collaborator
Author
|
Status: draft. It waits for #46072 and #42026, and for a maintainer's yes or no on the How I reproduced it (1.4.3 and main print the import): mkdir -p node_modules/react
echo '{"name":"react","version":"19.3.1"}' > node_modules/react/package.json
echo 'const pkg = require("react/package.json"); console.log(pkg.name, pkg.version);' > entry.js
bun build entry.js --target=node --external react --outfile out.mjs && node out.mjs
# main: TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module ".../react/package.json" needs an import attribute of "type: json"
# this PR: react 19.3.1 |
The first spawn of node after a build can take more than the 5 s test timeout. The cases keep their proof: each one fails without the fix.
…-require-stays-require
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
bun build --target=node --external reactprintsrequire("react/package.json")asimport * as pkg from "react/package.json". Node throwsTypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module ".../react/package.json" needs an import attribute of "type: json".pkg.versionis right on main. The whole value is not:Object.keys(pkg)gainsdefault.transpose_require(src/js_parser/p.rs:1649) looks at the package name only.Fix
require()whose specifier ends in.jsonstays arequire(), as for every package off the list.require()that main does not unwrap runs no new code.test/bundler/bundler_cjs2esm.test.ts(17 new cases, 15 fail on main). 280 of 280 React builds stay byte-identical.Background
require()ofreactand 6 more packages becomesimport * as ns.@frontegg/reactreaches the Node error. Atrybody or--format=cjsavoids it in your own code.--external.Downsides
@frontegg/reactbuilt for the browser with--external reactruns on main after a second bundler. Here it throws at load (Notes).pkg.versionread grows from 31 B to 906 B minified. Each unwrapped JavaScriptrequire()costs 48 more instructions.Notes
Reach. No user report exists. The package-name test dates from e05cc8d (2023-04-16, first in bun-v0.6.0). On the tracker,
ERR_IMPORT_ATTRIBUTE_MISSINGmatches no issue and two PRs (#40836, #35914). The older codeERR_IMPORT_ASSERTION_TYPE_MISSINGmatches #4257 (closed), which is about a user-written import assertion. Another session found this while it worked on #46072, which lists the case under "What stays as on main".Published uses that I checked, both a read of
.version:@frontegg/react7.19.0,index.js:15, CommonJS only. An application bundle for Node reaches the error through it.@parcel/runtime-react-refresh2.13.3,lib/ReactRefreshRuntime.js:21. It is a Parcel runtime.Workaround on released versions. Put the
require()in atrybody (try { var pkg = require("react/package.json"); } finally {}), or build with--format=cjs. Both printreact 18.3.1under Node on 1.4.3 and on main. This does not reach a third-party package in the bundle, and the build exits 0, so nothing says that a workaround is necessary.Before and after (
bun build entry.js --target=node --external react, thennode out.mjs)A read of
pkg.version, by target and delivery (main, then this PR)__requireshimA use of the whole value is wrong on main in every bundled cell and right with this PR:
Object.keys(require("react/package.json")).lengthis 15 on main and 14 in Node, with--format=cjsand with this PR. A JSON array or adefaultkey shows the same defect (Array.isArrayis false,.defaultis the whole object). No listed package ships such a file, so those need an alias or a first-party package with a listed name.The question:
--target=browserwith the package external. Measured with@frontegg/react7.19.0 (its other dependencies stubbed) and--external react --external react-dom:--target=node: Node throws on main and loads with this PR.import * as _package from "react/package.json"and no__require(call. A browser rejects that import. An esbuild 0.21.5 bundle of the output loads. This PR prints one__require("react/package.json")call, the build exits 0, and the esbuild bundle throwsDynamic require of "react/package.json" is not supportedat load.Runtime::Features, set from the target inParseTask.rs, keeps browser output as on main. A use of the whole value then stays wrong there.The two shapes that wait for other PRs. Both builds exit 0.
<script src>whose only import is thisrequire()is CommonJS now. An HTML entry point never calls a wrapped script, on main too (bundler: call the wrapper of a CommonJS or lazy-ESM <script src> at its tag in an HTML entry point #42026). A script that also has animport, anexportor arequire("react")stays an ES module and runs. A test pins that.const a = require("react/package.json"); const { name } = require("react/package");printsvar { name } = __toESM((exports));and throwsReferenceError. Main prints the same line when the firstrequire()is in atrybody. bundler: read the import namespace for a destructured require() of an unwrapped package #46072 deletes that printer path.What changes for a
.jsonrequire of a listed package. A package off the list and esbuild already behave this way.require-callinonResolveand the metafile, and therequirecondition of anexportsmap.__commonJSwrapper,export default).require()in a function that never runs loads nothing.Cannot assign to import "extra"), and the nextrequire()sees it.Measurements. Output bytes and the test runs without the fix are from debug builds of 9634520 and of this diff on it. The operand count and the instruction, size and allocator numbers are from builds of the same diff on 0c864a0, six commits earlier. The 280 builds are byte-identical between the two bases. The branch has merged main since, and all 130 tests of the file pass on that build.
bun run, cjs, iife and off-list ESM requires, and 6 / 8 times in the React 18.3.1 / 19.3.1 client builds (gdb hit counts, debug build).transpose_require: +0 instructions per call when the gate fails (cjs 314, off-list ESM 378 and 489), +48 when it passes for a JavaScript target (1095 to 1143), 1426 to 678 for a.jsonspecifier (gdb stepi, third call, release). Most of the +48 is inlining:new_symbolis a call in the PR build and inline in the base build..text.transpose_require1808 + 1883 B to 1690 + 1690 B (size -A,nm -S). The stripped binary has the same size.--minifyon and off, targets browser, bun and node, plus--external,--packages=externaland--splitting.npm/ReactSSRstays at its pinned 222012 B..versionread ofreact/package.json, minified: 31 B to 906 B (off-list copy 908 B,--format=cjs866 B). Unused TypeScriptimport pkg = require("react/package.json"): 24 B to 870 B (off-list 872 B). Reported external entry: 91 B to 385 B. A use of the whole value: 1454 B to 918 B.require("react/package.json"):onResolveand metafile kindimport-statementtorequire-call,exportsconditionimporttorequire.require("react"): 0 differing lines..jsonrequire of a listed package: 7342 to 7429. The same build for an off-list package: 7428. Forrequire("react"): 7342 to 7342 (gdb hit counts on the mimalloc entry points, release).itBundledcases: 15 fail on debug and release builds without the fix and on 1.4.3, 17 pass on the PR build. The cases run their output in Bun, so the cases that read one key assert the printed call. Theformat: "cjs"control and thereact-dom/server.nodeguard pass on both. 0 existing lines of the test file changed.Left as on main. The defect is larger than
.json: an unwrappedrequire()gives a namespace wheremodule.exportsis owed. This PR fixes one spelling of it. I ran each line below on the PR build.Bundled:
.jsonsuffix, such as the extensionlessrequire("react/list")..jsonc,.json5,.toml,.yaml,.yml,.txt). They are left out by choice. The specifier text can decide most of them, but no use is known.react-dom/server.nodeis the JavaScript fileserver.node.js, and a test pins that it stays unwrapped.require("react-refresh/babel")is an object where Node gives a function (bundler: skip __toESM for an unwrapped require() of a package that keeps its CommonJS wrapper #46215).import list from "react/list.json"; list.lengthisundefined(bundler: apply the CommonJS unwrap list only to CommonJS files #42500).External:
require("scheduler/unstable_mock"): Node throwsERR_MODULE_NOT_FOUND.module.exports = require("react/package.json")that another file reads withimport: stillimport p from "react/package.json"and the same Node error (bundler: redirect amodule.exports = require()file per import, not per file #45830).with { type: "json" }is dropped for targets other than Bun (Carry import attributes through the parser, printer, bundler and runtime; add the bytes loader #40836).Runtime: after an
importof the same data file,require()returns the module namespace underbun run, for every package (#35973).Alternatives not taken. Each needs a maintainer's agreement first (
REVIEW.md:66), and each keeps this operand for an external.jsonspecifier. This PR does not depend on them.require()a require until the linker knows the file. 55 sites decide on the record kind, and 34 of them change branch.require()of a package that the options mark external. It changesrequire("scheduler")too, whichcjs2esm/ReactSpecificUnwrappingTargetIsExternalpins, and the parser cannot see an external that a plugin declares.Order. This PR lands after #46072 and #42026. It merges clean with #46072, #46215, #46216, #42026 and #45830 (
git merge-tree). #45343 is about a rebound variable, which is a different decision.Self-review. Changed after it: the tests moved below
cjs2esm/ReactSpecificUnwrappingTargetIsExternal, a case with an ES-module importer, a case that pins the browser output, this body, and the order. Not done: a tracking issue (this PR is the record), a comment on #46072 (with this order its text stays true), the lazy-export hunk of #42500 (it belongs there), a second question about the list members (a separate change), and counts of sibling shapes that I did not reproduce.Suites run with the debug build:
bundler_cjs2esm(130 pass),bundler_npm,bundler_edgecase,bundler_splitting,bundler_html,bundler_cjs,bundler_loader,esbuild/defaultandbundler_compile. The last two have 2 and 3 failures that are 5 s timeouts. The same tests time out on the debug build of the merge base. Only linux-x64 was run.