Skip to content

Compile function export objects; name conditional export objects - #1434

Merged
ctate merged 10 commits into
opt/npm-esm-wrappersfrom
opt/npm-cjs-exports
Oct 20, 2026
Merged

ctate merged 10 commits into
opt/npm-esm-wrappersfrom
opt/npm-cjs-exports

Conversation

@cramforce

@cramforce cramforce commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #1427 (which is stacked on #1422 and #1411). Review those first; this branch is based on opt/npm-lazy-loaders.

Summary

Function export objects. Some CommonJS modules make one of their own top-level functions the export object and attach members to it. semver 5 and 6 do this:

exports = module.exports = SemVer;
exports.parse = parse;
function SemVer(version) { … }

After that statement, module.exports is the function. After the rebinding form, so is exports. A new npm-static pass therefore respells member reads and writes through them as members of the function, which is the expando form both resolvers model, and drops the exports = rebinding.

The pass applies only where the rewrite is exact:

  • the statement is the file's only write of module.exports and exports;
  • only inert declarations and member writes precede it;
  • the function is a top-level declaration that nothing reassigns;
  • module and exports are the CommonJS globals.

This also fixes a silent divergence. A constructor function exported whole lost the function-valued members attached afterwards through module.exports: Node ran them, and the binary threw "is not a function".

Conditional export objects. Replacing module.exports, or rebinding exports, inside a branch makes the exports depend on which path ran. readable-stream 2.x does this with its READABLE_STREAM=disable switch. scriptc now names that construct precisely instead of the generic assignment fence.

Required builtin modules are truthy. The LLVM backend now treats a required builtin module object as truthy in boolean contexts, as any object is. readable-stream's … && Stream and Stream || exports guards failed to compile before.

Bundled builtin forwarders. A bundled chunk that only re-exports a builtin, module.exports = require("stream"), is now that builtin: its calls become the require, and no chunk module is emitted.

Evidence

  • New npm corpus cases:
    • 2583-cjs-function-exports: the semver idiom and a constructor function with members attached afterwards, run with both values of the environment variable the module reads.
    • 2584-cjs-conditional-exports: a readable-stream-style environment-dependent export object, which is refused by name.
  • New corpus program required-module-truthiness.cjs. Main fails it with SC3001 truthy:moduleNs.
  • A unit test runs a bundle with a forwarder chunk against its original under Node.
  • All new cases match Node byte for byte in the plain and sanitized lanes.
  • The npm-static suite, the compiler unit tests and order-parity pass.
  • Zero-cost gate against origin/main: PASS. All 2464 programs (bench, self, corpus) are byte-identical; bench geomean +0.1% and self −1.9%, within tolerance.
  • pnpm lint, pnpm format:check and test:ts7 --baselines-only pass. The self-hosted compiler builds its IR cleanly, and self-hosting-native-driver.test.ts passes.

🤖 Generated with Claude Code

cramforce and others added 3 commits October 19, 2026 18:21
A CommonJS module whose export object is one of its own top-level
functions (semver 5/6's `exports = module.exports = SemVer`) reaches
its members through that function after the statement: member reads
and writes through module.exports (and, after the rebinding, every
`exports` reference) become members of the function, the expando form
both resolvers model, and the rebinding itself drops. Constructor
functions exported whole with members attached afterwards no longer
lose those members at runtime.

Replacing module.exports or rebinding exports inside a branch (an
environment-dependent export object, readable-stream 2.x) now names the
construct instead of the generic assignment fence, and a required
builtin module object is truthy in the LLVM backend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A chunk whose whole body is `module.exports = require("<builtin>")`
(readable-stream's internal/streams/stream.js) is the builtin module:
its calls become that require, and no chunk module is emitted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
scriptc Ready Ready Preview, v0 Oct 20, 2026 8:14am UTC

Comment thread packages/compiler/src/frontend/npm-static-cjs-exports-alias.ts
Comment thread packages/compiler/src/frontend/npm-static-cjs-exports-alias.ts
cramforce and others added 2 commits October 19, 2026 19:51
Its number collided with another open change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renumbers this branch's npm cases 2567-2568 to 2583-2584: #1422 now uses
2567.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cramforce and others added 2 commits October 19, 2026 22:44
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	tests/harness/npm-cases.ts
Base automatically changed from opt/npm-lazy-loaders to opt/npm-esm-wrappers October 20, 2026 08:12
The pass respells `exports.x` and `module.exports.x` through the
exported function's name. A parameter, local, nested function or catch
binding of that name between a respelled read and the top level would
capture it, so the pass now applies only when the file binds the name
once, at its top-level declaration.

`{ exports }` after the rebinding respelled to `{ SemVer }`, which
renames the property; the shorthand now becomes `{ exports: SemVer }`.
@ctate
ctate merged commit b8a955b into opt/npm-esm-wrappers Oct 20, 2026
23 checks passed
@ctate
ctate deleted the opt/npm-cjs-exports branch October 20, 2026 08:16

This branch was successfully deployed

1 active deployment
Preview — 603c6720 Deployed Oct 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants