diff --git a/packages/nuxt/src/app/components/client-fallback.server.ts b/packages/nuxt/src/app/components/client-fallback.server.ts index c1bc3c3e02..dd4e0cdb28 100644 --- a/packages/nuxt/src/app/components/client-fallback.server.ts +++ b/packages/nuxt/src/app/components/client-fallback.server.ts @@ -54,8 +54,10 @@ const NuxtClientFallbackServer = defineComponent({ const defaultSlot = ctx.slots.default?.() const ssrVNodes = createBuffer() - for (let i = 0; i < (defaultSlot?.length || 0); i++) { - ssrRenderVNode(ssrVNodes.push, defaultSlot![i], vm!) + if (defaultSlot) { + for (let i = 0; i < defaultSlot.length; i++) { + ssrRenderVNode(ssrVNodes.push, defaultSlot[i]!, vm!) + } } const buffer = ssrVNodes.getBuffer() diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts index 937f9d3136..c6bb6d8657 100644 --- a/packages/nuxt/src/app/components/nuxt-island.ts +++ b/packages/nuxt/src/app/components/nuxt-island.ts @@ -29,17 +29,20 @@ const getId = import.meta.client ? () => (id++).toString() : randomUUID const components = import.meta.client ? new Map() : undefined async function loadComponents (source = appBaseURL, paths: NuxtIslandResponse['components']) { + if (!paths) { return } + const promises: Array> = [] - for (const component in paths) { + for (const [component, item] of Object.entries(paths)) { if (!(components!.has(component))) { 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) components!.set(component, c) })()) } } + await Promise.all(promises) } @@ -276,7 +279,7 @@ export default defineComponent({ teleports.push(createVNode(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}` }, - { 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)) }), ) } } diff --git a/packages/nuxt/src/app/components/nuxt-teleport-island-component.ts b/packages/nuxt/src/app/components/nuxt-teleport-island-component.ts index 875204ccf4..b8ef06f9c4 100644 --- a/packages/nuxt/src/app/components/nuxt-teleport-island-component.ts +++ b/packages/nuxt/src/app/components/nuxt-teleport-island-component.ts @@ -39,7 +39,7 @@ export default defineComponent({ const islandContext = nuxtApp.ssrContext!.islandContext! return () => { - const slot = slots.default!()[0] + const slot = slots.default!()[0]! const slotType = slot.type as ExtendedComponent const name = (slotType.__name || slotType.name) as string diff --git a/packages/nuxt/src/app/components/utils.ts b/packages/nuxt/src/app/components/utils.ts index bb16708588..0bde127ec5 100644 --- a/packages/nuxt/src/app/components/utils.ts +++ b/packages/nuxt/src/app/components/utils.ts @@ -100,7 +100,7 @@ export function vforToArray (source: any): any[] { const keys = Object.keys(source) const array = new Array(keys.length) for (let i = 0, l = keys.length; i < l; i++) { - const key = keys[i] + const key = keys[i]! array[i] = source[key] } return array diff --git a/packages/nuxt/src/app/composables/payload.ts b/packages/nuxt/src/app/composables/payload.ts index 1708bbad99..e24d34feab 100644 --- a/packages/nuxt/src/app/composables/payload.ts +++ b/packages/nuxt/src/app/composables/payload.ts @@ -23,7 +23,7 @@ export async function loadPayload (url: string, opts: LoadPayloadOptions = {}): const nuxtApp = useNuxtApp() const cache = nuxtApp._payloadCache = nuxtApp._payloadCache || {} if (payloadURL in cache) { - return cache[payloadURL] + return cache[payloadURL] || null } cache[payloadURL] = isPrerendered(url).then((prerendered) => { if (!prerendered) { diff --git a/packages/nuxt/src/app/composables/preload.ts b/packages/nuxt/src/app/composables/preload.ts index 1358273b8a..cac6f5c85a 100644 --- a/packages/nuxt/src/app/composables/preload.ts +++ b/packages/nuxt/src/app/composables/preload.ts @@ -14,7 +14,12 @@ export const preloadComponents = async (components: string | string[]) => { const nuxtApp = useNuxtApp() 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) + } + })) } /** diff --git a/packages/nuxt/src/app/plugins/revive-payload.client.ts b/packages/nuxt/src/app/plugins/revive-payload.client.ts index 3ab2f211df..2d2c2e8bbd 100644 --- a/packages/nuxt/src/app/plugins/revive-payload.client.ts +++ b/packages/nuxt/src/app/plugins/revive-payload.client.ts @@ -7,18 +7,18 @@ import { defineNuxtPlugin, useNuxtApp } from '../nuxt' // @ts-expect-error Virtual file. import { componentIslands } from '#build/nuxt.config.mjs' -const revivers: Record any> = { - NuxtError: data => createError(data), - EmptyShallowRef: data => shallowRef(data === '_' ? undefined : data === '0n' ? BigInt(0) : destr(data)), - EmptyRef: data => ref(data === '_' ? undefined : data === '0n' ? BigInt(0) : destr(data)), - ShallowRef: data => shallowRef(data), - ShallowReactive: data => shallowReactive(data), - Ref: data => ref(data), - Reactive: data => reactive(data), -} +const revivers: [string, (data: any) => any][] = [ + ['NuxtError', data => createError(data)], + ['EmptyShallowRef', data => shallowRef(data === '_' ? undefined : data === '0n' ? BigInt(0) : destr(data))], + ['EmptyRef', data => ref(data === '_' ? undefined : data === '0n' ? BigInt(0) : destr(data))], + ['ShallowRef', data => shallowRef(data)], + ['ShallowReactive', data => shallowReactive(data)], + ['Ref', data => ref(data)], + ['Reactive', data => reactive(data)], +] if (componentIslands) { - revivers.Island = ({ key, params, result }: any) => { + revivers.push(['Island', ({ key, params, result }: any) => { const nuxtApp = useNuxtApp() if (!nuxtApp.isHydrating) { nuxtApp.payload.data[key] = nuxtApp.payload.data[key] || $fetch(`/__nuxt_island/${key}.json`, { @@ -33,15 +33,15 @@ if (componentIslands) { html: '', ...result, } - } + }]) } export default defineNuxtPlugin({ name: 'nuxt:revive-payload:client', order: -30, async setup (nuxtApp) { - for (const reviver in revivers) { - definePayloadReviver(reviver, revivers[reviver as keyof typeof revivers]) + for (const [reviver, fn] of revivers) { + definePayloadReviver(reviver, fn) } Object.assign(nuxtApp.payload, await nuxtApp.runWithContext(getNuxtClientPayload)) delete window.__NUXT__ diff --git a/packages/nuxt/src/components/islandsTransform.ts b/packages/nuxt/src/components/islandsTransform.ts index d583e3425d..e01b2ea3cc 100644 --- a/packages/nuxt/src/components/islandsTransform.ts +++ b/packages/nuxt/src/components/islandsTransform.ts @@ -78,7 +78,7 @@ export const islandsTransform = createUnplugin((options: ServerOnlyComponentTran if (attributes.name) { delete attributes.name } 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 bindings = getPropsToString(attributes) @@ -137,7 +137,7 @@ function extractAttributes (attributes: Record, names: string[]) const extracted: Record = {} for (const name of names) { if (name in attributes) { - extracted[name] = attributes[name] + extracted[name] = attributes[name]! delete attributes[name] } } diff --git a/packages/nuxt/src/components/module.ts b/packages/nuxt/src/components/module.ts index 34da7e1c71..dbfd5b9653 100644 --- a/packages/nuxt/src/components/module.ts +++ b/packages/nuxt/src/components/module.ts @@ -140,10 +140,10 @@ export default defineNuxtModule({ nuxt.hook('build:manifest', (manifest) => { const sourceFiles = getComponents().filter(c => c.global).map(c => relative(nuxt.options.srcDir, c.filePath)) - for (const key in manifest) { - if (manifest[key].isEntry) { - manifest[key].dynamicImports = - manifest[key].dynamicImports?.filter(i => !sourceFiles.includes(i)) + for (const chunk of Object.values(manifest)) { + if (chunk.isEntry) { + chunk.dynamicImports = + chunk.dynamicImports?.filter(i => !sourceFiles.includes(i)) } } }) diff --git a/packages/nuxt/src/components/tree-shake.ts b/packages/nuxt/src/components/tree-shake.ts index 56ea65cd3b..8108a9434d 100644 --- a/packages/nuxt/src/components/tree-shake.ts +++ b/packages/nuxt/src/components/tree-shake.ts @@ -56,6 +56,8 @@ export const TreeShakeTemplatePlugin = createUnplugin((options: TreeShakeTemplat const node = _node as AcornNode if (isSsrRender(node)) { const [componentCall, _, children] = node.arguments + if (!componentCall) { return } + if (componentCall.type === 'Identifier' || componentCall.type === 'MemberExpression' || componentCall.type === 'CallExpression') { const componentName = getComponentName(node) const isClientComponent = COMPONENTS_IDENTIFIERS_RE.test(componentName) @@ -137,8 +139,10 @@ function removeFromSetupReturn (codeAst: Program, name: string, magicString: Mag 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') if (returnedVariableDeclaration) { - const init = returnedVariableDeclaration.declarations[0].init as ObjectExpression - removePropertyFromObject(init, name, magicString) + const init = returnedVariableDeclaration.declarations[0]?.init as ObjectExpression | undefined + if (init) { + removePropertyFromObject(init, name, magicString) + } } } } diff --git a/packages/nuxt/src/core/app.ts b/packages/nuxt/src/core/app.ts index 494b30b798..50ac4118e5 100644 --- a/packages/nuxt/src/core/app.ts +++ b/packages/nuxt/src/core/app.ts @@ -254,7 +254,7 @@ export async function annotatePlugins (nuxt: Nuxt, plugins: NuxtPlugin[]) { const _plugins: Array> = [] for (const plugin of plugins) { 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({ ...await extractMetadata(code, IS_TSX.test(plugin.src) ? 'tsx' : 'ts'), ...plugin, diff --git a/packages/nuxt/src/core/builder.ts b/packages/nuxt/src/core/builder.ts index b414cbf081..1e8d7eb0e1 100644 --- a/packages/nuxt/src/core/builder.ts +++ b/packages/nuxt/src/core/builder.ts @@ -138,9 +138,9 @@ function createGranularWatcher () { delete watchers[path] } if (event === 'addDir' && path !== dir && !ignoredDirs.has(path) && !pathsToWatch.includes(path) && !(path in watchers) && !isIgnored(path)) { - watchers[path] = chokidarWatch(path, { ...nuxt.options.watchers.chokidar, ignored: [isIgnored] }) - watchers[path].on('all', (event, p) => nuxt.callHook('builder:watch', event, normalize(p))) - nuxt.hook('close', () => watchers[path]?.close()) + const pathWatcher = watchers[path] = chokidarWatch(path, { ...nuxt.options.watchers.chokidar, ignored: [isIgnored] }) + pathWatcher.on('all', (event, p) => nuxt.callHook('builder:watch', event, normalize(p))) + nuxt.hook('close', () => pathWatcher?.close()) } }) watcher.on('ready', () => { diff --git a/packages/nuxt/src/core/nitro.ts b/packages/nuxt/src/core/nitro.ts index 33efcf3331..f2016c2175 100644 --- a/packages/nuxt/src/core/nitro.ts +++ b/packages/nuxt/src/core/nitro.ts @@ -102,7 +102,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { baseURL: nuxt.options.app.baseURL, virtual: { '#internal/nuxt.config.mjs': () => nuxt.vfs['#build/nuxt.config'], - '#internal/nuxt/app-config': () => nuxt.vfs['#build/app.config'].replace(/\/\*\* client \*\*\/[\s\S]*\/\*\* client-end \*\*\//, ''), + '#internal/nuxt/app-config': () => nuxt.vfs['#build/app.config']?.replace(/\/\*\* client \*\*\/[\s\S]*\/\*\* client-end \*\*\//, ''), '#spa-template': async () => `export const template = ${JSON.stringify(await spaLoadingTemplate(nuxt))}`, }, routeRules: { diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index 3549e0858f..a2b9c894da 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -345,10 +345,10 @@ async function initNuxt (nuxt: Nuxt) { // TODO: [Experimental] Avoid emitting assets when flag is enabled if (nuxt.options.features.noScripts && !nuxt.options.dev) { nuxt.hook('build:manifest', async (manifest) => { - for (const file in manifest) { - if (manifest[file].resourceType === 'script') { - await rm(resolve(nuxt.options.buildDir, 'dist/client', withoutLeadingSlash(nuxt.options.app.buildAssetsDir), manifest[file].file), { force: true }) - manifest[file].file = '' + for (const chunk of Object.values(manifest)) { + if (chunk.resourceType === 'script') { + await rm(resolve(nuxt.options.buildDir, 'dist/client', withoutLeadingSlash(nuxt.options.app.buildAssetsDir), chunk.file), { force: true }) + chunk.file = '' } } }) diff --git a/packages/nuxt/src/core/plugins/layer-aliasing.ts b/packages/nuxt/src/core/plugins/layer-aliasing.ts index 852cb0f281..dd5de8b88b 100644 --- a/packages/nuxt/src/core/plugins/layer-aliasing.ts +++ b/packages/nuxt/src/core/plugins/layer-aliasing.ts @@ -64,7 +64,7 @@ export const LayerAliasingPlugin = createUnplugin((options: LayerAliasingOptions if (!layer || !ALIAS_RE_SINGLE.test(code)) { return } 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()) { return { diff --git a/packages/nuxt/src/core/plugins/plugin-metadata.ts b/packages/nuxt/src/core/plugins/plugin-metadata.ts index b59e2fa36d..6d94a770be 100644 --- a/packages/nuxt/src/core/plugins/plugin-metadata.ts +++ b/packages/nuxt/src/core/plugins/plugin-metadata.ts @@ -70,7 +70,7 @@ export async function extractMetadata (code: string, loader = 'ts' as 'ts' | 'ts } const plugin = node.arguments[0] - if (plugin.type === 'ObjectExpression') { + if (plugin?.type === 'ObjectExpression') { meta = defu(extractMetaFromObject(plugin.properties), meta) } @@ -122,7 +122,7 @@ export const RemovePluginMetadataPlugin = (nuxt: Nuxt) => createUnplugin(() => { name: 'nuxt:remove-plugin-metadata', transform (code, 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 } const s = new MagicString(code) diff --git a/packages/nuxt/src/core/plugins/prehydrate.ts b/packages/nuxt/src/core/plugins/prehydrate.ts index e47fa3883e..c91e3cb5f7 100644 --- a/packages/nuxt/src/core/plugins/prehydrate.ts +++ b/packages/nuxt/src/core/plugins/prehydrate.ts @@ -32,6 +32,7 @@ export function prehydrateTransformPlugin (nuxt: Nuxt) { const node = _node as SimpleCallExpression & { start: number, end: number } const name = 'name' in node.callee && node.callee.name if (name === 'onPrehydrate') { + if (!node.arguments[0]) { return } if (node.arguments[0].type !== 'ArrowFunctionExpression' && node.arguments[0].type !== 'FunctionExpression') { return } const needsAttr = node.arguments[0].params.length > 0 diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 000e2facc4..e973d53f8e 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -325,7 +325,9 @@ export default defineRenderHandler(async (event): Promise | string[]): Promise const styleMap = await getSSRStyles() const inlinedStyles = new Set() for (const mod of usedModules) { - if (mod in styleMap) { + if (mod in styleMap && styleMap[mod]) { for (const style of await styleMap[mod]()) { inlinedStyles.add(style) } @@ -650,7 +651,7 @@ function splitPayload (ssrContext: NuxtSSRContext) { */ function getServerComponentHTML (body: string): string { const match = body.match(ROOT_NODE_REGEX) - return match ? match[1] : body[0] + return match?.[1] || body } const SSR_SLOT_TELEPORT_MARKER = /^uid=([^;]*);slot=(.*)$/ @@ -660,10 +661,10 @@ const SSR_CLIENT_SLOT_MARKER = /^island-slot=[^;]*;(.*)$/ function getSlotIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse['slots'] { if (!ssrContext.islandContext || !Object.keys(ssrContext.islandContext.slots).length) { return undefined } const response: NuxtIslandResponse['slots'] = {} - for (const slot in ssrContext.islandContext.slots) { - response[slot] = { - ...ssrContext.islandContext.slots[slot], - fallback: ssrContext.teleports?.[`island-fallback=${slot}`], + for (const [name, slot] of Object.entries(ssrContext.islandContext.slots)) { + response[name] = { + ...slot, + fallback: ssrContext.teleports?.[`island-fallback=${name}`], } } return response @@ -673,11 +674,11 @@ function getClientIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandRespons if (!ssrContext.islandContext || !Object.keys(ssrContext.islandContext.components).length) { return undefined } 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 - const html = ssrContext.teleports?.[clientUid].replaceAll('', '') || '' + const html = ssrContext.teleports?.[clientUid]?.replaceAll('', '') || '' response[clientUid] = { - ...ssrContext.islandContext.components[clientUid], + ...component, html, slots: getComponentSlotTeleport(ssrContext.teleports ?? {}), } diff --git a/packages/nuxt/src/core/utils/names.ts b/packages/nuxt/src/core/utils/names.ts index 3397cc99e7..df6732c96f 100644 --- a/packages/nuxt/src/core/utils/names.ts +++ b/packages/nuxt/src/core/utils/names.ts @@ -28,11 +28,12 @@ export function resolveComponentNameSegments (fileName: string, prefixParts: str let index = prefixParts.length - 1 const matchedSuffix: string[] = [] 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('/') if ((fileNamePartsContent === matchedSuffixContent || fileNamePartsContent.startsWith(matchedSuffixContent + '/')) || // e.g Item/Item/Item.vue -> Item - (prefixParts[index].toLowerCase() === fileNamePartsContent && + (prefixPart.toLowerCase() === fileNamePartsContent && prefixParts[index + 1] && prefixParts[index] === prefixParts[index + 1])) { componentNameParts.length = index diff --git a/packages/nuxt/src/head/runtime/components.ts b/packages/nuxt/src/head/runtime/components.ts index 7c3f4b8140..5913471dde 100644 --- a/packages/nuxt/src/head/runtime/components.ts +++ b/packages/nuxt/src/head/runtime/components.ts @@ -160,7 +160,7 @@ export const Title = defineComponent({ if (import.meta.dev) { 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(' can take only one string in its default slot.') } diff --git a/packages/nuxt/src/pages/module.ts b/packages/nuxt/src/pages/module.ts index 5e79e31746..ee7c937182 100644 --- a/packages/nuxt/src/pages/module.ts +++ b/packages/nuxt/src/pages/module.ts @@ -247,7 +247,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 } return pages.some(page => page.file === file) || pages.some(page => page.children && isPage(file, page.children)) } @@ -359,7 +359,7 @@ export default defineNuxtModule({ const updatePage = async function updatePage (path: string) { 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 { const extractedRule = await extractRouteRules(code) if (extractedRule) { @@ -408,8 +408,7 @@ export default defineNuxtModule({ nuxt.hook('pages:extend', (routes) => { const nitro = useNitro() let resolvedRoutes: string[] - for (const path in nitro.options.routeRules) { - const rule = nitro.options.routeRules[path] + for (const [path, rule] of Object.entries(nitro.options.routeRules)) { if (!rule.redirect) { continue } resolvedRoutes ||= routes.flatMap(route => resolveRoutePaths(route)) // skip if there's already a route matching this path @@ -455,14 +454,14 @@ export default defineNuxtModule({ if (nuxt.options.dev) { return } const sourceFiles = nuxt.apps.default?.pages?.length ? getSources(nuxt.apps.default.pages) : [] - for (const key in 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!)))) { + for (const [key, chunk] of Object.entries(manifest)) { + 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] continue } - if (manifest[key].isEntry) { - manifest[key].dynamicImports = - manifest[key].dynamicImports?.filter(i => !sourceFiles.includes(i)) + if (chunk.isEntry) { + chunk.dynamicImports = + chunk.dynamicImports?.filter(i => !sourceFiles.includes(i)) } } }) diff --git a/packages/nuxt/src/pages/route-rules.ts b/packages/nuxt/src/pages/route-rules.ts index 6f025f678e..1b76a75b52 100644 --- a/packages/nuxt/src/pages/route-rules.ts +++ b/packages/nuxt/src/pages/route-rules.ts @@ -14,7 +14,7 @@ const ruleCache: Record<string, NitroRouteConfig | null> = {} export async function extractRouteRules (code: string): Promise<NitroRouteConfig | null> { if (code in ruleCache) { - return ruleCache[code] + return ruleCache[code] || null } if (!ROUTE_RULE_RE.test(code)) { return null } diff --git a/packages/nuxt/src/pages/utils.ts b/packages/nuxt/src/pages/utils.ts index b45395bde6..e1e14517e9 100644 --- a/packages/nuxt/src/pages/utils.ts +++ b/packages/nuxt/src/pages/utils.ts @@ -102,7 +102,7 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate // Array where routes should be added, useful when adding child routes let parent = routes - const lastSegment = segments[segments.length - 1] + const lastSegment = segments[segments.length - 1]! if (lastSegment.endsWith('.server')) { segments[segments.length - 1] = lastSegment.replace('.server', '') if (options.shouldUseServerComponents) { @@ -116,7 +116,7 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate for (let i = 0; i < segments.length; i++) { const segment = segments[i] - const tokens = parseSegment(segment) + const tokens = parseSegment(segment!) // Skip group segments 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>()) { for (const route of routes) { 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) if (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)) { if (match?.groups?.content) { contents.push({ - loader: match.groups.attrs.includes('tsx') ? 'tsx' : 'ts', + loader: match.groups.attrs?.includes('tsx') ? 'tsx' : 'ts', code: match.groups.content.trim(), }) } @@ -196,7 +196,9 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro delete metaCache[absolutePath] } - if (absolutePath in metaCache) { return metaCache[absolutePath] } + if (absolutePath in metaCache && metaCache[absolutePath]) { + return metaCache[absolutePath] + } const loader = getLoader(absolutePath) const scriptBlocks = !loader ? null : loader === 'vue' ? extractScriptContent(contents) : [{ code: contents, loader }] @@ -403,7 +405,7 @@ function parseSegment (segment: string) { consumeBuffer() } state = SegmentParserState.initial - } else if (PARAM_CHAR_RE.test(c)) { + } else if (c && PARAM_CHAR_RE.test(c)) { buffer += c } else { // console.debug(`[pages]Ignored character "${c}" while building param "${buffer}" from "segment"`) diff --git a/packages/nuxt/test/app.test.ts b/packages/nuxt/test/app.test.ts index faf40e71be..1bdcb5f8aa 100644 --- a/packages/nuxt/test/app.test.ts +++ b/packages/nuxt/test/app.test.ts @@ -297,8 +297,8 @@ async function getResolvedApp (files: Array<string | { name: string, contents: s mw.path = normaliseToRepo(mw.path)! } - for (const layout in app.layouts) { - app.layouts[layout].file = normaliseToRepo(app.layouts[layout].file)! + for (const layout of Object.values(app.layouts)) { + layout.file = normaliseToRepo(layout.file)! } await nuxt.close() diff --git a/packages/nuxt/test/pages.test.ts b/packages/nuxt/test/pages.test.ts index 3b307387be..f2b2820e81 100644 --- a/packages/nuxt/test/pages.test.ts +++ b/packages/nuxt/test/pages.test.ts @@ -653,7 +653,7 @@ describe('pages:generateRoutesFromFiles', () => { }))).map((route, index) => { return { ...route, - meta: test.files![index].meta, + meta: test.files![index]!.meta, } }) diff --git a/packages/ui-templates/lib/dev.ts b/packages/ui-templates/lib/dev.ts index 8c2624dbfe..3469dd6257 100644 --- a/packages/ui-templates/lib/dev.ts +++ b/packages/ui-templates/lib/dev.ts @@ -31,7 +31,7 @@ export const DevRenderingPlugin = () => { const chunks = contents.split(/\{{2,3}[^{}]+\}{2,3}/g) let templateString = chunks.shift() for (const expression of contents.matchAll(/\{{2,3}([^{}]+)\}{2,3}/g)) { - const value = runInNewContext(expression[1].trim(), { + const value = runInNewContext(expression[1]!.trim(), { version, messages: { ...genericMessages, ...messages }, }) diff --git a/packages/vite/src/manifest.ts b/packages/vite/src/manifest.ts index 15172431fd..994f1d3f41 100644 --- a/packages/vite/src/manifest.ts +++ b/packages/vite/src/manifest.ts @@ -5,11 +5,13 @@ import { relative, resolve } from 'pathe' import { withTrailingSlash, withoutLeadingSlash } from 'ufo' import escapeRE from 'escape-string-regexp' import { normalizeViteManifest } 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' export async function writeManifest (ctx: ViteBuildContext) { // This is only used for ssr: false - when ssr is enabled we use vite-node runtime manifest - const devClientManifest = { + const devClientManifest: RendererManifest = { '@vite/client': { isEntry: true, file: '@vite/client', @@ -30,17 +32,17 @@ export async function writeManifest (ctx: ViteBuildContext) { const serverDist = resolve(ctx.nuxt.options.buildDir, 'dist/server') const manifestFile = resolve(clientDist, 'manifest.json') - const clientManifest = ctx.nuxt.options.dev ? devClientManifest : JSON.parse(readFileSync(manifestFile, 'utf-8')) + const clientManifest = ctx.nuxt.options.dev ? devClientManifest : JSON.parse(readFileSync(manifestFile, 'utf-8')) as ViteClientManifest + const manifestEntries = Object.values(clientManifest) const buildAssetsDir = withTrailingSlash(withoutLeadingSlash(ctx.nuxt.options.app.buildAssetsDir)) const BASE_RE = new RegExp(`^${escapeRE(buildAssetsDir)}`) - for (const key in clientManifest) { - const entry = clientManifest[key] + for (const entry of manifestEntries) { if (entry.file) { entry.file = entry.file.replace(BASE_RE, '') } - for (const item of ['css', 'assets']) { + for (const item of ['css', 'assets'] as const) { if (entry[item]) { entry[item] = entry[item].map((i: string) => i.replace(BASE_RE, '')) } @@ -50,12 +52,11 @@ export async function writeManifest (ctx: ViteBuildContext) { await mkdir(serverDist, { recursive: true }) if (ctx.config.build?.cssCodeSplit === false) { - for (const key in clientManifest as Record<string, { file?: string }>) { - const val = clientManifest[key] - if (val.file?.endsWith('.css')) { + for (const entry of manifestEntries) { + if (entry.file?.endsWith('.css')) { const key = relative(ctx.config.root!, ctx.entry) - clientManifest[key].css ||= [] - clientManifest[key].css!.push(val.file) + clientManifest[key]!.css ||= [] + ;(clientManifest[key]!.css as string[]).push(entry.file) break } } diff --git a/packages/vite/src/plugins/analyze.ts b/packages/vite/src/plugins/analyze.ts index edecf536c7..d5e8e4432e 100644 --- a/packages/vite/src/plugins/analyze.ts +++ b/packages/vite/src/plugins/analyze.ts @@ -18,8 +18,7 @@ export function analyzePlugin (ctx: ViteBuildContext): Plugin[] { const bundle = outputBundle[_bundleId] if (!bundle || bundle.type !== 'chunk') { continue } const minifiedModuleEntryPromises: Array<Promise<[string, RenderedModule]>> = [] - for (const moduleId in bundle.modules) { - const module = bundle.modules[moduleId]! + for (const [moduleId, module] of Object.entries(bundle.modules)) { minifiedModuleEntryPromises.push( transform(module.code || '', { minify: true }) .then(result => [moduleId, { ...module, code: result.code }]), diff --git a/packages/vite/src/plugins/public-dirs.ts b/packages/vite/src/plugins/public-dirs.ts index e2765510f5..a1fb15ee24 100644 --- a/packages/vite/src/plugins/public-dirs.ts +++ b/packages/vite/src/plugins/public-dirs.ts @@ -86,8 +86,7 @@ export const VitePublicDirsPlugin = createUnplugin((options: VitePublicDirsPlugi } }, generateBundle (_outputOptions, bundle) { - for (const file in bundle) { - const chunk = bundle[file]! + for (const [file, chunk] of Object.entries(bundle)) { if (!file.endsWith('.css') || chunk.type !== 'asset') { continue } let css = chunk.source.toString() diff --git a/packages/vite/src/plugins/ssr-styles.ts b/packages/vite/src/plugins/ssr-styles.ts index c7c83a3439..345cf96a09 100644 --- a/packages/vite/src/plugins/ssr-styles.ts +++ b/packages/vite/src/plugins/ssr-styles.ts @@ -62,8 +62,7 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin { if (options.mode === 'client') { return } const emitted: Record<string, string> = {} - for (const file in cssMap) { - const { files, inBundle } = cssMap[file]! + for (const [file, { files, inBundle }] of Object.entries(cssMap)) { // File has been tree-shaken out of build (or there are no styles to inline) if (!files.length || !inBundle) { continue } const fileName = filename(file) diff --git a/packages/vite/src/vite.ts b/packages/vite/src/vite.ts index 7b51573d10..856be03fa6 100644 --- a/packages/vite/src/vite.ts +++ b/packages/vite/src/vite.ts @@ -207,8 +207,7 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => { // Remove CSS entries for files that will have inlined styles ctx.nuxt.hook('build:manifest', (manifest) => { - for (const key in manifest) { - const entry = manifest[key]! + for (const [key, entry] of Object.entries(manifest)) { const shouldRemoveCSS = chunksWithInlinedCSS.has(key) && !entry.isEntry if (entry.isEntry && chunksWithInlinedCSS.has(key)) { // @ts-expect-error internal key diff --git a/packages/webpack/src/plugins/vue/client.ts b/packages/webpack/src/plugins/vue/client.ts index 23259c6cba..005afcf18e 100644 --- a/packages/webpack/src/plugins/vue/client.ts +++ b/packages/webpack/src/plugins/vue/client.ts @@ -33,9 +33,10 @@ export default class VueSSRClientPlugin { const stats = compilation.getStats().toJson() const initialFiles = new Set<string>() - for (const name in stats.entrypoints!) { - const entryAssets = stats.entrypoints![name]!.assets! - for (const asset of entryAssets) { + for (const { assets } of Object.values(stats.entrypoints!)) { + if (!assets) { continue } + + for (const asset of assets) { const file = asset.name if ((isJS(file) || isCSS(file)) && !isHotUpdate(file)) { initialFiles.add(file) @@ -71,52 +72,53 @@ export default class VueSSRClientPlugin { } const { entrypoints = {}, namedChunkGroups = {} } = stats - const assetModules = stats.modules!.filter(m => m.assets!.length) - const fileToIndex = (file: string) => webpackManifest.all.indexOf(file) - stats.modules!.forEach((m) => { + const fileToIndex = (file: string | number) => webpackManifest.all.indexOf(String(file)) + for (const m of stats.modules!) { // Ignore modules duplicated in multiple chunks - if (m.chunks!.length === 1) { - const [cid] = m.chunks! - const chunk = stats.chunks!.find(c => c.id === cid) - if (!chunk || !chunk.files || !cid) { - return - } - const id = m.identifier!.replace(/\s\w+$/, '') // remove appended hash - const filesSet = new Set(chunk.files.map(fileToIndex).filter(i => i !== -1)) + if (m.chunks?.length !== 1) { continue } - for (const chunkName of chunk.names!) { - if (!entrypoints[chunkName]) { - const chunkGroup = namedChunkGroups[chunkName] - if (chunkGroup) { - for (const asset of chunkGroup.assets!) { - filesSet.add(fileToIndex(asset.name)) - } - } - } - } - - const files = Array.from(filesSet) - webpackManifest.modules[hash(id)] = files - - // In production mode, modules may be concatenated by scope hoisting - // Include ConcatenatedModule for not losing module-component mapping - if (Array.isArray(m.modules)) { - for (const concatenatedModule of m.modules) { - const id = hash(concatenatedModule.identifier!.replace(/\s\w+$/, '')) - if (!webpackManifest.modules[id]) { - webpackManifest.modules[id] = files - } - } - } - - // Find all asset modules associated with the same chunk - assetModules.forEach((m) => { - if (m.chunks!.includes(cid)) { - files.push(...(m.assets as string[]).map(fileToIndex)) - } - }) + const [cid] = m.chunks + const chunk = stats.chunks!.find(c => c.id === cid) + if (!chunk || !chunk.files || !cid) { + continue } - }) + const id = m.identifier!.replace(/\s\w+$/, '') // remove appended hash + const filesSet = new Set(chunk.files.map(fileToIndex).filter(i => i !== -1)) + + for (const chunkName of chunk.names!) { + if (!entrypoints[chunkName]) { + const chunkGroup = namedChunkGroups[chunkName] + if (chunkGroup) { + for (const asset of chunkGroup.assets!) { + filesSet.add(fileToIndex(asset.name)) + } + } + } + } + + const files = Array.from(filesSet) + webpackManifest.modules[hash(id)] = files + + // In production mode, modules may be concatenated by scope hoisting + // Include ConcatenatedModule for not losing module-component mapping + if (Array.isArray(m.modules)) { + for (const concatenatedModule of m.modules) { + const id = hash(concatenatedModule.identifier!.replace(/\s\w+$/, '')) + if (!webpackManifest.modules[id]) { + webpackManifest.modules[id] = files + } + } + } + + // Find all asset modules associated with the same chunk + if (stats.modules) { + for (const m of stats.modules) { + if (m.assets?.length && m.chunks?.includes(cid)) { + files.push(...m.assets.map(fileToIndex)) + } + } + } + } const manifest = normalizeWebpackManifest(webpackManifest as any) await this.options.nuxt.callHook('build:manifest', manifest) diff --git a/test/bundle.test.ts b/test/bundle.test.ts index 96d480afc3..ec4dc393f6 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -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 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')))) - expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot(`"114k"`) - expect.soft(roundToKilobytes(clientStatsInlined.totalBytes)).toMatchInlineSnapshot(`"114k"`) + expect.soft(roundToKilobytes(clientStats!.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(` [ diff --git a/tsconfig.json b/tsconfig.json index c912996aa5..6763732a3f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,8 +12,7 @@ "verbatimModuleSyntax": true, /* Strictness */ "strict": true, - // TODO: enable noUncheckedIndexedAccess - // "noUncheckedIndexedAccess": true, + "noUncheckedIndexedAccess": true, "noUncheckedSideEffectImports": true, "forceConsistentCasingInFileNames": true, "noImplicitOverride": true,