diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index 37bc0f45b7..908d60653f 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -20,7 +20,7 @@ import type { LoadingIndicator } from '../app/composables/loading-indicator' import type { RouteAnnouncer } from '../app/composables/route-announcer' // @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' @@ -370,12 +370,14 @@ export function createNuxtApp (options: CreateOptions) { defineGetter(nuxtApp.vueApp, '$nuxt', nuxtApp) defineGetter(nuxtApp.vueApp.config.globalProperties, '$nuxt', nuxtApp) - // Listen to chunk load errors if (import.meta.client) { - window.addEventListener('nuxt.preloadError', (event) => { - nuxtApp.callHook('app:chunkError', { error: (event as Event & { payload: Error }).payload }) - }) - + // Listen to chunk load errors + if (chunkErrorEvent) { + window.addEventListener(chunkErrorEvent, (event) => { + nuxtApp.callHook('app:chunkError', { error: (event as Event & { payload: Error }).payload }) + event.preventDefault() + }) + } window.useNuxtApp = window.useNuxtApp || useNuxtApp // Log errors captured when running plugins, in the `app:created` and `app:beforeMount` hooks diff --git a/packages/nuxt/src/app/plugins/chunk-reload.client.ts b/packages/nuxt/src/app/plugins/chunk-reload.client.ts index e8eb5e2e48..c43d433428 100644 --- a/packages/nuxt/src/app/plugins/chunk-reload.client.ts +++ b/packages/nuxt/src/app/plugins/chunk-reload.client.ts @@ -26,7 +26,7 @@ export default defineNuxtPlugin({ }) router.onError((error, to) => { - if (chunkErrors.has(error)) { + if (chunkErrors.has(error) || error.message.includes('Failed to fetch dynamically imported module')) { reloadAppAtPath(to) } }) diff --git a/packages/nuxt/src/core/templates.ts b/packages/nuxt/src/core/templates.ts index 7b1845cb4a..759d070562 100644 --- a/packages/nuxt/src/core/templates.ts +++ b/packages/nuxt/src/core/templates.ts @@ -516,6 +516,7 @@ export const nuxtConfigTemplate: NuxtTemplate = { `export const appId = ${JSON.stringify(ctx.nuxt.options.appId)}`, `export const outdatedBuildInterval = ${ctx.nuxt.options.experimental.checkOutdatedBuildInterval}`, `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') }, } diff --git a/packages/vite/src/client.ts b/packages/vite/src/client.ts index c9e8518a90..1821e21d38 100644 --- a/packages/vite/src/client.ts +++ b/packages/vite/src/client.ts @@ -11,7 +11,6 @@ import { defu } from 'defu' import { env, nodeless } from 'unenv' import { appendCorsHeaders, appendCorsPreflightHeaders, defineEventHandler } from 'h3' import type { ViteConfig } from '@nuxt/schema' -import { chunkErrorPlugin } from './plugins/chunk-error' import type { ViteBuildContext } from './vite' import { devStyleSSRPlugin } from './plugins/dev-ssr-css' import { runtimePathsPlugin } from './plugins/paths' @@ -167,11 +166,6 @@ export async function buildClient (ctx: ViteBuildContext) { 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 const useViteCors = clientConfig.server?.cors !== undefined if (!useViteCors) { diff --git a/packages/vite/src/plugins/chunk-error.ts b/packages/vite/src/plugins/chunk-error.ts deleted file mode 100644 index 4502435978..0000000000 --- a/packages/vite/src/plugins/chunk-error.ts +++ /dev/null @@ -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, - } - }, - } -} diff --git a/packages/webpack/src/plugins/chunk.ts b/packages/webpack/src/plugins/chunk.ts index 4832aeb941..56edb4c2fc 100644 --- a/packages/webpack/src/plugins/chunk.ts +++ b/packages/webpack/src/plugins/chunk.ts @@ -7,11 +7,11 @@ const script = ` if (typeof ${webpack.RuntimeGlobals.require} !== "undefined") { var _ensureChunk = ${webpack.RuntimeGlobals.ensureChunk}; ${webpack.RuntimeGlobals.ensureChunk} = function (chunkId) { - return Promise.resolve(_ensureChunk(chunkId)).catch(err => { - const e = new Event("nuxt.preloadError"); - e.payload = err; - window.dispatchEvent(e); - throw err; + return Promise.resolve(_ensureChunk(chunkId)).catch(error => { + const e = new Event('nuxt:preloadError', { cancelable: true }) + e.payload = error + window.dispatchEvent(e) + throw error }); }; };` diff --git a/test/basic.test.ts b/test/basic.test.ts index 5c99fa652b..fb68b45020 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -1164,14 +1164,15 @@ describe('errors', () => { }) // 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('/') await page.getByText('Increment state').click() await page.getByText('Increment state').click() 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.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') await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/chunk-error') await page.locator('div').getByText('State: 3').waitFor() diff --git a/test/fixtures/basic/pages/chunk-error.vue b/test/fixtures/basic/pages/chunk-error.vue index 7c9cb9da82..ad5e561940 100644 --- a/test/fixtures/basic/pages/chunk-error.vue +++ b/test/fixtures/basic/pages/chunk-error.vue @@ -1,14 +1,4 @@ diff --git a/test/fixtures/basic/pages/index.vue b/test/fixtures/basic/pages/index.vue index 5c59090757..3723544e03 100644 --- a/test/fixtures/basic/pages/index.vue +++ b/test/fixtures/basic/pages/index.vue @@ -36,8 +36,8 @@ Immediate remove unmounted Chunk error diff --git a/test/fixtures/basic/plugins/chunk-error.ts b/test/fixtures/basic/plugins/chunk-error.ts deleted file mode 100644 index e23e5ecdd9..0000000000 --- a/test/fixtures/basic/plugins/chunk-error.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtPlugin((nuxtApp) => { - nuxtApp.hook('app:chunkError', () => { - console.log('caught chunk load error') - }) -})