Skip to content

Unwrap tsc's interop helpers around builtin modules in packages - #1683

Merged
ctate merged 1 commit into
mainfrom
opt/npm-tsc-builtin-interop
Oct 21, 2026
Merged

ctate merged 1 commit into
mainfrom
opt/npm-tsc-builtin-interop

Conversation

@cramforce

Copy link
Copy Markdown
Contributor

Summary

tsc wraps every CommonJS import in its interop 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 (npm-static-tsc-helpers.ts) now respells the binding as the plain require() of the builtin and x.default as x. Any other reference (the binding used as a value, ns.default of a namespace copy, a program module) keeps the text unchanged. Dropping the probe instead would be wrong: cjs-tsc-interop.ts recognizes the helpers by that probe.

Why this PR

This is salvaged from #1584, which was merged only into #1558's branch (opt/npm-cjs-var-export), not main. Under the scope-B plan, #1558's bundled-CommonJS emulation goes to Node islands and is closed. The tsc helpers stay native because typed, tsc-emitted packages need them. This PR carries only the tsc-helper commit (d60d1f2, with the formatting fixes #1584 later applied to the same files) and its fixtures. It leaves out #1584's bundled-ESM namespace work (bundler interop, D1) and #1610 (exports object as this, a CommonJS export form, D2).

Evidence (Linux sandbox, against main 0237d70)

  • 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. Its output is byte-identical to Node's; before this change it failed at load. Its order baseline is recorded.
  • npm-static-tsc-helpers.test.ts runs rewritten modules against their originals under Node, and checks which shapes keep their text.
  • packages/compiler/src/frontend unit tests: 891 passed. tests/harness/npm.test.ts -t tsc: 3 passed. npm-static.test.ts and package-shims.test.ts: 138 passed.
  • pnpm lint, pnpm format:check and pnpm test:ts7 --baselines-only pass. The self-hosted --emit=ir build of packages/compiler/src/native/cli.ts reports no diagnostics.
  • Zero-cost gate (bench, self and TypeScript corpus, 2718 programs): 2717 identical. The one difference is fs-extra-package, whose IR embeds the fs-extra shim's absolute checkout path (it differs between any two checkouts). Compile time: bench geomean +0.53% [95% CI -0.84%, +1.87%], self-hosted +0.75%, both within tolerance.

🤖 Generated with Claude Code

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>
@vercel

vercel Bot commented Oct 21, 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 3:51pm UTC

@ctate
ctate merged commit c8193b9 into main Oct 21, 2026
66 checks passed
@ctate
ctate deleted the opt/npm-tsc-builtin-interop branch October 21, 2026 16:14
@cramforce
cramforce requested a review from ctate October 21, 2026 16:24

This branch was successfully deployed

1 active deployment
Preview — f793009e 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