From 64969dce79a5832bde6a03d99c61d114615ffada Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Thu, 6 Aug 2026 10:21:42 +0200 Subject: [PATCH 1/2] Rewrite .js imports to .d.ts in emitted declaration chunks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rolldown-plugin-dts writes the relative imports between declaration chunks with a `.js` extension (mirroring the JS module graph). But the declaration chunks are content-hashed independently from the JS chunks, so the referenced `./Foo-.js` never exists on disk — only `./Foo-.d.ts` does, because the JS chunk carries a different hash. Served over HTTP, the editor's LSP fetches the literal `.js` specifier, 404s, and type resolution breaks for any code-split subpath. The specifier already carries the declaration chunk's own basename and hash; only the extension is wrong. Swap `.js` -> `.d.ts` on relative specifiers in emitted `.d.ts` chunks so every type import resolves to a real file. Bare specifiers (e.g. @studiometa/js-toolkit/utils) are left untouched. Co-authored-by: Claude Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Botz34NmFLdgRgm2QJpKkZ --- .../PlaygroundDependenciesPlugin.test.ts | 39 +++++++++++++++++++ .../plugins/PlaygroundDependenciesPlugin.ts | 31 ++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/packages/playground/src/lib/plugins/PlaygroundDependenciesPlugin.test.ts b/packages/playground/src/lib/plugins/PlaygroundDependenciesPlugin.test.ts index e16aaa1..893ce1b 100644 --- a/packages/playground/src/lib/plugins/PlaygroundDependenciesPlugin.test.ts +++ b/packages/playground/src/lib/plugins/PlaygroundDependenciesPlugin.test.ts @@ -481,6 +481,45 @@ describe('PlaygroundDependenciesPlugin', () => { expect(emitted.get('static/deps/multi/index.d.ts')).toBe('entry types'); }); + it('rewrites relative .js imports to .d.ts inside emitted declaration chunks', () => { + // rolldown-plugin-dts writes cross-chunk type imports with a `.js` + // extension, but the declaration chunks are hashed independently from the + // JS chunks, so `./Foo-.js` never exists — only the matching + // `./Foo-.d.ts` does. The emit step must swap the extension so + // the served type files resolve. + const emitted = emitAndCollect( + [ + { + fileName: 'entry.d.ts', + code: + 'import { Action } from "./Action-Bt0NVlQH.js";\n' + + 'export { type Action } from "./Action-Bt0NVlQH.js";\n' + + 'export declare const x: typeof import("./AccordionItem-DP2oetGf.js");\n' + + 'import { isDefined } from "@studiometa/js-toolkit/utils";\n', + isEntry: true, + }, + { fileName: 'entry.js', code: 'export const a = 1;', isEntry: true }, + { + fileName: 'Action-Bt0NVlQH.d.ts', + code: 'export declare class Action {}', + isEntry: false, + }, + ], + 'static/deps/@studiometa/ui', + ); + + const dts = emitted.get('static/deps/@studiometa/ui/index.d.ts')!; + // Relative type imports now target the real `.d.ts` chunks. + expect(dts).toContain('from "./Action-Bt0NVlQH.d.ts"'); + expect(dts).toContain('import("./AccordionItem-DP2oetGf.d.ts")'); + // No relative `.js` specifier remains. + expect(dts).not.toMatch(/["']\.[^"']*\.js["']/); + // Bare (non-relative) specifiers are untouched. + expect(dts).toContain('from "@studiometa/js-toolkit/utils"'); + // The JS entry is unaffected. + expect(emitted.get('static/deps/@studiometa/ui/index.js')).toBe('export const a = 1;'); + }); + it('keeps the import map / _headers pointing at .../index.js and .../index.d.ts', () => { // The import map value and the _headers x-typescript-types entry are both // derived from the specifier as `.../index.js` and `.../index.d.ts`. The diff --git a/packages/playground/src/lib/plugins/PlaygroundDependenciesPlugin.ts b/packages/playground/src/lib/plugins/PlaygroundDependenciesPlugin.ts index 9a62c06..8093a5e 100644 --- a/packages/playground/src/lib/plugins/PlaygroundDependenciesPlugin.ts +++ b/packages/playground/src/lib/plugins/PlaygroundDependenciesPlugin.ts @@ -345,15 +345,44 @@ export class PlaygroundDependenciesPlugin { const fileName = pinEntry && chunk.isEntry ? (isDts ? 'index.d.ts' : 'index.js') : chunk.fileName; const assetPath = posix.join(outputBase, fileName); + const code = isDts ? this.rewriteDtsChunkImports(chunk.code) : chunk.code; compilation.emitAsset( assetPath, - new compilation.compiler.webpack.sources.RawSource(chunk.code), + new compilation.compiler.webpack.sources.RawSource(code), ); } } } } + /** + * Rewrite relative `.js` module specifiers to `.d.ts` inside an emitted + * declaration chunk. + * + * rolldown-plugin-dts writes the relative imports between declaration chunks + * with a `.js` extension (mirroring the JS module graph, the convention TS + * expects on disk where `./foo.js` resolves to a sibling `./foo.d.ts`). But + * the declaration chunks are content-hashed **independently** from the JS + * chunks, so the referenced `./Foo-.js` file never exists — only + * `./Foo-.d.ts` does (the JS chunk carries a different hash). Served + * over HTTP the editor's LSP fetches the literal specifier and 404s, breaking + * type resolution. + * + * The specifier already carries the declaration chunk's own basename + hash; + * only the extension is wrong. Swapping `.js` → `.d.ts` points every relative + * type import at the real emitted declaration file. Bare specifiers (e.g. + * `@studiometa/js-toolkit/utils`) are untouched — only relative (`.`/`..`) + * paths are rewritten. + * + * @private + */ + private rewriteDtsChunkImports(code: string): string { + return code.replace( + /(\b(?:from|import)\b\s*\(?\s*)(['"])(\.[^'"\n]*?)\.js\2/g, + (_match, keyword, quote, specifier) => `${keyword}${quote}${specifier}.d.ts${quote}`, + ); + } + /** * Check whether a source string refers to a local path (relative, absolute, or glob) * as opposed to a bare npm package specifier. From dfef01c586e4fc4482b514d80acc38ffa27271f6 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Thu, 6 Aug 2026 10:21:42 +0200 Subject: [PATCH 2/2] Update changelog for declaration chunk import fix Co-authored-by: Claude Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Botz34NmFLdgRgm2QJpKkZ --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 625e138..1be60d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- Rewrite relative `.js` imports to `.d.ts` inside emitted declaration chunks of self-hosted dependencies. rolldown-plugin-dts writes cross-chunk type imports with a `.js` extension, but declaration chunks are content-hashed independently from the JS chunks, so `./Foo-.js` never existed on disk (only `./Foo-.d.ts` did) — the editor's LSP fetched the dead `.js` URL and type resolution broke for code-split subpaths + ## v0.3.12 - 2026.08.06 ### Added