From ff563253d3fea13e6985de5b88cdade70720ee61 Mon Sep 17 00:00:00 2001 From: ump45nose Date: Fri, 16 Oct 2026 05:24:30 +0800 Subject: [PATCH 1/2] fix: remove empty type output directories for removed routes After a route is deleted, sync already removes its generated $types files but leaves the empty directory under .svelte-kit/types. Walk empty dirs bottom-up after file cleanup so removed routes leave no empty output directories, while nested routes that still exist keep their parents. Fixes #17452 --- .changeset/remove-empty-type-dirs.md | 5 ++ .../kit/src/core/sync/write_types/index.js | 25 +++++++ .../src/core/sync/write_types/index.spec.js | 65 ++++++++++++++++++- 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 .changeset/remove-empty-type-dirs.md diff --git a/.changeset/remove-empty-type-dirs.md b/.changeset/remove-empty-type-dirs.md new file mode 100644 index 000000000000..40091a26ca31 --- /dev/null +++ b/.changeset/remove-empty-type-dirs.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +fix: remove empty type output directories for removed routes diff --git a/packages/kit/src/core/sync/write_types/index.js b/packages/kit/src/core/sync/write_types/index.js index a542b76344eb..05ee58e01aec 100644 --- a/packages/kit/src/core/sync/write_types/index.js +++ b/packages/kit/src/core/sync/write_types/index.js @@ -32,6 +32,29 @@ const is_whitespace = (/** @type {string} */ char) => /\s/.test(char); * @param {import('types').ManifestData} manifest_data * @param {string} root The project root directory */ + +/** + * Recursively remove empty directories under `root`, leaving `root` itself. + * @param {string} dir + * @param {string} root + */ +function remove_empty_directories(dir, root = dir) { + if (!fs.existsSync(dir)) return; + + for (const name of fs.readdirSync(dir)) { + const child = path.join(dir, name); + if (fs.statSync(child).isDirectory()) { + remove_empty_directories(child, root); + } + } + + if (path.resolve(dir) === path.resolve(root)) return; + + if (fs.readdirSync(dir).length === 0) { + fs.rmdirSync(dir); + } +} + export function write_all_types(config, manifest_data, root) { if (!ts) return; @@ -52,6 +75,8 @@ export function write_all_types(config, manifest_data, root) { fs.rmSync(path.join(types_dir, file), { force: true, recursive: true }); } } + // Stale files are removed above; empty route directories are left behind. + remove_empty_directories(types_dir); } // Read/write meta data on each invocation, not once per node process, diff --git a/packages/kit/src/core/sync/write_types/index.spec.js b/packages/kit/src/core/sync/write_types/index.spec.js index 2a4fde5b74c0..5beba40cc488 100644 --- a/packages/kit/src/core/sync/write_types/index.spec.js +++ b/packages/kit/src/core/sync/write_types/index.spec.js @@ -1,8 +1,9 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; import process from 'node:process'; -import { assert, describe, expect, test } from 'vitest'; +import { afterEach, assert, beforeEach, describe, expect, test } from 'vitest'; import create_manifest_data from '../create_manifest_data/index.js'; import { tweak_types, write_all_types } from './index.js'; import { write_app_types } from '../write_app_types.js'; @@ -309,3 +310,65 @@ test('Leaves satisfies operator untouched', () => { assert.equal(rewritten?.modified, false); assert.equal(rewritten?.code, source); }); + +describe('Cleans up unused files', () => { + /** @type {string} */ + let root; + + beforeEach(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'kit-write-types-')); + + for (const dir of ['', 'about']) { + const route_dir = path.join(root, 'src/routes', dir); + fs.mkdirSync(route_dir, { recursive: true }); + fs.writeFileSync(path.join(route_dir, '+page.svelte'), ''); + } + }); + + afterEach(() => { + fs.rmSync(root, { force: true, recursive: true }); + }); + + function sync() { + const config = validate_config({}); + config.files.assets = path.join(root, 'static'); + config.files.params = path.join(root, 'src/params'); + config.files.routes = path.join(root, 'src/routes'); + config.outDir = path.join(root, '.svelte-kit'); + + write_all_types(config, create_manifest_data(config, root), root); + + return path.join(config.outDir, 'types'); + } + + test('removes empty output directories for removed routes', () => { + const types_dir = sync(); + const route_types_dir = path.join(types_dir, 'src/routes/about'); + const types_file = path.join(route_types_dir, '$types.d.ts'); + assert.ok(fs.existsSync(types_file)); + + fs.rmSync(path.join(root, 'src/routes/about'), { recursive: true }); + sync(); + + assert.ok(!fs.existsSync(types_file)); + assert.ok(!fs.existsSync(route_types_dir)); + }); + + test('keeps parent directories that still have nested routes', () => { + const nested = path.join(root, 'src/routes/about/team'); + fs.mkdirSync(nested, { recursive: true }); + fs.writeFileSync(path.join(nested, '+page.svelte'), ''); + + const types_dir = sync(); + const about_dir = path.join(types_dir, 'src/routes/about'); + const team_types = path.join(about_dir, 'team/$types.d.ts'); + assert.ok(fs.existsSync(team_types)); + + fs.rmSync(path.join(root, 'src/routes/about/team'), { recursive: true }); + sync(); + + assert.ok(!fs.existsSync(team_types)); + assert.ok(!fs.existsSync(path.join(about_dir, 'team'))); + assert.ok(fs.existsSync(path.join(about_dir, '$types.d.ts'))); + }); +}); From 009007ad85dfb4cd13b8b1700034530e111ce308 Mon Sep 17 00:00:00 2001 From: ump45nose Date: Fri, 16 Oct 2026 07:23:48 +0800 Subject: [PATCH 2/2] fix: keep write_all_types JSDoc attached to the export Inserting remove_empty_directories between the write_all_types JSDoc and its export left the parameters untyped under check, so tsc failed with implicit any errors on config/manifest_data/root/route. --- packages/kit/src/core/sync/write_types/index.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/kit/src/core/sync/write_types/index.js b/packages/kit/src/core/sync/write_types/index.js index 05ee58e01aec..95e0d0f47453 100644 --- a/packages/kit/src/core/sync/write_types/index.js +++ b/packages/kit/src/core/sync/write_types/index.js @@ -26,13 +26,6 @@ const is_whitespace = (/** @type {string} */ char) => /\s/.test(char); * @typedef {Map} RoutesMap */ -/** - * Creates types for the whole manifest - * @param {import('types').ValidatedConfig} config - * @param {import('types').ManifestData} manifest_data - * @param {string} root The project root directory - */ - /** * Recursively remove empty directories under `root`, leaving `root` itself. * @param {string} dir @@ -55,6 +48,12 @@ function remove_empty_directories(dir, root = dir) { } } +/** + * Creates types for the whole manifest + * @param {import('types').ValidatedConfig} config + * @param {import('types').ManifestData} manifest_data + * @param {string} root The project root directory + */ export function write_all_types(config, manifest_data, root) { if (!ts) return;