From 74ffa8db809c1a99c464b1d321b6a610fd09e232 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 24 Feb 2025 10:44:55 +0000 Subject: [PATCH] feat(nuxt): allow specifying glob patterns for scanning `pages/` (#31090) --- packages/nuxt/src/pages/module.ts | 76 ++++++++++++++++------------- packages/nuxt/src/pages/utils.ts | 4 +- packages/schema/src/config/adhoc.ts | 2 +- 3 files changed, 45 insertions(+), 37 deletions(-) diff --git a/packages/nuxt/src/pages/module.ts b/packages/nuxt/src/pages/module.ts index ee3640ddb8..864a5701d5 100644 --- a/packages/nuxt/src/pages/module.ts +++ b/packages/nuxt/src/pages/module.ts @@ -4,7 +4,7 @@ import { addBuildPlugin, addComponent, addPlugin, addTemplate, addTypeTemplate, import { dirname, join, relative, resolve } from 'pathe' import { genImport, genObjectFromRawEntries, genString } from 'knitwork' import { joinURL } from 'ufo' -import type { NuxtPage } from 'nuxt/schema' +import type { Nuxt, NuxtPage } from 'nuxt/schema' import { createRoutesContext } from 'unplugin-vue-router' import { resolveOptions } from 'unplugin-vue-router/options' import type { EditableTreeNode, Options as TypedRouterOptions } from 'unplugin-vue-router' @@ -22,14 +22,40 @@ import { RouteInjectionPlugin } from './plugins/route-injection' const OPTIONAL_PARAM_RE = /^\/?:.*(?:\?|\(\.\*\)\*)$/ +const runtimeDir = resolve(distDir, 'pages/runtime') + +async function resolveRouterOptions (nuxt: Nuxt, builtInRouterOptions: string) { + const context = { + files: [] as Array<{ path: string, optional?: boolean }>, + } + + for (const layer of nuxt.options._layers) { + const path = await findPath(resolve(layer.config.srcDir, layer.config.dir?.app || 'app', 'router.options')) + if (path) { context.files.unshift({ path }) } + } + + // Add default options at beginning + context.files.unshift({ path: builtInRouterOptions, optional: true }) + + await nuxt.callHook('pages:routerOptions', context) + return context.files +} + export default defineNuxtModule({ meta: { name: 'nuxt:pages', configKey: 'pages', }, + defaults: nuxt => ({ + enabled: undefined as undefined | boolean, + pattern: `**/*{${nuxt.options.extensions.join(',')}}` as string | string[], + }), async setup (_options, nuxt) { + const options = typeof _options === 'boolean' ? { enabled: _options, pattern: `**/*{${nuxt.options.extensions.join(',')}}` } : _options + const useExperimentalTypedPages = nuxt.options.experimental.typedPages - const runtimeDir = resolve(distDir, 'pages/runtime') + const builtInRouterOptions = await findPath(resolve(runtimeDir, 'router.options')) || resolve(runtimeDir, 'router.options') + const pagesDirs = nuxt.options._layers.map( layer => resolve(layer.config.srcDir, (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options : layer.config).dir?.pages || 'pages'), ) @@ -43,32 +69,14 @@ export default defineNuxtModule({ delete tsConfig.compilerOptions.paths['#vue-router/*'] }) - const builtInRouterOptions = await findPath(resolve(runtimeDir, 'router.options')) || resolve(runtimeDir, 'router.options') - async function resolveRouterOptions () { - const context = { - files: [] as Array<{ path: string, optional?: boolean }>, - } - - for (const layer of nuxt.options._layers) { - const path = await findPath(resolve(layer.config.srcDir, layer.config.dir?.app || 'app', 'router.options')) - if (path) { context.files.unshift({ path }) } - } - - // Add default options at beginning - context.files.unshift({ path: builtInRouterOptions, optional: true }) - - await nuxt.callHook('pages:routerOptions', context) - return context.files - } - // 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 userPreference = options.enabled const isPagesEnabled = async () => { if (typeof userPreference === 'boolean') { return userPreference } - const routerOptionsFiles = await resolveRouterOptions() + const routerOptionsFiles = await resolveRouterOptions(nuxt, builtInRouterOptions) if (routerOptionsFiles.filter(p => !p.optional).length > 0) { return true } @@ -76,7 +84,7 @@ export default defineNuxtModule({ return true } - const pages = await resolvePagesRoutes(nuxt) + const pages = await resolvePagesRoutes(options.pattern, nuxt) if (pages.length) { if (nuxt.apps.default) { nuxt.apps.default.pages = pages @@ -86,9 +94,9 @@ export default defineNuxtModule({ return false } - nuxt.options.pages = await isPagesEnabled() + options.enabled = await isPagesEnabled() - if (nuxt.options.dev && nuxt.options.pages) { + if (nuxt.options.dev && options.enabled) { // Add plugin to check if pages are enabled without NuxtPage being instantiated addPlugin(resolve(runtimeDir, 'plugins/check-if-page-unused')) } @@ -112,14 +120,14 @@ export default defineNuxtModule({ const path = resolve(nuxt.options.srcDir, relativePath) if (restartPaths.some(p => p === path || path.startsWith(p + '/'))) { const newSetting = await isPagesEnabled() - if (nuxt.options.pages !== newSetting) { + if (options.enabled !== newSetting) { logger.info('Pages', newSetting ? 'enabled' : 'disabled') return nuxt.callHook('restart') } } }) - if (!nuxt.options.pages) { + if (!options.enabled) { addPlugin(resolve(distDir, 'app/plugins/router')) addTemplate({ filename: 'pages.mjs', @@ -171,13 +179,13 @@ export default defineNuxtModule({ if (useExperimentalTypedPages) { const declarationFile = './types/typed-router.d.ts' - const options: TypedRouterOptions = { + const typedRouterOptions: TypedRouterOptions = { routesFolder: [], dts: resolve(nuxt.options.buildDir, declarationFile), logs: nuxt.options.debug && nuxt.options.debug.router, async beforeWriteFiles (rootPage) { rootPage.children.forEach(child => child.delete()) - const pages = nuxt.apps.default?.pages || await resolvePagesRoutes(nuxt) + const pages = nuxt.apps.default?.pages || await resolvePagesRoutes(options.pattern, nuxt) if (nuxt.apps.default) { nuxt.apps.default.pages = pages } @@ -218,7 +226,7 @@ export default defineNuxtModule({ references.push({ types: 'unplugin-vue-router/client' }) }) - const context = createRoutesContext(resolveOptions(options)) + const context = createRoutesContext(resolveOptions(typedRouterOptions)) const dtsFile = resolve(nuxt.options.buildDir, declarationFile) await mkdir(dirname(dtsFile), { recursive: true }) await context.scanPages(false) @@ -269,7 +277,7 @@ export default defineNuxtModule({ } nuxt.hooks.hookOnce('app:templates', async (app) => { - app.pages ||= await resolvePagesRoutes(nuxt) + app.pages ||= await resolvePagesRoutes(options.pattern, nuxt) }) nuxt.hook('builder:watch', async (event, relativePath) => { @@ -279,7 +287,7 @@ export default defineNuxtModule({ if (event === 'change' && !shouldAlwaysRegenerate) { return } if (shouldAlwaysRegenerate || updateTemplatePaths.some(dir => path.startsWith(dir))) { - nuxt.apps.default!.pages = await resolvePagesRoutes(nuxt) + nuxt.apps.default!.pages = await resolvePagesRoutes(options.pattern, nuxt) } }) @@ -538,9 +546,9 @@ export default defineNuxtModule({ // Add router options template addTemplate({ filename: 'router.options.mjs', - getContents: async () => { + getContents: async ({ nuxt }) => { // Scan and register app/router.options files - const routerOptionsFiles = await resolveRouterOptions() + const routerOptionsFiles = await resolveRouterOptions(nuxt, builtInRouterOptions) const configRouterOptions = genObjectFromRawEntries(Object.entries(nuxt.options.router.options) .map(([key, value]) => [key, genString(value as string)])) diff --git a/packages/nuxt/src/pages/utils.ts b/packages/nuxt/src/pages/utils.ts index bb5d516123..21db600c0d 100644 --- a/packages/nuxt/src/pages/utils.ts +++ b/packages/nuxt/src/pages/utils.ts @@ -42,14 +42,14 @@ interface ScannedFile { absolutePath: string } -export async function resolvePagesRoutes (nuxt = useNuxt()): Promise { +export async function resolvePagesRoutes (pattern: string | string[], nuxt = useNuxt()): Promise { const pagesDirs = nuxt.options._layers.map( layer => resolve(layer.config.srcDir, (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options : layer.config).dir?.pages || 'pages'), ) const scannedFiles: ScannedFile[] = [] for (const dir of pagesDirs) { - const files = await resolveFiles(dir, `**/*{${nuxt.options.extensions.join(',')}}`) + const files = await resolveFiles(dir, pattern) scannedFiles.push(...files.map(file => ({ relativePath: relative(dir, file), absolutePath: file }))) } diff --git a/packages/schema/src/config/adhoc.ts b/packages/schema/src/config/adhoc.ts index 26942cc85d..728d7d9546 100644 --- a/packages/schema/src/config/adhoc.ts +++ b/packages/schema/src/config/adhoc.ts @@ -54,7 +54,7 @@ export default defineResolvers({ /** * Whether to use the vue-router integration in Nuxt 3. If you do not provide a value it will be * enabled if you have a `pages/` directory in your source folder. - * @type {boolean} + * @type {boolean | { enabled?: boolean, pattern?: string | string[] }} */ pages: undefined,