Describe the bug
tanstackViteConfig's declaration post-processing (ensureImportFileExtension in packages/vite-config/src/index.ts) appends .js / .cjs to every extensionless relative specifier in emitted .d.ts files, without checking whether the specifier targets a directory barrel.
Given a source file that imports from a directory with an index.ts:
// src/adapters/image.ts
import type { GeminiClientConfig } from '../utils' // src/utils/index.ts
the emitted declaration becomes:
// dist/esm/adapters/image.d.ts
import { GeminiClientConfig } from '../utils.js';
but the build only emits dist/esm/utils/index.d.ts — there is no dist/esm/utils.d.ts — so the specifier should have been '../utils/index.js'. Any consumer compiling with skipLibCheck: false fails with:
error TS2307: Cannot find module '../utils.js' or its corresponding type declarations.
The emitted JS is unaffected (rollup/rolldown resolves the barrel itself, pointing at the concrete module, e.g. '../utils/client.js'), so this is invisible at runtime and only breaks the type layer.
Both the ESM (.js) and CJS (.cjs) branches of ensureImportFileExtension have the same problem. Verified present in 0.4.1 and still present in the 0.5.2 source.
Why it goes unnoticed
- The rewrite happens in
beforeWriteFile, after type-checking — afterDiagnostic validates the source compile, and the rewritten output text is never re-checked.
- Consumers overwhelmingly use
skipLibCheck: true, which suppresses TS2307 inside .d.ts files; the affected types silently degrade instead of erroring.
publint doesn't catch it (it checks the export map, not deep declaration resolution). @arethetypeswrong/cli does flag it, as InternalResolutionError.
Real-world occurrence
Published packages built with this config ship the broken specifiers today, e.g. @tanstack/ai-gemini@0.19.1:
dist/esm/adapters/image.d.ts → import { GeminiClientConfig } from '../utils.js' (only dist/esm/utils/index.d.ts exists)
and @tanstack/ai (dist/esm/activities/generate*/index.d.ts → '../middleware.js', where middleware is a directory barrel).
Steps to reproduce
- Scaffold a lib using
tanstackViteConfig with srcDir: './src', entry ./src/index.ts.
- Add
src/utils/index.ts exporting anything, and import it from a sibling as from '../utils'.
vite build, then inspect the emitted .d.ts — the specifier is '../utils.js'.
- Compile a file importing the package with
skipLibCheck: false → TS2307.
Suggested fix
Make the rewrite resolution-aware instead of purely textual. beforeWriteFile receives the output filePath, and the dist tree mirrors entryRoot/srcDir, so for each relative specifier the plugin can check whether the target resolves to a directory (or equivalently, whether <target>/index.d.ts rather than <target>.d.ts is being emitted) and append /index.js in that case:
'../utils' → '../utils/index.js' // when utils/ is a directory barrel
'../client' → '../client.js' // unchanged for plain files
Happy to send a PR if the approach sounds right.
Describe the bug
tanstackViteConfig's declaration post-processing (ensureImportFileExtensioninpackages/vite-config/src/index.ts) appends.js/.cjsto every extensionless relative specifier in emitted.d.tsfiles, without checking whether the specifier targets a directory barrel.Given a source file that imports from a directory with an
index.ts:the emitted declaration becomes:
but the build only emits
dist/esm/utils/index.d.ts— there is nodist/esm/utils.d.ts— so the specifier should have been'../utils/index.js'. Any consumer compiling withskipLibCheck: falsefails with:The emitted JS is unaffected (rollup/rolldown resolves the barrel itself, pointing at the concrete module, e.g.
'../utils/client.js'), so this is invisible at runtime and only breaks the type layer.Both the ESM (
.js) and CJS (.cjs) branches ofensureImportFileExtensionhave the same problem. Verified present in 0.4.1 and still present in the 0.5.2 source.Why it goes unnoticed
beforeWriteFile, after type-checking —afterDiagnosticvalidates the source compile, and the rewritten output text is never re-checked.skipLibCheck: true, which suppresses TS2307 inside.d.tsfiles; the affected types silently degrade instead of erroring.publintdoesn't catch it (it checks the export map, not deep declaration resolution).@arethetypeswrong/clidoes flag it, asInternalResolutionError.Real-world occurrence
Published packages built with this config ship the broken specifiers today, e.g.
@tanstack/ai-gemini@0.19.1:dist/esm/adapters/image.d.ts→import { GeminiClientConfig } from '../utils.js'(onlydist/esm/utils/index.d.tsexists)and
@tanstack/ai(dist/esm/activities/generate*/index.d.ts→'../middleware.js', wheremiddlewareis a directory barrel).Steps to reproduce
tanstackViteConfigwithsrcDir: './src', entry./src/index.ts.src/utils/index.tsexporting anything, and import it from a sibling asfrom '../utils'.vite build, then inspect the emitted.d.ts— the specifier is'../utils.js'.skipLibCheck: false→ TS2307.Suggested fix
Make the rewrite resolution-aware instead of purely textual.
beforeWriteFilereceives the outputfilePath, and the dist tree mirrorsentryRoot/srcDir, so for each relative specifier the plugin can check whether the target resolves to a directory (or equivalently, whether<target>/index.d.tsrather than<target>.d.tsis being emitted) and append/index.jsin that case:Happy to send a PR if the approach sounds right.