chore: add more checks around indexed access (#29060)

This commit is contained in:
Daniel Roe 2024-09-18 21:41:53 +02:00
parent da019ba39a
commit 6a39c657f5
No known key found for this signature in database
GPG Key ID: 3714AB03996F442B
33 changed files with 165 additions and 149 deletions

View File

@ -33,7 +33,7 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
throw new Error(`Cannot find any nuxt version from ${opts.cwd}`) throw new Error(`Cannot find any nuxt version from ${opts.cwd}`)
} }
const pkg = await readPackageJSON(nearestNuxtPkg) const pkg = await readPackageJSON(nearestNuxtPkg)
const majorVersion = pkg.version ? Number.parseInt(pkg.version.split('.')[0]) : '' const majorVersion = pkg.version ? Number.parseInt(pkg.version.split('.')[0]!) : ''
const rootDir = pathToFileURL(opts.cwd || process.cwd()).href const rootDir = pathToFileURL(opts.cwd || process.cwd()).href

View File

@ -54,8 +54,10 @@ const NuxtClientFallbackServer = defineComponent({
const defaultSlot = ctx.slots.default?.() const defaultSlot = ctx.slots.default?.()
const ssrVNodes = createBuffer() const ssrVNodes = createBuffer()
for (let i = 0; i < (defaultSlot?.length || 0); i++) { if (defaultSlot) {
ssrRenderVNode(ssrVNodes.push, defaultSlot![i], vm!) for (let i = 0; i < defaultSlot.length; i++) {
ssrRenderVNode(ssrVNodes.push, defaultSlot[i]!, vm!)
}
} }
const buffer = ssrVNodes.getBuffer() const buffer = ssrVNodes.getBuffer()

View File

@ -29,17 +29,20 @@ const getId = import.meta.client ? () => (id++).toString() : randomUUID
const components = import.meta.client ? new Map<string, Component>() : undefined const components = import.meta.client ? new Map<string, Component>() : undefined
async function loadComponents (source = appBaseURL, paths: NuxtIslandResponse['components']) { async function loadComponents (source = appBaseURL, paths: NuxtIslandResponse['components']) {
if (!paths) { return }
const promises: Array<Promise<void>> = [] const promises: Array<Promise<void>> = []
for (const component in paths) { for (const [component, item] of Object.entries(paths)) {
if (!(components!.has(component))) { if (!(components!.has(component))) {
promises.push((async () => { promises.push((async () => {
const chunkSource = join(source, paths[component].chunk) const chunkSource = join(source, item.chunk)
const c = await import(/* @vite-ignore */ chunkSource).then(m => m.default || m) const c = await import(/* @vite-ignore */ chunkSource).then(m => m.default || m)
components!.set(component, c) components!.set(component, c)
})()) })())
} }
} }
await Promise.all(promises) await Promise.all(promises)
} }
@ -276,7 +279,7 @@ export default defineComponent({
teleports.push(createVNode(Teleport, teleports.push(createVNode(Teleport,
// use different selectors for even and odd teleportKey to force trigger the teleport // use different selectors for even and odd teleportKey to force trigger the teleport
{ to: import.meta.client ? `${isKeyOdd ? 'div' : ''}[data-island-uid="${uid.value}"][data-island-slot="${slot}"]` : `uid=${uid.value};slot=${slot}` }, { to: import.meta.client ? `${isKeyOdd ? 'div' : ''}[data-island-uid="${uid.value}"][data-island-slot="${slot}"]` : `uid=${uid.value};slot=${slot}` },
{ default: () => (payloads.slots?.[slot].props?.length ? payloads.slots[slot].props : [{}]).map((data: any) => slots[slot]?.(data)) }), { default: () => (payloads.slots?.[slot]?.props?.length ? payloads.slots[slot].props : [{}]).map((data: any) => slots[slot]?.(data)) }),
) )
} }
} }

View File

@ -39,7 +39,7 @@ export default defineComponent({
const islandContext = nuxtApp.ssrContext!.islandContext! const islandContext = nuxtApp.ssrContext!.islandContext!
return () => { return () => {
const slot = slots.default!()[0] const slot = slots.default!()[0]!
const slotType = slot.type as ExtendedComponent const slotType = slot.type as ExtendedComponent
const name = (slotType.__name || slotType.name) as string const name = (slotType.__name || slotType.name) as string

View File

@ -100,7 +100,7 @@ export function vforToArray (source: any): any[] {
const keys = Object.keys(source) const keys = Object.keys(source)
const array = new Array(keys.length) const array = new Array(keys.length)
for (let i = 0, l = keys.length; i < l; i++) { for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i] const key = keys[i]!
array[i] = source[key] array[i] = source[key]
} }
return array return array

View File

@ -23,7 +23,7 @@ export async function loadPayload (url: string, opts: LoadPayloadOptions = {}):
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
const cache = nuxtApp._payloadCache = nuxtApp._payloadCache || {} const cache = nuxtApp._payloadCache = nuxtApp._payloadCache || {}
if (payloadURL in cache) { if (payloadURL in cache) {
return cache[payloadURL] return cache[payloadURL] || null
} }
cache[payloadURL] = isPrerendered(url).then((prerendered) => { cache[payloadURL] = isPrerendered(url).then((prerendered) => {
if (!prerendered) { if (!prerendered) {

View File

@ -14,7 +14,12 @@ export const preloadComponents = async (components: string | string[]) => {
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
components = toArray(components) components = toArray(components)
await Promise.all(components.map(name => _loadAsyncComponent(nuxtApp.vueApp._context.components[name]))) await Promise.all(components.map((name) => {
const component = nuxtApp.vueApp._context.components[name]
if (component) {
return _loadAsyncComponent(component)
}
}))
} }
/** /**

View File

@ -7,18 +7,18 @@ import { defineNuxtPlugin, useNuxtApp } from '../nuxt'
// @ts-expect-error Virtual file. // @ts-expect-error Virtual file.
import { componentIslands } from '#build/nuxt.config.mjs' import { componentIslands } from '#build/nuxt.config.mjs'
const revivers: Record<string, (data: any) => any> = { const revivers: [string, (data: any) => any][] = [
NuxtError: data => createError(data), ['NuxtError', data => createError(data)],
EmptyShallowRef: data => shallowRef(data === '_' ? undefined : data === '0n' ? BigInt(0) : destr(data)), ['EmptyShallowRef', data => shallowRef(data === '_' ? undefined : data === '0n' ? BigInt(0) : destr(data))],
EmptyRef: data => ref(data === '_' ? undefined : data === '0n' ? BigInt(0) : destr(data)), ['EmptyRef', data => ref(data === '_' ? undefined : data === '0n' ? BigInt(0) : destr(data))],
ShallowRef: data => shallowRef(data), ['ShallowRef', data => shallowRef(data)],
ShallowReactive: data => shallowReactive(data), ['ShallowReactive', data => shallowReactive(data)],
Ref: data => ref(data), ['Ref', data => ref(data)],
Reactive: data => reactive(data), ['Reactive', data => reactive(data)],
} ]
if (componentIslands) { if (componentIslands) {
revivers.Island = ({ key, params, result }: any) => { revivers.push(['Island', ({ key, params, result }: any) => {
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
if (!nuxtApp.isHydrating) { if (!nuxtApp.isHydrating) {
nuxtApp.payload.data[key] = nuxtApp.payload.data[key] || $fetch(`/__nuxt_island/${key}.json`, { nuxtApp.payload.data[key] = nuxtApp.payload.data[key] || $fetch(`/__nuxt_island/${key}.json`, {
@ -33,15 +33,15 @@ if (componentIslands) {
html: '', html: '',
...result, ...result,
} }
} }])
} }
export default defineNuxtPlugin({ export default defineNuxtPlugin({
name: 'nuxt:revive-payload:client', name: 'nuxt:revive-payload:client',
order: -30, order: -30,
async setup (nuxtApp) { async setup (nuxtApp) {
for (const reviver in revivers) { for (const [reviver, fn] of revivers) {
definePayloadReviver(reviver, revivers[reviver as keyof typeof revivers]) definePayloadReviver(reviver, fn)
} }
Object.assign(nuxtApp.payload, await nuxtApp.runWithContext(getNuxtClientPayload)) Object.assign(nuxtApp.payload, await nuxtApp.runWithContext(getNuxtClientPayload))
// For backwards compatibility - TODO: remove later // For backwards compatibility - TODO: remove later

View File

@ -78,7 +78,7 @@ export const islandsTransform = createUnplugin((options: ServerOnlyComponentTran
if (attributes.name) { delete attributes.name } if (attributes.name) { delete attributes.name }
if (attributes['v-bind']) { if (attributes['v-bind']) {
attributes._bind = extractAttributes(attributes, ['v-bind'])['v-bind'] attributes._bind = extractAttributes(attributes, ['v-bind'])['v-bind']!
} }
const teleportAttributes = extractAttributes(attributes, ['v-if', 'v-else-if', 'v-else']) const teleportAttributes = extractAttributes(attributes, ['v-if', 'v-else-if', 'v-else'])
const bindings = getPropsToString(attributes) const bindings = getPropsToString(attributes)
@ -137,7 +137,7 @@ function extractAttributes (attributes: Record<string, string>, names: string[])
const extracted: Record<string, string> = {} const extracted: Record<string, string> = {}
for (const name of names) { for (const name of names) {
if (name in attributes) { if (name in attributes) {
extracted[name] = attributes[name] extracted[name] = attributes[name]!
delete attributes[name] delete attributes[name]
} }
} }

View File

@ -140,10 +140,10 @@ export default defineNuxtModule<ComponentsOptions>({
nuxt.hook('build:manifest', (manifest) => { nuxt.hook('build:manifest', (manifest) => {
const sourceFiles = getComponents().filter(c => c.global).map(c => relative(nuxt.options.srcDir, c.filePath)) const sourceFiles = getComponents().filter(c => c.global).map(c => relative(nuxt.options.srcDir, c.filePath))
for (const key in manifest) { for (const chunk of Object.values(manifest)) {
if (manifest[key].isEntry) { if (chunk.isEntry) {
manifest[key].dynamicImports = chunk.dynamicImports =
manifest[key].dynamicImports?.filter(i => !sourceFiles.includes(i)) chunk.dynamicImports?.filter(i => !sourceFiles.includes(i))
} }
} }
}) })

View File

@ -56,6 +56,8 @@ export const TreeShakeTemplatePlugin = createUnplugin((options: TreeShakeTemplat
const node = _node as AcornNode<Node> const node = _node as AcornNode<Node>
if (isSsrRender(node)) { if (isSsrRender(node)) {
const [componentCall, _, children] = node.arguments const [componentCall, _, children] = node.arguments
if (!componentCall) { return }
if (componentCall.type === 'Identifier' || componentCall.type === 'MemberExpression' || componentCall.type === 'CallExpression') { if (componentCall.type === 'Identifier' || componentCall.type === 'MemberExpression' || componentCall.type === 'CallExpression') {
const componentName = getComponentName(node) const componentName = getComponentName(node)
const isClientComponent = COMPONENTS_IDENTIFIERS_RE.test(componentName) const isClientComponent = COMPONENTS_IDENTIFIERS_RE.test(componentName)
@ -137,11 +139,13 @@ function removeFromSetupReturn (codeAst: Program, name: string, magicString: Mag
const variableList = node.value.body.body.filter((statement): statement is VariableDeclaration => statement.type === 'VariableDeclaration') const variableList = node.value.body.body.filter((statement): statement is VariableDeclaration => statement.type === 'VariableDeclaration')
const returnedVariableDeclaration = variableList.find(declaration => declaration.declarations[0]?.id.type === 'Identifier' && declaration.declarations[0]?.id.name === '__returned__' && declaration.declarations[0]?.init?.type === 'ObjectExpression') const returnedVariableDeclaration = variableList.find(declaration => declaration.declarations[0]?.id.type === 'Identifier' && declaration.declarations[0]?.id.name === '__returned__' && declaration.declarations[0]?.init?.type === 'ObjectExpression')
if (returnedVariableDeclaration) { if (returnedVariableDeclaration) {
const init = returnedVariableDeclaration.declarations[0].init as ObjectExpression const init = returnedVariableDeclaration.declarations[0]?.init as ObjectExpression | undefined
if (init) {
removePropertyFromObject(init, name, magicString) removePropertyFromObject(init, name, magicString)
} }
} }
} }
}
}, },
}) })
} }

View File

@ -256,7 +256,7 @@ export async function annotatePlugins (nuxt: Nuxt, plugins: NuxtPlugin[]) {
const _plugins: Array<NuxtPlugin & Omit<PluginMeta, 'enforce'>> = [] const _plugins: Array<NuxtPlugin & Omit<PluginMeta, 'enforce'>> = []
for (const plugin of plugins) { for (const plugin of plugins) {
try { try {
const code = plugin.src in nuxt.vfs ? nuxt.vfs[plugin.src] : await fsp.readFile(plugin.src!, 'utf-8') const code = plugin.src in nuxt.vfs ? nuxt.vfs[plugin.src]! : await fsp.readFile(plugin.src!, 'utf-8')
_plugins.push({ _plugins.push({
...await extractMetadata(code, IS_TSX.test(plugin.src) ? 'tsx' : 'ts'), ...await extractMetadata(code, IS_TSX.test(plugin.src) ? 'tsx' : 'ts'),
...plugin, ...plugin,

View File

@ -142,9 +142,9 @@ function createGranularWatcher () {
delete watchers[path] delete watchers[path]
} }
if (event === 'addDir' && path !== dir && !ignoredDirs.has(path) && !pathsToWatch.includes(path) && !(path in watchers) && !isIgnored(path)) { if (event === 'addDir' && path !== dir && !ignoredDirs.has(path) && !pathsToWatch.includes(path) && !(path in watchers) && !isIgnored(path)) {
watchers[path] = chokidar.watch(path, { ...nuxt.options.watchers.chokidar, ignored: [isIgnored] }) const pathWatcher = watchers[path] = chokidar.watch(path, { ...nuxt.options.watchers.chokidar, ignored: [isIgnored] })
// TODO: consider moving to emit absolute path in 3.8 or 4.0 // TODO: consider moving to emit absolute path in 3.8 or 4.0
watchers[path].on('all', (event, p) => nuxt.callHook('builder:watch', event, nuxt.options.experimental.relativeWatchPaths ? normalize(relative(nuxt.options.srcDir, p)) : normalize(p))) pathWatcher.on('all', (event, p) => nuxt.callHook('builder:watch', event, nuxt.options.experimental.relativeWatchPaths ? normalize(relative(nuxt.options.srcDir, p)) : normalize(p)))
nuxt.hook('close', () => watchers[path]?.close()) nuxt.hook('close', () => watchers[path]?.close())
} }
}) })

View File

@ -345,10 +345,10 @@ async function initNuxt (nuxt: Nuxt) {
// TODO: [Experimental] Avoid emitting assets when flag is enabled // TODO: [Experimental] Avoid emitting assets when flag is enabled
if (nuxt.options.features.noScripts && !nuxt.options.dev) { if (nuxt.options.features.noScripts && !nuxt.options.dev) {
nuxt.hook('build:manifest', async (manifest) => { nuxt.hook('build:manifest', async (manifest) => {
for (const file in manifest) { for (const chunk of Object.values(manifest)) {
if (manifest[file].resourceType === 'script') { if (chunk.resourceType === 'script') {
await rm(resolve(nuxt.options.buildDir, 'dist/client', withoutLeadingSlash(nuxt.options.app.buildAssetsDir), manifest[file].file), { force: true }) await rm(resolve(nuxt.options.buildDir, 'dist/client', withoutLeadingSlash(nuxt.options.app.buildAssetsDir), chunk.file), { force: true })
manifest[file].file = '' chunk.file = ''
} }
} }
}) })

View File

@ -64,7 +64,7 @@ export const LayerAliasingPlugin = createUnplugin((options: LayerAliasingOptions
if (!layer || !ALIAS_RE_SINGLE.test(code)) { return } if (!layer || !ALIAS_RE_SINGLE.test(code)) { return }
const s = new MagicString(code) const s = new MagicString(code)
s.replace(ALIAS_RE, r => aliases[layer][r as '~'] || r) s.replace(ALIAS_RE, r => aliases[layer]?.[r as '~'] || r)
if (s.hasChanged()) { if (s.hasChanged()) {
return { return {

View File

@ -70,7 +70,7 @@ export async function extractMetadata (code: string, loader = 'ts' as 'ts' | 'ts
} }
const plugin = node.arguments[0] const plugin = node.arguments[0]
if (plugin.type === 'ObjectExpression') { if (plugin?.type === 'ObjectExpression') {
meta = defu(extractMetaFromObject(plugin.properties), meta) meta = defu(extractMetaFromObject(plugin.properties), meta)
} }
@ -122,7 +122,7 @@ export const RemovePluginMetadataPlugin = (nuxt: Nuxt) => createUnplugin(() => {
name: 'nuxt:remove-plugin-metadata', name: 'nuxt:remove-plugin-metadata',
transform (code, id) { transform (code, id) {
id = normalize(id) id = normalize(id)
const plugin = nuxt.apps.default.plugins.find(p => p.src === id) const plugin = nuxt.apps.default?.plugins.find(p => p.src === id)
if (!plugin) { return } if (!plugin) { return }
const s = new MagicString(code) const s = new MagicString(code)
@ -163,7 +163,7 @@ export const RemovePluginMetadataPlugin = (nuxt: Nuxt) => createUnplugin(() => {
if (!name || !wrapperNames.has(name)) { return } if (!name || !wrapperNames.has(name)) { return }
wrapped = true wrapped = true
if (node.arguments[0].type !== 'ObjectExpression') { if (node.arguments[0] && node.arguments[0].type !== 'ObjectExpression') {
// TODO: Warn if legacy plugin format is detected // TODO: Warn if legacy plugin format is detected
if ('params' in node.arguments[0] && node.arguments[0].params.length > 1) { if ('params' in node.arguments[0] && node.arguments[0].params.length > 1) {
logger.warn(`Plugin \`${plugin.src}\` is in legacy Nuxt 2 format (context, inject) which is likely to be broken and will be ignored.`) logger.warn(`Plugin \`${plugin.src}\` is in legacy Nuxt 2 format (context, inject) which is likely to be broken and will be ignored.`)

View File

@ -32,6 +32,7 @@ export function prehydrateTransformPlugin (nuxt: Nuxt) {
const node = _node as SimpleCallExpression & { start: number, end: number } const node = _node as SimpleCallExpression & { start: number, end: number }
const name = 'name' in node.callee && node.callee.name const name = 'name' in node.callee && node.callee.name
if (name === 'onPrehydrate') { if (name === 'onPrehydrate') {
if (!node.arguments[0]) { return }
if (node.arguments[0].type !== 'ArrowFunctionExpression' && node.arguments[0].type !== 'FunctionExpression') { return } if (node.arguments[0].type !== 'ArrowFunctionExpression' && node.arguments[0].type !== 'FunctionExpression') { return }
const needsAttr = node.arguments[0].params.length > 0 const needsAttr = node.arguments[0].params.length > 0

View File

@ -326,8 +326,10 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Render 103 Early Hints // Render 103 Early Hints
if (process.env.NUXT_EARLY_HINTS && !isRenderingPayload && !import.meta.prerender) { if (process.env.NUXT_EARLY_HINTS && !isRenderingPayload && !import.meta.prerender) {
const { link } = renderResourceHeaders({}, renderer.rendererContext) const { link } = renderResourceHeaders({}, renderer.rendererContext)
if (link) {
writeEarlyHints(event, link) writeEarlyHints(event, link)
} }
}
if (process.env.NUXT_INLINE_STYLES && !isRenderingIsland) { if (process.env.NUXT_INLINE_STYLES && !isRenderingIsland) {
for (const id of await getEntryIds()) { for (const id of await getEntryIds()) {
@ -395,8 +397,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
} }
if (!isRenderingIsland || import.meta.dev) { if (!isRenderingIsland || import.meta.dev) {
const link: Link[] = [] const link: Link[] = []
for (const style in styles) { for (const resource of Object.values(styles)) {
const resource = styles[style]
// Do not add links to resources that are inlined (vite v5+) // Do not add links to resources that are inlined (vite v5+)
if (import.meta.dev && 'inline' in getURLQuery(resource.file)) { if (import.meta.dev && 'inline' in getURLQuery(resource.file)) {
continue continue
@ -495,7 +496,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
const islandResponse: NuxtIslandResponse = { const islandResponse: NuxtIslandResponse = {
id: islandContext.id, id: islandContext.id,
head: islandHead, head: islandHead,
html: getServerComponentHTML(htmlContext.body), html: getServerComponentHTML(htmlContext.body as [string, ...string[]]),
components: getClientIslandResponse(ssrContext), components: getClientIslandResponse(ssrContext),
slots: getSlotIslandResponse(ssrContext), slots: getSlotIslandResponse(ssrContext),
} }
@ -567,7 +568,7 @@ async function renderInlineStyles (usedModules: Set<string> | string[]): Promise
const styleMap = await getSSRStyles() const styleMap = await getSSRStyles()
const inlinedStyles = new Set<string>() const inlinedStyles = new Set<string>()
for (const mod of usedModules) { for (const mod of usedModules) {
if (mod in styleMap) { if (mod in styleMap && styleMap[mod]) {
for (const style of await styleMap[mod]()) { for (const style of await styleMap[mod]()) {
inlinedStyles.add(style) inlinedStyles.add(style)
} }
@ -649,9 +650,9 @@ function splitPayload (ssrContext: NuxtSSRContext) {
/** /**
* remove the root node from the html body * remove the root node from the html body
*/ */
function getServerComponentHTML (body: string[]): string { function getServerComponentHTML (body: [string, ...string[]]): string {
const match = body[0].match(ROOT_NODE_REGEX) const match = body[0].match(ROOT_NODE_REGEX)
return match ? match[1] : body[0] return match?.[1] || body[0]
} }
const SSR_SLOT_TELEPORT_MARKER = /^uid=([^;]*);slot=(.*)$/ const SSR_SLOT_TELEPORT_MARKER = /^uid=([^;]*);slot=(.*)$/
@ -661,10 +662,10 @@ 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 }
const response: NuxtIslandResponse['slots'] = {} const response: NuxtIslandResponse['slots'] = {}
for (const slot in ssrContext.islandContext.slots) { for (const [name, slot] of Object.entries(ssrContext.islandContext.slots)) {
response[slot] = { response[name] = {
...ssrContext.islandContext.slots[slot], ...slot,
fallback: ssrContext.teleports?.[`island-fallback=${slot}`], fallback: ssrContext.teleports?.[`island-fallback=${name}`],
} }
} }
return response return response
@ -674,11 +675,11 @@ function getClientIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandRespons
if (!ssrContext.islandContext || !Object.keys(ssrContext.islandContext.components).length) { return undefined } if (!ssrContext.islandContext || !Object.keys(ssrContext.islandContext.components).length) { return undefined }
const response: NuxtIslandResponse['components'] = {} const response: NuxtIslandResponse['components'] = {}
for (const clientUid in ssrContext.islandContext.components) { for (const [clientUid, component] of Object.entries(ssrContext.islandContext.components)) {
// remove teleport anchor to avoid hydration issues // remove teleport anchor to avoid hydration issues
const html = ssrContext.teleports?.[clientUid].replaceAll('<!--teleport start anchor-->', '') || '' const html = ssrContext.teleports?.[clientUid]?.replaceAll('<!--teleport start anchor-->', '') || ''
response[clientUid] = { response[clientUid] = {
...ssrContext.islandContext.components[clientUid], ...component,
html, html,
slots: getComponentSlotTeleport(ssrContext.teleports ?? {}), slots: getComponentSlotTeleport(ssrContext.teleports ?? {}),
} }

View File

@ -28,11 +28,12 @@ export function resolveComponentNameSegments (fileName: string, prefixParts: str
let index = prefixParts.length - 1 let index = prefixParts.length - 1
const matchedSuffix: string[] = [] const matchedSuffix: string[] = []
while (index >= 0) { while (index >= 0) {
matchedSuffix.unshift(...splitByCase(prefixParts[index]).map(p => p.toLowerCase())) const prefixPart = prefixParts[index]!
matchedSuffix.unshift(...splitByCase(prefixPart).map(p => p.toLowerCase()))
const matchedSuffixContent = matchedSuffix.join('/') const matchedSuffixContent = matchedSuffix.join('/')
if ((fileNamePartsContent === matchedSuffixContent || fileNamePartsContent.startsWith(matchedSuffixContent + '/')) || if ((fileNamePartsContent === matchedSuffixContent || fileNamePartsContent.startsWith(matchedSuffixContent + '/')) ||
// e.g Item/Item/Item.vue -> Item // e.g Item/Item/Item.vue -> Item
(prefixParts[index].toLowerCase() === fileNamePartsContent && (prefixPart.toLowerCase() === fileNamePartsContent &&
prefixParts[index + 1] && prefixParts[index + 1] &&
prefixParts[index] === prefixParts[index + 1])) { prefixParts[index] === prefixParts[index + 1])) {
componentNameParts.length = index componentNameParts.length = index

View File

@ -160,7 +160,7 @@ export const Title = defineComponent({
if (import.meta.dev) { if (import.meta.dev) {
const defaultSlot = slots.default?.() const defaultSlot = slots.default?.()
if (defaultSlot && (defaultSlot.length > 1 || typeof defaultSlot[0].children !== 'string')) { if (defaultSlot && (defaultSlot.length > 1 || (defaultSlot[0] && typeof defaultSlot[0].children !== 'string'))) {
console.error('<Title> can take only one string in its default slot.') console.error('<Title> can take only one string in its default slot.')
} }

View File

@ -242,7 +242,7 @@ export default defineNuxtModule({
] ]
}) })
function isPage (file: string, pages = nuxt.apps.default.pages): boolean { function isPage (file: string, pages = nuxt.apps.default?.pages): boolean {
if (!pages) { return false } if (!pages) { return false }
return pages.some(page => page.file === file) || pages.some(page => page.children && isPage(file, page.children)) return pages.some(page => page.file === file) || pages.some(page => page.children && isPage(file, page.children))
} }
@ -354,7 +354,7 @@ export default defineNuxtModule({
const updatePage = async function updatePage (path: string) { const updatePage = async function updatePage (path: string) {
const glob = pageToGlobMap[path] const glob = pageToGlobMap[path]
const code = path in nuxt.vfs ? nuxt.vfs[path] : await readFile(path!, 'utf-8') const code = path in nuxt.vfs ? nuxt.vfs[path]! : await readFile(path!, 'utf-8')
try { try {
const extractedRule = await extractRouteRules(code) const extractedRule = await extractRouteRules(code)
if (extractedRule) { if (extractedRule) {
@ -403,8 +403,7 @@ export default defineNuxtModule({
nuxt.hook('pages:extend', (routes) => { nuxt.hook('pages:extend', (routes) => {
const nitro = useNitro() const nitro = useNitro()
let resolvedRoutes: string[] let resolvedRoutes: string[]
for (const path in nitro.options.routeRules) { for (const [path, rule] of Object.entries(nitro.options.routeRules)) {
const rule = nitro.options.routeRules[path]
if (!rule.redirect) { continue } if (!rule.redirect) { continue }
resolvedRoutes ||= routes.flatMap(route => resolveRoutePaths(route)) resolvedRoutes ||= routes.flatMap(route => resolveRoutePaths(route))
// skip if there's already a route matching this path // skip if there's already a route matching this path
@ -450,14 +449,14 @@ export default defineNuxtModule({
if (nuxt.options.dev) { return } if (nuxt.options.dev) { return }
const sourceFiles = nuxt.apps.default?.pages?.length ? getSources(nuxt.apps.default.pages) : [] const sourceFiles = nuxt.apps.default?.pages?.length ? getSources(nuxt.apps.default.pages) : []
for (const key in manifest) { for (const [key, chunk] of Object.entries(manifest)) {
if (manifest[key].src && Object.values(nuxt.apps).some(app => app.pages?.some(page => page.mode === 'server' && page.file === join(nuxt.options.srcDir, manifest[key].src!)))) { if (chunk.src && Object.values(nuxt.apps).some(app => app.pages?.some(page => page.mode === 'server' && page.file === join(nuxt.options.srcDir, chunk.src!)))) {
delete manifest[key] delete manifest[key]
continue continue
} }
if (manifest[key].isEntry) { if (chunk.isEntry) {
manifest[key].dynamicImports = chunk.dynamicImports =
manifest[key].dynamicImports?.filter(i => !sourceFiles.includes(i)) chunk.dynamicImports?.filter(i => !sourceFiles.includes(i))
} }
} }
}) })

View File

@ -14,7 +14,7 @@ const ruleCache: Record<string, NitroRouteConfig | null> = {}
export async function extractRouteRules (code: string): Promise<NitroRouteConfig | null> { export async function extractRouteRules (code: string): Promise<NitroRouteConfig | null> {
if (code in ruleCache) { if (code in ruleCache) {
return ruleCache[code] return ruleCache[code] || null
} }
if (!ROUTE_RULE_RE.test(code)) { return null } if (!ROUTE_RULE_RE.test(code)) { return null }

View File

@ -102,7 +102,7 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
// Array where routes should be added, useful when adding child routes // Array where routes should be added, useful when adding child routes
let parent = routes let parent = routes
const lastSegment = segments[segments.length - 1] const lastSegment = segments[segments.length - 1]!
if (lastSegment.endsWith('.server')) { if (lastSegment.endsWith('.server')) {
segments[segments.length - 1] = lastSegment.replace('.server', '') segments[segments.length - 1] = lastSegment.replace('.server', '')
if (options.shouldUseServerComponents) { if (options.shouldUseServerComponents) {
@ -116,7 +116,7 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
for (let i = 0; i < segments.length; i++) { for (let i = 0; i < segments.length; i++) {
const segment = segments[i] const segment = segments[i]
const tokens = parseSegment(segment) const tokens = parseSegment(segment!)
// Skip group segments // Skip group segments
if (tokens.every(token => token.type === SegmentTokenType.group)) { if (tokens.every(token => token.type === SegmentTokenType.group)) {
@ -151,7 +151,7 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
export async function augmentPages (routes: NuxtPage[], vfs: Record<string, string>, augmentedPages = new Set<string>()) { export async function augmentPages (routes: NuxtPage[], vfs: Record<string, string>, augmentedPages = new Set<string>()) {
for (const route of routes) { for (const route of routes) {
if (route.file && !augmentedPages.has(route.file)) { if (route.file && !augmentedPages.has(route.file)) {
const fileContent = route.file in vfs ? vfs[route.file] : fs.readFileSync(await resolvePath(route.file), 'utf-8') const fileContent = route.file in vfs ? vfs[route.file]! : fs.readFileSync(await resolvePath(route.file), 'utf-8')
const routeMeta = await getRouteMeta(fileContent, route.file) const routeMeta = await getRouteMeta(fileContent, route.file)
if (route.meta) { if (route.meta) {
routeMeta.meta = { ...routeMeta.meta, ...route.meta } routeMeta.meta = { ...routeMeta.meta, ...route.meta }
@ -174,7 +174,7 @@ export function extractScriptContent (html: string) {
for (const match of html.matchAll(SFC_SCRIPT_RE)) { for (const match of html.matchAll(SFC_SCRIPT_RE)) {
if (match?.groups?.content) { if (match?.groups?.content) {
contents.push({ contents.push({
loader: match.groups.attrs.includes('tsx') ? 'tsx' : 'ts', loader: match.groups.attrs?.includes('tsx') ? 'tsx' : 'ts',
code: match.groups.content.trim(), code: match.groups.content.trim(),
}) })
} }
@ -196,7 +196,9 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
delete metaCache[absolutePath] delete metaCache[absolutePath]
} }
if (absolutePath in metaCache) { return metaCache[absolutePath] } if (absolutePath in metaCache && metaCache[absolutePath]) {
return metaCache[absolutePath]
}
const loader = getLoader(absolutePath) const loader = getLoader(absolutePath)
const scriptBlocks = !loader ? null : loader === 'vue' ? extractScriptContent(contents) : [{ code: contents, loader }] const scriptBlocks = !loader ? null : loader === 'vue' ? extractScriptContent(contents) : [{ code: contents, loader }]
@ -403,7 +405,7 @@ function parseSegment (segment: string) {
consumeBuffer() consumeBuffer()
} }
state = SegmentParserState.initial state = SegmentParserState.initial
} else if (PARAM_CHAR_RE.test(c)) { } else if (c && PARAM_CHAR_RE.test(c)) {
buffer += c buffer += c
} else { } else {
// console.debug(`[pages]Ignored character "${c}" while building param "${buffer}" from "segment"`) // console.debug(`[pages]Ignored character "${c}" while building param "${buffer}" from "segment"`)

View File

@ -297,8 +297,8 @@ async function getResolvedApp (files: Array<string | { name: string, contents: s
mw.path = normaliseToRepo(mw.path)! mw.path = normaliseToRepo(mw.path)!
} }
for (const layout in app.layouts) { for (const layout of Object.values(app.layouts)) {
app.layouts[layout].file = normaliseToRepo(app.layouts[layout].file)! layout.file = normaliseToRepo(layout.file)!
} }
await nuxt.close() await nuxt.close()

View File

@ -654,7 +654,7 @@ describe('pages:generateRoutesFromFiles', () => {
}))).map((route, index) => { }))).map((route, index) => {
return { return {
...route, ...route,
meta: test.files![index].meta, meta: test.files![index]!.meta,
} }
}) })

View File

@ -5,7 +5,8 @@ import { relative, resolve } from 'pathe'
import { withTrailingSlash, withoutLeadingSlash } from 'ufo' import { withTrailingSlash, withoutLeadingSlash } from 'ufo'
import escapeRE from 'escape-string-regexp' import escapeRE from 'escape-string-regexp'
import { normalizeViteManifest } from 'vue-bundle-renderer' import { normalizeViteManifest } from 'vue-bundle-renderer'
import type { Manifest } from 'vue-bundle-renderer' import type { Manifest as RendererManifest } from 'vue-bundle-renderer'
import type { Manifest as ViteClientManifest } from 'vite'
import type { ViteBuildContext } from './vite' import type { ViteBuildContext } from './vite'
export async function writeManifest (ctx: ViteBuildContext, css: string[] = []) { export async function writeManifest (ctx: ViteBuildContext, css: string[] = []) {
@ -13,7 +14,7 @@ export async function writeManifest (ctx: ViteBuildContext, css: string[] = [])
const clientDist = resolve(ctx.nuxt.options.buildDir, 'dist/client') const clientDist = resolve(ctx.nuxt.options.buildDir, 'dist/client')
const serverDist = resolve(ctx.nuxt.options.buildDir, 'dist/server') const serverDist = resolve(ctx.nuxt.options.buildDir, 'dist/server')
const devClientManifest: Manifest = { const devClientManifest: RendererManifest = {
'@vite/client': { '@vite/client': {
isEntry: true, isEntry: true,
file: '@vite/client', file: '@vite/client',
@ -32,17 +33,18 @@ export async function writeManifest (ctx: ViteBuildContext, css: string[] = [])
const manifestFile = resolve(clientDist, 'manifest.json') const manifestFile = resolve(clientDist, 'manifest.json')
const clientManifest = ctx.nuxt.options.dev const clientManifest = ctx.nuxt.options.dev
? devClientManifest ? devClientManifest
: JSON.parse(readFileSync(manifestFile, 'utf-8')) : JSON.parse(readFileSync(manifestFile, 'utf-8')) as ViteClientManifest
const manifestEntries = Object.values(clientManifest)
const buildAssetsDir = withTrailingSlash(withoutLeadingSlash(ctx.nuxt.options.app.buildAssetsDir)) const buildAssetsDir = withTrailingSlash(withoutLeadingSlash(ctx.nuxt.options.app.buildAssetsDir))
const BASE_RE = new RegExp(`^${escapeRE(buildAssetsDir)}`) const BASE_RE = new RegExp(`^${escapeRE(buildAssetsDir)}`)
for (const key in clientManifest) { for (const entry of manifestEntries) {
const entry = clientManifest[key]
if (entry.file) { if (entry.file) {
entry.file = entry.file.replace(BASE_RE, '') entry.file = entry.file.replace(BASE_RE, '')
} }
for (const item of ['css', 'assets']) { for (const item of ['css', 'assets'] as const) {
if (entry[item]) { if (entry[item]) {
entry[item] = entry[item].map((i: string) => i.replace(BASE_RE, '')) entry[item] = entry[item].map((i: string) => i.replace(BASE_RE, ''))
} }
@ -52,12 +54,11 @@ export async function writeManifest (ctx: ViteBuildContext, css: string[] = [])
await mkdir(serverDist, { recursive: true }) await mkdir(serverDist, { recursive: true })
if (ctx.config.build?.cssCodeSplit === false) { if (ctx.config.build?.cssCodeSplit === false) {
for (const key in clientManifest as Record<string, { file?: string }>) { for (const entry of manifestEntries) {
const val = clientManifest[key] if (entry.file?.endsWith('.css')) {
if (val.file?.endsWith('.css')) {
const key = relative(ctx.config.root!, ctx.entry) const key = relative(ctx.config.root!, ctx.entry)
clientManifest[key].css ||= [] clientManifest[key]!.css ||= []
clientManifest[key].css!.push(val.file) ;(clientManifest[key]!.css as string[]).push(entry.file)
break break
} }
} }

View File

@ -18,8 +18,7 @@ export function analyzePlugin (ctx: ViteBuildContext): Plugin[] {
const bundle = outputBundle[_bundleId] const bundle = outputBundle[_bundleId]
if (!bundle || 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, module] of Object.entries(bundle.modules)) {
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

@ -86,8 +86,7 @@ export const VitePublicDirsPlugin = createUnplugin((options: VitePublicDirsPlugi
} }
}, },
generateBundle (_outputOptions, bundle) { generateBundle (_outputOptions, bundle) {
for (const file in bundle) { for (const [file, chunk] of Object.entries(bundle)) {
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()

View File

@ -62,8 +62,7 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
if (options.mode === 'client') { return } if (options.mode === 'client') { return }
const emitted: Record<string, string> = {} const emitted: Record<string, string> = {}
for (const file in cssMap) { for (const [file, { files, inBundle }] of Object.entries(cssMap)) {
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)

View File

@ -208,8 +208,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, entry] of Object.entries(manifest)) {
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

@ -33,9 +33,10 @@ export default class VueSSRClientPlugin {
const stats = compilation.getStats().toJson() const stats = compilation.getStats().toJson()
const initialFiles = new Set<string>() const initialFiles = new Set<string>()
for (const name in stats.entrypoints!) { for (const { assets } of Object.values(stats.entrypoints!)) {
const entryAssets = stats.entrypoints![name]!.assets! if (!assets) { continue }
for (const asset of entryAssets) {
for (const asset of assets) {
const file = asset.name const file = asset.name
if ((isJS(file) || isCSS(file)) && !isHotUpdate(file)) { if ((isJS(file) || isCSS(file)) && !isHotUpdate(file)) {
initialFiles.add(file) initialFiles.add(file)
@ -71,15 +72,15 @@ export default class VueSSRClientPlugin {
} }
const { entrypoints = {}, namedChunkGroups = {} } = stats const { entrypoints = {}, namedChunkGroups = {} } = stats
const assetModules = stats.modules!.filter(m => m.assets!.length) const fileToIndex = (file: string | number) => webpackManifest.all.indexOf(String(file))
const fileToIndex = (file: string) => webpackManifest.all.indexOf(file) for (const m of stats.modules!) {
stats.modules!.forEach((m) => {
// Ignore modules duplicated in multiple chunks // Ignore modules duplicated in multiple chunks
if (m.chunks!.length === 1) { if (m.chunks?.length !== 1) { continue }
const [cid] = m.chunks!
const [cid] = m.chunks
const chunk = stats.chunks!.find(c => c.id === cid) const chunk = stats.chunks!.find(c => c.id === cid)
if (!chunk || !chunk.files || !cid) { if (!chunk || !chunk.files || !cid) {
return continue
} }
const id = m.identifier!.replace(/\s\w+$/, '') // remove appended hash const id = m.identifier!.replace(/\s\w+$/, '') // remove appended hash
const filesSet = new Set(chunk.files.map(fileToIndex).filter(i => i !== -1)) const filesSet = new Set(chunk.files.map(fileToIndex).filter(i => i !== -1))
@ -110,13 +111,14 @@ export default class VueSSRClientPlugin {
} }
// Find all asset modules associated with the same chunk // Find all asset modules associated with the same chunk
assetModules.forEach((m) => { if (stats.modules) {
if (m.chunks!.includes(cid)) { for (const m of stats.modules) {
files.push(...(m.assets as string[]).map(fileToIndex)) if (m.assets?.length && m.chunks?.includes(cid)) {
files.push(...m.assets.map(fileToIndex))
}
}
} }
})
} }
})
const manifest = normalizeWebpackManifest(webpackManifest as any) const manifest = normalizeWebpackManifest(webpackManifest as any)
await this.options.nuxt.callHook('build:manifest', manifest) await this.options.nuxt.callHook('build:manifest', manifest)

View File

@ -18,13 +18,13 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
// Identical behaviour between inline/external vue options as this should only affect the server build // Identical behaviour between inline/external vue options as this should only affect the server build
it('default client bundle size', async () => { it('default client bundle size', async () => {
const [clientStats, clientStatsInlined] = await Promise.all(['.output', '.output-inline'] const [clientStats, clientStatsInlined] = await Promise.all((['.output', '.output-inline'])
.map(outputDir => analyzeSizes(['**/*.js'], join(rootDir, outputDir, 'public')))) .map(outputDir => analyzeSizes(['**/*.js'], join(rootDir, outputDir, 'public'))))
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot(`"114k"`) expect.soft(roundToKilobytes(clientStats!.totalBytes)).toMatchInlineSnapshot(`"114k"`)
expect.soft(roundToKilobytes(clientStatsInlined.totalBytes)).toMatchInlineSnapshot(`"114k"`) expect.soft(roundToKilobytes(clientStatsInlined!.totalBytes)).toMatchInlineSnapshot(`"114k"`)
const files = new Set([...clientStats.files, ...clientStatsInlined.files].map(f => f.replace(/\..*\.js/, '.js'))) const files = new Set([...clientStats!.files, ...clientStatsInlined!.files].map(f => f.replace(/\..*\.js/, '.js')))
expect([...files]).toMatchInlineSnapshot(` expect([...files]).toMatchInlineSnapshot(`
[ [

View File

@ -12,8 +12,7 @@
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
/* Strictness */ /* Strictness */
"strict": true, "strict": true,
// TODO: enable noUncheckedIndexedAccess "noUncheckedIndexedAccess": true,
// "noUncheckedIndexedAccess": true,
"noUncheckedSideEffectImports": true, "noUncheckedSideEffectImports": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noImplicitOverride": true, "noImplicitOverride": true,