Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<hash>.js` never existed on disk (only `./Foo-<hash>.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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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-<dtsHash>.js` never exists — only the matching
// `./Foo-<dtsHash>.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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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-<dtsHash>.js` file never exists — only
* `./Foo-<dtsHash>.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.
Expand Down
Loading