From d9faf678416ccbcfd813147fdc7a4a3bec56c272 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sat, 13 May 2023 20:24:28 +0100 Subject: [PATCH 01/10] perf(nuxt): create granular watchers to avoid parsing dep --- packages/nuxt/src/core/builder.ts | 46 +++++++++++++++++++--------- packages/schema/src/config/common.ts | 3 +- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/packages/nuxt/src/core/builder.ts b/packages/nuxt/src/core/builder.ts index de0cb9e46f9b..a3ceb68273a4 100644 --- a/packages/nuxt/src/core/builder.ts +++ b/packages/nuxt/src/core/builder.ts @@ -1,5 +1,6 @@ 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 { interopDefault } from 'mlly' @@ -92,23 +93,38 @@ async function watch (nuxt: Nuxt) { console.time('[nuxt] builder:chokidar:watch') } - const watcher = chokidar.watch(nuxt.options._layers.map(i => i.config.srcDir as string).filter(Boolean), { - ...nuxt.options.watchers.chokidar, - cwd: nuxt.options.srcDir, - ignoreInitial: true, - ignored: [ - isIgnored, - '.nuxt', - 'node_modules' - ] - }) + let pending = 0 - if (nuxt.options.debug) { - watcher.on('ready', () => console.timeEnd('[nuxt] builder:chokidar:watch')) - } + const ignoredDirs = new Set([...nuxt.options.modulesDir, nuxt.options.buildDir]) + for (const layer of nuxt.options._layers) { + if (!layer.config.srcDir || isIgnored(layer.config.srcDir)) { continue } + pending++ + + const dir = layer.config.srcDir + const watcher = chokidar.watch(dir, { depth: 0, ignored: [isIgnored] }) + const watchers: Record = {} - watcher.on('all', (event, path) => nuxt.callHook('builder:watch', event, normalize(path))) - nuxt.hook('close', () => watcher.close()) + watcher.on('all', (event, path) => { + if (!pending || path !== dir) { + 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, { ignoreInitial: true, 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 bundle (nuxt: Nuxt) { diff --git a/packages/schema/src/config/common.ts b/packages/schema/src/config/common.ts index 16ebeaeef064..a60746a7fefc 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) }, From 42546c431749d0c1ad1a19324848d1e0f4df0ba4 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sat, 13 May 2023 20:28:17 +0100 Subject: [PATCH 02/10] feat: watch any `watch` paths outside of `srcDir` --- packages/nuxt/src/core/builder.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/nuxt/src/core/builder.ts b/packages/nuxt/src/core/builder.ts index a3ceb68273a4..b0df9681f78b 100644 --- a/packages/nuxt/src/core/builder.ts +++ b/packages/nuxt/src/core/builder.ts @@ -96,11 +96,14 @@ async function watch (nuxt: Nuxt) { let pending = 0 const ignoredDirs = new Set([...nuxt.options.modulesDir, nuxt.options.buildDir]) - for (const layer of nuxt.options._layers) { - if (!layer.config.srcDir || isIgnored(layer.config.srcDir)) { continue } + 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 dir = layer.config.srcDir const watcher = chokidar.watch(dir, { depth: 0, ignored: [isIgnored] }) const watchers: Record = {} From 08142a7634568805b317d139b86d3a3da3dbc57d Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sat, 13 May 2023 20:28:51 +0100 Subject: [PATCH 03/10] feat: respect `watchers.chokdar` options --- packages/nuxt/src/core/builder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/core/builder.ts b/packages/nuxt/src/core/builder.ts index b0df9681f78b..073936f3a48d 100644 --- a/packages/nuxt/src/core/builder.ts +++ b/packages/nuxt/src/core/builder.ts @@ -104,7 +104,7 @@ async function watch (nuxt: Nuxt) { } for (const dir of pathsToWatch) { pending++ - const watcher = chokidar.watch(dir, { depth: 0, ignored: [isIgnored] }) + const watcher = chokidar.watch(dir, { ...nuxt.options.watchers.chokidar, depth: 0, ignored: [isIgnored] }) const watchers: Record = {} watcher.on('all', (event, path) => { @@ -116,7 +116,7 @@ async function watch (nuxt: Nuxt) { delete watchers[path] } if (event === 'addDir' && path !== dir && !ignoredDirs.has(path) && !(path in watchers) && !isIgnored(path)) { - watchers[path] = chokidar.watch(path, { ignoreInitial: true, ignored: [isIgnored] }) + watchers[path] = chokidar.watch(path, { ...nuxt.options.watchers.chokidar, ignoreInitial: true, ignored: [isIgnored] }) watchers[path].on('all', (event, path) => nuxt.callHook('builder:watch', event, normalize(path))) nuxt.hook('close', () => watchers[path].close()) } From 35a60d384b242fd5a5acfe0a587fea3038c93701 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sat, 13 May 2023 20:59:10 +0100 Subject: [PATCH 04/10] fix: override ignoreInitial for watchers launch --- packages/nuxt/src/core/builder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/core/builder.ts b/packages/nuxt/src/core/builder.ts index 073936f3a48d..cd26495a6ed6 100644 --- a/packages/nuxt/src/core/builder.ts +++ b/packages/nuxt/src/core/builder.ts @@ -104,7 +104,7 @@ async function watch (nuxt: Nuxt) { } for (const dir of pathsToWatch) { pending++ - const watcher = chokidar.watch(dir, { ...nuxt.options.watchers.chokidar, depth: 0, ignored: [isIgnored] }) + const watcher = chokidar.watch(dir, { ...nuxt.options.watchers.chokidar, ignoreInitial: false, depth: 0, ignored: [isIgnored] }) const watchers: Record = {} watcher.on('all', (event, path) => { @@ -116,7 +116,7 @@ async function watch (nuxt: Nuxt) { 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, ignoreInitial: true, ignored: [isIgnored] }) + 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()) } From e7f1e9830cc128834ffc136d7731cdfe06d6d899 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sat, 13 May 2023 21:02:10 +0100 Subject: [PATCH 05/10] fix: ignore events emitted while starting watchers --- packages/nuxt/src/core/builder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/core/builder.ts b/packages/nuxt/src/core/builder.ts index cd26495a6ed6..1589d076aa16 100644 --- a/packages/nuxt/src/core/builder.ts +++ b/packages/nuxt/src/core/builder.ts @@ -108,7 +108,7 @@ async function watch (nuxt: Nuxt) { const watchers: Record = {} watcher.on('all', (event, path) => { - if (!pending || path !== dir) { + if (!pending) { nuxt.callHook('builder:watch', event, normalize(path)) } if (event === 'unlinkDir' && path in watchers) { From b67ce967341d39933d132e50a48c332a3ed1f6e2 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 16 May 2023 09:38:42 +0100 Subject: [PATCH 06/10] refactor: extract as alternative strategy --- packages/nuxt/src/core/builder.ts | 99 +++++++++++++++------- packages/schema/src/config/experimental.ts | 5 +- 2 files changed, 71 insertions(+), 33 deletions(-) diff --git a/packages/nuxt/src/core/builder.ts b/packages/nuxt/src/core/builder.ts index 1589d076aa16..771d71977089 100644 --- a/packages/nuxt/src/core/builder.ts +++ b/packages/nuxt/src/core/builder.ts @@ -2,7 +2,7 @@ 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' @@ -56,39 +56,38 @@ 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.experimental.watcher === 'granular') { + return createGranularWatcher() } + return createWatcher() +} + +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, + ignoreInitial: true, + ignored: [ + isIgnored, + '.nuxt', + 'node_modules' + ] + }) + + 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') } @@ -130,6 +129,42 @@ async function watch (nuxt: Nuxt) { } } +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` 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/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts index a0429f654996..b718bec0aa6d 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -165,10 +165,13 @@ export default defineUntypedSchema({ * `@parcel/watcher` instead. This may improve performance in large projects or * on Windows platforms. * + * You can also try setting this to `granular` to use an experimental granular + * watcher, which ignores top-level directories (like `node_modules` and `.git`). + * * @see https://github.com/paulmillr/chokidar * @see https://github.com/parcel-bundler/watcher * @default chokidar - * @type {'chokidar' | 'parcel'} + * @type {'chokidar' | 'parcel' | 'granular'} */ watcher: 'chokidar' } From c675e5086b6632f274ddb0f6b597f769311f9038 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 16 May 2023 09:39:09 +0100 Subject: [PATCH 07/10] test: run test fixtures with new strategy --- test/fixtures/basic/nuxt.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts index 7e9fdbca0a5a..59c7f7870ca3 100644 --- a/test/fixtures/basic/nuxt.config.ts +++ b/test/fixtures/basic/nuxt.config.ts @@ -197,6 +197,7 @@ export default defineNuxtConfig({ } }, experimental: { + watcher: 'granular', typedPages: true, polyfillVueUseHead: true, renderJsonPayloads: process.env.TEST_PAYLOAD !== 'js', From 60ed65532b64151cd2428ff16ea01a01a381c794 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 18 May 2023 14:17:07 +0100 Subject: [PATCH 08/10] refactor: enable by default and rename to `chokidar-granular` --- packages/nuxt/src/core/builder.ts | 6 +++--- packages/schema/src/config/experimental.ts | 15 ++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/nuxt/src/core/builder.ts b/packages/nuxt/src/core/builder.ts index 771d71977089..52d4db954cea 100644 --- a/packages/nuxt/src/core/builder.ts +++ b/packages/nuxt/src/core/builder.ts @@ -60,11 +60,11 @@ async function watch (nuxt: Nuxt) { if (success) { return } } - if (nuxt.options.experimental.watcher === 'granular') { - return createGranularWatcher() + if (nuxt.options.experimental.watcher === 'chokidar') { + return createWatcher() } - return createWatcher() + return createGranularWatcher() } function createWatcher () { diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts index b718bec0aa6d..136a9f9b1365 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -161,18 +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 also try setting this to `granular` to use an experimental granular - * watcher, which ignores top-level directories (like `node_modules` and `.git`). + * 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' | 'granular'} + * @type {'chokidar' | 'parcel' | 'chokidar-granular'} */ - watcher: 'chokidar' + watcher: 'chokidar-granular' } }) From 3597adc43bb23e980d67d785f9b919cbed14461d Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 18 May 2023 14:22:16 +0100 Subject: [PATCH 09/10] fix: update console warns --- packages/nuxt/src/core/builder.ts | 2 +- packages/nuxt/src/core/schema.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/core/builder.ts b/packages/nuxt/src/core/builder.ts index 52d4db954cea..b17daf7141b5 100644 --- a/packages/nuxt/src/core/builder.ts +++ b/packages/nuxt/src/core/builder.ts @@ -161,7 +161,7 @@ async function createParcelWatcher () { } return true } - 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.') return false } diff --git a/packages/nuxt/src/core/schema.ts b/packages/nuxt/src/core/schema.ts index e2f4d3b3cf0b..d529427b724e 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 => From 1138fa66e672605df5ef8a935556265e23c3efb6 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 18 May 2023 14:22:37 +0100 Subject: [PATCH 10/10] test: remove experimental flag --- test/fixtures/basic/nuxt.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts index 59c7f7870ca3..7e9fdbca0a5a 100644 --- a/test/fixtures/basic/nuxt.config.ts +++ b/test/fixtures/basic/nuxt.config.ts @@ -197,7 +197,6 @@ export default defineNuxtConfig({ } }, experimental: { - watcher: 'granular', typedPages: true, polyfillVueUseHead: true, renderJsonPayloads: process.env.TEST_PAYLOAD !== 'js',