perf(nuxt): use granular watcher to avoid crawling ignored dirs (#20836)

This commit is contained in:
Daniel Roe 2023-05-18 14:44:24 +01:00 committed by GitHub
parent ef8b5b593c
commit 9dea9bc1b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 104 additions and 45 deletions

View File

@ -1,7 +1,8 @@
import { pathToFileURL } from 'node:url' import { pathToFileURL } from 'node:url'
import type { EventType } from '@parcel/watcher' import type { EventType } from '@parcel/watcher'
import type { FSWatcher } from 'chokidar'
import chokidar from 'chokidar' import chokidar from 'chokidar'
import { isIgnored, tryResolveModule } from '@nuxt/kit' import { isIgnored, tryResolveModule, useNuxt } from '@nuxt/kit'
import { interopDefault } from 'mlly' import { interopDefault } from 'mlly'
import { debounce } from 'perfect-debounce' import { debounce } from 'perfect-debounce'
import { normalize } from 'pathe' import { normalize } from 'pathe'
@ -55,43 +56,20 @@ const watchEvents: Record<EventType, 'add' | 'addDir' | 'change' | 'unlink' | 'u
async function watch (nuxt: Nuxt) { async function watch (nuxt: Nuxt) {
if (nuxt.options.experimental.watcher === 'parcel') { if (nuxt.options.experimental.watcher === 'parcel') {
if (nuxt.options.debug) { const success = await createParcelWatcher()
console.time('[nuxt] builder:parcel:watch') if (success) { return }
}
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
}
console.warn('[nuxt] falling back to `chokidar` as `@parcel/watcher` cannot be resolved in your project.')
} }
if (nuxt.options.debug) { if (nuxt.options.experimental.watcher === 'chokidar') {
console.time('[nuxt] builder:chokidar:watch') 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), { const watcher = chokidar.watch(nuxt.options._layers.map(i => i.config.srcDir as string).filter(Boolean), {
...nuxt.options.watchers.chokidar, ...nuxt.options.watchers.chokidar,
cwd: nuxt.options.srcDir, 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))) watcher.on('all', (event, path) => nuxt.callHook('builder:watch', event, normalize(path)))
nuxt.hook('close', () => watcher.close()) 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<string, FSWatcher> = {}
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) { async function bundle (nuxt: Nuxt) {
try { try {
const { bundle } = typeof nuxt.options.builder === 'string' const { bundle } = typeof nuxt.options.builder === 'string'

View File

@ -76,7 +76,7 @@ export default defineNuxtModule({
} }
return 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 => const filesToWatch = await Promise.all(nuxt.options._layers.map(layer =>

View File

@ -145,7 +145,7 @@ export default defineUntypedSchema({
* If a relative path is specified, it will be relative to your `rootDir`. * If a relative path is specified, it will be relative to your `rootDir`.
*/ */
analyzeDir: { analyzeDir: {
$resolve: async (val, get) => val $resolve: async (val, get) => val
? resolve(await get('rootDir'), val) ? resolve(await get('rootDir'), val)
: resolve(await get('buildDir'), 'analyze') : resolve(await get('buildDir'), 'analyze')
}, },
@ -358,6 +358,7 @@ export default defineUntypedSchema({
'.output', '.output',
'.git', '.git',
await get('analyzeDir'), await get('analyzeDir'),
await get('buildDir'),
await get('ignorePrefix') && `**/${await get('ignorePrefix')}*.*` await get('ignorePrefix') && `**/${await get('ignorePrefix')}*.*`
].concat(val).filter(Boolean) ].concat(val).filter(Boolean)
}, },

View File

@ -161,15 +161,19 @@ export default defineUntypedSchema({
/** /**
* Set an alternative watcher that will be used as the watching service for Nuxt. * 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 * Nuxt uses 'chokidar-granular' by default, which will ignore top-level directories
* `@parcel/watcher` instead. This may improve performance in large projects or * (like `node_modules` and `.git`) that are excluded from watching.
* on Windows platforms. *
* 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/paulmillr/chokidar
* @see https://github.com/parcel-bundler/watcher * @see https://github.com/parcel-bundler/watcher
* @default chokidar * @default chokidar
* @type {'chokidar' | 'parcel'} * @type {'chokidar' | 'parcel' | 'chokidar-granular'}
*/ */
watcher: 'chokidar' watcher: 'chokidar-granular'
} }
}) })