From 4fc970fd1bab3280eca7f44cf96cf673a3fcc42d Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:46:46 -0400 Subject: [PATCH 1/5] remove chokidar from svelte-package watch --- .changeset/quiet-owls-watch.md | 5 +++ packages/package/package.json | 1 - packages/package/src/index.js | 50 +++++++++++++++++++++-------- packages/package/test/index.spec.js | 8 ++++- pnpm-lock.yaml | 17 ---------- 5 files changed, 48 insertions(+), 33 deletions(-) create mode 100644 .changeset/quiet-owls-watch.md diff --git a/.changeset/quiet-owls-watch.md b/.changeset/quiet-owls-watch.md new file mode 100644 index 000000000000..d6201a90ba3e --- /dev/null +++ b/.changeset/quiet-owls-watch.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/package': patch +--- + +chore: remove dependency on chokidar diff --git a/packages/package/package.json b/packages/package/package.json index a3844c24809a..1a44f8b3b08d 100644 --- a/packages/package/package.json +++ b/packages/package/package.json @@ -20,7 +20,6 @@ "homepage": "https://svelte.dev", "type": "module", "dependencies": { - "chokidar": "^5.0.0", "sade": "^1.8.1", "semver": "^7.8.5", "svelte2tsx": "~0.7.56" diff --git a/packages/package/src/index.js b/packages/package/src/index.js index 337cabc18cf3..c7256f691ef2 100644 --- a/packages/package/src/index.js +++ b/packages/package/src/index.js @@ -1,7 +1,6 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { styleText } from 'node:util'; -import chokidar from 'chokidar'; import { preprocess } from 'svelte/compiler'; import { copy, mkdirp, posixify, rimraf } from './filesystem.js'; import { @@ -93,6 +92,10 @@ export async function watch(options) { /** @type {Array<{ file: import('./types.js').File, type: string }>} */ const pending = []; + const pending_keys = new Set(); + + // Remember files because deleted paths cannot be stat-ed to distinguish them from directories. + const known_files = new Set(scan(input, extensions).map((file) => file.name)); /** @type {Array<(value?: any) => void>} */ const fulfillers = []; @@ -103,21 +106,38 @@ export async function watch(options) { /** @type {Map} */ const tsconfig_cache = new Map(); - const watcher = chokidar.watch(input, { ignoreInitial: true }); - /** @type {Promise} */ - const ready = new Promise((resolve) => watcher.on('ready', resolve)); + const watcher = fs.watch(input, { recursive: true }, (_, filename) => { + if (filename !== null) { + const name = posixify(filename); + const stats = fs.statSync(path.join(input, filename), { throwIfNoEntry: false }); + let type; + + if (stats?.isDirectory()) return; + + if (stats?.isFile()) { + known_files.add(name); + type = 'change'; + } else if (!stats && known_files.delete(name)) { + type = 'unlink'; + } else { + return; + } - watcher.on('all', (type, filepath) => { - const file = analyze(path.relative(input, filepath), extensions); + const file = analyze(name, extensions); + const key = `${type}\0${file.name}`; - pending.push({ file, type }); + if (!pending_keys.has(key)) { + pending.push({ file, type }); + pending_keys.add(key); + } - if ( - file.name.endsWith('tsconfig.json') || - file.name.endsWith('jsconfig.json') || - (options.tsconfig && posixify(filepath) === posixify(options.tsconfig)) - ) { - tsconfig_cache.clear(); + if ( + file.name.endsWith('tsconfig.json') || + file.name.endsWith('jsconfig.json') || + (options.tsconfig && posixify(path.join(input, filename)) === posixify(options.tsconfig)) + ) { + tsconfig_cache.clear(); + } } clearTimeout(timeout); @@ -126,6 +146,7 @@ export async function watch(options) { const events = pending.slice(); pending.length = 0; + pending_keys.clear(); let errored = false; @@ -151,7 +172,7 @@ export async function watch(options) { console.log(`Removed ${file.dest}`); } - if (type === 'add' || type === 'change') { + if (type === 'change') { console.log(`Processing ${file.name}`); try { await process_file( @@ -190,6 +211,7 @@ export async function watch(options) { fulfillers.forEach((fn) => fn()); }, 100); }); + const ready = Promise.resolve(); return { watcher, diff --git a/packages/package/test/index.spec.js b/packages/package/test/index.spec.js index 6ee504171a08..677b0b0929c6 100644 --- a/packages/package/test/index.spec.js +++ b/packages/package/test/index.spec.js @@ -181,7 +181,7 @@ test('create package with tsconfig specified', async () => { await test_make_package('tsconfig-specified', { tsconfig: 'tsconfig.build.json' }); }); -// chokidar doesn't fire events in github actions :shrug: +// File watching is unreliable in GitHub Actions if (!process.env.CI) { test('watches for changes', async () => { const cwd = join(import.meta.dirname, 'watch'); @@ -255,6 +255,12 @@ if (!process.env.CI) { write('src/lib/post-error.svelte', ''); await settled(); compare('post-error.svelte'); + + // removes outputs when a source file is deleted + remove('src/lib/a.js'); + await settled(); + expect(fs.existsSync(join(cwd, 'package/a.js'))).toBe(false); + expect(fs.existsSync(join(cwd, 'package/a.d.ts'))).toBe(false); } finally { await watcher.close(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef91f2258c1b..deee3bef607d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1412,9 +1412,6 @@ importers: packages/package: dependencies: - chokidar: - specifier: ^5.0.0 - version: 5.0.0 sade: specifier: ^1.8.1 version: 1.8.1 @@ -3036,10 +3033,6 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chokidar@5.0.0: - resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} - engines: {node: '>= 20.19.0'} - chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} @@ -3772,10 +3765,6 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - readdirp@5.0.0: - resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} - engines: {node: '>= 20.19.0'} - regexparam@3.0.0: resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==} engines: {node: '>=8'} @@ -5671,10 +5660,6 @@ snapshots: dependencies: readdirp: 4.1.2 - chokidar@5.0.0: - dependencies: - readdirp: 5.0.0 - chownr@3.0.0: {} cjs-module-lexer@2.2.0: {} @@ -6362,8 +6347,6 @@ snapshots: readdirp@4.1.2: {} - readdirp@5.0.0: {} - regexparam@3.0.0: {} require-directory@2.1.1: {} From bc15605ce516849ef965de083b02a8be85a005d3 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:51:46 -0400 Subject: [PATCH 2/5] simplify event classification and dedupe --- packages/package/src/index.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/package/src/index.js b/packages/package/src/index.js index c7256f691ef2..913e7fad02ea 100644 --- a/packages/package/src/index.js +++ b/packages/package/src/index.js @@ -92,7 +92,6 @@ export async function watch(options) { /** @type {Array<{ file: import('./types.js').File, type: string }>} */ const pending = []; - const pending_keys = new Set(); // Remember files because deleted paths cannot be stat-ed to distinguish them from directories. const known_files = new Set(scan(input, extensions).map((file) => file.name)); @@ -112,8 +111,6 @@ export async function watch(options) { const stats = fs.statSync(path.join(input, filename), { throwIfNoEntry: false }); let type; - if (stats?.isDirectory()) return; - if (stats?.isFile()) { known_files.add(name); type = 'change'; @@ -124,11 +121,9 @@ export async function watch(options) { } const file = analyze(name, extensions); - const key = `${type}\0${file.name}`; - if (!pending_keys.has(key)) { + if (!pending.some((event) => event.type === type && event.file.name === file.name)) { pending.push({ file, type }); - pending_keys.add(key); } if ( @@ -146,7 +141,6 @@ export async function watch(options) { const events = pending.slice(); pending.length = 0; - pending_keys.clear(); let errored = false; @@ -211,11 +205,10 @@ export async function watch(options) { fulfillers.forEach((fn) => fn()); }, 100); }); - const ready = Promise.resolve(); return { watcher, - ready, + ready: Promise.resolve(), settled: () => new Promise((fulfil, reject) => { fulfillers.push(fulfil); From 1c66b896afbefc4020ddb12358e7c3b618db7ab6 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:58:33 -0400 Subject: [PATCH 3/5] handle directory renames and coalesced deletes --- packages/package/src/index.js | 51 ++++++++++++++++++----------- packages/package/test/index.spec.js | 13 ++++++++ 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/packages/package/src/index.js b/packages/package/src/index.js index 913e7fad02ea..4e1c3e7ae5a1 100644 --- a/packages/package/src/index.js +++ b/packages/package/src/index.js @@ -105,33 +105,46 @@ export async function watch(options) { /** @type {Map} */ const tsconfig_cache = new Map(); + /** + * @param {string} name + * @param {string} type + */ + function enqueue(name, type) { + const file = analyze(name, extensions); + + if (!pending.some((event) => event.type === type && event.file.name === file.name)) { + pending.push({ file, type }); + } + + if ( + file.name.endsWith('tsconfig.json') || + file.name.endsWith('jsconfig.json') || + (options.tsconfig && posixify(path.join(input, name)) === posixify(options.tsconfig)) + ) { + tsconfig_cache.clear(); + } + } + const watcher = fs.watch(input, { recursive: true }, (_, filename) => { if (filename !== null) { const name = posixify(filename); const stats = fs.statSync(path.join(input, filename), { throwIfNoEntry: false }); - let type; - if (stats?.isFile()) { + if (stats) { + if (!stats.isFile()) return; known_files.add(name); - type = 'change'; - } else if (!stats && known_files.delete(name)) { - type = 'unlink'; + enqueue(name, 'change'); + } else if (known_files.delete(name)) { + enqueue(name, 'unlink'); } else { - return; - } - - const file = analyze(name, extensions); + // a removed directory only fires an event for itself, not for the files inside it + const children = [...known_files].filter((child) => child.startsWith(name + '/')); + if (children.length === 0) return; - if (!pending.some((event) => event.type === type && event.file.name === file.name)) { - pending.push({ file, type }); - } - - if ( - file.name.endsWith('tsconfig.json') || - file.name.endsWith('jsconfig.json') || - (options.tsconfig && posixify(path.join(input, filename)) === posixify(options.tsconfig)) - ) { - tsconfig_cache.clear(); + for (const child of children) { + known_files.delete(child); + enqueue(child, 'unlink'); + } } } diff --git a/packages/package/test/index.spec.js b/packages/package/test/index.spec.js index 677b0b0929c6..5362184fe21d 100644 --- a/packages/package/test/index.spec.js +++ b/packages/package/test/index.spec.js @@ -261,6 +261,17 @@ if (!process.env.CI) { await settled(); expect(fs.existsSync(join(cwd, 'package/a.js'))).toBe(false); expect(fs.existsSync(join(cwd, 'package/a.d.ts'))).toBe(false); + + // removes outputs when a directory is renamed + fs.mkdirSync(join(cwd, 'src/lib/sub')); + write('src/lib/sub/c.js', "export const c = 'c';"); + await settled(); + expect(fs.existsSync(join(cwd, 'package/sub/c.js'))).toBe(true); + + fs.renameSync(join(cwd, 'src/lib/sub'), join(cwd, 'src/lib/sub2')); + await settled(); + expect(fs.existsSync(join(cwd, 'package/sub/c.js'))).toBe(false); + expect(fs.existsSync(join(cwd, 'package/sub2/c.js'))).toBe(true); } finally { await watcher.close(); @@ -268,6 +279,8 @@ if (!process.env.CI) { remove('src/lib/a.js'); remove('src/lib/b.ts'); remove('src/lib/post-error.svelte'); + fs.rmSync(join(cwd, 'src/lib/sub'), { recursive: true, force: true }); + fs.rmSync(join(cwd, 'src/lib/sub2'), { recursive: true, force: true }); } }, 30_000); } From c9a171b854b84e1654a17a0ef255002b089c2d59 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:03:09 -0400 Subject: [PATCH 4/5] drop the ready handshake, fs.watch is active synchronously --- packages/package/src/index.js | 1 - packages/package/test/index.spec.js | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/package/src/index.js b/packages/package/src/index.js index 4e1c3e7ae5a1..21e932b93be9 100644 --- a/packages/package/src/index.js +++ b/packages/package/src/index.js @@ -221,7 +221,6 @@ export async function watch(options) { return { watcher, - ready: Promise.resolve(), settled: () => new Promise((fulfil, reject) => { fulfillers.push(fulfil); diff --git a/packages/package/test/index.spec.js b/packages/package/test/index.spec.js index 5362184fe21d..4a559bf9ded4 100644 --- a/packages/package/test/index.spec.js +++ b/packages/package/test/index.spec.js @@ -190,7 +190,7 @@ if (!process.env.CI) { const config = await load_config(); process.chdir(original_cwd); - const { watcher, ready, settled } = await watch({ + const { watcher, settled } = await watch({ cwd, input: 'src/lib', output: 'package', @@ -224,8 +224,6 @@ if (!process.env.CI) { } try { - await ready; - // completes initial build compare('index.js'); From 182409271e660c31b7331bc7ca98688e129b61e4 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:18:11 -0400 Subject: [PATCH 5/5] fs.watch close is synchronous --- packages/package/test/index.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/package/test/index.spec.js b/packages/package/test/index.spec.js index 4a559bf9ded4..a9fd489ee1a9 100644 --- a/packages/package/test/index.spec.js +++ b/packages/package/test/index.spec.js @@ -271,7 +271,7 @@ if (!process.env.CI) { expect(fs.existsSync(join(cwd, 'package/sub/c.js'))).toBe(false); expect(fs.existsSync(join(cwd, 'package/sub2/c.js'))).toBe(true); } finally { - await watcher.close(); + watcher.close(); remove('src/lib/Test.svelte'); remove('src/lib/a.js');