feat(nuxt): allow specifying glob patterns for scanning pages/ (#31090)

This commit is contained in:
Daniel Roe 2025-02-24 10:44:55 +00:00 committed by GitHub
parent 35df23f41f
commit 74ffa8db80
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 45 additions and 37 deletions

View File

@ -4,7 +4,7 @@ import { addBuildPlugin, addComponent, addPlugin, addTemplate, addTypeTemplate,
import { dirname, join, relative, resolve } from 'pathe' import { dirname, join, relative, resolve } from 'pathe'
import { genImport, genObjectFromRawEntries, genString } from 'knitwork' import { genImport, genObjectFromRawEntries, genString } from 'knitwork'
import { joinURL } from 'ufo' import { joinURL } from 'ufo'
import type { NuxtPage } from 'nuxt/schema' import type { Nuxt, NuxtPage } from 'nuxt/schema'
import { createRoutesContext } from 'unplugin-vue-router' import { createRoutesContext } from 'unplugin-vue-router'
import { resolveOptions } from 'unplugin-vue-router/options' import { resolveOptions } from 'unplugin-vue-router/options'
import type { EditableTreeNode, Options as TypedRouterOptions } from 'unplugin-vue-router' import type { EditableTreeNode, Options as TypedRouterOptions } from 'unplugin-vue-router'
@ -22,29 +22,9 @@ import { RouteInjectionPlugin } from './plugins/route-injection'
const OPTIONAL_PARAM_RE = /^\/?:.*(?:\?|\(\.\*\)\*)$/ const OPTIONAL_PARAM_RE = /^\/?:.*(?:\?|\(\.\*\)\*)$/
export default defineNuxtModule({
meta: {
name: 'nuxt:pages',
configKey: 'pages',
},
async setup (_options, nuxt) {
const useExperimentalTypedPages = nuxt.options.experimental.typedPages
const runtimeDir = resolve(distDir, 'pages/runtime') const runtimeDir = resolve(distDir, 'pages/runtime')
const pagesDirs = nuxt.options._layers.map(
layer => resolve(layer.config.srcDir, (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options : layer.config).dir?.pages || 'pages'),
)
nuxt.options.alias['#vue-router'] = 'vue-router' async function resolveRouterOptions (nuxt: Nuxt, builtInRouterOptions: string) {
const routerPath = await resolveTypePath('vue-router', '', nuxt.options.modulesDir) || 'vue-router'
nuxt.hook('prepare:types', ({ tsConfig }) => {
tsConfig.compilerOptions ||= {}
tsConfig.compilerOptions.paths ||= {}
tsConfig.compilerOptions.paths['#vue-router'] = [routerPath]
delete tsConfig.compilerOptions.paths['#vue-router/*']
})
const builtInRouterOptions = await findPath(resolve(runtimeDir, 'router.options')) || resolve(runtimeDir, 'router.options')
async function resolveRouterOptions () {
const context = { const context = {
files: [] as Array<{ path: string, optional?: boolean }>, files: [] as Array<{ path: string, optional?: boolean }>,
} }
@ -61,14 +41,42 @@ export default defineNuxtModule({
return context.files 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 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'),
)
nuxt.options.alias['#vue-router'] = 'vue-router'
const routerPath = await resolveTypePath('vue-router', '', nuxt.options.modulesDir) || 'vue-router'
nuxt.hook('prepare:types', ({ tsConfig }) => {
tsConfig.compilerOptions ||= {}
tsConfig.compilerOptions.paths ||= {}
tsConfig.compilerOptions.paths['#vue-router'] = [routerPath]
delete tsConfig.compilerOptions.paths['#vue-router/*']
})
// Disable module (and use universal router) if pages dir do not exists or user has disabled it // 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 isNonEmptyDir = (dir: string) => existsSync(dir) && readdirSync(dir).length
const userPreference = nuxt.options.pages const userPreference = options.enabled
const isPagesEnabled = async () => { const isPagesEnabled = async () => {
if (typeof userPreference === 'boolean') { if (typeof userPreference === 'boolean') {
return userPreference return userPreference
} }
const routerOptionsFiles = await resolveRouterOptions() const routerOptionsFiles = await resolveRouterOptions(nuxt, builtInRouterOptions)
if (routerOptionsFiles.filter(p => !p.optional).length > 0) { if (routerOptionsFiles.filter(p => !p.optional).length > 0) {
return true return true
} }
@ -76,7 +84,7 @@ export default defineNuxtModule({
return true return true
} }
const pages = await resolvePagesRoutes(nuxt) const pages = await resolvePagesRoutes(options.pattern, nuxt)
if (pages.length) { if (pages.length) {
if (nuxt.apps.default) { if (nuxt.apps.default) {
nuxt.apps.default.pages = pages nuxt.apps.default.pages = pages
@ -86,9 +94,9 @@ export default defineNuxtModule({
return false 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 // Add plugin to check if pages are enabled without NuxtPage being instantiated
addPlugin(resolve(runtimeDir, 'plugins/check-if-page-unused')) addPlugin(resolve(runtimeDir, 'plugins/check-if-page-unused'))
} }
@ -112,14 +120,14 @@ export default defineNuxtModule({
const path = resolve(nuxt.options.srcDir, relativePath) const path = resolve(nuxt.options.srcDir, relativePath)
if (restartPaths.some(p => p === path || path.startsWith(p + '/'))) { if (restartPaths.some(p => p === path || path.startsWith(p + '/'))) {
const newSetting = await isPagesEnabled() const newSetting = await isPagesEnabled()
if (nuxt.options.pages !== newSetting) { if (options.enabled !== newSetting) {
logger.info('Pages', newSetting ? 'enabled' : 'disabled') logger.info('Pages', newSetting ? 'enabled' : 'disabled')
return nuxt.callHook('restart') return nuxt.callHook('restart')
} }
} }
}) })
if (!nuxt.options.pages) { if (!options.enabled) {
addPlugin(resolve(distDir, 'app/plugins/router')) addPlugin(resolve(distDir, 'app/plugins/router'))
addTemplate({ addTemplate({
filename: 'pages.mjs', filename: 'pages.mjs',
@ -171,13 +179,13 @@ export default defineNuxtModule({
if (useExperimentalTypedPages) { if (useExperimentalTypedPages) {
const declarationFile = './types/typed-router.d.ts' const declarationFile = './types/typed-router.d.ts'
const options: TypedRouterOptions = { const typedRouterOptions: TypedRouterOptions = {
routesFolder: [], routesFolder: [],
dts: resolve(nuxt.options.buildDir, declarationFile), dts: resolve(nuxt.options.buildDir, declarationFile),
logs: nuxt.options.debug && nuxt.options.debug.router, logs: nuxt.options.debug && nuxt.options.debug.router,
async beforeWriteFiles (rootPage) { async beforeWriteFiles (rootPage) {
rootPage.children.forEach(child => child.delete()) 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) { if (nuxt.apps.default) {
nuxt.apps.default.pages = pages nuxt.apps.default.pages = pages
} }
@ -218,7 +226,7 @@ export default defineNuxtModule({
references.push({ types: 'unplugin-vue-router/client' }) references.push({ types: 'unplugin-vue-router/client' })
}) })
const context = createRoutesContext(resolveOptions(options)) const context = createRoutesContext(resolveOptions(typedRouterOptions))
const dtsFile = resolve(nuxt.options.buildDir, declarationFile) const dtsFile = resolve(nuxt.options.buildDir, declarationFile)
await mkdir(dirname(dtsFile), { recursive: true }) await mkdir(dirname(dtsFile), { recursive: true })
await context.scanPages(false) await context.scanPages(false)
@ -269,7 +277,7 @@ export default defineNuxtModule({
} }
nuxt.hooks.hookOnce('app:templates', async (app) => { 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) => { nuxt.hook('builder:watch', async (event, relativePath) => {
@ -279,7 +287,7 @@ export default defineNuxtModule({
if (event === 'change' && !shouldAlwaysRegenerate) { return } if (event === 'change' && !shouldAlwaysRegenerate) { return }
if (shouldAlwaysRegenerate || updateTemplatePaths.some(dir => path.startsWith(dir))) { 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 // Add router options template
addTemplate({ addTemplate({
filename: 'router.options.mjs', filename: 'router.options.mjs',
getContents: async () => { getContents: async ({ nuxt }) => {
// Scan and register app/router.options files // 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) const configRouterOptions = genObjectFromRawEntries(Object.entries(nuxt.options.router.options)
.map(([key, value]) => [key, genString(value as string)])) .map(([key, value]) => [key, genString(value as string)]))

View File

@ -42,14 +42,14 @@ interface ScannedFile {
absolutePath: string absolutePath: string
} }
export async function resolvePagesRoutes (nuxt = useNuxt()): Promise<NuxtPage[]> { export async function resolvePagesRoutes (pattern: string | string[], nuxt = useNuxt()): Promise<NuxtPage[]> {
const pagesDirs = nuxt.options._layers.map( const pagesDirs = nuxt.options._layers.map(
layer => resolve(layer.config.srcDir, (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options : layer.config).dir?.pages || 'pages'), layer => resolve(layer.config.srcDir, (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options : layer.config).dir?.pages || 'pages'),
) )
const scannedFiles: ScannedFile[] = [] const scannedFiles: ScannedFile[] = []
for (const dir of pagesDirs) { 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 }))) scannedFiles.push(...files.map(file => ({ relativePath: relative(dir, file), absolutePath: file })))
} }

View File

@ -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 * 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. * enabled if you have a `pages/` directory in your source folder.
* @type {boolean} * @type {boolean | { enabled?: boolean, pattern?: string | string[] }}
*/ */
pages: undefined, pages: undefined,