Repository navigation
Conversation
…function exports
`module.exports = x` and `module.exports = { x }` copy x's value at that
statement, so every var or let export was fenced. esbuild hoists a bundled
module's top-level const to a var in its __commonJS chunk, so a bundle's
`module.exports = toText2` stopped the program at load. The export now
compiles when the declaration and every write sit in top-level code above
it, outside functions and classes: the copied value is then the binding's
final value. A write that can run later keeps the fence.
The package-file rewrite that reaches members through a whole-function
export now also takes a var or const bound to a function expression or
arrow above the export, and treats bindings of required modules before the
export as inert. Without it the export's members (.default, .object) read
as undefined.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A minifier merges a module's consecutive declarations, so its requires and the bindings read from them share one statement (glob's `var minimatch = require_minimatch(), Minimatch = minimatch.Minimatch, …` in a bundle). Require bindings are modeled per statement: inside a mixed statement the required module was never stored, so `minimatch.Minimatch` read undefined, and a later reference to `Minimatch` met "a binding form with no lowering". A package-file rewrite stage now splits a top-level declaration that mixes require() initializers with other declarators into one declaration per declarator, in order and on the same lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
semver 6 starts with `exports = module.exports = SemVer`, then builds
`var t = exports.tokens = {}` and fills it through `function tok(n) {
t[n] = R++ }`. The hoisted SemVer reads `t`, and its export store counted
as a reference that could call it before `t` was set, so `t` became a
runtime-optional global and tok's computed write was fenced at load.
A function stored as a CommonJS export, or given members, can only run
before a binding settles if code re-enters the module first. When every
top-level statement up to the settling one makes no call into user code,
no construction, no require and no read of `module`/`exports`, such a
reference now counts as settled (builtin methods of literals and standard
globals cannot run user code).
The declaration split also respells `var t = exports.tokens = {}` as
`var t = {}; exports.tokens = t;`: writes through `t` were lost to the
export otherwise (`exports.tokens` printed `{}`).
Also records the order baselines of the new 6484 and 2591 fixtures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/npm-cjs-var-export
fs.realpath (glob 7 and 8 reach the filesystem through it) keeps Node's
own functions as values, `var origRealpathSync = fs.realpathSync`, and
calls them with its legacy cache object as the options argument. Reading
the function as a value was refused ("library functions as values").
The value is now an adapter over the native call. Node's second argument
can change the result's encoding, so the adapter takes the call only when
the path is a string and the options keep UTF-8 (none, "utf8", or an
object without another `encoding`); any other shape throws a located
SC1090 fence where the call runs instead of answering differently.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`module.exports = format; module.exports.shout = shout` (or the
`exports = module.exports = format` spelling) attaches `shout` to the
function. Destructured requires already found the member, but a read or
call through the requirer's binding of the function (`format.shout`)
answered undefined, and a call threw "is not a function".
A read through a `const` require binding now resolves to the member's
value when the module writes it once and the use site can read it again;
a call resolves to the member function when it ignores `this` (Node passes
the export as `this`) and otherwise fences by name.
Applying the package-file rewrite (`module.exports.x` respelled as `F.x`)
to program files was not the fix: it turns member exports into expandos
of the function, so a requirer's `const { shout } = require(…)` found no
member export and met "a binding form with no lowering" (corpus 6431).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oes not write A member read through a binding of a factory-returned object (corpus 2923) or one the requirer writes later is a runtime slot: it keeps reading the object. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tsc wraps every CommonJS import in its helpers: `const events_1 =
__importDefault(require("events"))`, `const path =
__importStar(require("path"))`. cjs-tsc-interop.ts reads such bindings of
program modules as plain require bindings, so the helper declarations go
dead, but a builtin module is not a program module: its binding kept the
helpers live, and their probe of the module's top-level `this`
(`(this && this.__importDefault) ||`) failed at load ("property access
through top-level 'this'"). That is the shape of hundreds of tsc-compiled
files in a typical dependency tree.
A builtin carries no `__esModule`, so `events_1.default` is the module and
the namespace copy reads the module's members. When every reference has
that form, a package-file stage now respells the binding as the plain
require() of the builtin and `x.default` as `x`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ry namespace
A bundled module that exports only types keeps `var x_exports = {}` with
no __export call. Its star re-export into the entry,
`__reExport(main, (init_x(), __toCommonJS(x_exports)), module.exports)`,
was left in place because the rewrite only knew namespaces with an export
table, and failed at load on the `module.exports` argument ("the CommonJS
'module' object in this position"). Such a namespace is now an empty
table: the re-export runs the initializer and adds nothing.
Two silent results on the same paths: a member read of a name a module
does not export now answers undefined (`__esModule` true) where it was
refused, and a read through the bundle's own entry namespace inside the
bundle (`(init_index(), __toCommonJS(index_exports)).KIND`) answered
undefined; it now reads the entry's export table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/npm-cjs-var-export
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ar-bound functions #1558 let the function-export rewrite run when require bindings come before `module.exports = F`. For a function declaration that respelled its member exports as members of the function (`module.exports.sync = spawnSync` became `spawn.sync = spawnSync`), which need dynamic values: a typed member such as cross-spawn 7's spawnSync (returning SpawnSyncReturns) failed at load with SC1101. A declaration's member exports already compile as member exports, so behind requires only a function bound to a variable (esbuild's hoisted `var`, whose members have no other form) is respelled now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # tests/harness/npm-cases.ts # tests/harness/npm-static.test.ts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s members
A builtin call above a function export's settling statement can call back
into the module (`Array.from(xs, () => module.exports())`), so it counts
as a possible early call unless each argument is a primitive: a literal,
`undefined`, a read under `process`, or an operator over those.
A member of a function whole export is a runtime property of the function
when program code may write it through a require: an assignment, update
or delete of `fmt.name`, `fmt["name"]` or `require("./lib").name` in any
program file, the module's own writes through the function's name, and,
for every member, a computed key or a use that hands the value elsewhere
(`Object.assign(fmt, …)`). The module's `module.exports.name = …` then
also stores the member on the function, so requirers read the value Node
reads before and after those writes.
…ling exports
retry 0.13.1's `exports.timeouts` reads `this.createTimeout`, and
`exports.operation` calls it through the exports object; async-retry wraps
every request in an operation. A call through the exports object passes
that object as `this`, which compiles to a static table, so the call was
fenced ("calling 'exports.timeouts' through the module's exports object
when the function may read 'this'").
When every `this` in an exported function expression names another
export the module writes (`this.X` with `exports.X = …`), the exports
object is never replaced, and the module calls the function only through
`exports.f(…)`, a package-file stage now respells `this.X` as `exports.X`:
the member Node reads for those calls. Only a caller that detaches the
function or rebinds its `this` would observe a difference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ty and entry namespaces (#1584)
… members `module.exports = spawn; module.exports.spawn = spawn` (cross-spawn 7) stores the function as one of its own member exports. That use of `spawn` counted as handing the value to code that can write its members, so every member was stored on the function as a runtime property, and the typed `module.exports.sync = spawnSync` failed at load again (SC1101; npm-static case 2596). A top-level member export store writes no member of the function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/npm-cjs-var-export
…/npm-exports-this
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion exports, and leave written interop wrappers - A class runs its heritage, decorators, computed member names, static field initializers and static blocks where it is defined, so a call there can call a function export before the variables it reads are assigned. The re-entry check now skips only method bodies and instance field initializers. - Passing a function whole export to other code (a re-export, a callback argument) no longer marks all of its members as runtime properties. Named writes, computed-key writes and the keys builtins such as Object.assign and Object.defineProperty write still do, so typed members such as `module.exports.sync = spawnSync` stay static exports. - A tsc interop binding of a builtin whose `.default` or namespace members are written keeps its helper: the write changes the helper's object, and respelling `x.default = …` as `x = …` assigned the const binding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A module that binds `const {a, b} = require(...)` above
`module.exports = f; module.exports.g = ...` (execa 5's index.js shape)
skipped the function-export rewrite, because only identifier require
bindings counted as inert. The members then never reached the inferred
export type, and `execa.command` read as missing. A plain object pattern
(identifier elements, no defaults, rest, computed keys or nesting) runs
no user code, so it counts as inert too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/npm-cjs-var-export
# Conflicts: # tests/harness/npm-cases.ts # tests/harness/npm-static.test.ts
…t shadow exports - A sibling read as a value through `this.f` inside the module (`var g = this.b; g()`) now counts as a detached use, as `exports.f` already did, so `b` keeps its `this`. - An exported function that declares its own `exports` binding (a parameter, a `var`, an arrow parameter or its own name) keeps its text, and so does a module that declares `exports` at module scope: the respelled reads would name that binding instead of the exports object.
A use of a function whole export other than a member access, a call or
`typeof` hands it to code that can write any member (`mutate(fmt)`), so
every member becomes a runtime property of the function again. A module
that re-exports the value whole (`module.exports = require("./lib")`)
contributes its own requirers' writes instead. A member whose value has no
dynamic form, such as a typed function, stays the export's static value,
so `module.exports.sync = spawnSync` keeps loading (npm case 2596).
Contributor
Author
|
Closing this under the scope-B plan the maintainer approved (native compilation for typed code; Node islands for the rest). Loading bundled CommonJS dependencies, which covers settled What is salvaged:
What is not salvaged:
The branch stays in place for reference. |
This branch was successfully 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.
Stacked on #1458. One commit per fix, in the order a bundled package hits them while it loads.
Summary
module.exports = xandmodule.exports = { x }copyx's value at that statement. Everyvarorletexport used to be fenced. esbuild hoists a bundled module's top-levelconstto avarin its__commonJSchunk, so a bundle stopped at load (into-stream'smodule.exports = intoStream2, async-sema'sSema). The export now compiles when the binding's declaration and every write sit in top-level code above it, outside functions and classes. A later write keeps the fence.var- orconst-bound function export. The package rewrite that reachesmodule.exports.xthrough a whole-function export now accepts a function expression or arrow bound to avar/const, and require bindings above the export. Without this, into-stream's.default/.objectreadundefined.var minimatch = require_minimatch(), Minimatch = minimatch.Minimatch, …(glob). Inside such a statement the required module was never stored, so the member readundefinedand later references failed. A package-file stage now splits a top-level declaration that mixes require initializers with other declarators into one declaration per declarator, in order and on the same lines.exports = module.exports = SemVercomes beforevar t = exports.tokens = {}, and the hoisted SemVer readst. The export store counted as a possible early call, sotbecame runtime-optional andtok'st[n] = R++was fenced at load. A reference that only stores or extends the function now counts as settled when no top-level code up to the settling statement can re-enter the module: no user-code call, construction, require, or read ofmodule/exports. The split stage also respellsvar t = exports.tokens = {}asvar t = {}; exports.tokens = t;; before, writes throughtsilently never reachedexports.tokens.fs.realpathSyncas a value (fs.realpath, under glob). The value is an adapter over the native call. Node's options can change the result's encoding, so the adapter takes the call only for a string path with UTF-8 options (none,"utf8", or an object without anotherencoding, such as fs.realpath's cache object). Other shapes throw a located SC1090 fence. Documented on the builtin function values page.module.exports = format; module.exports.shout = shout(with or withoutexports =) attachesshoutto the function. Readingformat.shoutthrough a requirer's binding answeredundefined, and calling it threw. Reads now resolve to the member's value when it is written once; calls resolve when the function ignoresthis, and otherwise fence by name. Applying the package rewrite to program files was not the fix: it turns member exports into expandos, which breaks destructured requires (corpus 6431).Follow-up (int/bc round 7 regression). The relaxation above (require bindings before the export count as inert) also let the rewrite respell a function declaration's member exports as members of the function. cross-spawn 7's typed
module.exports.sync = spawnSyncthen needed a dynamic value and failed at load (SC1101). Behind requires, only a function bound to a variable is respelled now; declarations keep their member exports (npm-static case2596-function-export-members, plus a unit test). Gate for that commit against the previous tip: all 3434 programs identical except thefs-extra-packageshim-path artifact. The driver passes.Evidence
All npm-static cases use real esbuild 0.27 output (
--bundle --platform=node --format=cjs --minify-whitespace --minify-syntax) unless noted:2591-bundled-var-export(into-stream's shape);2592-bundled-merged-requires(glob's merged declaration over a minimatch-like ES5 constructor);2593-bundled-semver-tokens(semver 6's load-time shape, with the hoisted constructor reading the table);2594-fs-realpath-values, with fs.realpath 1.0.0 vendored unmodified.Each matches Node byte for byte. Each fails on #1458's tip, either with the fence or with wrong output.
Corpus programs:
6484-cjs-settled-exports/: a hoistedvarfunction export, a table settled by loops and conditionals, andvar/letclasses;6485-cjs-whole-export-members/: both export spellings, reads and calls.Unit and coverage tests:
cjs-settled-exports.test.ts: fences stay for later writes and forthis-reading member calls;npm-static-cjs-exports-alias.test.tsandnpm-static-split-declarations.test.ts: rewritten modules run against their originals under Node.The real packages: semver 6.3.1's chunk now loads, and glob 8.0.3's bundle gets past
Minimatchand fs.realpath. Remaining stops are listed in the notes.--corpus=all, 3432 programs): bench, self and corpus are byte-identical exceptfs-extra-package, whose IR differs only by the fs-extra shim's absolute checkout path (identical once/vercel/sandbox/{pbase,cand}is normalized). Bench geomean −1.21%.self-hosting-native-driver.test.tspasses on the final tip (1213 s).pnpm lint,pnpm format:checkandtest:ts7 --baselines-onlypass; only the new fixtures' baselines were added.package-shimssuites, the frontend unit tests andcjs-settled-exports.test.tspass 1137 of 1138. The one failure,node24-url-authority, comes from the sandbox's Node 24.15 versus the pinned 24.22.Note for reviewers:
fs.realpathSyncas a value (commit "Store fs.realpathSync as a function value") was also on another agent's queue; it is complete here, so that work can be dropped.🤖 Generated with Claude Code