You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[Feature] The build tsconfig resolves modules as Bundler rather than NodeNext, so nothing validates the emitted specifiers under the resolver the package actually publishes to #1008
No bundler runs. bunx tsc emits ESM straight into dist/, and the consumer is Node ≥ 24
or Deno resolving those files through the real ESM resolver. Under Bundler, TypeScript
deliberately relaxes exactly the rules that resolver enforces: extensionless relative
specifiers are accepted, directory-index resolution is accepted, and exports-map-aware .d.ts resolution is not modelled. The compiler is therefore validating the emitted
specifiers against a set of rules that nothing downstream applies.
Today that costs nothing, and it is worth being precise about why: the project's mandatory .js-suffix convention (AGENTS.md, Code style) happens to satisfy every rule NodeNext would add. That is a convention held by review discipline, with no compiler
behind it. The guard is one forgotten suffix away from mattering, and the failure mode is
a runtime ERR_MODULE_NOT_FOUND in a consumer's process rather than a compile error here.
A correction to the original review note, which claimed NodeNext would have turned
W9-01 and W9-02 into compile errors: it would not. Both live in docs/**, and the build
tsconfig's include is ["src/**/*.ts"] — documentation code fences are outside every
tsconfig in the repo. That gap is W9-03, and it is not addressed by changing a resolution
mode.
Proposed shape
Switch the build to "module": "NodeNext" + "moduleResolution": "NodeNext".
The change is currently free — see Verification, it compiles clean — which is the
argument for doing it now rather than after the first violation. What it buys:
Relative specifiers are checked against the resolver that will actually run them, so a
missing .js becomes TS2835 at build time instead of a consumer-side runtime failure.
.d.ts resolution becomes exports-map-aware, which matters once [Feature] Add subpath exports to package.json #414 introduces
subpaths: NodeNext verifies that a subpath's types condition resolves, Bundler
does not.
The compiler's view matches attw --profile esm-only, which already checks the
emitted package under node16/ESM. Two tools currently disagree about which resolver
is authoritative; NodeNext makes them agree.
Two things to watch when landing it:
NodeNext implies module: NodeNext, which changes how import type elision and verbatimModuleSyntax interact. verbatimModuleSyntax is currently false
(tsconfig.json:24), so this is likely inert, but it should be checked rather than
assumed.
tsconfig.dev.json, tsconfig.bench.json and devtools-ui/tsconfig.json extend or
parallel the build config; they need the same treatment or an explicit override, or typecheck:dev will diverge from typecheck.
Low priority, and explicitly a guard rather than a fix: there is no current defect behind
it.
Acceptance
tsconfig.json sets module and moduleResolution to NodeNext.
bun run typecheck is green, and dist/ is unchanged apart from any deliberate specifier fixes.
bun run smoke still passes on Bun, Node and Deno.
The sibling tsconfigs (dev, bench, devtools-ui) are consistent with the build config.
A deliberately extensionless relative import fails bun run typecheck.
Verification status
Confirmed by reading for the configuration; the claimed consequence was tested and
does not hold.
The config fact is quoted verbatim above. To establish whether the relaxation is currently
masking anything, the source tree was compiled under the strict resolver:
All 550 source files resolve cleanly under NodeNext — so the .js-suffix convention is
being held perfectly today, and this issue is hardening with no latent defect behind it.
The note's claim that NodeNext would have caught W9-01/W9-02 is refuted: those are
in docs/**, which no tsconfig includes.
git status --porcelain is clean afterwards.
Related: #414 (subpath exports) is where exports-aware .d.ts resolution starts to
earn its keep. W9-03 is the gap that would actually have caught W9-01 and W9-02.
Part of the production-readiness review batch — tracked in #913.
Use case
The build tsconfig resolves modules as if a bundler will run:
No bundler runs.
bunx tscemits ESM straight intodist/, and the consumer is Node ≥ 24or Deno resolving those files through the real ESM resolver. Under
Bundler, TypeScriptdeliberately relaxes exactly the rules that resolver enforces: extensionless relative
specifiers are accepted, directory-index resolution is accepted, and
exports-map-aware.d.tsresolution is not modelled. The compiler is therefore validating the emittedspecifiers against a set of rules that nothing downstream applies.
Today that costs nothing, and it is worth being precise about why: the project's mandatory
.js-suffix convention (AGENTS.md, Code style) happens to satisfy every ruleNodeNextwould add. That is a convention held by review discipline, with no compilerbehind it. The guard is one forgotten suffix away from mattering, and the failure mode is
a runtime
ERR_MODULE_NOT_FOUNDin a consumer's process rather than a compile error here.A correction to the original review note, which claimed
NodeNextwould have turnedW9-01 and W9-02 into compile errors: it would not. Both live in
docs/**, and the buildtsconfig's
includeis["src/**/*.ts"]— documentation code fences are outside everytsconfig in the repo. That gap is W9-03, and it is not addressed by changing a resolution
mode.
Proposed shape
Switch the build to
"module": "NodeNext"+"moduleResolution": "NodeNext".The change is currently free — see Verification, it compiles clean — which is the
argument for doing it now rather than after the first violation. What it buys:
missing
.jsbecomes TS2835 at build time instead of a consumer-side runtime failure..d.tsresolution becomesexports-map-aware, which matters once [Feature] Add subpath exports to package.json #414 introducessubpaths:
NodeNextverifies that a subpath'stypescondition resolves,Bundlerdoes not.
attw --profile esm-only, which already checks theemitted package under
node16/ESM. Two tools currently disagree about which resolveris authoritative;
NodeNextmakes them agree.Two things to watch when landing it:
NodeNextimpliesmodule: NodeNext, which changes howimport typeelision andverbatimModuleSyntaxinteract.verbatimModuleSyntaxis currentlyfalse(
tsconfig.json:24), so this is likely inert, but it should be checked rather thanassumed.
tsconfig.dev.json,tsconfig.bench.jsonanddevtools-ui/tsconfig.jsonextend orparallel the build config; they need the same treatment or an explicit override, or
typecheck:devwill diverge fromtypecheck.Low priority, and explicitly a guard rather than a fix: there is no current defect behind
it.
Acceptance
tsconfig.jsonsetsmoduleandmoduleResolutiontoNodeNext.bun run typecheckis green, anddist/is unchanged apart from any deliberate specifier fixes.bun run smokestill passes on Bun, Node and Deno.dev,bench,devtools-ui) are consistent with the build config.bun run typecheck.Verification status
Confirmed by reading for the configuration; the claimed consequence was tested and
does not hold.
The config fact is quoted verbatim above. To establish whether the relaxation is currently
masking anything, the source tree was compiled under the strict resolver:
All 550 source files resolve cleanly under
NodeNext— so the.js-suffix convention isbeing held perfectly today, and this issue is hardening with no latent defect behind it.
The note's claim that
NodeNextwould have caught W9-01/W9-02 is refuted: those arein
docs/**, which no tsconfig includes.git status --porcelainis clean afterwards.Related: #414 (subpath exports) is where
exports-aware.d.tsresolution starts toearn its keep. W9-03 is the gap that would actually have caught W9-01 and W9-02.
Part of the production-readiness review batch — tracked in #913.