refactor(nuxt): use vite:preloadError event (#28862)

This commit is contained in:
Daniel Roe 2024-09-11 11:01:55 +01:00 committed by GitHub
parent f7efc3d359
commit 2f0a28d47e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 19 additions and 68 deletions

View File

@ -20,7 +20,7 @@ import type { LoadingIndicator } from '../app/composables/loading-indicator'
import type { RouteAnnouncer } from '../app/composables/route-announcer' import type { RouteAnnouncer } from '../app/composables/route-announcer'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { appId, multiApp } from '#build/nuxt.config.mjs' import { appId, chunkErrorEvent, multiApp } from '#build/nuxt.config.mjs'
import type { NuxtAppLiterals } from '#app' import type { NuxtAppLiterals } from '#app'
@ -370,12 +370,14 @@ export function createNuxtApp (options: CreateOptions) {
defineGetter(nuxtApp.vueApp, '$nuxt', nuxtApp) defineGetter(nuxtApp.vueApp, '$nuxt', nuxtApp)
defineGetter(nuxtApp.vueApp.config.globalProperties, '$nuxt', nuxtApp) defineGetter(nuxtApp.vueApp.config.globalProperties, '$nuxt', nuxtApp)
// Listen to chunk load errors
if (import.meta.client) { if (import.meta.client) {
window.addEventListener('nuxt.preloadError', (event) => { // Listen to chunk load errors
if (chunkErrorEvent) {
window.addEventListener(chunkErrorEvent, (event) => {
nuxtApp.callHook('app:chunkError', { error: (event as Event & { payload: Error }).payload }) nuxtApp.callHook('app:chunkError', { error: (event as Event & { payload: Error }).payload })
event.preventDefault()
}) })
}
window.useNuxtApp = window.useNuxtApp || useNuxtApp window.useNuxtApp = window.useNuxtApp || useNuxtApp
// Log errors captured when running plugins, in the `app:created` and `app:beforeMount` hooks // Log errors captured when running plugins, in the `app:created` and `app:beforeMount` hooks

View File

@ -26,7 +26,7 @@ export default defineNuxtPlugin({
}) })
router.onError((error, to) => { router.onError((error, to) => {
if (chunkErrors.has(error)) { if (chunkErrors.has(error) || error.message.includes('Failed to fetch dynamically imported module')) {
reloadAppAtPath(to) reloadAppAtPath(to)
} }
}) })

View File

@ -516,6 +516,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
`export const appId = ${JSON.stringify(ctx.nuxt.options.appId)}`, `export const appId = ${JSON.stringify(ctx.nuxt.options.appId)}`,
`export const outdatedBuildInterval = ${ctx.nuxt.options.experimental.checkOutdatedBuildInterval}`, `export const outdatedBuildInterval = ${ctx.nuxt.options.experimental.checkOutdatedBuildInterval}`,
`export const multiApp = ${!!ctx.nuxt.options.future.multiApp}`, `export const multiApp = ${!!ctx.nuxt.options.future.multiApp}`,
`export const chunkErrorEvent = ${ctx.nuxt.options.experimental.emitRouteChunkError ? ctx.nuxt.options.builder === '@nuxt/vite-builder' ? '"vite:preloadError"' : '"nuxt:preloadError"' : 'false'}`,
].join('\n\n') ].join('\n\n')
}, },
} }

View File

@ -11,7 +11,6 @@ import { defu } from 'defu'
import { env, nodeless } from 'unenv' import { env, nodeless } from 'unenv'
import { appendCorsHeaders, appendCorsPreflightHeaders, defineEventHandler } from 'h3' import { appendCorsHeaders, appendCorsPreflightHeaders, defineEventHandler } from 'h3'
import type { ViteConfig } from '@nuxt/schema' import type { ViteConfig } from '@nuxt/schema'
import { chunkErrorPlugin } from './plugins/chunk-error'
import type { ViteBuildContext } from './vite' import type { ViteBuildContext } from './vite'
import { devStyleSSRPlugin } from './plugins/dev-ssr-css' import { devStyleSSRPlugin } from './plugins/dev-ssr-css'
import { runtimePathsPlugin } from './plugins/paths' import { runtimePathsPlugin } from './plugins/paths'
@ -167,11 +166,6 @@ export async function buildClient (ctx: ViteBuildContext) {
clientConfig.server!.hmr = false clientConfig.server!.hmr = false
} }
// Emit chunk errors if the user has opted in to `experimental.emitRouteChunkError`
if (ctx.nuxt.options.experimental.emitRouteChunkError) {
clientConfig.plugins!.push(chunkErrorPlugin({ sourcemap: !!ctx.nuxt.options.sourcemap.client }))
}
// Inject an h3-based CORS handler in preference to vite's // Inject an h3-based CORS handler in preference to vite's
const useViteCors = clientConfig.server?.cors !== undefined const useViteCors = clientConfig.server?.cors !== undefined
if (!useViteCors) { if (!useViteCors) {

View File

@ -1,32 +0,0 @@
import MagicString from 'magic-string'
import type { Plugin } from 'vite'
const vitePreloadHelperId = '\0vite/preload-helper'
// TODO: remove this function when we upgrade to vite 5
export function chunkErrorPlugin (options: { sourcemap?: boolean }): Plugin {
return {
name: 'nuxt:chunk-error',
transform (code, id) {
// Vite 5 has an id with extension
if (!(id === vitePreloadHelperId || id === `${vitePreloadHelperId}.js`) || code.includes('nuxt.preloadError')) { return }
const s = new MagicString(code)
s.replace(/__vitePreload/g, '___vitePreload')
s.append(`
export const __vitePreload = (...args) => ___vitePreload(...args).catch(err => {
const e = new Event("nuxt.preloadError");
e.payload = err;
window.dispatchEvent(e);
throw err;
})`)
return {
code: s.toString(),
map: options.sourcemap
? s.generateMap({ hires: true })
: undefined,
}
},
}
}

View File

@ -7,11 +7,11 @@ const script = `
if (typeof ${webpack.RuntimeGlobals.require} !== "undefined") { if (typeof ${webpack.RuntimeGlobals.require} !== "undefined") {
var _ensureChunk = ${webpack.RuntimeGlobals.ensureChunk}; var _ensureChunk = ${webpack.RuntimeGlobals.ensureChunk};
${webpack.RuntimeGlobals.ensureChunk} = function (chunkId) { ${webpack.RuntimeGlobals.ensureChunk} = function (chunkId) {
return Promise.resolve(_ensureChunk(chunkId)).catch(err => { return Promise.resolve(_ensureChunk(chunkId)).catch(error => {
const e = new Event("nuxt.preloadError"); const e = new Event('nuxt:preloadError', { cancelable: true })
e.payload = err; e.payload = error
window.dispatchEvent(e); window.dispatchEvent(e)
throw err; throw error
}); });
}; };
};` };`

View File

@ -1164,14 +1164,15 @@ describe('errors', () => {
}) })
// TODO: need to create test for webpack // TODO: need to create test for webpack
it.runIf(!isDev() && !isWebpack)('should handle chunk loading errors', async () => { it.runIf(!isDev())('should handle chunk loading errors', async () => {
const { page, consoleLogs } = await renderPage('/') const { page, consoleLogs } = await renderPage('/')
await page.getByText('Increment state').click() await page.getByText('Increment state').click()
await page.getByText('Increment state').click() await page.getByText('Increment state').click()
expect(await page.innerText('div')).toContain('Some value: 3') expect(await page.innerText('div')).toContain('Some value: 3')
await page.route(/.*/, route => route.abort('timedout'), { times: 1 })
await page.getByText('Chunk error').click() await page.getByText('Chunk error').click()
await page.waitForURL(url('/chunk-error')) await page.waitForURL(url('/chunk-error'))
expect(consoleLogs.map(c => c.text).join('')).toContain('caught chunk load error') expect(consoleLogs.map(c => c.text).join('')).toContain('Failed to load resource')
expect(await page.innerText('div')).toContain('Chunk error page') expect(await page.innerText('div')).toContain('Chunk error page')
await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/chunk-error') await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/chunk-error')
await page.locator('div').getByText('State: 3').waitFor() await page.locator('div').getByText('State: 3').waitFor()

View File

@ -1,14 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({
async middleware (to, from) {
await new Promise(resolve => setTimeout(resolve, 1))
const nuxtApp = useNuxtApp()
if (import.meta.client && from !== to && !nuxtApp.isHydrating) {
// trigger a loading error when navigated to via client-side navigation
await import(/* webpackIgnore: true */ /* @vite-ignore */ `some-non-exis${''}ting-module`)
}
},
})
const someValue = useState('val', () => 1) const someValue = useState('val', () => 1)
</script> </script>

View File

@ -36,8 +36,8 @@
Immediate remove unmounted Immediate remove unmounted
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink
no-prefetch
to="/chunk-error" to="/chunk-error"
:prefetch="false"
> >
Chunk error Chunk error
</NuxtLink> </NuxtLink>

View File

@ -1,5 +0,0 @@
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('app:chunkError', () => {
console.log('caught chunk load error')
})
})