diff --git a/packages/nuxi/src/commands/dev.ts b/packages/nuxi/src/commands/dev.ts index bd27389d43..3eb1594a2b 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -1,7 +1,6 @@ import type { AddressInfo } from 'node:net' import type { RequestListener } from 'node:http' -import { existsSync, readdirSync } from 'node:fs' -import { resolve, relative, normalize } from 'pathe' +import { resolve, relative } from 'pathe' import chokidar from 'chokidar' import { debounce } from 'perfect-debounce' import type { Nuxt } from '@nuxt/schema' @@ -160,42 +159,11 @@ export default defineNuxtCommand({ // Watch for config changes // TODO: Watcher service, modules, and requireTree const dLoad = debounce(load) - const watcher = chokidar.watch([rootDir], { ignoreInitial: true, depth: 1 }) - watcher.on('all', (event, _file) => { - if (!currentNuxt) { return } - const file = normalize(_file) - const buildDir = withTrailingSlash(normalize(currentNuxt.options.buildDir)) - if (file.startsWith(buildDir)) { return } - const relativePath = relative(rootDir, file) - if (file.match(/(nuxt\.config\.(js|ts|mjs|cjs)|\.nuxtignore|\.env|\.nuxtrc)$/)) { - dLoad(true, `${relativePath} updated`) - } - - const isDirChange = ['addDir', 'unlinkDir'].includes(event) - const isFileChange = ['add', 'unlink'].includes(event) - const pagesDir = resolve(currentNuxt.options.srcDir, currentNuxt.options.dir.pages) - const reloadDirs = ['components', 'composables', 'utils'].map(d => resolve(currentNuxt.options.srcDir, d)) - - if (isDirChange) { - if (reloadDirs.includes(file)) { - return dLoad(true, `Directory \`${relativePath}/\` ${event === 'addDir' ? 'created' : 'removed'}`) - } - } - - if (isFileChange) { - if (file.match(/(app|error|app\.config)\.(js|ts|mjs|jsx|tsx|vue)$/)) { - return dLoad(true, `\`${relativePath}\` ${event === 'add' ? 'created' : 'removed'}`) - } - } - - if (file.startsWith(pagesDir)) { - const hasPages = existsSync(pagesDir) ? readdirSync(pagesDir).length > 0 : false - if (currentNuxt && !currentNuxt.options.pages && hasPages) { - return dLoad(true, 'Pages enabled') - } - if (currentNuxt && currentNuxt.options.pages && !hasPages) { - return dLoad(true, 'Pages disabled') - } + const watcher = chokidar.watch([rootDir], { ignoreInitial: true, depth: 0 }) + watcher.on('all', (_event, _file) => { + const file = relative(rootDir, _file) + if (file.match(/^(nuxt\.config\.(js|ts|mjs|cjs)|\.nuxtignore|\.env|\.nuxtrc)$/)) { + dLoad(true, `${file} updated`) } }) diff --git a/packages/nuxt/src/components/module.ts b/packages/nuxt/src/components/module.ts index 716b611b91..324572405e 100644 --- a/packages/nuxt/src/components/module.ts +++ b/packages/nuxt/src/components/module.ts @@ -148,6 +148,17 @@ export default defineNuxtModule({ } }) + // Restart dev server when component directories are added/removed + nuxt.hook('builder:watch', (event, path) => { + const isDirChange = ['addDir', 'unlinkDir'].includes(event) + const fullPath = resolve(nuxt.options.srcDir, path) + + if (isDirChange && componentDirs.some(dir => dir.path === fullPath)) { + console.info(`Directory \`${path}/\` ${event === 'addDir' ? 'created' : 'removed'}`) + return nuxt.callHook('restart') + } + }) + // Scan components and add to plugin nuxt.hook('app:templates', async () => { const newComponents = await scanComponents(componentDirs, nuxt.options.srcDir!) diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index 2e7423100b..f852088c20 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -161,10 +161,6 @@ async function initNuxt (nuxt: Nuxt) { // Register user and then ad-hoc modules modulesToInstall.push(...nuxt.options.modules, ...nuxt.options._modules) - nuxt.hooks.hookOnce('builder:watch', (event, path) => { - if (watchedPaths.has(path)) { nuxt.callHook('restart', { hard: true }) } - }) - // Add addComponent({ name: 'NuxtWelcome', @@ -285,6 +281,29 @@ async function initNuxt (nuxt: Nuxt) { await nuxt.callHook('modules:done') + nuxt.hooks.hook('builder:watch', (event, path) => { + // Local module patterns + if (watchedPaths.has(path)) { + return nuxt.callHook('restart', { hard: true }) + } + + // User provided patterns + for (const pattern of nuxt.options.watch) { + if (typeof pattern === 'string') { + if (pattern === path) { return nuxt.callHook('restart') } + continue + } + if (pattern.test(path)) { return nuxt.callHook('restart') } + } + + // Core Nuxt files: app.vue, error.vue and app.config.ts + const isFileChange = ['add', 'unlink'].includes(event) + if (isFileChange && path.match(/^(app|error|app\.config)\.(js|ts|mjs|jsx|tsx|vue)$/i)) { + console.info(`\`${path}\` ${event === 'add' ? 'created' : 'removed'}`) + return nuxt.callHook('restart') + } + }) + // Normalize windows transpile paths added by modules nuxt.options.build.transpile = nuxt.options.build.transpile.map(t => typeof t === 'string' ? normalize(t) : t) diff --git a/packages/nuxt/src/imports/module.ts b/packages/nuxt/src/imports/module.ts index 9d0333b24a..dc86779b18 100644 --- a/packages/nuxt/src/imports/module.ts +++ b/packages/nuxt/src/imports/module.ts @@ -61,6 +61,17 @@ export default defineNuxtModule>({ await nuxt.callHook('imports:dirs', composablesDirs) composablesDirs = composablesDirs.map(dir => normalize(dir)) + // Restart nuxt when composable directories are added/removed + nuxt.hook('builder:watch', (event, path) => { + const isDirChange = ['addDir', 'unlinkDir'].includes(event) + const fullPath = resolve(nuxt.options.srcDir, path) + + if (isDirChange && composablesDirs.includes(fullPath)) { + console.info(`Directory \`${path}/\` ${event === 'addDir' ? 'created' : 'removed'}`) + return nuxt.callHook('restart') + } + }) + // Support for importing from '#imports' addTemplate({ filename: 'imports.mjs', diff --git a/packages/nuxt/src/pages/module.ts b/packages/nuxt/src/pages/module.ts index 69745092d0..f9bb321a0e 100644 --- a/packages/nuxt/src/pages/module.ts +++ b/packages/nuxt/src/pages/module.ts @@ -1,6 +1,6 @@ import { existsSync, readdirSync } from 'node:fs' import { defineNuxtModule, addTemplate, addPlugin, addVitePlugin, addWebpackPlugin, findPath, addComponent, updateTemplates } from '@nuxt/kit' -import { relative, resolve } from 'pathe' +import { join, relative, resolve } from 'pathe' import { genString, genImport, genObjectFromRawEntries } from 'knitwork' import escapeRE from 'escape-string-regexp' import { joinURL } from 'ufo' @@ -21,9 +21,10 @@ export default defineNuxtModule({ // Disable module (and use universal router) if pages dir do not exists or user has disabled it const isNonEmptyDir = (dir: string) => existsSync(dir) && readdirSync(dir).length + const userPreference = nuxt.options.pages const isPagesEnabled = () => { - if (typeof nuxt.options.pages === 'boolean') { - return nuxt.options.pages + if (typeof userPreference === 'boolean') { + return userPreference } if (nuxt.options._layers.some(layer => existsSync(resolve(layer.config.srcDir, 'app/router.options.ts')))) { return true @@ -35,6 +36,22 @@ export default defineNuxtModule({ } nuxt.options.pages = isPagesEnabled() + // Restart Nuxt when pages dir is added or removed + const restartPaths = nuxt.options._layers.flatMap(layer => [ + join(layer.config.srcDir, 'app/router.options.ts'), + join(layer.config.srcDir, layer.config.dir?.pages || 'pages') + ]) + nuxt.hooks.hook('builder:watch', (event, path) => { + const fullPath = join(nuxt.options.srcDir, path) + if (restartPaths.some(path => path === fullPath || fullPath.startsWith(path + '/'))) { + const newSetting = isPagesEnabled() + if (nuxt.options.pages !== newSetting) { + console.info('Pages', newSetting ? 'enabled' : 'disabled') + return nuxt.callHook('restart') + } + } + }) + if (!nuxt.options.pages) { addPlugin(resolve(distDir, 'app/plugins/router')) addTemplate({ diff --git a/packages/schema/src/config/common.ts b/packages/schema/src/config/common.ts index cda2f8b736..7270458e9a 100644 --- a/packages/schema/src/config/common.ts +++ b/packages/schema/src/config/common.ts @@ -345,6 +345,18 @@ export default defineUntypedSchema({ ].concat(val).filter(Boolean) }, + /** + * The watch property lets you define patterns that will restart the Nuxt dev server when changed. + * + * It is an array of strings or regular expressions, which will be matched against the file path + * relative to the project `srcDir`. + * + * @type {Array} + */ + watch: { + $resolve: val => [].concat(val).filter((b: unknown) => typeof b === 'string' || b instanceof RegExp), + }, + /** * The watchers property lets you overwrite watchers configuration in your `nuxt.config`. */