From 57db706ad15cf9f820215b7a019ba748da52d0da Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Tue, 16 Jun 2026 10:37:16 -0400 Subject: [PATCH 1/4] feat: rewrite `.ts` to `.js` in dist files --- .changeset/humble-paths-fly.md | 5 + src/lib/dist_rewrite_imports.ts | 113 ++++++++++++ src/lib/gro_plugin_sveltekit_library.ts | 9 +- src/test/dist_rewrite_imports.test.ts | 221 ++++++++++++++++++++++++ 4 files changed, 347 insertions(+), 1 deletion(-) create mode 100644 .changeset/humble-paths-fly.md create mode 100644 src/lib/dist_rewrite_imports.ts create mode 100644 src/test/dist_rewrite_imports.test.ts diff --git a/.changeset/humble-paths-fly.md b/.changeset/humble-paths-fly.md new file mode 100644 index 0000000000..a33d0ea32b --- /dev/null +++ b/.changeset/humble-paths-fly.md @@ -0,0 +1,5 @@ +--- +'@fuzdev/gro': minor +--- + +feat: rewrite `.ts` to `.js` in dist files diff --git a/src/lib/dist_rewrite_imports.ts b/src/lib/dist_rewrite_imports.ts new file mode 100644 index 0000000000..525b072d3b --- /dev/null +++ b/src/lib/dist_rewrite_imports.ts @@ -0,0 +1,113 @@ +import {fs_search} from '@fuzdev/fuz_util/fs.js'; +import type {Logger} from '@fuzdev/fuz_util/log.js'; +import {readFile, writeFile} from 'node:fs/promises'; + +import {SVELTE_SCRIPT_MATCHER, SVELTEKIT_DIST_DIRNAME} from './constants.ts'; + +/* + +Post-`svelte-package` pass that rewrites relative `.ts` import specifiers to `.js` +in the published `dist` output. + +The ecosystem writes import specifiers with the real source extension +(`./foo.ts`, `./bar.svelte.ts`) and relies on emit-only rewrites to make `dist` +resolve for external consumers. `tsc`'s `rewriteRelativeImportExtensions` handles +the `.ts`/`.svelte.ts` → `dist/*.js` emit, but two outputs are left untouched: + +- `.svelte` files, which `svelte-package` ships verbatim (source ` + +{icon_link} +`; + const expected = ` + +{icon_link} +`; + assert.equal(rewrite_svelte_ts_imports(input), expected); + }); + + test('rewrites both module and instance ` + +`; + const expected = ` + +`; + assert.equal(rewrite_svelte_ts_imports(input), expected); + }); + + test('leaves a `.svelte` with no rewritable specifiers untouched', () => { + const input = ` +
hi
+`; + assert.equal(rewrite_svelte_ts_imports(input), input); + }); +}); + +describe('rewrite_dist_imports', () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), 'gro-dist-rewrite-')); + }); + + afterEach(async () => { + await rm(dir, {recursive: true, force: true}); + }); + + test('rewrites `.d.ts`, `.svelte.d.ts`, and `.svelte`, leaving `.js` and maps alone', async () => { + await mkdir(join(dir, 'nested'), {recursive: true}); + await writeFile(join(dir, 'a.d.ts'), `import type {B} from './b.ts';\n`); + await writeFile(join(dir, 'Comp.svelte.d.ts'), `import {type P} from './helpers.ts';\n`); + await writeFile( + join(dir, 'Comp.svelte'), + `\n`, + ); + // already emitted by tsc — must stay untouched + await writeFile(join(dir, 'a.js'), `import {b} from './b.js';\n`); + await writeFile(join(dir, 'a.d.ts.map'), `{"sources":["../src/lib/a.ts"]}\n`); + await writeFile(join(dir, 'nested', 'c.d.ts'), `export * from '../a.ts';\n`); + + const result = await rewrite_dist_imports(dir); + + assert.equal(await readFile(join(dir, 'a.d.ts'), 'utf8'), `import type {B} from './b.js';\n`); + assert.equal( + await readFile(join(dir, 'Comp.svelte.d.ts'), 'utf8'), + `import {type P} from './helpers.js';\n`, + ); + assert.equal( + await readFile(join(dir, 'Comp.svelte'), 'utf8'), + `\n`, + ); + assert.equal(await readFile(join(dir, 'a.js'), 'utf8'), `import {b} from './b.js';\n`); + assert.equal( + await readFile(join(dir, 'a.d.ts.map'), 'utf8'), + `{"sources":["../src/lib/a.ts"]}\n`, + ); + assert.equal( + await readFile(join(dir, 'nested', 'c.d.ts'), 'utf8'), + `export * from '../a.js';\n`, + ); + + // 4 files scanned (a.d.ts, Comp.svelte.d.ts, Comp.svelte, nested/c.d.ts); all 4 rewritten + assert.equal(result.scanned, 4); + assert.equal(result.rewritten, 4); + }); + + test('returns zero rewrites for a missing directory', async () => { + const result = await rewrite_dist_imports(join(dir, 'does-not-exist')); + assert.equal(result.scanned, 0); + assert.equal(result.rewritten, 0); + }); +}); From 594fb1a818271514a3e12db8faac46a4d352b3f2 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Tue, 16 Jun 2026 10:45:02 -0400 Subject: [PATCH 2/4] fixes --- src/lib/dist_rewrite_imports.ts | 41 +++++++++++++++------------ src/test/dist_rewrite_imports.test.ts | 21 ++++++++++++++ 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/lib/dist_rewrite_imports.ts b/src/lib/dist_rewrite_imports.ts index 525b072d3b..c8122ffcb8 100644 --- a/src/lib/dist_rewrite_imports.ts +++ b/src/lib/dist_rewrite_imports.ts @@ -1,4 +1,5 @@ import {fs_search} from '@fuzdev/fuz_util/fs.js'; +import {map_concurrent} from '@fuzdev/fuz_util/async.js'; import type {Logger} from '@fuzdev/fuz_util/log.js'; import {readFile, writeFile} from 'node:fs/promises'; @@ -37,11 +38,12 @@ This is intentionally parse-light; the long-term home for the rewrite is tsv. * and the path body so the trailing `.ts` can be swapped for `.js`. * * Anchored on a module-specifier introducer (`from`, `import`, `require`) so it - * doesn't touch incidental relative-looking string literals, and on a `./`/`../` - * prefix so bare specifiers like `@fuzdev/fuz_util/foo.ts` are left untouched. + * doesn't touch incidental relative-looking string literals, on a `./`/`../` + * prefix so bare specifiers like `@fuzdev/fuz_util/foo.ts` are left untouched, and + * on a `(? export const rewrite_svelte_ts_imports = (content: string): string => content.replace(SVELTE_SCRIPT_MATCHER, (full: string, inner: string) => { const rewritten = rewrite_relative_ts_imports(inner); - return rewritten === inner ? full : full.replace(inner, rewritten); + if (rewritten === inner) return full; + // replace via a function so `$`-sequences in the script (`$$props`, `$:`, + // `$&`, …) aren't interpreted as `String.prototype.replace` substitution patterns + return full.replace(inner, () => rewritten); }); export interface RewriteDistImportsResult { @@ -71,6 +76,9 @@ export interface RewriteDistImportsResult { rewritten: number; } +/** Bounds open file descriptors while rewriting the dist tree. */ +const DIST_REWRITE_CONCURRENCY = 16; + /** * Walks `dist_dir` and rewrites relative `.ts` import specifiers to `.js` in every * `.d.ts` declaration file (whole-file) and `.svelte` file (` +`; + const expected = ` +`; + assert.equal(rewrite_svelte_ts_imports(input), expected); + }); + test('leaves a `.svelte` with no rewritable specifiers untouched', () => { const input = `