chore: improve type safety with indexed access (#27626)

This commit is contained in:
Daniel Roe 2024-06-27 16:27:08 +02:00 committed by GitHub
parent ef1cfa0508
commit 53df20ef2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 75 additions and 69 deletions

View File

@ -55,7 +55,7 @@ export async function addComponent (opts: AddComponentOptions) {
nuxt.hook('components:extend', (components: Component[]) => { nuxt.hook('components:extend', (components: Component[]) => {
const existingComponentIndex = components.findIndex(c => (c.pascalName === component.pascalName || c.kebabName === component.kebabName) && c.mode === component.mode) const existingComponentIndex = components.findIndex(c => (c.pascalName === component.pascalName || c.kebabName === component.kebabName) && c.mode === component.mode)
if (existingComponentIndex !== -1) { if (existingComponentIndex !== -1) {
const existingComponent = components[existingComponentIndex] const existingComponent = components[existingComponentIndex]!
const existingPriority = existingComponent.priority ?? 0 const existingPriority = existingComponent.priority ?? 0
const newPriority = component.priority ?? 0 const newPriority = component.priority ?? 0

View File

@ -50,7 +50,7 @@ export function resolveIgnorePatterns (relativePath?: string): string[] {
// Map ignore patterns based on if they start with * or !* // Map ignore patterns based on if they start with * or !*
return ignorePatterns.map((p) => { return ignorePatterns.map((p) => {
const [_, negation = '', pattern] = p.match(NEGATION_RE) || [] const [_, negation = '', pattern] = p.match(NEGATION_RE) || []
if (pattern[0] === '*') { if (pattern && pattern[0] === '*') {
return p return p
} }
return negation + relative(relativePath, resolve(nuxt.options.rootDir, pattern || p)) return negation + relative(relativePath, resolve(nuxt.options.rootDir, pattern || p))
@ -73,7 +73,7 @@ export function resolveGroupSyntax (group: string): string[] {
groups = groups.flatMap((group) => { groups = groups.flatMap((group) => {
const [head, ...tail] = group.split('{') const [head, ...tail] = group.split('{')
if (tail.length) { if (tail.length) {
const [body, ...rest] = tail.join('{').split('}') const [body = '', ...rest] = tail.join('{').split('}')
return body.split(',').map(part => `${head}${part}${rest.join('')}`) return body.split(',').map(part => `${head}${part}${rest.join('')}`)
} }

View File

@ -13,7 +13,7 @@ export function addLayout (this: any, template: NuxtTemplate | string, name?: st
// Nuxt 3 adds layouts on app // Nuxt 3 adds layouts on app
nuxt.hook('app:templates', (app) => { nuxt.hook('app:templates', (app) => {
if (layoutName in app.layouts) { if (layoutName in app.layouts) {
const relativePath = relative(nuxt.options.srcDir, app.layouts[layoutName].file) const relativePath = relative(nuxt.options.srcDir, app.layouts[layoutName]!.file)
return logger.warn( return logger.warn(
`Not overriding \`${layoutName}\` (provided by \`~/${relativePath}\`) with \`${src || filename}\`.`, `Not overriding \`${layoutName}\` (provided by \`~/${relativePath}\`) with \`${src || filename}\`.`,
) )

View File

@ -44,7 +44,7 @@ export function addRouteMiddleware (input: NuxtMiddleware | NuxtMiddleware[], op
for (const middleware of middlewares) { for (const middleware of middlewares) {
const find = app.middleware.findIndex(item => item.name === middleware.name) const find = app.middleware.findIndex(item => item.name === middleware.name)
if (find >= 0) { if (find >= 0) {
const foundPath = app.middleware[find].path const foundPath = app.middleware[find]!.path
if (foundPath === middleware.path) { continue } if (foundPath === middleware.path) { continue }
if (options.override === true) { if (options.override === true) {
app.middleware[find] = { ...middleware } app.middleware[find] = { ...middleware }

View File

@ -174,7 +174,7 @@ export async function resolveNuxtModule (base: string, paths: string[]): Promise
for (const path of paths) { for (const path of paths) {
if (path.startsWith(base)) { if (path.startsWith(base)) {
resolved.push(path.split('/index.ts')[0]) resolved.push(path.split('/index.ts')[0]!)
} else { } else {
const resolvedPath = await resolver.resolvePath(path) const resolvedPath = await resolver.resolvePath(path)
resolved.push(resolvedPath.slice(0, resolvedPath.lastIndexOf(path) + path.length)) resolved.push(resolvedPath.slice(0, resolvedPath.lastIndexOf(path) + path.length))

View File

@ -228,10 +228,10 @@ export async function _generateTypes (nuxt: Nuxt) {
if (excludedAlias.some(re => re.test(alias))) { if (excludedAlias.some(re => re.test(alias))) {
continue continue
} }
let absolutePath = resolve(basePath, aliases[alias]) let absolutePath = resolve(basePath, aliases[alias]!)
let stats = await fsp.stat(absolutePath).catch(() => null /* file does not exist */) let stats = await fsp.stat(absolutePath).catch(() => null /* file does not exist */)
if (!stats) { if (!stats) {
const resolvedModule = await tryResolveModule(aliases[alias], nuxt.options.modulesDir) const resolvedModule = await tryResolveModule(aliases[alias]!, nuxt.options.modulesDir)
if (resolvedModule) { if (resolvedModule) {
absolutePath = resolvedModule absolutePath = resolvedModule
stats = await fsp.stat(resolvedModule).catch(() => null) stats = await fsp.stat(resolvedModule).catch(() => null)
@ -251,7 +251,7 @@ export async function _generateTypes (nuxt: Nuxt) {
// remove extension // remove extension
? relativePath.replace(/\b\.\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]!
tsConfig.compilerOptions.paths[alias] = [path] tsConfig.compilerOptions.paths[alias] = [path]
@ -334,7 +334,7 @@ function renderAttrs (obj: Record<string, string>) {
return attrs.join(' ') return attrs.join(' ')
} }
function renderAttr (key: string, value: string) { function renderAttr (key: string, value?: string) {
return value ? `${key}="${value}"` : '' return value ? `${key}="${value}"` : ''
} }

View File

@ -139,9 +139,14 @@ export const RenderPlugin = () => {
} }
return lastChar || '' return lastChar || ''
}).replace(/@media[^{]*\{\}/g, '') }).replace(/@media[^{]*\{\}/g, '')
const inlineScripts = Array.from(html.matchAll(/<script>([\s\S]*?)<\/script>/g))
.map(block => block[1]) const inlineScripts: string[] = []
.filter(i => !i.includes('const t=document.createElement("link")')) for (const [_, i] of html.matchAll(/<script>([\s\S]*?)<\/script>/g)) {
if (i && !i.includes('const t=document.createElement("link")')) {
inlineScripts.push(i)
}
}
const props = genObjectFromRawEntries(Object.entries({ ...genericMessages, ...messages }).map(([key, value]) => [key, { const props = genObjectFromRawEntries(Object.entries({ ...genericMessages, ...messages }).map(([key, value]) => [key, {
type: typeof value === 'string' ? 'String' : typeof value === 'number' ? 'Number' : typeof value === 'boolean' ? 'Boolean' : 'undefined', type: typeof value === 'string' ? 'String' : typeof value === 'number' ? 'Number' : typeof value === 'boolean' ? 'Boolean' : 'undefined',
default: JSON.stringify(value), default: JSON.stringify(value),

View File

@ -16,10 +16,10 @@ export function analyzePlugin (ctx: ViteBuildContext): Plugin[] {
async generateBundle (_opts, outputBundle) { async generateBundle (_opts, outputBundle) {
for (const _bundleId in outputBundle) { for (const _bundleId in outputBundle) {
const bundle = outputBundle[_bundleId] const bundle = outputBundle[_bundleId]
if (bundle.type !== 'chunk') { continue } if (!bundle || bundle.type !== 'chunk') { continue }
const minifiedModuleEntryPromises: Array<Promise<[string, RenderedModule]>> = [] const minifiedModuleEntryPromises: Array<Promise<[string, RenderedModule]>> = []
for (const moduleId in bundle.modules) { for (const moduleId in bundle.modules) {
const module = bundle.modules[moduleId] const module = bundle.modules[moduleId]!
minifiedModuleEntryPromises.push( minifiedModuleEntryPromises.push(
transform(module.code || '', { minify: true }) transform(module.code || '', { minify: true })
.then(result => [moduleId, { ...module, code: result.code }]), .then(result => [moduleId, { ...module, code: result.code }]),

View File

@ -17,7 +17,7 @@ interface ComposableKeysOptions {
composables: Array<{ name: string, source?: string | RegExp, argumentLength: number }> composables: Array<{ name: string, source?: string | RegExp, argumentLength: number }>
} }
const stringTypes = ['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)/
@ -182,7 +182,7 @@ class ScopeTracker {
leaveScope () { leaveScope () {
this.scopeIndexStack.pop() this.scopeIndexStack.pop()
this.curScopeKey = this.getKey() this.curScopeKey = this.getKey()
this.scopeIndexStack[this.scopeIndexStack.length - 1]++ this.scopeIndexStack[this.scopeIndexStack.length - 1]!++
} }
} }
@ -255,9 +255,9 @@ export function detectImportNames (code: string, composableMeta: Record<string,
for (const i of findStaticImports(code)) { for (const i of findStaticImports(code)) {
if (NUXT_IMPORT_RE.test(i.specifier)) { continue } if (NUXT_IMPORT_RE.test(i.specifier)) { continue }
const { namedImports, defaultImport, namespacedImport } = parseStaticImport(i) const { namedImports = {}, defaultImport, namespacedImport } = parseStaticImport(i)
for (const name in namedImports || {}) { for (const name in namedImports) {
addName(namedImports![name], i.specifier) addName(namedImports[name]!, i.specifier)
} }
if (defaultImport) { if (defaultImport) {
addName(defaultImport, i.specifier) addName(defaultImport, i.specifier)

View File

@ -38,7 +38,7 @@ export const VitePublicDirsPlugin = createUnplugin((options: { sourcemap?: boole
const s = new MagicString(code) const s = new MagicString(code)
const q = code.match(/(?<= = )['"`]/)?.[0] || '"' const q = code.match(/(?<= = )['"`]/)?.[0] || '"'
for (const [full, url] of code.matchAll(CSS_URL_RE)) { for (const [full, url] of code.matchAll(CSS_URL_RE)) {
if (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})`)
} }
} }
@ -53,13 +53,13 @@ export const VitePublicDirsPlugin = createUnplugin((options: { sourcemap?: boole
}, },
generateBundle (_outputOptions, bundle) { generateBundle (_outputOptions, bundle) {
for (const file in bundle) { for (const file in bundle) {
const chunk = bundle[file] const chunk = bundle[file]!
if (!file.endsWith('.css') || chunk.type !== 'asset') { continue } if (!file.endsWith('.css') || chunk.type !== 'asset') { continue }
let css = chunk.source.toString() let css = chunk.source.toString()
let wasReplaced = false let wasReplaced = false
for (const [full, url] of css.matchAll(CSS_URL_RE)) { for (const [full, url] of css.matchAll(CSS_URL_RE)) {
if (resolveFromPublicAssets(url)) { if (url && resolveFromPublicAssets(url)) {
const relativeURL = relative(withLeadingSlash(dirname(file)), url) const relativeURL = relative(withLeadingSlash(dirname(file)), url)
css = css.replace(full, `url(${relativeURL})`) css = css.replace(full, `url(${relativeURL})`)
wasReplaced = true wasReplaced = true

View File

@ -24,7 +24,7 @@ interface SSRStylePluginOptions {
const SUPPORTED_FILES_RE = /\.(?:vue|(?:[cm]?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 }> = {}
const idRefMap: Record<string, string> = {} const idRefMap: Record<string, string> = {}
const relativeToSrcDir = (path: string) => relative(options.srcDir, path) const relativeToSrcDir = (path: string) => relative(options.srcDir, path)
@ -63,7 +63,7 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
const emitted: Record<string, string> = {} const emitted: Record<string, string> = {}
for (const file in cssMap) { for (const file in cssMap) {
const { files, inBundle } = cssMap[file] const { files, inBundle } = cssMap[file]!
// File has been tree-shaken out of build (or there are no styles to inline) // File has been tree-shaken out of build (or there are no styles to inline)
if (!files.length || !inBundle) { continue } if (!files.length || !inBundle) { continue }
const fileName = filename(file) const fileName = filename(file)
@ -115,18 +115,19 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
// 'Teleport' CSS chunks that made it into the bundle on the client side // 'Teleport' CSS chunks that made it into the bundle on the client side
// to be inlined on server rendering // to be inlined on server rendering
if (options.mode === 'client') { if (options.mode === 'client') {
options.clientCSSMap[moduleId] ||= new Set() const moduleMap = options.clientCSSMap[moduleId] ||= new Set()
if (isCSS(moduleId)) { if (isCSS(moduleId)) {
// Vue files can (also) be their own entrypoints as they are tracked separately // Vue files can (also) be their own entrypoints as they are tracked separately
if (isVue(moduleId)) { if (isVue(moduleId)) {
options.clientCSSMap[moduleId].add(moduleId) moduleMap.add(moduleId)
const parent = moduleId.replace(/\?.+$/, '') const parent = moduleId.replace(/\?.+$/, '')
options.clientCSSMap[parent] ||= new Set() const parentMap = options.clientCSSMap[parent] ||= new Set()
options.clientCSSMap[parent].add(moduleId) parentMap.add(moduleId)
} }
// This is required to track CSS in entry chunk // This is required to track CSS in entry chunk
if (isEntry) { if (isEntry && chunk.facadeModuleId) {
options.clientCSSMap[chunk.facadeModuleId!].add(moduleId) const facadeMap = options.clientCSSMap[chunk.facadeModuleId] ||= new Set()
facadeMap.add(moduleId)
} }
} }
continue continue
@ -134,7 +135,7 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
const relativePath = relativeToSrcDir(moduleId) const relativePath = relativeToSrcDir(moduleId)
if (relativePath in cssMap) { if (relativePath in cssMap) {
cssMap[relativePath].inBundle = cssMap[relativePath].inBundle ?? ((isVue(moduleId) && relativeToSrcDir(moduleId)) || isEntry) cssMap[relativePath]!.inBundle = cssMap[relativePath]!.inBundle ?? ((isVue(moduleId) && !!relativeToSrcDir(moduleId)) || isEntry)
} }
} }
@ -146,7 +147,7 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
// or include it here in the client build so it is emitted in the CSS. // or include it here in the client build so it is emitted in the CSS.
if (id === options.entry && (options.shouldInline === true || (typeof options.shouldInline === 'function' && options.shouldInline(id)))) { if (id === options.entry && (options.shouldInline === true || (typeof options.shouldInline === 'function' && options.shouldInline(id)))) {
const s = new MagicString(code) const s = new MagicString(code)
options.clientCSSMap[id] ||= new Set() const idClientCSSMap = options.clientCSSMap[id] ||= new Set()
if (!options.globalCSS.length) { return } if (!options.globalCSS.length) { return }
for (const file of options.globalCSS) { for (const file of options.globalCSS) {
@ -160,7 +161,7 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
s.prepend(`${genImport(file)}\n`) s.prepend(`${genImport(file)}\n`)
continue continue
} }
options.clientCSSMap[id].add(resolved.id) idClientCSSMap.add(resolved.id)
} }
if (s.hasChanged()) { if (s.hasChanged()) {
return { return {
@ -184,7 +185,7 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
} }
const relativeId = relativeToSrcDir(id) const relativeId = relativeToSrcDir(id)
cssMap[relativeId] = cssMap[relativeId] || { files: [] } const idMap = cssMap[relativeId] ||= { files: [] }
const emittedIds = new Set<string>() const emittedIds = new Set<string>()
@ -208,7 +209,7 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
}) })
idRefMap[relativeToSrcDir(file)] = ref idRefMap[relativeToSrcDir(file)] = ref
cssMap[relativeId].files.push(ref) idMap.files.push(ref)
} }
if (!SUPPORTED_FILES_RE.test(pathname)) { return } if (!SUPPORTED_FILES_RE.test(pathname)) { return }
@ -235,7 +236,7 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
}) })
idRefMap[relativeToSrcDir(resolved.id)] = ref idRefMap[relativeToSrcDir(resolved.id)] = ref
cssMap[relativeId].files.push(ref) idMap.files.push(ref)
} }
}, },
} }

View File

@ -9,7 +9,7 @@ export function typeCheckPlugin (options: { sourcemap?: boolean } = {}): Plugin
name: 'nuxt:type-check', name: 'nuxt:type-check',
configResolved (config) { configResolved (config) {
const input = config.build.rollupOptions.input const input = config.build.rollupOptions.input
if (input && typeof input !== 'string' && !Array.isArray(input)) { if (input && typeof input !== 'string' && !Array.isArray(input) && input.entry) {
entry = input.entry entry = input.entry
} }
}, },

View File

@ -39,7 +39,7 @@ export default function virtual (vfs: Record<string, string>): Plugin {
const idNoPrefix = id.slice(PREFIX.length) const idNoPrefix = id.slice(PREFIX.length)
if (idNoPrefix in vfs) { if (idNoPrefix in vfs) {
return { return {
code: vfs[idNoPrefix], code: vfs[idNoPrefix] || '',
map: null, map: null,
} }
} }

View File

@ -10,7 +10,7 @@ interface Envs {
export function transpile (envs: Envs): Array<string | RegExp> { export function transpile (envs: Envs): Array<string | RegExp> {
const nuxt = useNuxt() const nuxt = useNuxt()
const transpile: Array<string | RegExp> = [] const transpile: RegExp[] = []
for (let pattern of nuxt.options.build.transpile) { for (let pattern of nuxt.options.build.transpile) {
if (typeof pattern === 'function') { if (typeof pattern === 'function') {

View File

@ -41,8 +41,8 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
...app.plugins.map(p => dirname(p.src)), ...app.plugins.map(p => dirname(p.src)),
...app.middleware.map(m => dirname(m.path)), ...app.middleware.map(m => dirname(m.path)),
...Object.values(app.layouts || {}).map(l => dirname(l.file)), ...Object.values(app.layouts || {}).map(l => dirname(l.file)),
dirname(nuxt.apps.default.rootComponent!), dirname(nuxt.apps.default!.rootComponent!),
dirname(nuxt.apps.default.errorComponent!), dirname(nuxt.apps.default!.errorComponent!),
]), ]),
].filter(d => d && existsSync(d)) ].filter(d => d && existsSync(d))
@ -183,7 +183,7 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
clientCSSMap, clientCSSMap,
chunksWithInlinedCSS, chunksWithInlinedCSS,
shouldInline: ctx.nuxt.options.features.inlineStyles, shouldInline: ctx.nuxt.options.features.inlineStyles,
components: ctx.nuxt.apps.default.components, components: ctx.nuxt.apps.default!.components || [],
globalCSS: ctx.nuxt.options.css, globalCSS: ctx.nuxt.options.css,
mode: isServer ? 'server' : 'client', mode: isServer ? 'server' : 'client',
entry: ctx.entry, entry: ctx.entry,
@ -193,7 +193,7 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
// Remove CSS entries for files that will have inlined styles // Remove CSS entries for files that will have inlined styles
ctx.nuxt.hook('build:manifest', (manifest) => { ctx.nuxt.hook('build:manifest', (manifest) => {
for (const key in manifest) { for (const key in manifest) {
const entry = manifest[key] const entry = manifest[key]!
const shouldRemoveCSS = chunksWithInlinedCSS.has(key) && !entry.isEntry const shouldRemoveCSS = chunksWithInlinedCSS.has(key) && !entry.isEntry
if (entry.isEntry && chunksWithInlinedCSS.has(key)) { if (entry.isEntry && chunksWithInlinedCSS.has(key)) {
// @ts-expect-error internal key // @ts-expect-error internal key

View File

@ -716,7 +716,7 @@ describe('nuxt links', () => {
for (const selector of ['nuxt-link', 'router-link', 'link-with-trailing-slash', 'link-without-trailing-slash']) { for (const selector of ['nuxt-link', 'router-link', 'link-with-trailing-slash', 'link-without-trailing-slash']) {
data[selector] = [] data[selector] = []
for (const match of html.matchAll(new RegExp(`href="([^"]*)"[^>]*class="[^"]*\\b${selector}\\b`, 'g'))) { for (const match of html.matchAll(new RegExp(`href="([^"]*)"[^>]*class="[^"]*\\b${selector}\\b`, 'g'))) {
data[selector].push(match[1]) data[selector]!.push(match[1]!)
} }
} }
expect(data).toMatchInlineSnapshot(` expect(data).toMatchInlineSnapshot(`
@ -1410,7 +1410,7 @@ describe('extends support', () => {
describe('app', () => { describe('app', () => {
it('extends foo/app/router.options & bar/app/router.options', async () => { it('extends foo/app/router.options & bar/app/router.options', async () => {
const html: string = await $fetch<string>('/') const html: string = await $fetch<string>('/')
const routerLinkClasses = html.match(/href="\/" class="([^"]*)"/)?.[1].split(' ') const routerLinkClasses = html.match(/href="\/" class="([^"]*)"/)![1]!.split(' ')
expect(routerLinkClasses).toContain('foo-active-class') expect(routerLinkClasses).toContain('foo-active-class')
expect(routerLinkClasses).toContain('bar-exact-active-class') expect(routerLinkClasses).toContain('bar-exact-active-class')
}) })
@ -1453,7 +1453,7 @@ describe('nested suspense', () => {
['/suspense/sync-1/sync-1/', '/suspense/sync-2/async-1/'], ['/suspense/sync-1/sync-1/', '/suspense/sync-2/async-1/'],
['/suspense/async-1/async-1/', '/suspense/async-2/async-1/'], ['/suspense/async-1/async-1/', '/suspense/async-2/async-1/'],
['/suspense/async-1/sync-1/', '/suspense/async-2/async-1/'], ['/suspense/async-1/sync-1/', '/suspense/async-2/async-1/'],
]).flatMap(([start, end]) => [ ] as const).flatMap(([start, end]) => [
[start, end], [start, end],
[start, end + '?layout=custom'], [start, end + '?layout=custom'],
[start + '?layout=custom', end], [start + '?layout=custom', end],
@ -1719,7 +1719,7 @@ describe.skipIf(isDev() || isWebpack)('inlining component styles', () => {
it('should not include inlined CSS in generated CSS file', async () => { it('should not include inlined CSS in generated CSS file', async () => {
const html: string = await $fetch<string>('/styles') const html: string = await $fetch<string>('/styles')
const cssFiles = new Set([...html.matchAll(/<link [^>]*href="([^"]*\.css)">/g)].map(m => m[1])) const cssFiles = new Set([...html.matchAll(/<link [^>]*href="([^"]*\.css)">/g)].map(m => m[1]!))
let css = '' let css = ''
for (const file of cssFiles || []) { for (const file of cssFiles || []) {
css += await $fetch<string>(file) css += await $fetch<string>(file)
@ -1970,7 +1970,7 @@ describe.skipIf(isDev())('dynamic paths', () => {
it('should work with no overrides', async () => { it('should work with no overrides', async () => {
const html: string = await $fetch<string>('/assets') const html: string = await $fetch<string>('/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/') || isPublicFile('/', url)).toBeTruthy() expect(url.startsWith('/_nuxt/') || isPublicFile('/', url)).toBeTruthy()
} }
}) })
@ -1979,10 +1979,10 @@ describe.skipIf(isDev())('dynamic paths', () => {
it.skipIf(isWebpack)('adds relative paths to CSS', async () => { it.skipIf(isWebpack)('adds relative paths to CSS', async () => {
const html: string = await $fetch<string>('/assets') const html: string = await $fetch<string>('/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 = await $fetch<string>(cssURL!) const css = await $fetch<string>(cssURL!)
const imageUrls = new Set(Array.from(css.matchAll(/url\(([^)]*)\)/g)).map(m => m[1].replace(/[-.]\w{8}\./g, '.'))) const imageUrls = new Set(Array.from(css.matchAll(/url\(([^)]*)\)/g)).map(m => m[1]!.replace(/[-.]\w{8}\./g, '.')))
expect([...imageUrls]).toMatchInlineSnapshot(` expect([...imageUrls]).toMatchInlineSnapshot(`
[ [
"./logo.svg", "./logo.svg",
@ -2001,7 +2001,7 @@ describe.skipIf(isDev())('dynamic paths', () => {
const html = await $fetch<string>('/foo/assets') const html = await $fetch<string>('/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(url.startsWith('/foo/_other/') || isPublicFile('/foo/', url)).toBeTruthy() expect(url.startsWith('/foo/_other/') || isPublicFile('/foo/', url)).toBeTruthy()
} }
@ -2017,7 +2017,7 @@ describe.skipIf(isDev())('dynamic paths', () => {
const html = await $fetch<string>('/assets') const html = await $fetch<string>('/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/') || isPublicFile('./', url)).toBeTruthy() expect(url.startsWith('./_nuxt/') || isPublicFile('./', url)).toBeTruthy()
expect(url.startsWith('./_nuxt/_nuxt')).toBeFalsy() expect(url.startsWith('./_nuxt/_nuxt')).toBeFalsy()
} }
@ -2046,7 +2046,7 @@ describe.skipIf(isDev())('dynamic paths', () => {
const html = await $fetch<string>('/foo/assets') const html = await $fetch<string>('/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(url.startsWith('https://example.com/_cdn/') || isPublicFile('https://example.com/', url)).toBeTruthy() expect(url.startsWith('https://example.com/_cdn/') || isPublicFile('https://example.com/', url)).toBeTruthy()
} }
}) })
@ -2082,7 +2082,7 @@ describe('component islands', () => {
result.html = result.html.replace(/ data-island-uid="[^"]*"/g, '') result.html = result.html.replace(/ data-island-uid="[^"]*"/g, '')
if (isDev()) { if (isDev()) {
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates') && (l.href.startsWith('_nuxt/components/islands/') && l.href.includes('_nuxt/components/islands/RouteComponent'))) result.head.link = result.head.link.filter(l => !l.href!.includes('@nuxt+ui-templates') && (l.href!.startsWith('_nuxt/components/islands/') && l.href!.includes('_nuxt/components/islands/RouteComponent')))
} }
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
@ -2104,7 +2104,7 @@ describe('component islands', () => {
}), }),
})) }))
if (isDev()) { if (isDev()) {
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates') && (l.href.startsWith('_nuxt/components/islands/') && l.href.includes('_nuxt/components/islands/LongAsyncComponent'))) result.head.link = result.head.link.filter(l => !l.href!.includes('@nuxt+ui-templates') && (l.href!.startsWith('_nuxt/components/islands/') && l.href!.includes('_nuxt/components/islands/LongAsyncComponent')))
} }
result.html = result.html.replaceAll(/ (data-island-uid|data-island-component)="([^"]*)"/g, '') result.html = result.html.replaceAll(/ (data-island-uid|data-island-component)="([^"]*)"/g, '')
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
@ -2162,7 +2162,7 @@ describe('component islands', () => {
}), }),
})) }))
if (isDev()) { if (isDev()) {
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates') && (l.href.startsWith('_nuxt/components/islands/') && l.href.includes('_nuxt/components/islands/AsyncServerComponent'))) result.head.link = result.head.link.filter(l => !l.href!.includes('@nuxt+ui-templates') && (l.href!.startsWith('_nuxt/components/islands/') && l.href!.includes('_nuxt/components/islands/AsyncServerComponent')))
} }
result.props = {} result.props = {}
result.components = {} result.components = {}
@ -2187,7 +2187,7 @@ describe('component islands', () => {
it('render server component with selective client hydration', async () => { it('render server component with selective client hydration', async () => {
const result = await $fetch<NuxtIslandResponse>('/__nuxt_island/ServerWithClient') const result = await $fetch<NuxtIslandResponse>('/__nuxt_island/ServerWithClient')
if (isDev()) { if (isDev()) {
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates') && (l.href.startsWith('_nuxt/components/islands/') && l.href.includes('_nuxt/components/islands/AsyncServerComponent'))) result.head.link = result.head.link.filter(l => !l.href!.includes('@nuxt+ui-templates') && (l.href!.startsWith('_nuxt/components/islands/') && l.href!.includes('_nuxt/components/islands/AsyncServerComponent')))
} }
const { components } = result const { components } = result
result.components = {} result.components = {}
@ -2208,13 +2208,13 @@ describe('component islands', () => {
} }
`) `)
expect(teleportsEntries).toHaveLength(1) expect(teleportsEntries).toHaveLength(1)
expect(teleportsEntries[0][0].startsWith('Counter-')).toBeTruthy() expect(teleportsEntries[0]![0].startsWith('Counter-')).toBeTruthy()
expect(teleportsEntries[0][1].props).toMatchInlineSnapshot(` expect(teleportsEntries[0]![1].props).toMatchInlineSnapshot(`
{ {
"multiplier": 1, "multiplier": 1,
} }
`) `)
expect(teleportsEntries[0][1].html).toMatchInlineSnapshot('"<div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--teleport anchor-->"') expect(teleportsEntries[0]![1].html).toMatchInlineSnapshot('"<div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--teleport anchor-->"')
}) })
} }
@ -2232,10 +2232,10 @@ describe('component islands', () => {
if (isDev()) { if (isDev()) {
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-z0-9]+$/i, '') 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!))
} }
// TODO: fix rendering of styles in webpack // TODO: fix rendering of styles in webpack
@ -2254,7 +2254,7 @@ describe('component islands', () => {
} else if (isDev() && !isWebpack) { } else if (isDev() && !isWebpack) {
// TODO: resolve dev bug triggered by earlier fetch of /vueuse-head page // TODO: resolve dev bug triggered by earlier fetch of /vueuse-head page
// https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/runtime/nitro/renderer.ts#L139 // https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/runtime/nitro/renderer.ts#L139
result.head.link = result.head.link.filter(h => !h.href.includes('SharedComponent')) result.head.link = result.head.link.filter(h => !h.href!.includes('SharedComponent'))
expect(result.head).toMatchInlineSnapshot(` expect(result.head).toMatchInlineSnapshot(`
{ {
"link": [ "link": [

View File

@ -111,7 +111,7 @@ if (process.env.TEST_ENV !== 'built' && !isWindows) {
const resolveHmrId = async () => { const resolveHmrId = async () => {
const node = await page.$('#hmr-id') const node = await page.$('#hmr-id')
const text = await node?.innerText() || '' const text = await node?.innerText() || ''
return Number(text?.trim().split(':')[1].trim()) return Number(text.trim().split(':')[1]?.trim() || '')
} }
const componentPath = join(fixturePath, 'components/islands/HmrComponent.vue') const componentPath = join(fixturePath, 'components/islands/HmrComponent.vue')
const triggerHmr = async () => fsp.writeFile( const triggerHmr = async () => fsp.writeFile(

View File

@ -107,17 +107,17 @@ export function parsePayload (payload: string) {
} }
export function parseData (html: string) { export function parseData (html: string) {
if (!isRenderingJson) { if (!isRenderingJson) {
const { script } = html.match(/<script>(?<script>window.__NUXT__.*?)<\/script>/)?.groups || {} const { script = '' } = html.match(/<script>(?<script>window.__NUXT__.*?)<\/script>/)?.groups || {}
const _script = new Script(script) const _script = new Script(script)
return { return {
script: _script.runInContext(createContext({ window: {} })), script: _script.runInContext(createContext({ window: {} })),
attrs: {}, attrs: {},
} }
} }
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 {
script: parsePayload(script || ''), script: parsePayload(script || ''),