diff --git a/packages/nuxt3/package.json b/packages/nuxt3/package.json index 12458f98fb..36a2334d6e 100644 --- a/packages/nuxt3/package.json +++ b/packages/nuxt3/package.json @@ -33,6 +33,7 @@ "cookie-es": "^0.5.0", "defu": "^5.0.1", "destr": "^1.1.0", + "escape-string-regexp": "^5.0.0", "globby": "^13.1.0", "h3": "^0.3.9", "hash-sum": "^2.0.0", diff --git a/packages/nuxt3/src/auto-imports/context.ts b/packages/nuxt3/src/auto-imports/context.ts index 880685c827..dcec647874 100644 --- a/packages/nuxt3/src/auto-imports/context.ts +++ b/packages/nuxt3/src/auto-imports/context.ts @@ -1,4 +1,5 @@ import type { AutoImport } from '@nuxt/schema' +import escapeRE from 'escape-string-regexp' export interface AutoImportContext { autoImports: AutoImport[] @@ -36,7 +37,7 @@ export function updateAutoImportContext (ctx: AutoImportContext) { ctx.autoImports = ctx.autoImports.filter(i => i.disabled !== true) // Create regex - ctx.matchRE = new RegExp(`\\b(${ctx.autoImports.map(i => i.as).join('|')})\\b`, 'g') + ctx.matchRE = new RegExp(`\\b(${ctx.autoImports.map(i => escapeRE(i.as)).join('|')})\\b`, 'g') // Create map ctx.map.clear() diff --git a/packages/nuxt3/src/core/plugins/import-protection.ts b/packages/nuxt3/src/core/plugins/import-protection.ts index d6ff6446d1..6598ac9a6f 100644 --- a/packages/nuxt3/src/core/plugins/import-protection.ts +++ b/packages/nuxt3/src/core/plugins/import-protection.ts @@ -3,6 +3,7 @@ import { createUnplugin } from 'unplugin' import consola from 'consola' import { isAbsolute, relative, resolve } from 'pathe' import type { Nuxt } from '@nuxt/schema' +import escapeRE from 'escape-string-regexp' const _require = createRequire(import.meta.url) @@ -11,8 +12,6 @@ interface ImportProtectionOptions { patterns: [importPattern: string | RegExp, warning?: string][] } -const escapeRE = (str: string) => str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') - export const vueAppPatterns = (nuxt: Nuxt) => [ [/^(nuxt3|nuxt)/, '`nuxt3`/`nuxt` cannot be imported directly. Instead, import runtime Nuxt composables from `#app` or `#imports`.'], [/nuxt\.config/, 'Importing directly from a `nuxt.config` file is not allowed. Instead, use runtime config or a module.'], diff --git a/packages/nuxt3/src/core/templates.ts b/packages/nuxt3/src/core/templates.ts index 5955095b0a..41b58491a2 100644 --- a/packages/nuxt3/src/core/templates.ts +++ b/packages/nuxt3/src/core/templates.ts @@ -2,6 +2,7 @@ import { templateUtils } from '@nuxt/kit' import type { Nuxt, NuxtApp } from '@nuxt/schema' import { relative } from 'pathe' +import escapeRE from 'escape-string-regexp' type TemplateContext = { nuxt: Nuxt; @@ -94,7 +95,7 @@ export const pluginsDeclaration = { filename: 'plugins.d.ts', write: true, getContents: (ctx: TemplateContext) => { - const EXTENSION_RE = new RegExp(`(?<=\\w)(${ctx.nuxt.options.extensions.map(e => `\\${e}`).join('|')})$`, 'g') + const EXTENSION_RE = new RegExp(`(?<=\\w)(${ctx.nuxt.options.extensions.map(e => `\\${escapeRE(e)}`).join('|')})$`, 'g') const tsImports = ctx.app.plugins.map(p => relative(ctx.nuxt.options.buildDir, p.src).replace(EXTENSION_RE, '')) return `// Generated by Nuxt3' diff --git a/packages/nuxt3/src/pages/module.ts b/packages/nuxt3/src/pages/module.ts index 7e51754713..8fbd60b00b 100644 --- a/packages/nuxt3/src/pages/module.ts +++ b/packages/nuxt3/src/pages/module.ts @@ -1,6 +1,7 @@ import { existsSync } from 'fs' import { defineNuxtModule, addTemplate, addPlugin, templateUtils, addVitePlugin, addWebpackPlugin } from '@nuxt/kit' import { resolve } from 'pathe' +import escapeRE from 'escape-string-regexp' import { distDir } from '../dirs' import { resolveLayouts, resolvePagesRoutes, normalizeRoutes, resolveMiddleware, getImportName } from './utils' import { TransformMacroPlugin, TransformMacroPluginOptions } from './macros' @@ -25,7 +26,13 @@ export default defineNuxtModule({ // Regenerate templates when adding or removing pages nuxt.hook('builder:watch', async (event, path) => { - const pathPattern = new RegExp(`^(${nuxt.options.dir.pages}|${nuxt.options.dir.layouts})/`) + const dirs = [ + nuxt.options.dir.pages, + nuxt.options.dir.layouts, + nuxt.options.dir.middleware + ].filter(Boolean) + + const pathPattern = new RegExp(`^(${dirs.map(escapeRE).join('|')})/`) if (event !== 'change' && path.match(pathPattern)) { await nuxt.callHook('builder:generateApp') } @@ -83,13 +90,13 @@ export default defineNuxtModule({ filename: 'middleware.mjs', async getContents () { const middleware = await resolveMiddleware() - await nuxt.callHook('pages:middleware:extend', middleware) - const middlewareObject = Object.fromEntries(middleware.map(mw => [mw.name, `{() => import('${mw.path}')}`])) const globalMiddleware = middleware.filter(mw => mw.global) + const namedMiddleware = middleware.filter(mw => !mw.global) + const namedMiddlewareObject = Object.fromEntries(namedMiddleware.map(mw => [mw.name, `{() => import('${mw.path}')}`])) return [ ...globalMiddleware.map(mw => `import ${getImportName(mw.name)} from '${mw.path}'`), `export const globalMiddleware = [${globalMiddleware.map(mw => getImportName(mw.name)).join(', ')}]`, - `export const namedMiddleware = ${templateUtils.serialize(middlewareObject)}` + `export const namedMiddleware = ${templateUtils.serialize(namedMiddlewareObject)}` ].join('\n') } }) @@ -100,9 +107,10 @@ export default defineNuxtModule({ getContents: async () => { const composablesFile = resolve(runtimeDir, 'composables') const middleware = await resolveMiddleware() + const namedMiddleware = middleware.filter(mw => !mw.global) return [ 'import type { NavigationGuard } from \'vue-router\'', - `export type MiddlewareKey = ${middleware.map(mw => `"${mw.name}"`).join(' | ') || 'string'}`, + `export type MiddlewareKey = ${namedMiddleware.map(mw => `"${mw.name}"`).join(' | ') || 'string'}`, `declare module '${composablesFile}' {`, ' interface PageMeta {', ' middleware?: MiddlewareKey | NavigationGuard | Array', @@ -130,11 +138,6 @@ export default defineNuxtModule({ } }) - nuxt.hook('prepare:types', ({ references }) => { - references.push({ path: resolve(nuxt.options.buildDir, 'middleware.d.ts') }) - references.push({ path: resolve(nuxt.options.buildDir, 'layouts.d.ts') }) - }) - // Add layouts template addTemplate({ filename: 'layouts.mjs', @@ -149,5 +152,11 @@ export default defineNuxtModule({ ].join('\n') } }) + + // Add declarations for middleware and layout keys + nuxt.hook('prepare:types', ({ references }) => { + references.push({ path: resolve(nuxt.options.buildDir, 'middleware.d.ts') }) + references.push({ path: resolve(nuxt.options.buildDir, 'layouts.d.ts') }) + }) } }) diff --git a/packages/nuxt3/src/pages/utils.ts b/packages/nuxt3/src/pages/utils.ts index 2a166169ab..ea23be2bfd 100644 --- a/packages/nuxt3/src/pages/utils.ts +++ b/packages/nuxt3/src/pages/utils.ts @@ -3,6 +3,7 @@ import { encodePath } from 'ufo' import type { Nuxt, NuxtMiddleware, NuxtPage } from '@nuxt/schema' import { resolveFiles, useNuxt } from '@nuxt/kit' import { kebabCase, pascalCase } from 'scule' +import escapeRE from 'escape-string-regexp' enum SegmentParserState { initial, @@ -37,7 +38,7 @@ export function generateRoutesFromFiles (files: string[], pagesDir: string): Nux for (const file of files) { const segments = relative(pagesDir, file) - .replace(new RegExp(`${extname(file)}$`), '') + .replace(new RegExp(`${escapeRE(extname(file))}$`), '') .split('/') const route: NuxtPage = { @@ -219,7 +220,9 @@ export async function resolveLayouts (nuxt: Nuxt) { const layoutDir = resolve(nuxt.options.srcDir, nuxt.options.dir.layouts) const files = await resolveFiles(layoutDir, `*{${nuxt.options.extensions.join(',')}}`) - return files.map(file => ({ name: getNameFromPath(file), file })) + const layouts = files.map(file => ({ name: getNameFromPath(file), file })) + await nuxt.callHook('pages:layouts:extend', layouts) + return layouts } export function normalizeRoutes (routes: NuxtPage[], metaImports: Set = new Set()): { imports: Set, routes: NuxtPage[]} { @@ -243,7 +246,9 @@ export async function resolveMiddleware (): Promise { const nuxt = useNuxt() const middlewareDir = resolve(nuxt.options.srcDir, nuxt.options.dir.middleware) const files = await resolveFiles(middlewareDir, `*{${nuxt.options.extensions.join(',')}}`) - return files.map(path => ({ name: getNameFromPath(path), path, global: hasSuffix(path, '.global') })) + const middleware = files.map(path => ({ name: getNameFromPath(path), path, global: hasSuffix(path, '.global') })) + await nuxt.callHook('pages:middleware:extend', middleware) + return middleware } function getNameFromPath (path: string) { diff --git a/packages/schema/src/config/_common.ts b/packages/schema/src/config/_common.ts index c483bf012f..cd8325d0c8 100644 --- a/packages/schema/src/config/_common.ts +++ b/packages/schema/src/config/_common.ts @@ -433,6 +433,7 @@ export default { layouts: 'layouts', /** * The middleware directory, each file of which will be auto-registered as a Nuxt middleware. + * @version 3 * @version 2 */ middleware: 'middleware', diff --git a/packages/schema/src/types/hooks.ts b/packages/schema/src/types/hooks.ts index 2adbb2a2a4..83dc2c2743 100644 --- a/packages/schema/src/types/hooks.ts +++ b/packages/schema/src/types/hooks.ts @@ -47,6 +47,11 @@ export type NuxtMiddleware = { global?: boolean } +export type NuxtLayout = { + name: string + file: string +} + export interface NuxtHooks { // Kit 'kit:compatibility': (compatibility: NuxtCompatibility, issues: NuxtCompatibilityIssues) => HookResult @@ -58,6 +63,7 @@ export interface NuxtHooks { 'builder:generateApp': () => HookResult 'pages:extend': (pages: NuxtPage[]) => HookResult 'pages:middleware:extend': (middleware: NuxtMiddleware[]) => HookResult + 'pages:layouts:extend': (layouts: NuxtLayout[]) => HookResult // Auto imports 'autoImports:sources': (autoImportSources: AutoImportSource[]) => HookResult diff --git a/packages/vite/package.json b/packages/vite/package.json index b1b9bcf157..276ba3eb3f 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -26,6 +26,7 @@ "consola": "^2.15.3", "defu": "^5.0.1", "esbuild": "^0.14.14", + "escape-string-regexp": "^5.0.0", "externality": "^0.1.6", "fs-extra": "^10.0.0", "magic-string": "^0.25.7", diff --git a/packages/vite/src/plugins/dynamic-base.ts b/packages/vite/src/plugins/dynamic-base.ts index efb30dc9e7..3a52b396c3 100644 --- a/packages/vite/src/plugins/dynamic-base.ts +++ b/packages/vite/src/plugins/dynamic-base.ts @@ -1,4 +1,5 @@ import { createUnplugin } from 'unplugin' +import escapeRE from 'escape-string-regexp' import type { Plugin } from 'vite' interface DynamicBasePluginOptions { @@ -7,8 +8,6 @@ interface DynamicBasePluginOptions { globalPublicPath?: string } -const escapeRE = (str: string) => str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') - export const RelativeAssetPlugin = function (): Plugin { return { name: 'nuxt:vite-relative-asset', diff --git a/packages/webpack/package.json b/packages/webpack/package.json index 38a5fc2f73..3c6cbced03 100644 --- a/packages/webpack/package.json +++ b/packages/webpack/package.json @@ -27,6 +27,7 @@ "css-minimizer-webpack-plugin": "^3.4.1", "cssnano": "^5.0.16", "esbuild-loader": "^2.18.0", + "escape-string-regexp": "^5.0.0", "file-loader": "^6.2.0", "fs-extra": "^10.0.0", "glob": "^7.2.0", diff --git a/packages/webpack/src/presets/base.ts b/packages/webpack/src/presets/base.ts index ef45ec1c16..92e3f40ad6 100644 --- a/packages/webpack/src/presets/base.ts +++ b/packages/webpack/src/presets/base.ts @@ -4,7 +4,7 @@ import WebpackBar from 'webpackbar' import consola from 'consola' import webpack from 'webpack' import FriendlyErrorsWebpackPlugin from '@nuxt/friendly-errors-webpack-plugin' -import { escapeRegExp } from 'lodash-es' +import escapeRegExp from 'escape-string-regexp' import { joinURL } from 'ufo' import WarningIgnorePlugin from '../plugins/warning-ignore' import { WebpackConfigContext, applyPresets, fileName } from '../utils/config' diff --git a/yarn.lock b/yarn.lock index fbd54affec..285657d9b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3182,6 +3182,7 @@ __metadata: consola: ^2.15.3 defu: ^5.0.1 esbuild: ^0.14.14 + escape-string-regexp: ^5.0.0 externality: ^0.1.6 fs-extra: ^10.0.0 magic-string: ^0.25.7 @@ -3298,6 +3299,7 @@ __metadata: css-minimizer-webpack-plugin: ^3.4.1 cssnano: ^5.0.16 esbuild-loader: ^2.18.0 + escape-string-regexp: ^5.0.0 file-loader: ^6.2.0 fs-extra: ^10.0.0 glob: ^7.2.0 @@ -9556,6 +9558,13 @@ __metadata: languageName: node linkType: hard +"escape-string-regexp@npm:^5.0.0": + version: 5.0.0 + resolution: "escape-string-regexp@npm:5.0.0" + checksum: 20daabe197f3cb198ec28546deebcf24b3dbb1a5a269184381b3116d12f0532e06007f4bc8da25669d6a7f8efb68db0758df4cd981f57bc5b57f521a3e12c59e + languageName: node + linkType: hard + "eslint-config-standard@npm:^16.0.3": version: 16.0.3 resolution: "eslint-config-standard@npm:16.0.3" @@ -14841,6 +14850,7 @@ __metadata: cookie-es: ^0.5.0 defu: ^5.0.1 destr: ^1.1.0 + escape-string-regexp: ^5.0.0 globby: ^13.1.0 h3: ^0.3.9 hash-sum: ^2.0.0