From 9dea9bc1b20d3392f61b1459a27f24fc991980fc Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 18 May 2023 14:44:24 +0100 Subject: [PATCH] perf(nuxt): use granular watcher to avoid crawling ignored dirs (#20836) --- packages/nuxt/src/core/builder.ts | 130 +++++++++++++++------ packages/nuxt/src/core/schema.ts | 2 +- packages/schema/src/config/common.ts | 3 +- packages/schema/src/config/experimental.ts | 14 ++- 4 files changed, 104 insertions(+), 45 deletions(-) diff --git a/packages/nuxt/src/core/builder.ts b/packages/nuxt/src/core/builder.ts index de0cb9e46f..b17daf7141 100644 --- a/packages/nuxt/src/core/builder.ts +++ b/packages/nuxt/src/core/builder.ts @@ -1,7 +1,8 @@ import { pathToFileURL } from 'node:url' import type { EventType } from '@parcel/watcher' +import type { FSWatcher } from 'chokidar' import chokidar from 'chokidar' -import { isIgnored, tryResolveModule } from '@nuxt/kit' +import { isIgnored, tryResolveModule, useNuxt } from '@nuxt/kit' import { interopDefault } from 'mlly' import { debounce } from 'perfect-debounce' import { normalize } from 'pathe' @@ -55,43 +56,20 @@ const watchEvents: Record { - if (err) { return } - for (const event of events) { - if (isIgnored(event.path)) { continue } - nuxt.callHook('builder:watch', watchEvents[event.type], normalize(event.path)) - } - }, { - ignore: [ - ...nuxt.options.ignore, - '.nuxt', - 'node_modules' - ] - }) - watcher.then((subscription) => { - if (nuxt.options.debug) { - console.timeEnd('[nuxt] builder:parcel:watch') - } - nuxt.hook('close', () => subscription.unsubscribe()) - }) - } - return - } - console.warn('[nuxt] falling back to `chokidar` as `@parcel/watcher` cannot be resolved in your project.') + const success = await createParcelWatcher() + if (success) { return } } - if (nuxt.options.debug) { - console.time('[nuxt] builder:chokidar:watch') + if (nuxt.options.experimental.watcher === 'chokidar') { + return createWatcher() } + return createGranularWatcher() +} + +function createWatcher () { + const nuxt = useNuxt() + const watcher = chokidar.watch(nuxt.options._layers.map(i => i.config.srcDir as string).filter(Boolean), { ...nuxt.options.watchers.chokidar, cwd: nuxt.options.srcDir, @@ -103,14 +81,90 @@ async function watch (nuxt: Nuxt) { ] }) - if (nuxt.options.debug) { - watcher.on('ready', () => console.timeEnd('[nuxt] builder:chokidar:watch')) - } - watcher.on('all', (event, path) => nuxt.callHook('builder:watch', event, normalize(path))) nuxt.hook('close', () => watcher.close()) } +function createGranularWatcher () { + const nuxt = useNuxt() + + if (nuxt.options.debug) { + console.time('[nuxt] builder:chokidar:watch') + } + + let pending = 0 + + const ignoredDirs = new Set([...nuxt.options.modulesDir, nuxt.options.buildDir]) + const pathsToWatch = nuxt.options._layers.map(layer => layer.config.srcDir).filter(d => d && !isIgnored(d)) + for (const path of nuxt.options.watch) { + if (typeof path !== 'string') { continue } + if (pathsToWatch.some(w => path.startsWith(w.replace(/[^/]$/, '$&/')))) { continue } + pathsToWatch.push(path) + } + for (const dir of pathsToWatch) { + pending++ + const watcher = chokidar.watch(dir, { ...nuxt.options.watchers.chokidar, ignoreInitial: false, depth: 0, ignored: [isIgnored] }) + const watchers: Record = {} + + watcher.on('all', (event, path) => { + if (!pending) { + nuxt.callHook('builder:watch', event, normalize(path)) + } + if (event === 'unlinkDir' && path in watchers) { + watchers[path].close() + delete watchers[path] + } + if (event === 'addDir' && path !== dir && !ignoredDirs.has(path) && !(path in watchers) && !isIgnored(path)) { + watchers[path] = chokidar.watch(path, { ...nuxt.options.watchers.chokidar, ignored: [isIgnored] }) + watchers[path].on('all', (event, path) => nuxt.callHook('builder:watch', event, normalize(path))) + nuxt.hook('close', () => watchers[path].close()) + } + }) + watcher.on('ready', () => { + pending-- + if (nuxt.options.debug && !pending) { + console.timeEnd('[nuxt] builder:chokidar:watch') + } + }) + } +} + +async function createParcelWatcher () { + const nuxt = useNuxt() + if (nuxt.options.debug) { + console.time('[nuxt] builder:parcel:watch') + } + const watcherPath = await tryResolveModule('@parcel/watcher', [nuxt.options.rootDir, ...nuxt.options.modulesDir]) + if (watcherPath) { + const { subscribe } = await import(pathToFileURL(watcherPath).href).then(interopDefault) as typeof import('@parcel/watcher') + for (const layer of nuxt.options._layers) { + if (!layer.config.srcDir) { continue } + const watcher = subscribe(layer.config.srcDir, (err, events) => { + if (err) { return } + for (const event of events) { + if (isIgnored(event.path)) { continue } + nuxt.callHook('builder:watch', watchEvents[event.type], normalize(event.path)) + } + }, { + ignore: [ + ...nuxt.options.ignore, + '.nuxt', + 'node_modules' + ] + }) + watcher.then((subscription) => { + if (nuxt.options.debug) { + console.timeEnd('[nuxt] builder:parcel:watch') + } + nuxt.hook('close', () => subscription.unsubscribe()) + }) + } + return true + } + console.warn('[nuxt] falling back to `chokidar-granular` as `@parcel/watcher` cannot be resolved in your project.') + return false +} + async function bundle (nuxt: Nuxt) { try { const { bundle } = typeof nuxt.options.builder === 'string' diff --git a/packages/nuxt/src/core/schema.ts b/packages/nuxt/src/core/schema.ts index e2f4d3b3cf..d529427b72 100644 --- a/packages/nuxt/src/core/schema.ts +++ b/packages/nuxt/src/core/schema.ts @@ -76,7 +76,7 @@ export default defineNuxtModule({ } return } - console.warn('[nuxt] falling back to `chokidar` as `@parcel/watcher` cannot be resolved in your project.') + console.warn('[nuxt] falling back to `chokidar-granular` as `@parcel/watcher` cannot be resolved in your project.') } const filesToWatch = await Promise.all(nuxt.options._layers.map(layer => diff --git a/packages/schema/src/config/common.ts b/packages/schema/src/config/common.ts index 16ebeaeef0..a60746a7fe 100644 --- a/packages/schema/src/config/common.ts +++ b/packages/schema/src/config/common.ts @@ -145,7 +145,7 @@ export default defineUntypedSchema({ * If a relative path is specified, it will be relative to your `rootDir`. */ analyzeDir: { - $resolve: async (val, get) => val + $resolve: async (val, get) => val ? resolve(await get('rootDir'), val) : resolve(await get('buildDir'), 'analyze') }, @@ -358,6 +358,7 @@ export default defineUntypedSchema({ '.output', '.git', await get('analyzeDir'), + await get('buildDir'), await get('ignorePrefix') && `**/${await get('ignorePrefix')}*.*` ].concat(val).filter(Boolean) }, diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts index a0429f6549..136a9f9b13 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -161,15 +161,19 @@ export default defineUntypedSchema({ /** * Set an alternative watcher that will be used as the watching service for Nuxt. * - * Nuxt uses 'chokidar' by default, but by setting this to `parcel` it will use - * `@parcel/watcher` instead. This may improve performance in large projects or - * on Windows platforms. + * Nuxt uses 'chokidar-granular' by default, which will ignore top-level directories + * (like `node_modules` and `.git`) that are excluded from watching. + * + * You can set this instead to `parcel` to use `@parcel/watcher`, which may improve + * performance in large projects or on Windows platforms. + * + * You can also set this to `chokidar` to watch all files in your source directory. * * @see https://github.com/paulmillr/chokidar * @see https://github.com/parcel-bundler/watcher * @default chokidar - * @type {'chokidar' | 'parcel'} + * @type {'chokidar' | 'parcel' | 'chokidar-granular'} */ - watcher: 'chokidar' + watcher: 'chokidar-granular' } })