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 83f8eed24bd4..326c05b4b641 100644 --- a/packages/kit/src/core/sync/write_types/index.js +++ b/packages/kit/src/core/sync/write_types/index.js @@ -26,6 +26,28 @@ const is_whitespace = (/** @type {string} */ char) => /\s/.test(char); * @typedef {Map} RoutesMap */ +/** + * 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); + } +} + /** * Creates types for the whole manifest * @param {import('types').ValidatedConfig} config @@ -52,6 +74,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 ac750aa47962..78471e98656d 100644 --- a/packages/kit/src/core/sync/write_types/index.spec.js +++ b/packages/kit/src/core/sync/write_types/index.spec.js @@ -3,7 +3,7 @@ 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'; @@ -311,6 +311,68 @@ test('Leaves satisfies operator untouched', () => { 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'))); + }); +}); + test('prunes stale entries from route_meta_data.json when routes are deleted', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sveltekit-prune-test-')); try {