perf(kit,nuxt,vite,webpack): hoist regex patterns (#29620)

This commit is contained in:
Michael Brevard 2024-10-22 16:39:50 +03:00 committed by GitHub
parent d275b382ec
commit 5dad6e6233
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 136 additions and 74 deletions

View File

@ -3,8 +3,9 @@ import { readPackageJSON } from 'pkg-types'
import type { Nuxt, NuxtCompatibility, NuxtCompatibilityIssues } from '@nuxt/schema' import type { Nuxt, NuxtCompatibility, NuxtCompatibilityIssues } from '@nuxt/schema'
import { useNuxt } from './context' import { useNuxt } from './context'
const SEMANTIC_VERSION_RE = /-\d+\.[0-9a-f]+/
export function normalizeSemanticVersion (version: string) { export function normalizeSemanticVersion (version: string) {
return version.replace(/-\d+\.[0-9a-f]+/, '') // Remove edge prefix return version.replace(SEMANTIC_VERSION_RE, '') // Remove edge prefix
} }
const builderMap = { const builderMap = {
@ -104,6 +105,7 @@ export function isNuxt3 (nuxt: Nuxt = useNuxt()) {
return isNuxtMajorVersion(3, nuxt) return isNuxtMajorVersion(3, nuxt)
} }
const NUXT_VERSION_RE = /^v/g
/** /**
* Get nuxt version * Get nuxt version
*/ */
@ -112,5 +114,5 @@ export function getNuxtVersion (nuxt: Nuxt | any = useNuxt() /* TODO: LegacyNuxt
if (typeof rawVersion !== 'string') { if (typeof rawVersion !== 'string') {
throw new TypeError('Cannot determine nuxt version! Is current instance passed?') throw new TypeError('Cannot determine nuxt version! Is current instance passed?')
} }
return rawVersion.replace(/^v/g, '') return rawVersion.replace(NUXT_VERSION_RE, '')
} }

View File

@ -3,6 +3,7 @@ import type { Component, ComponentsDir } from '@nuxt/schema'
import { useNuxt } from './context' import { useNuxt } from './context'
import { assertNuxtCompatibility } from './compatibility' import { assertNuxtCompatibility } from './compatibility'
import { logger } from './logger' import { logger } from './logger'
import { MODE_RE } from './utils'
/** /**
* Register a directory to be scanned for components and imported only when used. * Register a directory to be scanned for components and imported only when used.
@ -28,7 +29,7 @@ export async function addComponent (opts: AddComponentOptions) {
nuxt.options.components = nuxt.options.components || [] nuxt.options.components = nuxt.options.components || []
if (!opts.mode) { if (!opts.mode) {
const [, mode = 'all'] = opts.filePath.match(/\.(server|client)(\.\w+)*$/) || [] const [, mode = 'all'] = opts.filePath.match(MODE_RE) || []
opts.mode = mode as 'all' | 'client' | 'server' opts.mode = mode as 'all' | 'client' | 'server'
} }

View File

@ -5,10 +5,11 @@ import { useNuxt } from './context'
import { logger } from './logger' import { logger } from './logger'
import { addTemplate } from './template' import { addTemplate } from './template'
const LAYOUT_RE = /["']/g
export function addLayout (template: NuxtTemplate | string, name?: string) { export function addLayout (template: NuxtTemplate | string, name?: string) {
const nuxt = useNuxt() const nuxt = useNuxt()
const { filename, src } = addTemplate(template) const { filename, src } = addTemplate(template)
const layoutName = kebabCase(name || parse(filename).name).replace(/["']/g, '') const layoutName = kebabCase(name || parse(filename).name).replace(LAYOUT_RE, '')
// Nuxt 3 adds layouts on app // Nuxt 3 adds layouts on app
nuxt.hook('app:templates', (app) => { nuxt.hook('app:templates', (app) => {

View File

@ -4,13 +4,14 @@ import { normalize } from 'pathe'
import { useNuxt } from './context' import { useNuxt } from './context'
import { toArray } from './utils' import { toArray } from './utils'
const HANDLER_METHOD_RE = /\.(get|head|patch|post|put|delete|connect|options|trace)(\.\w+)*$/
/** /**
* normalize handler object * normalize handler object
* *
*/ */
function normalizeHandlerMethod (handler: NitroEventHandler) { function normalizeHandlerMethod (handler: NitroEventHandler) {
// retrieve method from handler file name // retrieve method from handler file name
const [, method = undefined] = handler.handler.match(/\.(get|head|patch|post|put|delete|connect|options|trace)(\.\w+)*$/) || [] const [, method = undefined] = handler.handler.match(HANDLER_METHOD_RE) || []
return { return {
method: method as 'get' | 'head' | 'patch' | 'post' | 'put' | 'delete' | 'connect' | 'options' | 'trace' | undefined, method: method as 'get' | 'head' | 'patch' | 'post' | 'put' | 'delete' | 'connect' | 'options' | 'trace' | undefined,
...handler, ...handler,

View File

@ -3,6 +3,7 @@ import type { NuxtPlugin, NuxtPluginTemplate } from '@nuxt/schema'
import { useNuxt } from './context' import { useNuxt } from './context'
import { addTemplate } from './template' import { addTemplate } from './template'
import { resolveAlias } from './resolve' import { resolveAlias } from './resolve'
import { MODE_RE } from './utils'
/** /**
* Normalize a nuxt plugin object * Normalize a nuxt plugin object
@ -27,7 +28,7 @@ export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin {
plugin.mode = 'server' plugin.mode = 'server'
} }
if (!plugin.mode) { if (!plugin.mode) {
const [, mode = 'all'] = plugin.src.match(/\.(server|client)(\.\w+)*$/) || [] const [, mode = 'all'] = plugin.src.match(MODE_RE) || []
plugin.mode = mode as 'all' | 'client' | 'server' plugin.mode = mode as 'all' | 'client' | 'server'
} }

View File

@ -123,6 +123,9 @@ export async function updateTemplates (options?: { filter?: (template: ResolvedN
return await tryUseNuxt()?.hooks.callHook('builder:generateApp', options) return await tryUseNuxt()?.hooks.callHook('builder:generateApp', options)
} }
const EXTENSION_RE = /\b\.\w+$/g
// Exclude bridge alias types to support Volar
const excludedAlias = [/^@vue\/.*$/, /^#internal\/nuxt/]
export async function _generateTypes (nuxt: Nuxt) { export async function _generateTypes (nuxt: Nuxt) {
const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir) const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir)
const relativeRootDir = relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir) const relativeRootDir = relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir)
@ -225,9 +228,6 @@ export async function _generateTypes (nuxt: Nuxt) {
const aliases: Record<string, string> = nuxt.options.alias const aliases: Record<string, string> = nuxt.options.alias
// Exclude bridge alias types to support Volar
const excludedAlias = [/^@vue\/.*$/, /^#internal\/nuxt/]
const basePath = tsConfig.compilerOptions!.baseUrl const basePath = tsConfig.compilerOptions!.baseUrl
? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl) ? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl)
: nuxt.options.buildDir : nuxt.options.buildDir
@ -260,7 +260,7 @@ export async function _generateTypes (nuxt: Nuxt) {
} else { } else {
const path = stats?.isFile() const path = stats?.isFile()
// remove extension // remove extension
? relativePath.replace(/\b\.\w+$/g, '') ? relativePath.replace(EXTENSION_RE, '')
// non-existent file probably shouldn't be resolved // non-existent file probably shouldn't be resolved
: aliases[alias]! : aliases[alias]!
@ -289,7 +289,7 @@ export async function _generateTypes (nuxt: Nuxt) {
tsConfig.compilerOptions!.paths[alias] = await Promise.all(paths.map(async (path: string) => { tsConfig.compilerOptions!.paths[alias] = await Promise.all(paths.map(async (path: string) => {
if (!isAbsolute(path)) { return path } if (!isAbsolute(path)) { return path }
const stats = await fsp.stat(path).catch(() => null /* file does not exist */) const stats = await fsp.stat(path).catch(() => null /* file does not exist */)
return relativeWithDot(nuxt.options.buildDir, stats?.isFile() ? path.replace(/\b\.\w+$/g, '') /* remove extension */ : path) return relativeWithDot(nuxt.options.buildDir, stats?.isFile() ? path.replace(EXTENSION_RE, '') /* remove extension */ : path)
})) }))
} }
@ -344,6 +344,7 @@ function renderAttr (key: string, value?: string) {
return value ? `${key}="${value}"` : '' return value ? `${key}="${value}"` : ''
} }
const RELATIVE_WITH_DOT_RE = /^([^.])/
function relativeWithDot (from: string, to: string) { function relativeWithDot (from: string, to: string) {
return relative(from, to).replace(/^([^.])/, './$1') || '.' return relative(from, to).replace(RELATIVE_WITH_DOT_RE, './$1') || '.'
} }

View File

@ -2,3 +2,5 @@
export function toArray<T> (value: T | T[]): T[] { export function toArray<T> (value: T | T[]): T[] {
return Array.isArray(value) ? value : [value] return Array.isArray(value) ? value : [value]
} }
export const MODE_RE = /\.(server|client)(\.\w+)*$/

View File

@ -22,6 +22,7 @@ const SSR_UID_RE = /data-island-uid="([^"]*)"/
const DATA_ISLAND_UID_RE = /data-island-uid(="")?(?!="[^"])/g const DATA_ISLAND_UID_RE = /data-island-uid(="")?(?!="[^"])/g
const SLOTNAME_RE = /data-island-slot="([^"]*)"/g const SLOTNAME_RE = /data-island-slot="([^"]*)"/g
const SLOT_FALLBACK_RE = / data-island-slot="([^"]*)"[^>]*>/g const SLOT_FALLBACK_RE = / data-island-slot="([^"]*)"[^>]*>/g
const ISLAND_SCOPE_ID_RE = /^<[^> ]*/
let id = 1 let id = 1
const getId = import.meta.client ? () => (id++).toString() : randomUUID const getId = import.meta.client ? () => (id++).toString() : randomUUID
@ -142,7 +143,7 @@ export default defineComponent({
let html = ssrHTML.value let html = ssrHTML.value
if (props.scopeId) { if (props.scopeId) {
html = html.replace(/^<[^> ]*/, full => full + ' ' + props.scopeId) html = html.replace(ISLAND_SCOPE_ID_RE, full => full + ' ' + props.scopeId)
} }
if (import.meta.client && !canLoadClientComponent.value) { if (import.meta.client && !canLoadClientComponent.value) {

View File

@ -521,11 +521,12 @@ function useObserver (): { observe: ObserveFn } | undefined {
return _observer return _observer
} }
const IS_2G_RE = /2g/
function isSlowConnection () { function isSlowConnection () {
if (import.meta.server) { return } if (import.meta.server) { return }
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/connection // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/connection
const cn = (navigator as any).connection as { saveData: boolean, effectiveType: string } | null const cn = (navigator as any).connection as { saveData: boolean, effectiveType: string } | null
if (cn && (cn.saveData || /2g/.test(cn.effectiveType))) { return true } if (cn && (cn.saveData || IS_2G_RE.test(cn.effectiveType))) { return true }
return false return false
} }

View File

@ -15,13 +15,16 @@ export const _wrapIf = (component: Component, props: any, slots: any) => {
return { default: () => props ? h(component, props, slots) : slots.default?.() } return { default: () => props ? h(component, props, slots) : slots.default?.() }
} }
const ROUTE_KEY_PARENTHESES_RE = /(:\w+)\([^)]+\)/g
const ROUTE_KEY_SYMBOLS_RE = /(:\w+)[?+*]/g
const ROUTE_KEY_NORMAL_RE = /:\w+/g
// TODO: consider refactoring into single utility // TODO: consider refactoring into single utility
// See https://github.com/nuxt/nuxt/tree/main/packages/nuxt/src/pages/runtime/utils.ts#L8-L19 // See https://github.com/nuxt/nuxt/tree/main/packages/nuxt/src/pages/runtime/utils.ts#L8-L19
function generateRouteKey (route: RouteLocationNormalized) { function generateRouteKey (route: RouteLocationNormalized) {
const source = route?.meta.key ?? route.path const source = route?.meta.key ?? route.path
.replace(/(:\w+)\([^)]+\)/g, '$1') .replace(ROUTE_KEY_PARENTHESES_RE, '$1')
.replace(/(:\w+)[?+*]/g, '$1') .replace(ROUTE_KEY_SYMBOLS_RE, '$1')
.replace(/:\w+/g, r => route.params[r.slice(1)]?.toString() || '') .replace(ROUTE_KEY_NORMAL_RE, r => route.params[r.slice(1)]?.toString() || '')
return typeof source === 'function' ? source(route) : source return typeof source === 'function' ? source(route) : source
} }

View File

@ -114,6 +114,7 @@ export interface NavigateToOptions {
open?: OpenOptions open?: OpenOptions
} }
const URL_QUOTE_RE = /"/g
/** @since 3.0.0 */ /** @since 3.0.0 */
export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: NavigateToOptions): Promise<void | NavigationFailure | false> | false | void | RouteLocationRaw => { export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: NavigateToOptions): Promise<void | NavigationFailure | false> | false | void | RouteLocationRaw => {
if (!to) { if (!to) {
@ -166,7 +167,7 @@ export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: Na
const redirect = async function (response: any) { const redirect = async function (response: any) {
// TODO: consider deprecating in favour of `app:rendered` and removing // TODO: consider deprecating in favour of `app:rendered` and removing
await nuxtApp.callHook('app:redirected') await nuxtApp.callHook('app:redirected')
const encodedLoc = location.replace(/"/g, '%22') const encodedLoc = location.replace(URL_QUOTE_RE, '%22')
const encodedHeader = encodeURL(location, isExternalHost) const encodedHeader = encodeURL(location, isExternalHost)
nuxtApp.ssrContext!._renderResponse = { nuxtApp.ssrContext!._renderResponse = {

View File

@ -16,11 +16,13 @@ import { ComponentNamePlugin } from './plugins/component-names'
const isPureObjectOrString = (val: any) => (!Array.isArray(val) && typeof val === 'object') || typeof val === 'string' const isPureObjectOrString = (val: any) => (!Array.isArray(val) && typeof val === 'object') || typeof val === 'string'
const isDirectory = (p: string) => { try { return statSync(p).isDirectory() } catch { return false } } const isDirectory = (p: string) => { try { return statSync(p).isDirectory() } catch { return false } }
const SLASH_SEPARATOR_RE = /[\\/]/
function compareDirByPathLength ({ path: pathA }: { path: string }, { path: pathB }: { path: string }) { function compareDirByPathLength ({ path: pathA }: { path: string }, { path: pathB }: { path: string }) {
return pathB.split(/[\\/]/).filter(Boolean).length - pathA.split(/[\\/]/).filter(Boolean).length return pathB.split(SLASH_SEPARATOR_RE).filter(Boolean).length - pathA.split(SLASH_SEPARATOR_RE).filter(Boolean).length
} }
const DEFAULT_COMPONENTS_DIRS_RE = /\/components(?:\/(?:global|islands))?$/ const DEFAULT_COMPONENTS_DIRS_RE = /\/components(?:\/(?:global|islands))?$/
const STARTER_DOT_RE = /^\./g
export type getComponentsT = (mode?: 'client' | 'server' | 'all') => Component[] export type getComponentsT = (mode?: 'client' | 'server' | 'all') => Component[]
@ -89,7 +91,7 @@ export default defineNuxtModule<ComponentsOptions>({
const dirOptions: ComponentsDir = typeof dir === 'object' ? dir : { path: dir } const dirOptions: ComponentsDir = typeof dir === 'object' ? dir : { path: dir }
const dirPath = resolveAlias(dirOptions.path) const dirPath = resolveAlias(dirOptions.path)
const transpile = typeof dirOptions.transpile === 'boolean' ? dirOptions.transpile : 'auto' const transpile = typeof dirOptions.transpile === 'boolean' ? dirOptions.transpile : 'auto'
const extensions = (dirOptions.extensions || nuxt.options.extensions).map(e => e.replace(/^\./g, '')) const extensions = (dirOptions.extensions || nuxt.options.extensions).map(e => e.replace(STARTER_DOT_RE, ''))
const present = isDirectory(dirPath) const present = isDirectory(dirPath)
if (!present && !DEFAULT_COMPONENTS_DIRS_RE.test(dirOptions.path)) { if (!present && !DEFAULT_COMPONENTS_DIRS_RE.test(dirOptions.path)) {

View File

@ -12,6 +12,7 @@ interface LoaderOptions {
} }
const CLIENT_FALLBACK_RE = /<(?:NuxtClientFallback|nuxt-client-fallback)(?: [^>]*)?>/ const CLIENT_FALLBACK_RE = /<(?:NuxtClientFallback|nuxt-client-fallback)(?: [^>]*)?>/
const CLIENT_FALLBACK_GLOBAL_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/g const CLIENT_FALLBACK_GLOBAL_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/g
const UID_RE = / :?uid=/
export const ClientFallbackAutoIdPlugin = (options: LoaderOptions) => createUnplugin(() => { export const ClientFallbackAutoIdPlugin = (options: LoaderOptions) => createUnplugin(() => {
const exclude = options.transform?.exclude || [] const exclude = options.transform?.exclude || []
const include = options.transform?.include || [] const include = options.transform?.include || []
@ -37,7 +38,7 @@ export const ClientFallbackAutoIdPlugin = (options: LoaderOptions) => createUnpl
s.replace(CLIENT_FALLBACK_GLOBAL_RE, (full, name, attrs) => { s.replace(CLIENT_FALLBACK_GLOBAL_RE, (full, name, attrs) => {
count++ count++
if (/ :?uid=/.test(attrs)) { return full } if (UID_RE.test(attrs)) { return full }
return `<${name} :uid="'${hash(relativeID)}' + JSON.stringify($props) + '${count}'" ${attrs ?? ''}>` return `<${name} :uid="'${hash(relativeID)}' + JSON.stringify($props) + '${count}'" ${attrs ?? ''}>`
}) })

View File

@ -1,12 +1,13 @@
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import type { Component } from 'nuxt/schema' import type { Component } from 'nuxt/schema'
import { isVue } from '../../core/utils' import { SX_RE, isVue } from '../../core/utils'
interface NameDevPluginOptions { interface NameDevPluginOptions {
sourcemap: boolean sourcemap: boolean
getComponents: () => Component[] getComponents: () => Component[]
} }
const FILENAME_RE = /([^/\\]+)\.\w+$/
/** /**
* Set the default name of components to their PascalCase name * Set the default name of components to their PascalCase name
*/ */
@ -15,10 +16,10 @@ export const ComponentNamePlugin = (options: NameDevPluginOptions) => createUnpl
name: 'nuxt:component-name-plugin', name: 'nuxt:component-name-plugin',
enforce: 'post', enforce: 'post',
transformInclude (id) { transformInclude (id) {
return isVue(id) || !!id.match(/\.[tj]sx$/) return isVue(id) || !!id.match(SX_RE)
}, },
transform (code, id) { transform (code, id) {
const filename = id.match(/([^/\\]+)\.\w+$/)?.[1] const filename = id.match(FILENAME_RE)?.[1]
if (!filename) { if (!filename) {
return return
} }

View File

@ -30,6 +30,7 @@ const TEMPLATE_RE = /<template>([\s\S]*)<\/template>/
const NUXTCLIENT_ATTR_RE = /\s:?nuxt-client(="[^"]*")?/g const NUXTCLIENT_ATTR_RE = /\s:?nuxt-client(="[^"]*")?/g
const IMPORT_CODE = '\nimport { mergeProps as __mergeProps } from \'vue\'' + '\nimport { vforToArray as __vforToArray } from \'#app/components/utils\'' + '\nimport NuxtTeleportIslandComponent from \'#app/components/nuxt-teleport-island-component\'' + '\nimport NuxtTeleportSsrSlot from \'#app/components/nuxt-teleport-island-slot\'' const IMPORT_CODE = '\nimport { mergeProps as __mergeProps } from \'vue\'' + '\nimport { vforToArray as __vforToArray } from \'#app/components/utils\'' + '\nimport NuxtTeleportIslandComponent from \'#app/components/nuxt-teleport-island-component\'' + '\nimport NuxtTeleportSsrSlot from \'#app/components/nuxt-teleport-island-slot\''
const EXTRACTED_ATTRS_RE = /v-(?:if|else-if|else)(="[^"]*")?/g const EXTRACTED_ATTRS_RE = /v-(?:if|else-if|else)(="[^"]*")?/g
const KEY_RE = /:?key="[^"]"/g
function wrapWithVForDiv (code: string, vfor: string): string { function wrapWithVForDiv (code: string, vfor: string): string {
return `<div v-for="${vfor}" style="display: contents;">${code}</div>` return `<div v-for="${vfor}" style="display: contents;">${code}</div>`
@ -90,7 +91,7 @@ export const IslandsTransformPlugin = (options: ServerOnlyComponentTransformPlug
if (children.length) { if (children.length) {
// pass slot fallback to NuxtTeleportSsrSlot fallback // pass slot fallback to NuxtTeleportSsrSlot fallback
const attrString = attributeToString(attributes) const attrString = attributeToString(attributes)
const slice = code.slice(startingIndex + loc[0].end, startingIndex + loc[1].start).replaceAll(/:?key="[^"]"/g, '') const slice = code.slice(startingIndex + loc[0].end, startingIndex + loc[1].start).replaceAll(KEY_RE, '')
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[1].end, `<slot${attrString.replaceAll(EXTRACTED_ATTRS_RE, '')}/><template #fallback>${attributes['v-for'] ? wrapWithVForDiv(slice, attributes['v-for']) : slice}</template>`) s.overwrite(startingIndex + loc[0].start, startingIndex + loc[1].end, `<slot${attrString.replaceAll(EXTRACTED_ATTRS_RE, '')}/><template #fallback>${attributes['v-for'] ? wrapWithVForDiv(slice, attributes['v-for']) : slice}</template>`)
} else { } else {
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[0].end, code.slice(startingIndex + loc[0].start, startingIndex + loc[0].end).replaceAll(EXTRACTED_ATTRS_RE, '')) s.overwrite(startingIndex + loc[0].start, startingIndex + loc[0].end, code.slice(startingIndex + loc[0].start, startingIndex + loc[0].end).replaceAll(EXTRACTED_ATTRS_RE, ''))

View File

@ -6,7 +6,7 @@ import { relative } from 'pathe'
import type { Component, ComponentsOptions } from 'nuxt/schema' import type { Component, ComponentsOptions } from 'nuxt/schema'
import { logger, tryUseNuxt } from '@nuxt/kit' import { logger, tryUseNuxt } from '@nuxt/kit'
import { isVue } from '../../core/utils' import { QUOTE_RE, SX_RE, isVue } from '../../core/utils'
interface LoaderOptions { interface LoaderOptions {
getComponents (): Component[] getComponents (): Component[]
@ -17,6 +17,7 @@ interface LoaderOptions {
experimentalComponentIslands?: boolean experimentalComponentIslands?: boolean
} }
const REPLACE_COMPONENT_TO_DIRECT_IMPORT_RE = /(?<=[ (])_?resolveComponent\(\s*["'](lazy-|Lazy(?=[A-Z]))?([^'"]*)["'][^)]*\)/g
export const LoaderPlugin = (options: LoaderOptions) => createUnplugin(() => { export const LoaderPlugin = (options: LoaderOptions) => createUnplugin(() => {
const exclude = options.transform?.exclude || [] const exclude = options.transform?.exclude || []
const include = options.transform?.include || [] const include = options.transform?.include || []
@ -32,7 +33,7 @@ export const LoaderPlugin = (options: LoaderOptions) => createUnplugin(() => {
if (include.some(pattern => pattern.test(id))) { if (include.some(pattern => pattern.test(id))) {
return true return true
} }
return isVue(id, { type: ['template', 'script'] }) || !!id.match(/\.[tj]sx$/) return isVue(id, { type: ['template', 'script'] }) || !!id.match(SX_RE)
}, },
transform (code, id) { transform (code, id) {
const components = options.getComponents() const components = options.getComponents()
@ -43,7 +44,7 @@ export const LoaderPlugin = (options: LoaderOptions) => createUnplugin(() => {
const s = new MagicString(code) const s = new MagicString(code)
// replace `_resolveComponent("...")` to direct import // replace `_resolveComponent("...")` to direct import
s.replace(/(?<=[ (])_?resolveComponent\(\s*["'](lazy-|Lazy(?=[A-Z]))?([^'"]*)["'][^)]*\)/g, (full: string, lazy: string, name: string) => { s.replace(REPLACE_COMPONENT_TO_DIRECT_IMPORT_RE, (full: string, lazy: string, name: string) => {
const component = findComponent(components, name, options.mode) const component = findComponent(components, name, options.mode)
if (component) { if (component) {
// TODO: refactor to nuxi // TODO: refactor to nuxi
@ -111,7 +112,7 @@ export const LoaderPlugin = (options: LoaderOptions) => createUnplugin(() => {
}) })
function findComponent (components: Component[], name: string, mode: LoaderOptions['mode']) { function findComponent (components: Component[], name: string, mode: LoaderOptions['mode']) {
const id = pascalCase(name).replace(/["']/g, '') const id = pascalCase(name).replace(QUOTE_RE, '')
// Prefer exact match // Prefer exact match
const component = components.find(component => id === component.pascalName && ['all', mode, undefined].includes(component.mode)) const component = components.find(component => id === component.pascalName && ['all', mode, undefined].includes(component.mode))
if (component) { return component } if (component) { return component }

View File

@ -6,8 +6,12 @@ import { isIgnored, logger, useNuxt } from '@nuxt/kit'
import { withTrailingSlash } from 'ufo' import { withTrailingSlash } from 'ufo'
import type { Component, ComponentsDir } from 'nuxt/schema' import type { Component, ComponentsDir } from 'nuxt/schema'
import { resolveComponentNameSegments } from '../core/utils' import { QUOTE_RE, resolveComponentNameSegments } from '../core/utils'
const ISLAND_RE = /\.island(?:\.global)?$/
const GLOBAL_RE = /\.global(?:\.island)?$/
const COMPONENT_MODE_RE = /(?<=\.)(client|server)(\.global|\.island)*$/
const MODE_REPLACEMENT_RE = /(\.(client|server))?(\.global|\.island)*$/
/** /**
* Scan the components inside different components folders * Scan the components inside different components folders
* and return a unique list of components * and return a unique list of components
@ -83,17 +87,17 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
*/ */
let fileName = basename(filePath, extname(filePath)) let fileName = basename(filePath, extname(filePath))
const island = /\.island(?:\.global)?$/.test(fileName) || dir.island const island = ISLAND_RE.test(fileName) || dir.island
const global = /\.global(?:\.island)?$/.test(fileName) || dir.global const global = GLOBAL_RE.test(fileName) || dir.global
const mode = island ? 'server' : (fileName.match(/(?<=\.)(client|server)(\.global|\.island)*$/)?.[1] || 'all') as 'client' | 'server' | 'all' const mode = island ? 'server' : (fileName.match(COMPONENT_MODE_RE)?.[1] || 'all') as 'client' | 'server' | 'all'
fileName = fileName.replace(/(\.(client|server))?(\.global|\.island)*$/, '') fileName = fileName.replace(MODE_REPLACEMENT_RE, '')
if (fileName.toLowerCase() === 'index') { if (fileName.toLowerCase() === 'index') {
fileName = dir.pathPrefix === false ? basename(dirname(filePath)) : '' /* inherits from path */ fileName = dir.pathPrefix === false ? basename(dirname(filePath)) : '' /* inherits from path */
} }
const suffix = (mode !== 'all' ? `-${mode}` : '') const suffix = (mode !== 'all' ? `-${mode}` : '')
const componentNameSegments = resolveComponentNameSegments(fileName.replace(/["']/g, ''), prefixParts) const componentNameSegments = resolveComponentNameSegments(fileName.replace(QUOTE_RE, ''), prefixParts)
const pascalName = pascalCase(componentNameSegments) const pascalName = pascalCase(componentNameSegments)
if (LAZY_COMPONENT_NAME_REGEX.test(pascalName)) { if (LAZY_COMPONENT_NAME_REGEX.test(pascalName)) {

View File

@ -102,14 +102,15 @@ export const componentsIslandsTemplate: NuxtTemplate = {
}, },
} }
const NON_VUE_RE = /\b\.(?!vue)\w+$/g
export const componentsTypeTemplate = { export const componentsTypeTemplate = {
filename: 'components.d.ts' as const, filename: 'components.d.ts' as const,
getContents: ({ app, nuxt }) => { getContents: ({ app, nuxt }) => {
const buildDir = nuxt.options.buildDir const buildDir = nuxt.options.buildDir
const componentTypes = app.components.filter(c => !c.island).map((c) => { const componentTypes = app.components.filter(c => !c.island).map((c) => {
const type = `typeof ${genDynamicImport(isAbsolute(c.filePath) const type = `typeof ${genDynamicImport(isAbsolute(c.filePath)
? relative(buildDir, c.filePath).replace(/\b\.(?!vue)\w+$/g, '') ? relative(buildDir, c.filePath).replace(NON_VUE_RE, '')
: c.filePath.replace(/\b\.(?!vue)\w+$/g, ''), { wrapper: false })}['${c.export}']` : c.filePath.replace(NON_VUE_RE, ''), { wrapper: false })}['${c.export}']`
return [ return [
c.pascalName, c.pascalName,
c.island || c.mode === 'server' ? `IslandComponent<${type}>` : type, c.island || c.mode === 'server' ? `IslandComponent<${type}>` : type,

View File

@ -57,7 +57,7 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
const writes: Array<() => void> = [] const writes: Array<() => void> = []
const changedTemplates: Array<ResolvedNuxtTemplate<any>> = [] const changedTemplates: Array<ResolvedNuxtTemplate<any>> = []
const FORWARD_SLASH_RE = /\//g
async function processTemplate (template: ResolvedNuxtTemplate) { async function processTemplate (template: ResolvedNuxtTemplate) {
const fullPath = template.dst || resolve(nuxt.options.buildDir, template.filename!) const fullPath = template.dst || resolve(nuxt.options.buildDir, template.filename!)
const start = performance.now() const start = performance.now()
@ -77,7 +77,7 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
// In case a non-normalized absolute path is called for on Windows // In case a non-normalized absolute path is called for on Windows
if (process.platform === 'win32') { if (process.platform === 'win32') {
nuxt.vfs[fullPath.replace(/\//g, '\\')] = contents nuxt.vfs[fullPath.replace(FORWARD_SLASH_RE, '\\')] = contents
} }
changedTemplates.push(template) changedTemplates.push(template)

View File

@ -10,6 +10,7 @@ import { generateApp as _generateApp, createApp } from './app'
import { checkForExternalConfigurationFiles } from './external-config-files' import { checkForExternalConfigurationFiles } from './external-config-files'
import { cleanupCaches, getVueHash } from './cache' import { cleanupCaches, getVueHash } from './cache'
const IS_RESTART_PATH_RE = /^(?:app\.|error\.|plugins\/|middleware\/|layouts\/)/i
export async function build (nuxt: Nuxt) { export async function build (nuxt: Nuxt) {
const app = createApp(nuxt) const app = createApp(nuxt)
nuxt.apps.default = app nuxt.apps.default = app
@ -23,7 +24,7 @@ export async function build (nuxt: Nuxt) {
if (event === 'change') { return } if (event === 'change') { return }
const path = resolve(nuxt.options.srcDir, relativePath) const path = resolve(nuxt.options.srcDir, relativePath)
const relativePaths = nuxt.options._layers.map(l => relative(l.config.srcDir || l.cwd, path)) const relativePaths = nuxt.options._layers.map(l => relative(l.config.srcDir || l.cwd, path))
const restartPath = relativePaths.find(relativePath => /^(?:app\.|error\.|plugins\/|middleware\/|layouts\/)/i.test(relativePath)) const restartPath = relativePaths.find(relativePath => IS_RESTART_PATH_RE.test(relativePath))
if (restartPath) { if (restartPath) {
if (restartPath.startsWith('app')) { if (restartPath.startsWith('app')) {
app.mainComponent = undefined app.mainComponent = undefined

View File

@ -18,6 +18,7 @@ import { distDir } from '../dirs'
import { toArray } from '../utils' import { toArray } from '../utils'
import { template as defaultSpaLoadingTemplate } from '../../../ui-templates/dist/templates/spa-loading-icon' import { template as defaultSpaLoadingTemplate } from '../../../ui-templates/dist/templates/spa-loading-icon'
import { nuxtImportProtections } from './plugins/import-protection' import { nuxtImportProtections } from './plugins/import-protection'
import { EXTENSION_RE } from './utils'
const logLevelMapReverse = { const logLevelMapReverse = {
silent: 0, silent: 0,
@ -25,12 +26,14 @@ const logLevelMapReverse = {
verbose: 3, verbose: 3,
} satisfies Record<NuxtOptions['logLevel'], NitroConfig['logLevel']> } satisfies Record<NuxtOptions['logLevel'], NitroConfig['logLevel']>
const NODE_MODULES_RE = /(?<=\/)node_modules\/(.+)$/
const PNPM_NODE_MODULES_RE = /\.pnpm\/.+\/node_modules\/(.+)$/
export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
// Resolve config // Resolve config
const excludePaths = nuxt.options._layers const excludePaths = nuxt.options._layers
.flatMap(l => [ .flatMap(l => [
l.cwd.match(/(?<=\/)node_modules\/(.+)$/)?.[1], l.cwd.match(NODE_MODULES_RE)?.[1],
l.cwd.match(/\.pnpm\/.+\/node_modules\/(.+)$/)?.[1], l.cwd.match(PNPM_NODE_MODULES_RE)?.[1],
]) ])
.filter((dir): dir is string => Boolean(dir)) .filter((dir): dir is string => Boolean(dir))
.map(dir => escapeRE(dir)) .map(dir => escapeRE(dir))
@ -339,11 +342,12 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
} }
// Add fallback server for `ssr: false` // Add fallback server for `ssr: false`
const FORWARD_SLASH_RE = /\//g
if (!nuxt.options.ssr) { if (!nuxt.options.ssr) {
nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}' nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}'
// In case a non-normalized absolute path is called for on Windows // In case a non-normalized absolute path is called for on Windows
if (process.platform === 'win32') { if (process.platform === 'win32') {
nitroConfig.virtual!['#build/dist/server/server.mjs'.replace(/\//g, '\\')] = 'export default () => {}' nitroConfig.virtual!['#build/dist/server/server.mjs'.replace(FORWARD_SLASH_RE, '\\')] = 'export default () => {}'
} }
} }
@ -351,7 +355,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
nitroConfig.virtual!['#build/dist/server/styles.mjs'] = 'export default {}' nitroConfig.virtual!['#build/dist/server/styles.mjs'] = 'export default {}'
// In case a non-normalized absolute path is called for on Windows // In case a non-normalized absolute path is called for on Windows
if (process.platform === 'win32') { if (process.platform === 'win32') {
nitroConfig.virtual!['#build/dist/server/styles.mjs'.replace(/\//g, '\\')] = 'export default {}' nitroConfig.virtual!['#build/dist/server/styles.mjs'.replace(FORWARD_SLASH_RE, '\\')] = 'export default {}'
} }
} }
@ -389,7 +393,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
tsConfig.compilerOptions.paths[alias] = [absolutePath] tsConfig.compilerOptions.paths[alias] = [absolutePath]
tsConfig.compilerOptions.paths[`${alias}/*`] = [`${absolutePath}/*`] tsConfig.compilerOptions.paths[`${alias}/*`] = [`${absolutePath}/*`]
} else { } else {
tsConfig.compilerOptions.paths[alias] = [absolutePath.replace(/\b\.\w+$/g, '')] /* remove extension */ tsConfig.compilerOptions.paths[alias] = [absolutePath.replace(EXTENSION_RE, '')] /* remove extension */
} }
} }
@ -566,8 +570,9 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
} }
} }
const RELATIVE_RE = /^([^.])/
function relativeWithDot (from: string, to: string) { function relativeWithDot (from: string, to: string) {
return relative(from, to).replace(/^([^.])/, './$1') || '.' return relative(from, to).replace(RELATIVE_RE, './$1') || '.'
} }
async function spaLoadingTemplatePath (nuxt: Nuxt) { async function spaLoadingTemplatePath (nuxt: Nuxt) {

View File

@ -178,9 +178,10 @@ async function initNuxt (nuxt: Nuxt) {
const coreTypePackages = nuxt.options.typescript.hoist || [] const coreTypePackages = nuxt.options.typescript.hoist || []
const packageJSON = await readPackageJSON(nuxt.options.rootDir).catch(() => ({}) as PackageJson) const packageJSON = await readPackageJSON(nuxt.options.rootDir).catch(() => ({}) as PackageJson)
const NESTED_PKG_RE = /^[^@]+\//
nuxt._dependencies = new Set([...Object.keys(packageJSON.dependencies || {}), ...Object.keys(packageJSON.devDependencies || {})]) nuxt._dependencies = new Set([...Object.keys(packageJSON.dependencies || {}), ...Object.keys(packageJSON.devDependencies || {})])
const paths = Object.fromEntries(await Promise.all(coreTypePackages.map(async (pkg) => { const paths = Object.fromEntries(await Promise.all(coreTypePackages.map(async (pkg) => {
const [_pkg = pkg, _subpath] = /^[^@]+\//.test(pkg) ? pkg.split('/') : [pkg] const [_pkg = pkg, _subpath] = NESTED_PKG_RE.test(pkg) ? pkg.split('/') : [pkg]
const subpath = _subpath ? '/' + _subpath : '' const subpath = _subpath ? '/' + _subpath : ''
// ignore packages that exist in `package.json` as these can be resolved by TypeScript // ignore packages that exist in `package.json` as these can be resolved by TypeScript

View File

@ -10,6 +10,7 @@ interface VirtualFSPluginOptions {
alias?: Record<string, string> alias?: Record<string, string>
} }
const RELATIVE_ID_RE = /^\.{1,2}[\\/]/
export const VirtualFSPlugin = (nuxt: Nuxt, options: VirtualFSPluginOptions) => createUnplugin(() => { export const VirtualFSPlugin = (nuxt: Nuxt, options: VirtualFSPluginOptions) => createUnplugin(() => {
const extensions = ['', ...nuxt.options.extensions] const extensions = ['', ...nuxt.options.extensions]
const alias = { ...nuxt.options.alias, ...options.alias } const alias = { ...nuxt.options.alias, ...options.alias }
@ -40,7 +41,7 @@ export const VirtualFSPlugin = (nuxt: Nuxt, options: VirtualFSPluginOptions) =>
return PREFIX + resolvedId return PREFIX + resolvedId
} }
if (importer && /^\.{1,2}[\\/]/.test(id)) { if (importer && RELATIVE_ID_RE.test(id)) {
const path = resolve(dirname(withoutPrefix(importer)), id) const path = resolve(dirname(withoutPrefix(importer)), id)
const resolved = resolveWithExt(path) const resolved = resolveWithExt(path)
if (resolved) { if (resolved) {

View File

@ -11,6 +11,7 @@ import type { NuxtTemplate } from 'nuxt/schema'
import type { Nitro } from 'nitro/types' import type { Nitro } from 'nitro/types'
import { annotatePlugins, checkForCircularDependencies } from './app' import { annotatePlugins, checkForCircularDependencies } from './app'
import { EXTENSION_RE } from './utils'
export const vueShim: NuxtTemplate = { export const vueShim: NuxtTemplate = {
filename: 'types/vue-shim.d.ts', filename: 'types/vue-shim.d.ts',
@ -57,6 +58,7 @@ export const cssTemplate: NuxtTemplate = {
getContents: ctx => ctx.nuxt.options.css.map(i => genImport(i)).join('\n'), getContents: ctx => ctx.nuxt.options.css.map(i => genImport(i)).join('\n'),
} }
const PLUGIN_TEMPLATE_RE = /_(45|46|47)/g
export const clientPluginTemplate: NuxtTemplate = { export const clientPluginTemplate: NuxtTemplate = {
filename: 'plugins.client.mjs', filename: 'plugins.client.mjs',
async getContents (ctx) { async getContents (ctx) {
@ -66,7 +68,7 @@ export const clientPluginTemplate: NuxtTemplate = {
const imports: string[] = [] const imports: string[] = []
for (const plugin of clientPlugins) { for (const plugin of clientPlugins) {
const path = relative(ctx.nuxt.options.rootDir, plugin.src) const path = relative(ctx.nuxt.options.rootDir, plugin.src)
const variable = genSafeVariableName(filename(plugin.src)).replace(/_(45|46|47)/g, '_') + '_' + hash(path) const variable = genSafeVariableName(filename(plugin.src)).replace(PLUGIN_TEMPLATE_RE, '_') + '_' + hash(path)
exports.push(variable) exports.push(variable)
imports.push(genImport(plugin.src, variable)) imports.push(genImport(plugin.src, variable))
} }
@ -86,7 +88,7 @@ export const serverPluginTemplate: NuxtTemplate = {
const imports: string[] = [] const imports: string[] = []
for (const plugin of serverPlugins) { for (const plugin of serverPlugins) {
const path = relative(ctx.nuxt.options.rootDir, plugin.src) const path = relative(ctx.nuxt.options.rootDir, plugin.src)
const variable = genSafeVariableName(filename(path)).replace(/_(45|46|47)/g, '_') + '_' + hash(path) const variable = genSafeVariableName(filename(path)).replace(PLUGIN_TEMPLATE_RE, '_') + '_' + hash(path)
exports.push(variable) exports.push(variable)
imports.push(genImport(plugin.src, variable)) imports.push(genImport(plugin.src, variable))
} }
@ -98,7 +100,9 @@ export const serverPluginTemplate: NuxtTemplate = {
} }
const TS_RE = /\.[cm]?tsx?$/ const TS_RE = /\.[cm]?tsx?$/
const JS_LETTER_RE = /\.(?<letter>[cm])?jsx?$/
const JS_RE = /\.[cm]jsx?$/
const JS_CAPTURE_RE = /\.[cm](jsx?)$/
export const pluginsDeclaration: NuxtTemplate = { export const pluginsDeclaration: NuxtTemplate = {
filename: 'types/plugins.d.ts', filename: 'types/plugins.d.ts',
getContents: async ({ nuxt, app }) => { getContents: async ({ nuxt, app }) => {
@ -120,18 +124,18 @@ export const pluginsDeclaration: NuxtTemplate = {
const pluginPath = resolve(typesDir, plugin.src) const pluginPath = resolve(typesDir, plugin.src)
const relativePath = relative(typesDir, pluginPath) const relativePath = relative(typesDir, pluginPath)
const correspondingDeclaration = pluginPath.replace(/\.(?<letter>[cm])?jsx?$/, '.d.$<letter>ts') const correspondingDeclaration = pluginPath.replace(JS_LETTER_RE, '.d.$<letter>ts')
// if `.d.ts` file exists alongside a `.js` plugin, or if `.d.mts` file exists alongside a `.mjs` plugin, we can use the entire path // if `.d.ts` file exists alongside a `.js` plugin, or if `.d.mts` file exists alongside a `.mjs` plugin, we can use the entire path
if (correspondingDeclaration !== pluginPath && exists(correspondingDeclaration)) { if (correspondingDeclaration !== pluginPath && exists(correspondingDeclaration)) {
tsImports.push(relativePath) tsImports.push(relativePath)
continue continue
} }
const incorrectDeclaration = pluginPath.replace(/\.[cm]jsx?$/, '.d.ts') const incorrectDeclaration = pluginPath.replace(JS_RE, '.d.ts')
// if `.d.ts` file exists, but plugin is `.mjs`, add `.js` extension to the import // if `.d.ts` file exists, but plugin is `.mjs`, add `.js` extension to the import
// to hotfix issue until ecosystem updates to `@nuxt/module-builder@>=0.8.0` // to hotfix issue until ecosystem updates to `@nuxt/module-builder@>=0.8.0`
if (incorrectDeclaration !== pluginPath && exists(incorrectDeclaration)) { if (incorrectDeclaration !== pluginPath && exists(incorrectDeclaration)) {
tsImports.push(relativePath.replace(/\.[cm](jsx?)$/, '.$1')) tsImports.push(relativePath.replace(JS_CAPTURE_RE, '.$1'))
continue continue
} }
@ -174,11 +178,13 @@ export { }
} }
const adHocModules = ['router', 'pages', 'imports', 'meta', 'components', 'nuxt-config-schema'] const adHocModules = ['router', 'pages', 'imports', 'meta', 'components', 'nuxt-config-schema']
const IMPORT_NAME_RE = /\.\w+$/
const GIT_RE = /^git\+/
export const schemaTemplate: NuxtTemplate = { export const schemaTemplate: NuxtTemplate = {
filename: 'types/schema.d.ts', filename: 'types/schema.d.ts',
getContents: async ({ nuxt }) => { getContents: async ({ nuxt }) => {
const relativeRoot = relative(resolve(nuxt.options.buildDir, 'types'), nuxt.options.rootDir) const relativeRoot = relative(resolve(nuxt.options.buildDir, 'types'), nuxt.options.rootDir)
const getImportName = (name: string) => (name[0] === '.' ? './' + join(relativeRoot, name) : name).replace(/\.\w+$/, '') const getImportName = (name: string) => (name[0] === '.' ? './' + join(relativeRoot, name) : name).replace(IMPORT_NAME_RE, '')
const modules = nuxt.options._installedModules const modules = nuxt.options._installedModules
.filter(m => m.meta && m.meta.configKey && m.meta.name && !adHocModules.includes(m.meta.name)) .filter(m => m.meta && m.meta.configKey && m.meta.name && !adHocModules.includes(m.meta.name))
@ -210,7 +216,7 @@ export const schemaTemplate: NuxtTemplate = {
} }
if (link) { if (link) {
if (link.startsWith('git+')) { if (link.startsWith('git+')) {
link = link.replace(/^git\+/, '') link = link.replace(GIT_RE, '')
} }
if (!link.startsWith('http')) { if (!link.startsWith('http')) {
link = 'https://github.com/' + link link = 'https://github.com/' + link
@ -377,7 +383,7 @@ export const appConfigDeclarationTemplate: NuxtTemplate = {
filename: 'types/app.config.d.ts', filename: 'types/app.config.d.ts',
getContents ({ app, nuxt }) { getContents ({ app, nuxt }) {
const typesDir = join(nuxt.options.buildDir, 'types') const typesDir = join(nuxt.options.buildDir, 'types')
const configPaths = app.configs.map(path => relative(typesDir, path).replace(/\b\.\w+$/g, '')) const configPaths = app.configs.map(path => relative(typesDir, path).replace(EXTENSION_RE, ''))
return ` return `
import type { CustomAppConfig } from 'nuxt/schema' import type { CustomAppConfig } from 'nuxt/schema'

View File

@ -14,3 +14,7 @@ export function uniqueBy<T, K extends keyof T> (arr: T[], key: K) {
} }
return res return res
} }
export const QUOTE_RE = /["']/g
export const EXTENSION_RE = /\b\.\w+$/g
export const SX_RE = /\.[tj]sx$/

View File

@ -1,6 +1,7 @@
import { basename, dirname, extname, normalize } from 'pathe' import { basename, dirname, extname, normalize } from 'pathe'
import { kebabCase, splitByCase } from 'scule' import { kebabCase, splitByCase } from 'scule'
import { withTrailingSlash } from 'ufo' import { withTrailingSlash } from 'ufo'
import { QUOTE_RE } from '.'
export function getNameFromPath (path: string, relativeTo?: string) { export function getNameFromPath (path: string, relativeTo?: string) {
const relativePath = relativeTo const relativePath = relativeTo
@ -9,7 +10,7 @@ export function getNameFromPath (path: string, relativeTo?: string) {
const prefixParts = splitByCase(dirname(relativePath)) const prefixParts = splitByCase(dirname(relativePath))
const fileName = basename(relativePath, extname(relativePath)) const fileName = basename(relativePath, extname(relativePath))
const segments = resolveComponentNameSegments(fileName.toLowerCase() === 'index' ? '' : fileName, prefixParts).filter(Boolean) const segments = resolveComponentNameSegments(fileName.toLowerCase() === 'index' ? '' : fileName, prefixParts).filter(Boolean)
return kebabCase(segments).replace(/["']/g, '') return kebabCase(segments).replace(QUOTE_RE, '')
} }
export function hasSuffix (path: string, suffix: string) { export function hasSuffix (path: string, suffix: string) {

View File

@ -176,8 +176,10 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions) => createUnplugin
// https://github.com/vuejs/vue-loader/pull/1911 // https://github.com/vuejs/vue-loader/pull/1911
// https://github.com/vitejs/vite/issues/8473 // https://github.com/vitejs/vite/issues/8473
const QUERY_START_RE = /^\?/
const MACRO_RE = /&macro=true/
function rewriteQuery (id: string) { function rewriteQuery (id: string) {
return id.replace(/\?.+$/, r => '?macro=true&' + r.replace(/^\?/, '').replace(/&macro=true/, '')) return id.replace(/\?.+$/, r => '?macro=true&' + r.replace(QUERY_START_RE, '').replace(MACRO_RE, ''))
} }
function parseMacroQuery (id: string) { function parseMacroQuery (id: string) {
@ -189,6 +191,7 @@ function parseMacroQuery (id: string) {
return query return query
} }
const QUOTED_SPECIFIER_RE = /(["']).*\1/
function getQuotedSpecifier (id: string) { function getQuotedSpecifier (id: string) {
return id.match(/(["']).*\1/)?.[0] return id.match(QUOTED_SPECIFIER_RE)?.[0]
} }

View File

@ -5,11 +5,14 @@ type InstanceOf<T> = T extends new (...args: any[]) => infer R ? R : never
type RouterViewSlot = Exclude<InstanceOf<typeof RouterView>['$slots']['default'], undefined> type RouterViewSlot = Exclude<InstanceOf<typeof RouterView>['$slots']['default'], undefined>
export type RouterViewSlotProps = Parameters<RouterViewSlot>[0] export type RouterViewSlotProps = Parameters<RouterViewSlot>[0]
const ROUTE_KEY_PARENTHESES_RE = /(:\w+)\([^)]+\)/g
const ROUTE_KEY_SYMBOLS_RE = /(:\w+)[?+*]/g
const ROUTE_KEY_NORMAL_RE = /:\w+/g
const interpolatePath = (route: RouteLocationNormalizedLoaded, match: RouteLocationMatched) => { const interpolatePath = (route: RouteLocationNormalizedLoaded, match: RouteLocationMatched) => {
return match.path return match.path
.replace(/(:\w+)\([^)]+\)/g, '$1') .replace(ROUTE_KEY_PARENTHESES_RE, '$1')
.replace(/(:\w+)[?+*]/g, '$1') .replace(ROUTE_KEY_SYMBOLS_RE, '$1')
.replace(/:\w+/g, r => route.params[r.slice(1)]?.toString() || '') .replace(ROUTE_KEY_NORMAL_RE, r => route.params[r.slice(1)]?.toString() || '')
} }
export const generateRouteKey = (routeProps: RouterViewSlotProps, override?: string | ((route: RouteLocationNormalizedLoaded) => string)) => { export const generateRouteKey = (routeProps: RouterViewSlotProps, override?: string | ((route: RouteLocationNormalizedLoaded) => string)) => {

View File

@ -90,6 +90,7 @@ type GenerateRoutesFromFilesOptions = {
shouldUseServerComponents?: boolean shouldUseServerComponents?: boolean
} }
const INDEX_PAGE_RE = /\/index$/
export function generateRoutesFromFiles (files: ScannedFile[], options: GenerateRoutesFromFilesOptions = {}): NuxtPage[] { export function generateRoutesFromFiles (files: ScannedFile[], options: GenerateRoutesFromFilesOptions = {}): NuxtPage[] {
const routes: NuxtPage[] = [] const routes: NuxtPage[] = []
@ -135,7 +136,7 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
route.name += (route.name && '/') + segmentName route.name += (route.name && '/') + segmentName
// ex: parent.vue + parent/child.vue // ex: parent.vue + parent/child.vue
const path = withLeadingSlash(joinURL(route.path, getRoutePath(tokens).replace(/\/index$/, '/'))) const path = withLeadingSlash(joinURL(route.path, getRoutePath(tokens).replace(INDEX_PAGE_RE, '/')))
const child = parent.find(parentRoute => parentRoute.name === route.name && parentRoute.path === path) const child = parent.find(parentRoute => parentRoute.name === route.name && parentRoute.path === path)
if (child && child.children) { if (child && child.children) {
@ -307,6 +308,7 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
return extractedMeta return extractedMeta
} }
const COLON_RE = /:/g
function getRoutePath (tokens: SegmentToken[]): string { function getRoutePath (tokens: SegmentToken[]): string {
return tokens.reduce((path, token) => { return tokens.reduce((path, token) => {
return ( return (
@ -319,7 +321,7 @@ function getRoutePath (tokens: SegmentToken[]): string {
? `:${token.value}(.*)*` ? `:${token.value}(.*)*`
: token.type === SegmentTokenType.group : token.type === SegmentTokenType.group
? '' ? ''
: encodePath(token.value).replace(/:/g, '\\:')) : encodePath(token.value).replace(COLON_RE, '\\:'))
) )
}, '/') }, '/')
} }
@ -439,13 +441,14 @@ function findRouteByName (name: string, routes: NuxtPage[]): NuxtPage | undefine
return findRouteByName(name, routes) return findRouteByName(name, routes)
} }
const NESTED_PAGE_RE = /\//g
function prepareRoutes (routes: NuxtPage[], parent?: NuxtPage, names = new Set<string>()) { function prepareRoutes (routes: NuxtPage[], parent?: NuxtPage, names = new Set<string>()) {
for (const route of routes) { for (const route of routes) {
// Remove -index // Remove -index
if (route.name) { if (route.name) {
route.name = route.name route.name = route.name
.replace(/\/index$/, '') .replace(INDEX_PAGE_RE, '')
.replace(/\//g, '-') .replace(NESTED_PAGE_RE, '-')
if (names.has(route.name)) { if (names.has(route.name)) {
const existingRoute = findRouteByName(route.name, routes) const existingRoute = findRouteByName(route.name, routes)
@ -608,6 +611,7 @@ async function createClientPage(loader) {
} }
} }
const PATH_TO_NITRO_GLOB_RE = /\/[^:/]*:\w.*$/
export function pathToNitroGlob (path: string) { export function pathToNitroGlob (path: string) {
if (!path) { if (!path) {
return null return null
@ -617,7 +621,7 @@ export function pathToNitroGlob (path: string) {
return null return null
} }
return path.replace(/\/[^:/]*:\w.*$/, '/**') return path.replace(PATH_TO_NITRO_GLOB_RE, '/**')
} }
export function resolveRoutePaths (page: NuxtPage, parent = '/'): string[] { export function resolveRoutePaths (page: NuxtPage, parent = '/'): string[] {

View File

@ -20,6 +20,7 @@ interface ComposableKeysOptions {
const stringTypes: Array<string | undefined> = ['Literal', 'TemplateLiteral'] const stringTypes: Array<string | undefined> = ['Literal', 'TemplateLiteral']
const NUXT_LIB_RE = /node_modules\/(?:nuxt|nuxt3|nuxt-nightly)\// const NUXT_LIB_RE = /node_modules\/(?:nuxt|nuxt3|nuxt-nightly)\//
const SUPPORTED_EXT_RE = /\.(?:m?[jt]sx?|vue)/ const SUPPORTED_EXT_RE = /\.(?:m?[jt]sx?|vue)/
const SCRIPT_RE = /(?<=<script[^>]*>)[\s\S]*?(?=<\/script>)/i
export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptions) => { export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptions) => {
const composableMeta: Record<string, any> = {} const composableMeta: Record<string, any> = {}
@ -43,7 +44,7 @@ export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptio
}, },
transform (code, id) { transform (code, id) {
if (!KEYED_FUNCTIONS_RE.test(code)) { return } if (!KEYED_FUNCTIONS_RE.test(code)) { return }
const { 0: script = code, index: codeIndex = 0 } = code.match(/(?<=<script[^>]*>)[\s\S]*?(?=<\/script>)/i) || { index: 0, 0: code } const { 0: script = code, index: codeIndex = 0 } = code.match(SCRIPT_RE) || { index: 0, 0: code }
const s = new MagicString(code) const s = new MagicString(code)
// https://github.com/unjs/unplugin/issues/90 // https://github.com/unjs/unplugin/issues/90
let imports: Set<string> | undefined let imports: Set<string> | undefined

View File

@ -10,6 +10,7 @@ import { isCSSRequest } from 'vite'
const PREFIX = 'virtual:public?' const PREFIX = 'virtual:public?'
const CSS_URL_RE = /url\((\/[^)]+)\)/g const CSS_URL_RE = /url\((\/[^)]+)\)/g
const CSS_URL_SINGLE_RE = /url\(\/[^)]+\)/ const CSS_URL_SINGLE_RE = /url\(\/[^)]+\)/
const RENDER_CHUNK_RE = /(?<= = )['"`]/
interface VitePublicDirsPluginOptions { interface VitePublicDirsPluginOptions {
dev?: boolean dev?: boolean
@ -70,7 +71,7 @@ export const VitePublicDirsPlugin = createUnplugin((options: VitePublicDirsPlugi
if (!chunk.facadeModuleId?.includes('?inline&used')) { return } if (!chunk.facadeModuleId?.includes('?inline&used')) { return }
const s = new MagicString(code) const s = new MagicString(code)
const q = code.match(/(?<= = )['"`]/)?.[0] || '"' const q = code.match(RENDER_CHUNK_RE)?.[0] || '"'
for (const [full, url] of code.matchAll(CSS_URL_RE)) { for (const [full, url] of code.matchAll(CSS_URL_RE)) {
if (url && resolveFromPublicAssets(url)) { if (url && resolveFromPublicAssets(url)) {
s.replace(full, `url(${q} + publicAssetsURL(${q}${url}${q}) + ${q})`) s.replace(full, `url(${q} + publicAssetsURL(${q}${url}${q}) + ${q})`)
@ -108,13 +109,14 @@ export const VitePublicDirsPlugin = createUnplugin((options: VitePublicDirsPlugi
] ]
}) })
const PUBLIC_ASSETS_RE = /[?#].*$/
export function useResolveFromPublicAssets () { export function useResolveFromPublicAssets () {
const nitro = useNitro() const nitro = useNitro()
function resolveFromPublicAssets (id: string) { function resolveFromPublicAssets (id: string) {
for (const dir of nitro.options.publicAssets) { for (const dir of nitro.options.publicAssets) {
if (!id.startsWith(withTrailingSlash(dir.baseURL || '/'))) { continue } if (!id.startsWith(withTrailingSlash(dir.baseURL || '/'))) { continue }
const path = id.replace(/[?#].*$/, '').replace(withTrailingSlash(dir.baseURL || '/'), withTrailingSlash(dir.dir)) const path = id.replace(PUBLIC_ASSETS_RE, '').replace(withTrailingSlash(dir.baseURL || '/'), withTrailingSlash(dir.dir))
if (existsSync(path)) { if (existsSync(path)) {
return id return id
} }

View File

@ -23,6 +23,7 @@ const logLevelMapReverse: Record<NonNullable<vite.UserConfig['logLevel']>, numbe
info: 3, info: 3,
} }
const RUNTIME_RESOLVE_REF_RE = /^([^ ]+) referenced in/m
export function createViteLogger (config: vite.InlineConfig): vite.Logger { export function createViteLogger (config: vite.InlineConfig): vite.Logger {
const loggedErrors = new WeakSet<any>() const loggedErrors = new WeakSet<any>()
const canClearScreen = hasTTY && !isCI && config.clearScreen const canClearScreen = hasTTY && !isCI && config.clearScreen
@ -37,7 +38,7 @@ export function createViteLogger (config: vite.InlineConfig): vite.Logger {
if (msg.startsWith('Sourcemap') && msg.includes('node_modules')) { return } if (msg.startsWith('Sourcemap') && msg.includes('node_modules')) { return }
// Hide warnings about externals produced by https://github.com/vitejs/vite/blob/v5.2.11/packages/vite/src/node/plugins/css.ts#L350-L355 // Hide warnings about externals produced by https://github.com/vitejs/vite/blob/v5.2.11/packages/vite/src/node/plugins/css.ts#L350-L355
if (msg.includes('didn\'t resolve at build time, it will remain unchanged to be resolved at runtime')) { if (msg.includes('didn\'t resolve at build time, it will remain unchanged to be resolved at runtime')) {
const id = msg.trim().match(/^([^ ]+) referenced in/m)?.[1] const id = msg.trim().match(RUNTIME_RESOLVE_REF_RE)?.[1]
if (id && resolveFromPublicAssets(id)) { return } if (id && resolveFromPublicAssets(id)) { return }
} }
} }

View File

@ -7,6 +7,7 @@ import { importModule } from '@nuxt/kit'
const PLUGIN_NAME = 'dynamic-require' const PLUGIN_NAME = 'dynamic-require'
const HELPER_DYNAMIC = `\0${PLUGIN_NAME}.mjs` const HELPER_DYNAMIC = `\0${PLUGIN_NAME}.mjs`
const DYNAMIC_REQUIRE_RE = /import\("\.\/" ?\+(.*)\).then/g const DYNAMIC_REQUIRE_RE = /import\("\.\/" ?\+(.*)\).then/g
const BACKWARD_SLASH_RE = /\\/g
interface Options { interface Options {
dir: string dir: string
@ -75,7 +76,7 @@ export function dynamicRequire ({ dir, ignore, inline }: Options): Plugin {
await Promise.all( await Promise.all(
files.map(async id => ({ files.map(async id => ({
id, id,
src: resolve(dir, id).replace(/\\/g, '/'), src: resolve(dir, id).replace(BACKWARD_SLASH_RE, '/'),
name: genSafeVariableName(id), name: genSafeVariableName(id),
meta: await getWebpackChunkMeta(resolve(dir, id)), meta: await getWebpackChunkMeta(resolve(dir, id)),
})), })),

View File

@ -24,6 +24,8 @@ export const isJS = (file: string) => isJSRegExp.test(file)
export const extractQueryPartJS = (file: string) => isJSRegExp.exec(file)?.[1] export const extractQueryPartJS = (file: string) => isJSRegExp.exec(file)?.[1]
export const isCSS = (file: string) => /\.css(?:\?[^.]+)?$/.test(file) const isCSSRegExp = /\.css(?:\?[^.]+)?$/
export const isCSS = (file: string) => isCSSRegExp.test(file)
export const isHotUpdate = (file: string) => file.includes('hot-update') export const isHotUpdate = (file: string) => file.includes('hot-update')