refactor: improve regexp performance (#27207)

This commit is contained in:
Anthony Fu 2024-05-14 19:54:37 +02:00 committed by GitHub
parent 7189dafd26
commit b96b62ecd2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 113 additions and 61 deletions

View File

@ -5,6 +5,7 @@ import noOnlyTests from 'eslint-plugin-no-only-tests'
import typegen from 'eslint-typegen' import typegen from 'eslint-typegen'
// @ts-expect-error missing types // @ts-expect-error missing types
import perfectionist from 'eslint-plugin-perfectionist' import perfectionist from 'eslint-plugin-perfectionist'
import regex from 'eslint-plugin-regexp'
export default createConfigForNuxt({ export default createConfigForNuxt({
features: { features: {
@ -216,6 +217,9 @@ export default createConfigForNuxt({
'vue/multi-word-component-names': 'off', 'vue/multi-word-component-names': 'off',
}, },
}, },
// @ts-ignore types misaligned
regex.configs['flat/recommended'],
) )
// Generate type definitions for the eslint config // Generate type definitions for the eslint config

View File

@ -64,6 +64,7 @@
"eslint": "9.2.0", "eslint": "9.2.0",
"eslint-plugin-no-only-tests": "3.1.0", "eslint-plugin-no-only-tests": "3.1.0",
"eslint-plugin-perfectionist": "2.10.0", "eslint-plugin-perfectionist": "2.10.0",
"eslint-plugin-regexp": "^2.5.0",
"eslint-typegen": "0.2.4", "eslint-typegen": "0.2.4",
"execa": "9.1.0", "execa": "9.1.0",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",

View File

@ -4,7 +4,7 @@ import type { Nuxt, NuxtCompatibility, NuxtCompatibilityIssues } from '@nuxt/sch
import { useNuxt } from './context' import { useNuxt } from './context'
export function normalizeSemanticVersion (version: string) { export function normalizeSemanticVersion (version: string) {
return version.replace(/-[0-9]+\.[0-9a-f]+/, '') // Remove edge prefix return version.replace(/-\d+\.[0-9a-f]+/, '') // Remove edge prefix
} }
const builderMap = { const builderMap = {

View File

@ -27,7 +27,7 @@ export async function compileTemplate<T> (template: NuxtTemplate<T>, ctx: any) {
} }
/** @deprecated */ /** @deprecated */
const serialize = (data: any) => JSON.stringify(data, null, 2).replace(/"{(.+)}"(?=,?$)/gm, r => JSON.parse(r).replace(/^{(.*)}$/, '$1')) const serialize = (data: any) => JSON.stringify(data, null, 2).replace(/"\{(.+)\}"(?=,?$)/gm, r => JSON.parse(r).replace(/^\{(.*)\}$/, '$1'))
/** @deprecated */ /** @deprecated */
const importSources = (sources: string | string[], { lazy = false } = {}) => { const importSources = (sources: string | string[], { lazy = false } = {}) => {

View File

@ -94,7 +94,7 @@ function applyEnv (
return obj return obj
} }
const envExpandRx = /{{(.*?)}}/g const envExpandRx = /\{\{(.*?)\}\}/g
function _expandFromEnv (value: string, env: Record<string, any> = process.env) { function _expandFromEnv (value: string, env: Record<string, any> = process.env) {
return value.replace(envExpandRx, (match, key) => { return value.replace(envExpandRx, (match, key) => {

View File

@ -202,7 +202,7 @@ export async function _generateTypes (nuxt: Nuxt) {
} else { } else {
const path = stats?.isFile() const path = stats?.isFile()
// remove extension // remove extension
? relativePath.replace(/(?<=\w)\.\w+$/g, '') ? relativePath.replace(/\b\.\w+$/g, '')
// non-existent file probably shouldn't be resolved // non-existent file probably shouldn't be resolved
: aliases[alias] : aliases[alias]
@ -230,7 +230,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(/(?<=\w)\.\w+$/g, '') /* remove extension */ : path) return relativeWithDot(nuxt.options.buildDir, stats?.isFile() ? path.replace(/\b\.\w+$/g, '') /* remove extension */ : path)
})) }))
} }

View File

@ -10,7 +10,7 @@ interface LoaderOptions {
transform?: ComponentsOptions['transform'] transform?: ComponentsOptions['transform']
rootDir: string rootDir: string
} }
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
export const clientFallbackAutoIdPlugin = createUnplugin((options: LoaderOptions) => { export const clientFallbackAutoIdPlugin = createUnplugin((options: LoaderOptions) => {
const exclude = options.transform?.exclude || [] const exclude = options.transform?.exclude || []
@ -37,7 +37,7 @@ export const clientFallbackAutoIdPlugin = createUnplugin((options: LoaderOptions
s.replace(CLIENT_FALLBACK_GLOBAL_RE, (full, name, attrs) => { s.replace(CLIENT_FALLBACK_GLOBAL_RE, (full, name, attrs) => {
count++ count++
if (/ :?uid=/g.test(attrs)) { return full } if (/ :?uid=/.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

@ -25,7 +25,7 @@ interface ComponentChunkOptions {
} }
const SCRIPT_RE = /<script[^>]*>/g const SCRIPT_RE = /<script[^>]*>/g
const HAS_SLOT_OR_CLIENT_RE = /(<slot[^>]*>)|(nuxt-client)/ const HAS_SLOT_OR_CLIENT_RE = /<slot[^>]*>|nuxt-client/
const TEMPLATE_RE = /<template>([\s\S]*)<\/template>/ 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 { 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 { 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\''

View File

@ -43,7 +43,7 @@ export const loaderPlugin = createUnplugin((options: LoaderOptions) => {
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)?([^'"]*?)["'][\s,]*[^)]*\)/g, (full: string, lazy: string, name: string) => { s.replace(/(?<=[ (])_?resolveComponent\(\s*["'](lazy-|Lazy)?([^'"]*)["'][^)]*\)/g, (full: string, lazy: string, name: string) => {
const component = findComponent(components, name, options.mode) const component = findComponent(components, name, options.mode)
if (component) { if (component) {
// @ts-expect-error TODO: refactor to nuxi // @ts-expect-error TODO: refactor to nuxi

View File

@ -18,7 +18,7 @@ function compareDirByPathLength ({ path: pathA }: { path: string }, { path: path
return pathB.split(/[\\/]/).filter(Boolean).length - pathA.split(/[\\/]/).filter(Boolean).length return pathB.split(/[\\/]/).filter(Boolean).length - pathA.split(/[\\/]/).filter(Boolean).length
} }
const DEFAULT_COMPONENTS_DIRS_RE = /\/components(\/global|\/islands)?$/ const DEFAULT_COMPONENTS_DIRS_RE = /\/components\/(?:global|islands)?$/
export type getComponentsT = (mode?: 'client' | 'server' | 'all') => Component[] export type getComponentsT = (mode?: 'client' | 'server' | 'all') => Component[]

View File

@ -83,8 +83,8 @@ 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(?:\.global)?$/.test(fileName) || dir.island
const global = /\.(global)(\.island)?$/.test(fileName) || dir.global const global = /\.global(?:\.island)?$/.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(/(?<=\.)(client|server)(\.global|\.island)*$/)?.[1] || 'all') as 'client' | 'server' | 'all'
fileName = fileName.replace(/(\.(client|server))?(\.global|\.island)*$/, '') fileName = fileName.replace(/(\.(client|server))?(\.global|\.island)*$/, '')

View File

@ -104,8 +104,8 @@ export const componentsTypeTemplate = {
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(/(?<=\w)\.(?!vue)\w+$/g, '') ? relative(buildDir, c.filePath).replace(/\b\.(?!vue)\w+$/g, '')
: c.filePath.replace(/(?<=\w)\.(?!vue)\w+$/g, ''), { wrapper: false })}['${c.export}']` : c.filePath.replace(/\b\.(?!vue)\w+$/g, ''), { 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

@ -16,7 +16,7 @@ interface TreeShakeTemplatePluginOptions {
type AcornNode<N extends Node> = N & { start: number, end: number } type AcornNode<N extends Node> = N & { start: number, end: number }
const SSR_RENDER_RE = /ssrRenderComponent/ const SSR_RENDER_RE = /ssrRenderComponent/
const PLACEHOLDER_EXACT_RE = /^(fallback|placeholder)$/ const PLACEHOLDER_EXACT_RE = /^(?:fallback|placeholder)$/
const CLIENT_ONLY_NAME_RE = /^(?:_unref\()?(?:_component_)?(?:Lazy|lazy_)?(?:client_only|ClientOnly\)?)$/ const CLIENT_ONLY_NAME_RE = /^(?:_unref\()?(?:_component_)?(?:Lazy|lazy_)?(?:client_only|ClientOnly\)?)$/
const PARSER_OPTIONS = { sourceType: 'module', ecmaVersion: 'latest' } const PARSER_OPTIONS = { sourceType: 'module', ecmaVersion: 'latest' }

View File

@ -24,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 => /^(?:app\.|error\.|plugins\/|middleware\/|layouts\/)/i.test(relativePath))
if (restartPath) { if (restartPath) {
if (restartPath.startsWith('app')) { if (restartPath.startsWith('app')) {
app.mainComponent = undefined app.mainComponent = undefined

View File

@ -383,7 +383,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(/(?<=\w)\.\w+$/g, '')] /* remove extension */ tsConfig.compilerOptions.paths[alias] = [absolutePath.replace(/\b\.\w+$/g, '')] /* remove extension */
} }
} }

View File

@ -621,4 +621,4 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
return nuxt return nuxt
} }
const RESTART_RE = /^(app|error|app\.config)\.(js|ts|mjs|jsx|tsx|vue)$/i const RESTART_RE = /^(?:app|error|app\.config)\.(?:js|ts|mjs|jsx|tsx|vue)$/i

View File

@ -22,7 +22,7 @@ export const nuxtImportProtections = (nuxt: { options: NuxtOptions }, options: {
]) ])
patterns.push([ patterns.push([
/^((|~|~~|@|@@)\/)?nuxt\.config(\.|$)/, /^((~|~~|@|@@)?\/)?nuxt\.config(\.|$)/,
'Importing directly from a `nuxt.config` file is not allowed. Instead, use runtime config or a module.', 'Importing directly from a `nuxt.config` file is not allowed. Instead, use runtime config or a module.',
]) ])

View File

@ -70,8 +70,8 @@ function getStack () {
return stack.stack?.replace(EXCLUDE_TRACE_RE, '').replace(/^Error.*\n/, '') || '' return stack.stack?.replace(EXCLUDE_TRACE_RE, '').replace(/^Error.*\n/, '') || ''
} }
const FILENAME_RE = /at.*\(([^:)]+)[):]/ const FILENAME_RE = /at[^(]*\(([^:)]+)[):]/
const FILENAME_RE_GLOBAL = /at.*\(([^)]+)\)/g const FILENAME_RE_GLOBAL = /at[^(]*\(([^)]+)\)/g
function extractFilenameFromStack (stacktrace: string) { function extractFilenameFromStack (stacktrace: string) {
return stacktrace.match(FILENAME_RE)?.[1].replace(withTrailingSlash(rootDir), '') return stacktrace.match(FILENAME_RE)?.[1].replace(withTrailingSlash(rootDir), '')
} }

View File

@ -646,7 +646,7 @@ function getServerComponentHTML (body: string[]): string {
const SSR_SLOT_TELEPORT_MARKER = /^uid=([^;]*);slot=(.*)$/ const SSR_SLOT_TELEPORT_MARKER = /^uid=([^;]*);slot=(.*)$/
const SSR_CLIENT_TELEPORT_MARKER = /^uid=([^;]*);client=(.*)$/ const SSR_CLIENT_TELEPORT_MARKER = /^uid=([^;]*);client=(.*)$/
const SSR_CLIENT_SLOT_MARKER = /^island-slot=(?:[^;]*);(.*)$/ const SSR_CLIENT_SLOT_MARKER = /^island-slot=[^;]*;(.*)$/
function getSlotIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse['slots'] { function getSlotIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse['slots'] {
if (!ssrContext.islandContext || !Object.keys(ssrContext.islandContext.slots).length) { return undefined } if (!ssrContext.islandContext || !Object.keys(ssrContext.islandContext.slots).length) { return undefined }

View File

@ -269,7 +269,7 @@ export const appConfigDeclarationTemplate: NuxtTemplate = {
return ` return `
import type { CustomAppConfig } from 'nuxt/schema' import type { CustomAppConfig } from 'nuxt/schema'
import type { Defu } from 'defu' import type { Defu } from 'defu'
${app.configs.map((id: string, index: number) => `import ${`cfg${index}`} from ${JSON.stringify(id.replace(/(?<=\w)\.\w+$/g, ''))}`).join('\n')} ${app.configs.map((id: string, index: number) => `import ${`cfg${index}`} from ${JSON.stringify(id.replace(/\b\.\w+$/g, ''))}`).join('\n')}
declare const inlineConfig = ${JSON.stringify(nuxt.options.appConfig, null, 2)} declare const inlineConfig = ${JSON.stringify(nuxt.options.appConfig, null, 2)}
type ResolvedAppConfig = Defu<typeof inlineConfig, [${app.configs.map((_id: string, index: number) => `typeof cfg${index}`).join(', ')}]> type ResolvedAppConfig = Defu<typeof inlineConfig, [${app.configs.map((_id: string, index: number) => `typeof cfg${index}`).join(', ')}]>

View File

@ -34,7 +34,7 @@ export function isVue (id: string, opts: { type?: Array<'template' | 'script' |
return true return true
} }
const JS_RE = /\.((c|m)?j|t)sx?$/ const JS_RE = /\.(?:[cm]?j|t)sx?$/
export function isJS (id: string) { export function isJS (id: string) {
// JavaScript files // JavaScript files

View File

@ -23,7 +23,7 @@ export default defineNuxtPlugin(async () => {
// Implementation // Implementation
const OPTIONAL_PARAM_RE = /^\/?:.*(\?|\(\.\*\)\*)$/ const OPTIONAL_PARAM_RE = /^\/?:.*(?:\?|\(\.\*\)\*)$/
function processRoutes (routes: RouteRecordRaw[], currentPath = '/', routesToPrerender = new Set<string>()) { function processRoutes (routes: RouteRecordRaw[], currentPath = '/', routesToPrerender = new Set<string>()) {
for (const route of routes) { for (const route of routes) {

View File

@ -135,7 +135,7 @@ export async function generateRoutesFromFiles (files: ScannedFile[], options: Ge
return prepareRoutes(routes) return prepareRoutes(routes)
} }
const SFC_SCRIPT_RE = /<script\s*[^>]*>([\s\S]*?)<\/script\s*[^>]*>/i const SFC_SCRIPT_RE = /<script[^>]*>([\s\S]*?)<\/script[^>]*>/i
export function extractScriptContent (html: string) { export function extractScriptContent (html: string) {
const match = html.match(SFC_SCRIPT_RE) const match = html.match(SFC_SCRIPT_RE)
@ -146,7 +146,7 @@ export function extractScriptContent (html: string) {
return null return null
} }
const PAGE_META_RE = /(definePageMeta\([\s\S]*?\))/ const PAGE_META_RE = /definePageMeta\([\s\S]*?\)/
const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const
const pageContentsCache: Record<string, string> = {} const pageContentsCache: Record<string, string> = {}
@ -261,7 +261,7 @@ function getRoutePath (tokens: SegmentToken[]): string {
}, '/') }, '/')
} }
const PARAM_CHAR_RE = /[\w\d_.]/ const PARAM_CHAR_RE = /[\w.]/
function parseSegment (segment: string) { function parseSegment (segment: string) {
let state: SegmentParserState = SegmentParserState.initial let state: SegmentParserState = SegmentParserState.initial
@ -537,7 +537,7 @@ export function pathToNitroGlob (path: string) {
return null return null
} }
return path.replace(/\/(?:[^:/]+)?:\w+.*$/, '/**') return path.replace(/\/[^:/]*:\w.*$/, '/**')
} }
export function resolveRoutePaths (page: NuxtPage, parent = '/'): string[] { export function resolveRoutePaths (page: NuxtPage, parent = '/'): string[] {

View File

@ -182,7 +182,7 @@ describe('treeshake client only in ssr', () => {
expect(treeshaken).not.toContain('ssrRenderComponent(_unref(HelloWorld') expect(treeshaken).not.toContain('ssrRenderComponent(_unref(HelloWorld')
expect(treeshaken).toContain('ssrRenderComponent(_unref(Glob') expect(treeshaken).toContain('ssrRenderComponent(_unref(Glob')
} }
expect(treeshaken.replace(/data-v-[\d\w]{8}/g, 'data-v-one-hash').replace(/scoped=[\d\w]{8}/g, 'scoped=one-hash')).toMatchSnapshot() expect(treeshaken.replace(/data-v-\w{8}/g, 'data-v-one-hash').replace(/scoped=\w{8}/g, 'scoped=one-hash')).toMatchSnapshot()
}) })
} }

View File

@ -23,7 +23,7 @@ export const DevRenderingPlugin = () => {
const messages = JSON.parse(await fsp.readFile(r(page, 'messages.json'), 'utf-8')) const messages = JSON.parse(await fsp.readFile(r(page, 'messages.json'), 'utf-8'))
return template(contents, { return template(contents, {
interpolate: /{{{?([\s\S]+?)}?}}/g, interpolate: /\{\{\{?([\s\S]+?)\}?\}\}/g,
})({ })({
messages: { ...genericMessages, ...messages }, messages: { ...genericMessages, ...messages },
}) })

View File

@ -58,7 +58,7 @@ export const RenderPlugin = () => {
} }
// Inline our scripts // Inline our scripts
const scriptSources = Array.from(html.matchAll(/<script[^>]*src="(.*)"[^>]*>[\s\S]*?<\/script>/g)) const scriptSources = Array.from(html.matchAll(/<script[^>]*src="([^"]*)"[^>]*>[\s\S]*?<\/script>/g))
.filter(([_block, src]) => src?.match(/^\/.*\.js$/)) .filter(([_block, src]) => src?.match(/^\/.*\.js$/))
for (const [scriptBlock, src] of scriptSources) { for (const [scriptBlock, src] of scriptSources) {
@ -79,10 +79,10 @@ export const RenderPlugin = () => {
const messages = JSON.parse(readFileSync(r(`templates/${templateName}/messages.json`), 'utf-8')) const messages = JSON.parse(readFileSync(r(`templates/${templateName}/messages.json`), 'utf-8'))
// Serialize into a js function // Serialize into a js function
const chunks = html.split(/\{{2,3}\s*[^{}]+\s*\}{2,3}/g).map(chunk => JSON.stringify(chunk)) const chunks = html.split(/\{{2,3}[^{}]+\}{2,3}/g).map(chunk => JSON.stringify(chunk))
const hasMessages = chunks.length > 1 const hasMessages = chunks.length > 1
let templateString = chunks.shift() let templateString = chunks.shift()
for (const expression of html.matchAll(/\{{2,3}(\s*[^{}]+\s*)\}{2,3}/g)) { for (const expression of html.matchAll(/\{{2,3}([^{}]+)\}{2,3}/g)) {
templateString += ` + (${expression[1].trim()}) + ${chunks.shift()}` templateString += ` + (${expression[1].trim()}) + ${chunks.shift()}`
} }
if (chunks.length > 0) { if (chunks.length > 0) {
@ -98,28 +98,28 @@ export const RenderPlugin = () => {
].join('\n') ].join('\n')
const templateContent = html const templateContent = html
.match(/<body.*?>([\s\S]*)<\/body>/)?.[0] .match(/<body[^>]*>([\s\S]*)<\/body>/)?.[0]
.replace(/(?<=<|<\/)body/g, 'div') .replace(/(?<=<\/|<)body/g, 'div')
.replace(/messages\./g, '') .replace(/messages\./g, '')
.replace(/<script[^>]*>([\s\S]*?)<\/script>/g, '') .replace(/<script[^>]*>([\s\S]*?)<\/script>/g, '')
.replace(/<a href="(\/[^"]*)"([^>]*)>([\s\S]*)<\/a>/g, '<NuxtLink to="$1"$2>\n$3\n</NuxtLink>') .replace(/<a href="(\/[^"]*)"([^>]*)>([\s\S]*)<\/a>/g, '<NuxtLink to="$1"$2>\n$3\n</NuxtLink>')
.replace(/<([^>]+) ([a-z]+)="([^"]*)({{\s*(\w+?)\s*}})([^"]*)"([^>]*)>/g, '<$1 :$2="`$3${$5}$6`"$7>') .replace(/<([^>]+) ([a-z]+)="([^"]*)(\{\{\s*(\w+)\s*\}\})([^"]*)"([^>]*)>/g, '<$1 :$2="`$3${$5}$6`"$7>')
.replace(/>{{\s*(\w+?)\s*}}<\/[\w-]*>/g, ' v-text="$1" />') .replace(/>\{\{\s*(\w+)\s*\}\}<\/[\w-]*>/g, ' v-text="$1" />')
.replace(/>{{{\s*(\w+?)\s*}}}<\/[\w-]*>/g, ' v-html="$1" />') .replace(/>\{\{\{\s*(\w+)\s*\}\}\}<\/[\w-]*>/g, ' v-html="$1" />')
// We are not matching <link> <script> and <meta> tags as these aren't used yet in nuxt/ui // We are not matching <link> <script> and <meta> tags as these aren't used yet in nuxt/ui
// and should be taken care of wherever this SFC is used // and should be taken care of wherever this SFC is used
const title = html.match(/<title.*?>([\s\S]*)<\/title>/)?.[1].replace(/{{([\s\S]+?)}}/g, (r) => { const title = html.match(/<title[^>]*>([\s\S]*)<\/title>/)?.[1].replace(/\{\{([\s\S]+?)\}\}/g, (r) => {
return `\${${r.slice(2, -2)}}`.replace(/messages\./g, 'props.') return `\${${r.slice(2, -2)}}`.replace(/messages\./g, 'props.')
}) })
const styleContent = Array.from(html.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)).map(block => block[1]).join('\n') const styleContent = Array.from(html.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)).map(block => block[1]).join('\n')
const globalStyles = styleContent.replace(/(\.[^{\d][^{]*?\{[^}]*?\})+.?/g, (r) => { const globalStyles = styleContent.replace(/(\.[^{\d][^{]*\{[^}]*\})+.?/g, (r) => {
const lastChar = r[r.length - 1] const lastChar = r[r.length - 1]
if (lastChar && !['}', '.', '@', '*', ':'].includes(lastChar)) { if (lastChar && !['}', '.', '@', '*', ':'].includes(lastChar)) {
return ';' + lastChar return ';' + lastChar
} }
return lastChar return lastChar
}).replace(/@media[^{]*?\{\}/g, '') }).replace(/@media[^{]*\{\}/g, '')
const inlineScripts = Array.from(html.matchAll(/<script>([\s\S]*?)<\/script>/g)) const inlineScripts = Array.from(html.matchAll(/<script>([\s\S]*?)<\/script>/g))
.map(block => block[1]) .map(block => block[1])
.filter(i => !i.includes('const t=document.createElement("link")')) .filter(i => !i.includes('const t=document.createElement("link")'))

View File

@ -18,8 +18,8 @@ interface ComposableKeysOptions {
} }
const stringTypes = ['Literal', 'TemplateLiteral'] const stringTypes = ['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)/
export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptions) => { export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptions) => {
const composableMeta: Record<string, any> = {} const composableMeta: Record<string, any> = {}
@ -40,7 +40,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>)/) || { index: 0, 0: code } const { 0: script = code, index: codeIndex = 0 } = code.match(/(?<=<script[^>]*>)[\s\S]*?(?=<\/script>)/) || { 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

@ -21,7 +21,7 @@ interface SSRStylePluginOptions {
mode: 'server' | 'client' mode: 'server' | 'client'
} }
const SUPPORTED_FILES_RE = /\.(vue|((c|m)?j|t)sx?)$/ const SUPPORTED_FILES_RE = /\.(?:vue|(?:[cm]?j|t)sx?)$/
export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin { export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
const cssMap: Record<string, { files: string[], inBundle: boolean }> = {} const cssMap: Record<string, { files: string[], inBundle: boolean }> = {}

View File

@ -7,7 +7,7 @@ export function uniq<T> (arr: T[]): T[] {
} }
// Copied from vue-bundle-renderer utils // Copied from vue-bundle-renderer utils
const IS_CSS_RE = /\.(?:css|scss|sass|postcss|pcss|less|stylus|styl)(\?[^.]+)?$/ const IS_CSS_RE = /\.(?:css|scss|sass|postcss|pcss|less|stylus|styl)(?:\?[^.]+)?$/
export function isCSS (file: string) { export function isCSS (file: string) {
return IS_CSS_RE.test(file) return IS_CSS_RE.test(file)

View File

@ -7,7 +7,7 @@ import { applyPresets } from '../utils/config'
import { nuxt } from '../presets/nuxt' import { nuxt } from '../presets/nuxt'
import { node } from '../presets/node' import { node } from '../presets/node'
const assetPattern = /\.(css|s[ca]ss|png|jpe?g|gif|svg|woff2?|eot|ttf|otf|webp|webm|mp4|ogv)(\?.*)?$/i const assetPattern = /\.(?:css|s[ca]ss|png|jpe?g|gif|svg|woff2?|eot|ttf|otf|webp|webm|mp4|ogv)(?:\?.*)?$/i
export function server (ctx: WebpackConfigContext) { export function server (ctx: WebpackConfigContext) {
ctx.name = 'server' ctx.name = 'server'

View File

@ -25,6 +25,6 @@ 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) export const isCSS = (file: string) => /\.css(?:\?[^.]+)?$/.test(file)
export const isHotUpdate = (file: string) => file.includes('hot-update') export const isHotUpdate = (file: string) => file.includes('hot-update')

View File

@ -55,7 +55,7 @@ export function fileName (ctx: WebpackConfigContext, key: string) {
} }
if (typeof fileName === 'string' && ctx.options.dev) { if (typeof fileName === 'string' && ctx.options.dev) {
const hash = /\[(chunkhash|contenthash|hash)(?::(\d+))?]/.exec(fileName) const hash = /\[(chunkhash|contenthash|hash)(?::\d+)?\]/.exec(fileName)
if (hash) { if (hash) {
logger.warn(`Notice: Please do not use ${hash[1]} in dev mode to prevent memory leak`) logger.warn(`Notice: Please do not use ${hash[1]} in dev mode to prevent memory leak`)
} }

View File

@ -77,6 +77,9 @@ importers:
eslint-plugin-perfectionist: eslint-plugin-perfectionist:
specifier: 2.10.0 specifier: 2.10.0
version: 2.10.0(eslint@9.2.0)(typescript@5.4.5)(vue-eslint-parser@9.4.2(eslint@9.2.0)) version: 2.10.0(eslint@9.2.0)(typescript@5.4.5)(vue-eslint-parser@9.4.2(eslint@9.2.0))
eslint-plugin-regexp:
specifier: ^2.5.0
version: 2.5.0(eslint@9.2.0)
eslint-typegen: eslint-typegen:
specifier: 0.2.4 specifier: 0.2.4
version: 0.2.4(eslint@9.2.0) version: 0.2.4(eslint@9.2.0)
@ -3962,6 +3965,12 @@ packages:
vue-eslint-parser: vue-eslint-parser:
optional: true optional: true
eslint-plugin-regexp@2.5.0:
resolution: {integrity: sha512-I7vKcP0o75WS5SHiVNXN+Eshq49sbrweMQIuqSL3AId9AwDe9Dhbfug65vw64LxmOd4v+yf5l5Xt41y9puiq0g==}
engines: {node: ^18 || >=20}
peerDependencies:
eslint: '>=8.44.0'
eslint-plugin-unicorn@52.0.0: eslint-plugin-unicorn@52.0.0:
resolution: {integrity: sha512-1Yzm7/m+0R4djH0tjDjfVei/ju2w3AzUGjG6q8JnuNIL5xIwsflyCooW5sfBvQp2pMYQFSWWCFONsjCax1EHng==} resolution: {integrity: sha512-1Yzm7/m+0R4djH0tjDjfVei/ju2w3AzUGjG6q8JnuNIL5xIwsflyCooW5sfBvQp2pMYQFSWWCFONsjCax1EHng==}
engines: {node: '>=16'} engines: {node: '>=16'}
@ -6147,9 +6156,17 @@ packages:
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
engines: {node: '>=4'} engines: {node: '>=4'}
refa@0.12.1:
resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
regenerator-runtime@0.14.1: regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
regexp-ast-analysis@0.7.1:
resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
regexp-tree@0.1.27: regexp-tree@0.1.27:
resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==}
hasBin: true hasBin: true
@ -6306,6 +6323,10 @@ packages:
resolution: {integrity: sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==} resolution: {integrity: sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==}
engines: {node: '>= 12.13.0'} engines: {node: '>= 12.13.0'}
scslre@0.3.0:
resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==}
engines: {node: ^14.0.0 || >=16.0.0}
scule@1.3.0: scule@1.3.0:
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
@ -10806,6 +10827,17 @@ snapshots:
- supports-color - supports-color
- typescript - typescript
eslint-plugin-regexp@2.5.0(eslint@9.2.0):
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@9.2.0)
'@eslint-community/regexpp': 4.10.0
comment-parser: 1.4.1
eslint: 9.2.0
jsdoc-type-pratt-parser: 4.0.0
refa: 0.12.1
regexp-ast-analysis: 0.7.1
scslre: 0.3.0
eslint-plugin-unicorn@52.0.0(eslint@9.2.0): eslint-plugin-unicorn@52.0.0(eslint@9.2.0):
dependencies: dependencies:
'@babel/helper-validator-identifier': 7.24.5 '@babel/helper-validator-identifier': 7.24.5
@ -13437,8 +13469,17 @@ snapshots:
dependencies: dependencies:
redis-errors: 1.2.0 redis-errors: 1.2.0
refa@0.12.1:
dependencies:
'@eslint-community/regexpp': 4.10.0
regenerator-runtime@0.14.1: {} regenerator-runtime@0.14.1: {}
regexp-ast-analysis@0.7.1:
dependencies:
'@eslint-community/regexpp': 4.10.0
refa: 0.12.1
regexp-tree@0.1.27: {} regexp-tree@0.1.27: {}
regexp.prototype.flags@1.5.1: regexp.prototype.flags@1.5.1:
@ -13666,6 +13707,12 @@ snapshots:
ajv-formats: 2.1.1(ajv@8.12.0) ajv-formats: 2.1.1(ajv@8.12.0)
ajv-keywords: 5.1.0(ajv@8.12.0) ajv-keywords: 5.1.0(ajv@8.12.0)
scslre@0.3.0:
dependencies:
'@eslint-community/regexpp': 4.10.0
refa: 0.12.1
regexp-ast-analysis: 0.7.1
scule@1.3.0: {} scule@1.3.0: {}
semver@5.7.2: {} semver@5.7.2: {}

View File

@ -42,7 +42,7 @@ async function main () {
currentPR?.body.replace(/## 👉 Changelog[\s\S]*$/, '') || `> ${newVersion} is the next ${bumpType} release.\n>\n> **Timetable**: to be announced.`, currentPR?.body.replace(/## 👉 Changelog[\s\S]*$/, '') || `> ${newVersion} is the next ${bumpType} release.\n>\n> **Timetable**: to be announced.`,
'## 👉 Changelog', '## 👉 Changelog',
changelog changelog
.replace(/^## v.*?\n/, '') .replace(/^## v.*\n/, '')
.replace(`...${releaseBranch}`, `...v${newVersion}`) .replace(`...${releaseBranch}`, `...v${newVersion}`)
.replace(/### ❤️ Contributors[\s\S]*$/, ''), .replace(/### ❤️ Contributors[\s\S]*$/, ''),
'### ❤️ Contributors', '### ❤️ Contributors',

View File

@ -600,7 +600,7 @@ describe('nuxt composables', () => {
const { id1, id2 } = html.match(/<div[^>]* data-prehydrate-id=":(?<id1>[^:]+)::(?<id2>[^:]+):"> onPrehydrate testing <\/div>/)?.groups || {} const { id1, id2 } = html.match(/<div[^>]* data-prehydrate-id=":(?<id1>[^:]+)::(?<id2>[^:]+):"> onPrehydrate testing <\/div>/)?.groups || {}
expect(id1).toBeTruthy() expect(id1).toBeTruthy()
const matches = [ const matches = [
html.match(/<script[^>]*>\(\(\)=>{console.log\(window\)}\)\(\)<\/script>/), html.match(/<script[^>]*>\(\(\)=>\{console.log\(window\)\}\)\(\)<\/script>/),
html.match(new RegExp(`<script[^>]*>document.querySelectorAll\\('\\[data-prehydrate-id\\*=":${id1}:"]'\\).forEach\\(o=>{console.log\\(o.outerHTML\\)}\\)</script>`)), html.match(new RegExp(`<script[^>]*>document.querySelectorAll\\('\\[data-prehydrate-id\\*=":${id1}:"]'\\).forEach\\(o=>{console.log\\(o.outerHTML\\)}\\)</script>`)),
html.match(new RegExp(`<script[^>]*>document.querySelectorAll\\('\\[data-prehydrate-id\\*=":${id2}:"]'\\).forEach\\(o=>{console.log\\("other",o.outerHTML\\)}\\)</script>`)), html.match(new RegExp(`<script[^>]*>document.querySelectorAll\\('\\[data-prehydrate-id\\*=":${id2}:"]'\\).forEach\\(o=>{console.log\\("other",o.outerHTML\\)}\\)</script>`)),
] ]
@ -1911,7 +1911,7 @@ describe('public directories', () => {
describe.skipIf(isDev())('dynamic paths', () => { describe.skipIf(isDev())('dynamic paths', () => {
it('should work with no overrides', async () => { it('should work with no overrides', async () => {
const html: string = await $fetch('/assets') const html: string = await $fetch('/assets')
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)) { for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
const url = match[2] || match[3] const url = match[2] || match[3]
expect(url.startsWith('/_nuxt/') || url === '/public.svg').toBeTruthy() expect(url.startsWith('/_nuxt/') || url === '/public.svg').toBeTruthy()
} }
@ -1920,11 +1920,11 @@ describe.skipIf(isDev())('dynamic paths', () => {
// webpack injects CSS differently // webpack injects CSS differently
it.skipIf(isWebpack)('adds relative paths to CSS', async () => { it.skipIf(isWebpack)('adds relative paths to CSS', async () => {
const html: string = await $fetch('/assets') const html: string = await $fetch('/assets')
const urls = Array.from(html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)).map(m => m[2] || m[3]) const urls = Array.from(html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)).map(m => m[2] || m[3])
const cssURL = urls.find(u => /_nuxt\/assets.*\.css$/.test(u)) const cssURL = urls.find(u => /_nuxt\/assets.*\.css$/.test(u))
expect(cssURL).toBeDefined() expect(cssURL).toBeDefined()
const css: string = await $fetch(cssURL!) const css: string = await $fetch(cssURL!)
const imageUrls = Array.from(css.matchAll(/url\(([^)]*)\)/g)).map(m => m[1].replace(/[-.][\w]{8}\./g, '.')) const imageUrls = Array.from(css.matchAll(/url\(([^)]*)\)/g)).map(m => m[1].replace(/[-.]\w{8}\./g, '.'))
expect(imageUrls).toMatchInlineSnapshot(` expect(imageUrls).toMatchInlineSnapshot(`
[ [
"./logo.svg", "./logo.svg",
@ -1944,7 +1944,7 @@ describe.skipIf(isDev())('dynamic paths', () => {
}) })
const html = await $fetch('/foo/assets') const html = await $fetch('/foo/assets')
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)) { for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
const url = match[2] || match[3] const url = match[2] || match[3]
expect( expect(
url.startsWith('/foo/_other/') || url.startsWith('/foo/_other/') ||
@ -1965,7 +1965,7 @@ describe.skipIf(isDev())('dynamic paths', () => {
}) })
const html = await $fetch('/assets') const html = await $fetch('/assets')
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)) { for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
const url = match[2] || match[3] const url = match[2] || match[3]
expect( expect(
url.startsWith('./_nuxt/') || url.startsWith('./_nuxt/') ||
@ -1999,7 +1999,7 @@ describe.skipIf(isDev())('dynamic paths', () => {
}) })
const html = await $fetch('/foo/assets') const html = await $fetch('/foo/assets')
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)) { for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
const url = match[2] || match[3] const url = match[2] || match[3]
expect( expect(
url.startsWith('https://example.com/_cdn/') || url.startsWith('https://example.com/_cdn/') ||
@ -2198,7 +2198,7 @@ describe('component islands', () => {
const fixtureDir = normalize(fileURLToPath(new URL('./fixtures/basic', import.meta.url))) const fixtureDir = normalize(fileURLToPath(new URL('./fixtures/basic', import.meta.url)))
for (const link of result.head.link) { for (const link of result.head.link) {
link.href = link.href.replace(fixtureDir, '/<rootDir>').replaceAll('//', '/') link.href = link.href.replace(fixtureDir, '/<rootDir>').replaceAll('//', '/')
link.key = link.key.replace(/-[a-zA-Z0-9]+$/, '') link.key = link.key.replace(/-[a-z0-9]+$/i, '')
} }
result.head.link.sort((a, b) => b.href.localeCompare(a.href)) result.head.link.sort((a, b) => b.href.localeCompare(a.href))
} }
@ -2580,7 +2580,7 @@ function normaliseIslandResult (result: NuxtIslandResponse) {
style: result.head.style.map(s => ({ style: result.head.style.map(s => ({
...s, ...s,
innerHTML: (s.innerHTML || '').replace(/data-v-[a-z0-9]+/, 'data-v-xxxxx').replace(/\.[a-zA-Z0-9]+\.svg/, '.svg'), innerHTML: (s.innerHTML || '').replace(/data-v-[a-z0-9]+/, 'data-v-xxxxx').replace(/\.[a-zA-Z0-9]+\.svg/, '.svg'),
key: s.key.replace(/-[a-zA-Z0-9]+$/, ''), key: s.key.replace(/-[a-z0-9]+$/i, ''),
})), })),
}, },
} }

View File

@ -116,7 +116,7 @@ export function parseData (html: string) {
} }
const { script, attrs } = html.match(/<script type="application\/json" id="__NUXT_DATA__"(?<attrs>[^>]+)>(?<script>.*?)<\/script>/)?.groups || {} const { script, attrs } = html.match(/<script type="application\/json" id="__NUXT_DATA__"(?<attrs>[^>]+)>(?<script>.*?)<\/script>/)?.groups || {}
const _attrs: Record<string, string> = {} const _attrs: Record<string, string> = {}
for (const attr of attrs.matchAll(/( |^)(?<key>[\w-]+)+="(?<value>[^"]+)"/g)) { for (const attr of attrs.matchAll(/( |^)(?<key>[\w-]+)="(?<value>[^"]+)"/g)) {
_attrs[attr!.groups!.key] = attr!.groups!.value _attrs[attr!.groups!.key] = attr!.groups!.value
} }
return { return {