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..21e932b93be9 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 { @@ -94,6 +93,9 @@ export async function watch(options) { /** @type {Array<{ file: import('./types.js').File, type: string }>} */ const pending = []; + // 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,22 +105,48 @@ 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)); - - watcher.on('all', (type, filepath) => { - const file = analyze(path.relative(input, filepath), extensions); + /** + * @param {string} name + * @param {string} type + */ + function enqueue(name, type) { + const file = analyze(name, extensions); - pending.push({ file, type }); + 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(filepath) === posixify(options.tsconfig)) + (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 }); + + if (stats) { + if (!stats.isFile()) return; + known_files.add(name); + enqueue(name, 'change'); + } else if (known_files.delete(name)) { + enqueue(name, 'unlink'); + } else { + // 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; + + for (const child of children) { + known_files.delete(child); + enqueue(child, 'unlink'); + } + } + } clearTimeout(timeout); timeout = setTimeout(async () => { @@ -151,7 +179,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( @@ -193,7 +221,6 @@ export async function watch(options) { return { watcher, - ready, 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 6ee504171a08..a9fd489ee1a9 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'); @@ -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'); @@ -255,13 +253,32 @@ 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); + + // 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(); + watcher.close(); remove('src/lib/Test.svelte'); 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); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3fd29a293240..462adf62dbea 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: {}