diff --git a/packages/kit/src/compatibility.ts b/packages/kit/src/compatibility.ts index e2859e1812..e650dc0710 100644 --- a/packages/kit/src/compatibility.ts +++ b/packages/kit/src/compatibility.ts @@ -3,8 +3,9 @@ import { readPackageJSON } from 'pkg-types' import type { Nuxt, NuxtCompatibility, NuxtCompatibilityIssues } from '@nuxt/schema' import { useNuxt } from './context' +const SEMANTIC_VERSION_RE = /-\d+\.[0-9a-f]+/ 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 = { @@ -104,6 +105,7 @@ export function isNuxt3 (nuxt: Nuxt = useNuxt()) { return isNuxtMajorVersion(3, nuxt) } +const NUXT_VERSION_RE = /^v/g /** * Get nuxt version */ @@ -112,5 +114,5 @@ export function getNuxtVersion (nuxt: Nuxt | any = useNuxt() /* TODO: LegacyNuxt if (typeof rawVersion !== 'string') { throw new TypeError('Cannot determine nuxt version! Is current instance passed?') } - return rawVersion.replace(/^v/g, '') + return rawVersion.replace(NUXT_VERSION_RE, '') } diff --git a/packages/kit/src/components.ts b/packages/kit/src/components.ts index 669d6ce410..82394e3811 100644 --- a/packages/kit/src/components.ts +++ b/packages/kit/src/components.ts @@ -3,6 +3,7 @@ import type { Component, ComponentsDir } from '@nuxt/schema' import { useNuxt } from './context' import { assertNuxtCompatibility } from './compatibility' import { logger } from './logger' +import { MODE_RE } from './utils' /** * 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 || [] 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' } diff --git a/packages/kit/src/layout.ts b/packages/kit/src/layout.ts index 65fd183163..a416fa8b21 100644 --- a/packages/kit/src/layout.ts +++ b/packages/kit/src/layout.ts @@ -5,10 +5,11 @@ import { useNuxt } from './context' import { logger } from './logger' import { addTemplate } from './template' +const LAYOUT_RE = /["']/g export function addLayout (template: NuxtTemplate | string, name?: string) { const nuxt = useNuxt() 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.hook('app:templates', (app) => { diff --git a/packages/kit/src/nitro.ts b/packages/kit/src/nitro.ts index 29b0b44981..4c046f6a9d 100644 --- a/packages/kit/src/nitro.ts +++ b/packages/kit/src/nitro.ts @@ -4,13 +4,14 @@ import { normalize } from 'pathe' import { useNuxt } from './context' import { toArray } from './utils' +const HANDLER_METHOD_RE = /\.(get|head|patch|post|put|delete|connect|options|trace)(\.\w+)*$/ /** * normalize handler object * */ function normalizeHandlerMethod (handler: NitroEventHandler) { // 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 { method: method as 'get' | 'head' | 'patch' | 'post' | 'put' | 'delete' | 'connect' | 'options' | 'trace' | undefined, ...handler, diff --git a/packages/kit/src/plugin.ts b/packages/kit/src/plugin.ts index aadcdbc718..116721214f 100644 --- a/packages/kit/src/plugin.ts +++ b/packages/kit/src/plugin.ts @@ -3,6 +3,7 @@ import type { NuxtPlugin, NuxtPluginTemplate } from '@nuxt/schema' import { useNuxt } from './context' import { addTemplate } from './template' import { resolveAlias } from './resolve' +import { MODE_RE } from './utils' /** * Normalize a nuxt plugin object @@ -27,7 +28,7 @@ export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin { plugin.mode = 'server' } 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' } diff --git a/packages/kit/src/template.ts b/packages/kit/src/template.ts index 8c40713e76..8b4ae046fd 100644 --- a/packages/kit/src/template.ts +++ b/packages/kit/src/template.ts @@ -123,6 +123,9 @@ export async function updateTemplates (options?: { filter?: (template: ResolvedN 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) { const rootDirWithSlash = withTrailingSlash(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 = nuxt.options.alias - // Exclude bridge alias types to support Volar - const excludedAlias = [/^@vue\/.*$/, /^#internal\/nuxt/] - const basePath = tsConfig.compilerOptions!.baseUrl ? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl) : nuxt.options.buildDir @@ -260,7 +260,7 @@ export async function _generateTypes (nuxt: Nuxt) { } else { const path = stats?.isFile() // remove extension - ? relativePath.replace(/\b\.\w+$/g, '') + ? relativePath.replace(EXTENSION_RE, '') // non-existent file probably shouldn't be resolved : aliases[alias]! @@ -289,7 +289,7 @@ export async function _generateTypes (nuxt: Nuxt) { tsConfig.compilerOptions!.paths[alias] = await Promise.all(paths.map(async (path: string) => { if (!isAbsolute(path)) { return path } 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}"` : '' } +const RELATIVE_WITH_DOT_RE = /^([^.])/ function relativeWithDot (from: string, to: string) { - return relative(from, to).replace(/^([^.])/, './$1') || '.' + return relative(from, to).replace(RELATIVE_WITH_DOT_RE, './$1') || '.' } diff --git a/packages/kit/src/utils.ts b/packages/kit/src/utils.ts index 72b096120b..89fa591c50 100644 --- a/packages/kit/src/utils.ts +++ b/packages/kit/src/utils.ts @@ -2,3 +2,5 @@ export function toArray (value: T | T[]): T[] { return Array.isArray(value) ? value : [value] } + +export const MODE_RE = /\.(server|client)(\.\w+)*$/ diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts index 5d353c8375..20d1f11cbf 100644 --- a/packages/nuxt/src/app/components/nuxt-island.ts +++ b/packages/nuxt/src/app/components/nuxt-island.ts @@ -22,6 +22,7 @@ const SSR_UID_RE = /data-island-uid="([^"]*)"/ const DATA_ISLAND_UID_RE = /data-island-uid(="")?(?!="[^"])/g const SLOTNAME_RE = /data-island-slot="([^"]*)"/g const SLOT_FALLBACK_RE = / data-island-slot="([^"]*)"[^>]*>/g +const ISLAND_SCOPE_ID_RE = /^<[^> ]*/ let id = 1 const getId = import.meta.client ? () => (id++).toString() : randomUUID @@ -142,7 +143,7 @@ export default defineComponent({ let html = ssrHTML.value 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) { diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts index 77006518a2..aa50e11e09 100644 --- a/packages/nuxt/src/app/components/nuxt-link.ts +++ b/packages/nuxt/src/app/components/nuxt-link.ts @@ -521,11 +521,12 @@ function useObserver (): { observe: ObserveFn } | undefined { return _observer } +const IS_2G_RE = /2g/ function isSlowConnection () { if (import.meta.server) { return } // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/connection 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 } diff --git a/packages/nuxt/src/app/components/utils.ts b/packages/nuxt/src/app/components/utils.ts index 0bde127ec5..5fe9739e31 100644 --- a/packages/nuxt/src/app/components/utils.ts +++ b/packages/nuxt/src/app/components/utils.ts @@ -15,13 +15,16 @@ export const _wrapIf = (component: Component, props: any, slots: any) => { 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 // See https://github.com/nuxt/nuxt/tree/main/packages/nuxt/src/pages/runtime/utils.ts#L8-L19 function generateRouteKey (route: RouteLocationNormalized) { const source = route?.meta.key ?? route.path - .replace(/(:\w+)\([^)]+\)/g, '$1') - .replace(/(:\w+)[?+*]/g, '$1') - .replace(/:\w+/g, r => route.params[r.slice(1)]?.toString() || '') + .replace(ROUTE_KEY_PARENTHESES_RE, '$1') + .replace(ROUTE_KEY_SYMBOLS_RE, '$1') + .replace(ROUTE_KEY_NORMAL_RE, r => route.params[r.slice(1)]?.toString() || '') return typeof source === 'function' ? source(route) : source } diff --git a/packages/nuxt/src/app/composables/router.ts b/packages/nuxt/src/app/composables/router.ts index fa8be0805c..0814873ff0 100644 --- a/packages/nuxt/src/app/composables/router.ts +++ b/packages/nuxt/src/app/composables/router.ts @@ -114,6 +114,7 @@ export interface NavigateToOptions { open?: OpenOptions } +const URL_QUOTE_RE = /"/g /** @since 3.0.0 */ export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: NavigateToOptions): Promise | false | void | RouteLocationRaw => { if (!to) { @@ -166,7 +167,7 @@ export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: Na const redirect = async function (response: any) { // TODO: consider deprecating in favour of `app:rendered` and removing await nuxtApp.callHook('app:redirected') - const encodedLoc = location.replace(/"/g, '%22') + const encodedLoc = location.replace(URL_QUOTE_RE, '%22') const encodedHeader = encodeURL(location, isExternalHost) nuxtApp.ssrContext!._renderResponse = { diff --git a/packages/nuxt/src/components/module.ts b/packages/nuxt/src/components/module.ts index 8f1589bb08..a2a1d8ca7d 100644 --- a/packages/nuxt/src/components/module.ts +++ b/packages/nuxt/src/components/module.ts @@ -16,11 +16,13 @@ import { ComponentNamePlugin } from './plugins/component-names' 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 SLASH_SEPARATOR_RE = /[\\/]/ 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 STARTER_DOT_RE = /^\./g export type getComponentsT = (mode?: 'client' | 'server' | 'all') => Component[] @@ -89,7 +91,7 @@ export default defineNuxtModule({ const dirOptions: ComponentsDir = typeof dir === 'object' ? dir : { path: dir } const dirPath = resolveAlias(dirOptions.path) 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) if (!present && !DEFAULT_COMPONENTS_DIRS_RE.test(dirOptions.path)) { diff --git a/packages/nuxt/src/components/plugins/client-fallback-auto-id.ts b/packages/nuxt/src/components/plugins/client-fallback-auto-id.ts index 9f3e1b119c..85c3d8b82d 100644 --- a/packages/nuxt/src/components/plugins/client-fallback-auto-id.ts +++ b/packages/nuxt/src/components/plugins/client-fallback-auto-id.ts @@ -12,6 +12,7 @@ interface LoaderOptions { } const CLIENT_FALLBACK_RE = /<(?:NuxtClientFallback|nuxt-client-fallback)(?: [^>]*)?>/ const CLIENT_FALLBACK_GLOBAL_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/g +const UID_RE = / :?uid=/ export const ClientFallbackAutoIdPlugin = (options: LoaderOptions) => createUnplugin(() => { const exclude = options.transform?.exclude || [] const include = options.transform?.include || [] @@ -37,7 +38,7 @@ export const ClientFallbackAutoIdPlugin = (options: LoaderOptions) => createUnpl s.replace(CLIENT_FALLBACK_GLOBAL_RE, (full, name, attrs) => { count++ - if (/ :?uid=/.test(attrs)) { return full } + if (UID_RE.test(attrs)) { return full } return `<${name} :uid="'${hash(relativeID)}' + JSON.stringify($props) + '${count}'" ${attrs ?? ''}>` }) diff --git a/packages/nuxt/src/components/plugins/component-names.ts b/packages/nuxt/src/components/plugins/component-names.ts index 4a24ebe96b..af01adb5dc 100644 --- a/packages/nuxt/src/components/plugins/component-names.ts +++ b/packages/nuxt/src/components/plugins/component-names.ts @@ -1,12 +1,13 @@ import { createUnplugin } from 'unplugin' import MagicString from 'magic-string' import type { Component } from 'nuxt/schema' -import { isVue } from '../../core/utils' +import { SX_RE, isVue } from '../../core/utils' interface NameDevPluginOptions { sourcemap: boolean getComponents: () => Component[] } +const FILENAME_RE = /([^/\\]+)\.\w+$/ /** * 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', enforce: 'post', transformInclude (id) { - return isVue(id) || !!id.match(/\.[tj]sx$/) + return isVue(id) || !!id.match(SX_RE) }, transform (code, id) { - const filename = id.match(/([^/\\]+)\.\w+$/)?.[1] + const filename = id.match(FILENAME_RE)?.[1] if (!filename) { return } diff --git a/packages/nuxt/src/components/plugins/islands-transform.ts b/packages/nuxt/src/components/plugins/islands-transform.ts index 70ad9fb895..a3e2aba41a 100644 --- a/packages/nuxt/src/components/plugins/islands-transform.ts +++ b/packages/nuxt/src/components/plugins/islands-transform.ts @@ -30,6 +30,7 @@ const TEMPLATE_RE = /