Skip to content

Unwrap tsc's interop helpers around builtins; lower re-exports of empty and entry namespaces - #1584

Merged
ctate merged 8 commits into
opt/npm-cjs-var-exportfrom
opt/npm-tsc-helpers
Oct 21, 2026
Merged

ctate merged 8 commits into
opt/npm-cjs-var-exportfrom
opt/npm-tsc-helpers

Conversation

@cramforce

Copy link
Copy Markdown
Contributor

Stacked on #1558.

Summary

tsc's interop helpers around builtin modules. tsc wraps every CommonJS import in its helpers: const events_1 = __importDefault(require("events")), const path = __importStar(require("path")). For program modules, cjs-tsc-interop.ts reads such a binding as a plain require binding, so the helper declarations go dead. A builtin module is not a program module, though, so its binding kept the helpers live. Their probe of the module's top-level this, var __importDefault = (this && this.__importDefault) || …, then failed at load ("property access through top-level 'this'"). That shape appears in 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. (Dropping the probe instead would be wrong: cjs-tsc-interop.ts recognizes the helpers by that probe.)

Star re-exports of empty namespaces. 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 that have an export table. It then 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 doesn't export now answers undefined (__esModule answers true), where it was refused.
  • 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.

Evidence

  • New npm case tsc-builtin-interop: real tsc 5.6 output (module: commonjs, esModuleInterop) of a class extending events.default.EventEmitter and a namespace import of path. Output is byte-identical to Node's; before, it failed at load.
  • New npm-static case 2595-bundled-entry-namespace: real esbuild 0.27 output where a bundled module reads the entry's own namespace and an empty one. On Load bundled CommonJS dependencies: settled exports, merged requires, semver's tokens, fs.realpath, whole-export members #1558 it printed undefined where Node prints lambda.
  • Unit tests:
    • npm-static-tsc-helpers.test.ts runs rewritten modules against their originals under Node, and checks which shapes keep their text;
    • npm-static-bundled-esm.test.ts covers the empty namespace, the types-only star re-export into the entry, and entry namespace reads.
  • On the published @vercel/build-utils 14.21.0, the module fence at dist/index.js:119 is gone.
  • Zero-cost gate against Load bundled CommonJS dependencies: settled exports, merged requires, semver's tokens, fs.realpath, whole-export members #1558 with the full corpus: all 3434 programs identical except fs-extra-package (the fs-extra shim's absolute checkout path only).
  • The self-hosted compiler builds, and self-hosting-native-driver.test.ts passes (1118 s).
  • pnpm lint, pnpm format:check and test:ts7 --baselines-only pass; only the new fixtures' baselines were added.
  • The npm, npm-static and package-shims suites and the frontend unit tests pass 1143 of 1144. The one failure, node24-url-authority, comes from the sandbox's Node 24.15.

🤖 Generated with Claude Code

cramforce and others added 7 commits October 20, 2026 13:34
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	tests/harness/npm-cases.ts
#	tests/harness/npm-static.test.ts
@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 21, 2026 12:22am UTC

Comment thread packages/compiler/src/frontend/npm-static-bundled-esm.ts
Comment thread packages/compiler/src/frontend/npm-static-bundled-esm.ts
…re-exports

A member read of a name a module does not export answered undefined. Two
such names have values on the namespace object:

- Object.prototype's members (constructor, toString, __proto__ and the
  rest), since __toCommonJS builds an ordinary object.
- Names a star re-export into the bundle's entry adds: the entry's export
  table lists only its own exports.

Both keep the package refused, as other reads the rewrite cannot follow do.
@ctate
ctate merged commit f54f79e into opt/npm-cjs-var-export Oct 21, 2026
59 of 65 checks passed
@ctate
ctate deleted the opt/npm-tsc-helpers branch October 21, 2026 00:26

This branch was successfully deployed

1 active deployment
Preview — 8ac501ba Deployed Oct 21, 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