Skip to content

Compile the packages an import()-loaded module requires; compile TypeScript enums in packages - #1517

Merged
ctate merged 5 commits into
opt/npm-polyfill-shimsfrom
opt/npm-dynamic-import-deps
Oct 20, 2026
Merged

ctate merged 5 commits into
opt/npm-polyfill-shimsfrom
opt/npm-dynamic-import-deps

Conversation

@cramforce

@cramforce cramforce commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #1458. Review the stack below it first.

Summary

Packages required by a module loaded through import(). #1487 made npm selection follow the program modules a literal import() reaches, so a workspace package loaded on demand gets its npm dependencies compiled. That stopped one level too early. When the dependency was compiled CommonJS that requires a sibling package (a package shipping .d.ts plus JS, the way @typescript-eslint/* and many tsc-built packages ship), the build was refused with "SC1010: the '' module is not supported yet". The same program with a static import compiled. Two pieces were missing:

  • Selection. The scan of the import() graph skipped package files, so the dependency's own requires were never read. It now follows opted-in package JavaScript through its literal require() and import edges, the same files the program can load.
  • Module order. The lowering appended import()-only modules by their import declarations alone, so a CommonJS file's requires never joined the compiled graph and its require bindings stayed fenced. Preflight already resolves every file's module edges, require edges included. Those edges now reach the walks that append import(), fork() and Worker modules. Lazy requires are ordered after their module, and an inline require that closes a cycle there is answered as at startup.

TypeScript enums in npm packages. tsc compiles an enum to a bare var filled by an immediately invoked function:

var Kind;
(function (Kind) {
    Kind["Identifier"] = "Identifier";
})(Kind || (exports.Kind = Kind = {}));

Inference types Kind from the empty object the argument creates, so every member read landed on {}, and reading one failed at run time ("undefined is not representable in a '{}' slot"). When every member value is a literal, a new rewrite stage (npm-static-enum-iife.ts) turns the declaration into the object literal the function builds and the call into the export assignment it performs. Keys follow Node's own-property order: integer keys ascending, then the rest in insertion order, with a later write replacing an earlier value in place. Numeric enums keep their reverse mapping. The stage recognizes three spellings:

  • the TypeScript 5 spelling above;
  • the earlier Kind = exports.Kind || (exports.Kind = {});
  • unexported enums.

Merged declarations, computed members, members referencing other members, and any other write to the variable keep their original text.

Self-hosting. #1422's deadDeclarators.add(d).add(memoDecl) (in npm-static-bundled-esm.ts) stopped the self-hosted compiler with "chained Set method calls … has no scriptc lowering yet". The split into two calls now lives in #1422 (1092f69) and is merged up through #1458 into this branch. This branch's earlier copy of the same change merges cleanly with it.

Evidence

  • New npm case workspace-dynamic-import-deps. The program loads a workspace package through import(). That package imports a compiled CommonJS scoped package, which re-exports from a nested file that requires a sibling scoped package exporting a tsc-emitted string enum. Output is byte-identical to Node's. On Compile polyfill packages that are Node builtins as the builtins #1458's tip the build fails with SC2013/SC1010.
  • npm-static-enum-iife.test.ts runs original and rewritten modules under Node for exported string enums, numeric enums (reverse mapping, negative values, key order, a duplicate value), and an unexported enum. It also checks that line positions are preserved, and that merged, computed, side-effecting and reassigned enums are left alone.
  • The existing workspace-dynamic-import, dynamic-import, dynamic-import-local, workspace-* and hoisted-workspace npm cases pass.
  • Zero-cost gate against Compile polyfill packages that are Node builtins as the builtins #1458's tip: PASS. 2501 programs (bench, corpus) are byte-identical; bench geomean is +0.40% [95% CI −0.43%, +1.64%] with 3 timing runs. The self-hosted compiler failed on both sides (see below). With the same one-line split applied to the baseline, a self-only gate run is byte-identical too (−1.2%).
  • The self-hosted compiler builds its IR cleanly, and self-hosting-native-driver.test.ts passes on the final candidate (1673 s).
  • pnpm lint, pnpm format:check and test:ts7 --baselines-only pass. Re-recording all order baselines adds only the new case's entry.
  • The npm, npm-static and package-shims suites and the frontend unit tests pass: 1103 of 1104. The one failure, node24-url-authority, comes from the sandbox's Node 24.15 versus the pinned 24.22.

🤖 Generated with Claude Code

cramforce and others added 3 commits October 20, 2026 03:22
A compiled package spells each enum as a bare `var` filled by an
immediately invoked function (`(function (Kind) { Kind["A"] = "A"; })(Kind
|| (exports.Kind = Kind = {}))`). Inference types the variable from the
empty object the argument creates, so every member read landed on `{}` and
failed at run time. When every member value is a literal, the declaration
now becomes the object literal the function builds, keys in Node's
own-property order, and the call becomes the export assignment it
performs. The TypeScript 5 spelling, the earlier `Kind = exports.Kind ||
(exports.Kind = {})` spelling and unexported enums are recognized; merged,
computed or otherwise written enums keep their text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A workspace package loaded on demand had its npm dependency selected, but
the selection never scanned that dependency's own JavaScript: it is
outside the startup module order, so a compiled CommonJS dist requiring a
sibling package was refused with "the module is not supported yet". The
scan of the import() graph now follows opted-in package JavaScript through
its literal require() and import edges.

The lowering appended those modules by their import declarations only, so
a CommonJS file's requires never joined the compiled graph and their
bindings stayed fenced. Preflight's resolved module edges, require edges
included, now reach the walks that append import(), fork() and Worker
modules, with lazy requires ordered after their module and inline require
cycles answered as at startup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`deadDeclarators.add(d).add(memoDecl)` stopped the self-hosting build
with "chained Set method calls ... has no scriptc lowering yet".

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 1:29pm UTC

@socket-security

socket-security Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​gtdyn/​estree@​1.0.0N/AN/AN/AN/AN/A

View full report

Comment thread packages/compiler/src/frontend/npm-static-enum-iife.ts
TypeScript's ES module emit for an exported enum is `export var Kind;`
followed by the IIFE. The rewrite replaced the declaration with a plain
`var Kind = { ... };`, so importers of the module lost the export. The
replacement now keeps the modifiers before `var`.
@ctate
ctate merged commit 8a40fb5 into opt/npm-polyfill-shims Oct 20, 2026
60 of 65 checks passed
@ctate
ctate deleted the opt/npm-dynamic-import-deps branch October 20, 2026 13:55

This branch was successfully deployed

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