Uh oh!
There was an error while loading. Please reload this page.
fix(components,layout): emit explicit extensions in the published typings so nodenext consumers can follow them - #5441
Merged
os-support-ai merged 3 commits intoAug 20, 2026
Conversation
`vite-plugin-dts` copies module specifiers into the declaration output verbatim, so `export * from './ui'` shipped extensionless in `dist/index.d.ts` and no consumer on `moduleResolution: nodenext` could follow any hop — every named export read as missing (objectui#5365). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
…nsumers Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
…ument app-shell type-checks clean without the pin and shows 23 errors with it — 21 TS7006 traced to `@object-ui/plugin-chatbot`'s own extensionless typings (named re-exports degrade to `any` rather than going missing) and 2 TS2345 from `@monaco-editor/react` ESM interop. Neither is this card's defect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
os-support-ai
marked this pull request as ready for review
August 20, 2026 18:50
Uh oh!
There was an error while loading. Please reload this page.
os-support-ai
deleted the
claude/issue-5365-dts-extensionless-reexports
branch
August 20, 2026 18:50
This was referenced Aug 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#5365
vite-plugin-dtsemits one declaration file per source file and TypeScript copies a module specifier into the declaration verbatim, soexport * from './ui'shipped extensionless indist/index.d.ts. Node16/NodeNext resolution does not extension-search a relative specifier, so the compiler could follow none of the hops and every symbol they carried read as absent from the package.Dispatched at the scope the thread establishes, not the card body's:
@object-ui/layoutis in the same shape as@object-ui/components, and a fix covering one leavesapp-shell— the consumer that pulls in both — still unable to take the pin.What changed
scripts/vite-dts-explicit-extensions.ts(new) — a sharedvite-plugin-dtsbeforeWriteFile/afterBuildpair. It resolves each relative specifier against the source tree the declaration output mirrors (a file hop becomes./x.js, a directory hop./x/index.js), throws on anything it cannot resolve, and after the build re-parses the emitted declarations to assert every relative specifier both carries an extension and names a file the build really emitted. It lives inscripts/besidevite-crypto-stub.ts/vite-objectstack-spec-dist.ts, which is already aturbo.jsonbuild input ($TURBO_ROOT$/scripts/vite-*.ts), so cache invalidation comes for free.packages/components/vite.config.ts,packages/layout/vite.config.ts— three lines each, spreading the hooks into the existingdts({ ... })call.packages/fields/tsconfig.json— takes thenodenextpin, the same two linespackages/reacthas carried since finding(react): the built dist emits extensionless relative imports, so plain Node ESM cannot load the published entry #4538.scripts/__tests__/vite-dts-explicit-extensions.test.ts(new) — 10 cases.patchon both packages — the published typings change).Why the emit and not the sources
@object-ui/reacthad the same defect in its.js(#4538) and fixed it at the source, because it builds with a bare emittingtsc. These two are bundler builds, and that one difference decides the route: rolldown resolves their relative specifiers away, so the same source line produces a clean.jsand a broken.d.ts. No source edit can express that difference, andpnpm check:esm-specifiers— whose verdict is about specifier-preserving.jsbuilds — is correct to have never scanned either package.Why the parser and not a regex
removeCommentsis false in this repository's build configs and the comments really do contain specifiers —packages/app-shell/src/views/metadata-admin/i18n.tsdocuments itself withimport { t } from './i18n', andpackages/fields/src/widgets/MasterDetailField.tsxnames'./widgets/MasterDetailField'the same way. A text scan rewrites those, and when the prose names a path that does not exist it throws and breaks a correct build. The module locates specifiers withts.createSourceFileinstead, and a test pins that prose survives untouched.Acceptance, measured
The card's acceptance is named: the consumer-side pin goes green. The artifact is the thing under test, so both legs were built and the emitted
dist/**/*.d.tsinspected afterwards — BEFORE was measured on the pristine build, then the fix was applied,pnpm --filter @object-ui/components --filter @object-ui/layout buildre-run, and the consumers re-checked against the rebuilt tree.Emitted typings, extensionless relative specifiers (AST-located, prose excluded):
dist/index.d.tsdist/**/*.d.tstree@object-ui/components@object-ui/layouttsc --noEmitunder themodule/moduleResolution: nodenextpin:@object-ui/fields@object-ui/components), 57 TS7006, 50 files@object-ui/app-shellcomponents+ 16layout), 215 TS7006, 2 TS2345, 165 filesThe
app-shellbefore-numbers reproduce the thread's measurement exactly (1097 / 880 / 864+16 / 215 / 2).The pin lands on
fieldsonly —app-shellwas the instrumentStated explicitly because the dispatch asked for it.
packages/fieldsgoes fully clean, so its pin is landed here and the property is enforced by the compiler from now on.packages/app-shell's pin is not in this diff (itstsconfig.jsonis byte-identical tomain), because a control run settles where the residue comes from:app-shell/tsconfig.jsonapp-shelltype-checks clean today, so all 23 are produced by the pin, not by this card. Verified rather than assumed, as asked: 194 of the 215 TS7006 were indeed fallout and cleared; the remaining 21 are a different package's instance of the same defect class, and the 2 TS2345 are a third-party interop question. Both are filed:anyinstead of going missing #5439 — 13 more vite-built packages ship extensionless typings. Their named re-exports make this worse rather than better: a star re-export from an unresolvable module contributes no names (loud TS2305), a named one still declares the name and only loses its type (silentany). Probed two ways against the same builtdist:IsAny< typeof useHitlInChat >from@object-ui/plugin-chatbotistrueundernodenextandfalseunderbundler. That is the source of the 21 residual TS7006.@monaco-editor/reactlazy import is TS2345 under Node16/NodeNext #5440 —@monaco-editor/reactReact.lazyinterop under Node16/NodeNext, the 2 TS2345, blocked by 13 more vite-built packages ship extensionless typings, and their NAMED re-exports degrade toanyinstead of going missing #5439.Neither is addressed in this PR, per the card's fence.
(The generic above is written with a space after
<on purpose — GitHub's body sanitizer strips<followed by a letter as an HTML tag, and it ate that fragment on the first revision of this description.)Scope fence honoured
Triage ruled the "third gate leg watching published typings" a design add-on and out of this dispatch.
scripts/check-node-esm-load.mjsis untouched and its specifier leg is not widened — the assertions added here are a build's assertions about its own output, inside the two packages' owndtsinvocation, not a repository-wide gate. #5439 records the case for the gate leg so the decision is not lost.packages/plugin-listandpackages/plugin-vieware untouched (sibling cards in flight), even though the sweep found the same defect in both.Verification
Gate union re-run on the final commit,
22b7d781c, working tree clean:Reverse verification of the new pin test, direction predicted before running: plain RED, because the directory-hop branch is the sole input to two of the cases and removing it can only add findings. Deleting that branch from
resolveExplicitSpecifiergave 2 failed / 8 passed, the failure naming./uiat the throw site. Restored withgit checkout HEAD --, byte-identical. No build artifact sits between that edit and the test — vitest imports the helper source directly, so no rebuild leg applies to it. The fix itself is the opposite case, and both of its legs were rebuilt, as recorded above.Generated by Claude Code