mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 15:15:19 +00:00
feat(nuxt): add experimental app:chunkError
hook and reload strategy (#19038)
This commit is contained in:
parent
a1252d3a30
commit
96b09ea982
@ -33,6 +33,7 @@ export interface RuntimeNuxtHooks {
|
|||||||
'app:suspense:resolve': (Component?: VNode) => HookResult
|
'app:suspense:resolve': (Component?: VNode) => HookResult
|
||||||
'app:error': (err: any) => HookResult
|
'app:error': (err: any) => HookResult
|
||||||
'app:error:cleared': (options: { redirect?: string }) => HookResult
|
'app:error:cleared': (options: { redirect?: string }) => HookResult
|
||||||
|
'app:chunkError': (options: { error: any }) => HookResult
|
||||||
'app:data:refresh': (keys?: string[]) => HookResult
|
'app:data:refresh': (keys?: string[]) => HookResult
|
||||||
'link:prefetch': (link: string) => HookResult
|
'link:prefetch': (link: string) => HookResult
|
||||||
'page:start': (Component?: VNode) => HookResult
|
'page:start': (Component?: VNode) => HookResult
|
||||||
@ -186,6 +187,13 @@ export function createNuxtApp (options: CreateOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen to chunk load errors
|
||||||
|
if (process.client) {
|
||||||
|
window.addEventListener('nuxt.preloadError', (event) => {
|
||||||
|
nuxtApp.callHook('app:chunkError', { error: (event as Event & { payload: Error }).payload })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Expose runtime config
|
// Expose runtime config
|
||||||
const runtimeConfig = process.server
|
const runtimeConfig = process.server
|
||||||
? options.ssrContext!.runtimeConfig
|
? options.ssrContext!.runtimeConfig
|
||||||
|
25
packages/nuxt/src/app/plugins/chunk-reload.client.ts
Normal file
25
packages/nuxt/src/app/plugins/chunk-reload.client.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { defineNuxtPlugin } from '#app/nuxt'
|
||||||
|
import { useRouter } from '#app/composables/router'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const chunkErrors = new Set()
|
||||||
|
|
||||||
|
router.beforeEach(() => { chunkErrors.clear() })
|
||||||
|
nuxtApp.hook('app:chunkError', ({ error }) => { chunkErrors.add(error) })
|
||||||
|
|
||||||
|
router.onError((error, to) => {
|
||||||
|
if (!chunkErrors.has(error)) { return }
|
||||||
|
|
||||||
|
let handledPath: Record<string, any> = {}
|
||||||
|
try {
|
||||||
|
handledPath = JSON.parse(localStorage.getItem('nuxt:reload') || '{}')
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (handledPath?.path !== to.fullPath || handledPath?.expires < Date.now()) {
|
||||||
|
localStorage.setItem('nuxt:reload', JSON.stringify({ path: to.fullPath, expires: Date.now() + 10000 }))
|
||||||
|
window.location.href = to.fullPath
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
@ -192,6 +192,11 @@ async function initNuxt (nuxt: Nuxt) {
|
|||||||
addPlugin(resolve(nuxt.options.appDir, 'plugins/cross-origin-prefetch.client'))
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/cross-origin-prefetch.client'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add experimental page reload support
|
||||||
|
if (nuxt.options.experimental.emitRouteChunkError === 'reload') {
|
||||||
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/chunk-reload.client'))
|
||||||
|
}
|
||||||
|
|
||||||
// Track components used to render for webpack
|
// Track components used to render for webpack
|
||||||
if (nuxt.options.builder === '@nuxt/webpack-builder') {
|
if (nuxt.options.builder === '@nuxt/webpack-builder') {
|
||||||
addPlugin(resolve(nuxt.options.appDir, 'plugins/preload.server'))
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/preload.server'))
|
||||||
|
@ -27,6 +27,18 @@ export default defineUntypedSchema({
|
|||||||
*/
|
*/
|
||||||
treeshakeClientOnly: true,
|
treeshakeClientOnly: true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit `app:chunkError` hook when there is an error loading vite/webpack
|
||||||
|
* chunks.
|
||||||
|
*
|
||||||
|
* You can set this to `reload` to perform a hard reload of the new route
|
||||||
|
* when a chunk fails to load when navigating to a new route.
|
||||||
|
*
|
||||||
|
* @see https://github.com/nuxt/nuxt/pull/19038
|
||||||
|
* @type {boolean | 'reload'}
|
||||||
|
*/
|
||||||
|
emitRouteChunkError: false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use vite-node for on-demand server chunk loading
|
* Use vite-node for on-demand server chunk loading
|
||||||
*
|
*
|
||||||
|
@ -11,6 +11,7 @@ import { defu } from 'defu'
|
|||||||
import type { OutputOptions } from 'rollup'
|
import type { OutputOptions } from 'rollup'
|
||||||
import { defineEventHandler } from 'h3'
|
import { defineEventHandler } from 'h3'
|
||||||
import { cacheDirPlugin } from './plugins/cache-dir'
|
import { cacheDirPlugin } from './plugins/cache-dir'
|
||||||
|
import { chunkErrorPlugin } from './plugins/chunk-error'
|
||||||
import type { ViteBuildContext, ViteOptions } from './vite'
|
import type { ViteBuildContext, ViteOptions } 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'
|
||||||
@ -82,6 +83,11 @@ 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 }))
|
||||||
|
}
|
||||||
|
|
||||||
// We want to respect users' own rollup output options
|
// We want to respect users' own rollup output options
|
||||||
clientConfig.build!.rollupOptions = defu(clientConfig.build!.rollupOptions!, {
|
clientConfig.build!.rollupOptions = defu(clientConfig.build!.rollupOptions!, {
|
||||||
output: {
|
output: {
|
||||||
|
30
packages/vite/src/plugins/chunk-error.ts
Normal file
30
packages/vite/src/plugins/chunk-error.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
import MagicString from 'magic-string'
|
||||||
|
import type { Plugin } from 'vite'
|
||||||
|
import type { SourceMap } from 'rollup'
|
||||||
|
|
||||||
|
export function chunkErrorPlugin (options: { sourcemap?: boolean }): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'nuxt:chunk-error',
|
||||||
|
transform (code, id) {
|
||||||
|
if (id !== '\0vite/preload-helper' || 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({ source: id, includeContent: true }) as SourceMap
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
packages/webpack/src/plugins/chunk.ts
Normal file
28
packages/webpack/src/plugins/chunk.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import type { Compiler } from 'webpack'
|
||||||
|
import { RuntimeGlobals } from 'webpack'
|
||||||
|
|
||||||
|
const pluginName = 'ChunkErrorPlugin'
|
||||||
|
|
||||||
|
const script = `
|
||||||
|
if (typeof ${RuntimeGlobals.require} !== "undefined") {
|
||||||
|
var _ensureChunk = ${RuntimeGlobals.ensureChunk};
|
||||||
|
${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;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};`
|
||||||
|
|
||||||
|
export class ChunkErrorPlugin {
|
||||||
|
apply (compiler: Compiler) {
|
||||||
|
compiler.hooks.thisCompilation.tap(pluginName, compilation =>
|
||||||
|
compilation.mainTemplate.hooks.localVars.tap(
|
||||||
|
{ name: pluginName, stage: 1 },
|
||||||
|
source => source + script
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@ import { joinURL } from 'ufo'
|
|||||||
import { logger, useNuxt } from '@nuxt/kit'
|
import { logger, useNuxt } from '@nuxt/kit'
|
||||||
import { composableKeysPlugin } from '../../vite/src/plugins/composable-keys'
|
import { composableKeysPlugin } from '../../vite/src/plugins/composable-keys'
|
||||||
import { DynamicBasePlugin } from './plugins/dynamic-base'
|
import { DynamicBasePlugin } from './plugins/dynamic-base'
|
||||||
|
import { ChunkErrorPlugin } from './plugins/chunk'
|
||||||
import { createMFS } from './utils/mfs'
|
import { createMFS } from './utils/mfs'
|
||||||
import { registerVirtualModules } from './virtual-modules'
|
import { registerVirtualModules } from './virtual-modules'
|
||||||
import { client, server } from './configs'
|
import { client, server } from './configs'
|
||||||
@ -39,6 +40,10 @@ export async function bundle (nuxt: Nuxt) {
|
|||||||
config.plugins!.push(DynamicBasePlugin.webpack({
|
config.plugins!.push(DynamicBasePlugin.webpack({
|
||||||
sourcemap: nuxt.options.sourcemap[config.name as 'client' | 'server']
|
sourcemap: nuxt.options.sourcemap[config.name as 'client' | 'server']
|
||||||
}))
|
}))
|
||||||
|
// Emit chunk errors if the user has opted in to `experimental.emitRouteChunkError`
|
||||||
|
if (config.name === 'client' && nuxt.options.experimental.emitRouteChunkError) {
|
||||||
|
config.plugins!.push(new ChunkErrorPlugin())
|
||||||
|
}
|
||||||
config.plugins!.push(composableKeysPlugin.webpack({
|
config.plugins!.push(composableKeysPlugin.webpack({
|
||||||
sourcemap: nuxt.options.sourcemap[config.name as 'client' | 'server'],
|
sourcemap: nuxt.options.sourcemap[config.name as 'client' | 'server'],
|
||||||
rootDir: nuxt.options.rootDir
|
rootDir: nuxt.options.rootDir
|
||||||
|
@ -330,6 +330,15 @@ describe('errors', () => {
|
|||||||
const res = await fetch('/error')
|
const res = await fetch('/error')
|
||||||
expect(await res.text()).toContain('This is a custom error')
|
expect(await res.text()).toContain('This is a custom error')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO: need to create test for webpack
|
||||||
|
it.runIf(!isDev() && !isWebpack)('should handle chunk loading errors', async () => {
|
||||||
|
const { page, consoleLogs } = await renderPage('/')
|
||||||
|
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(await page.innerText('div')).toContain('Chunk error page')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('navigate external', () => {
|
describe('navigate external', () => {
|
||||||
|
1
test/fixtures/basic/nuxt.config.ts
vendored
1
test/fixtures/basic/nuxt.config.ts
vendored
@ -149,6 +149,7 @@ export default defineNuxtConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
|
emitRouteChunkError: 'reload',
|
||||||
inlineSSRStyles: id => !!id && !id.includes('assets.vue'),
|
inlineSSRStyles: id => !!id && !id.includes('assets.vue'),
|
||||||
componentIslands: true,
|
componentIslands: true,
|
||||||
reactivityTransform: true,
|
reactivityTransform: true,
|
||||||
|
17
test/fixtures/basic/pages/chunk-error.vue
vendored
Normal file
17
test/fixtures/basic/pages/chunk-error.vue
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
middleware: defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
if (process.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`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
Chunk error page
|
||||||
|
</div>
|
||||||
|
</template>
|
3
test/fixtures/basic/pages/index.vue
vendored
3
test/fixtures/basic/pages/index.vue
vendored
@ -14,6 +14,9 @@
|
|||||||
<NuxtLink to="/">
|
<NuxtLink to="/">
|
||||||
Link
|
Link
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/chunk-error" :prefetch="false">
|
||||||
|
Chunk error
|
||||||
|
</NuxtLink>
|
||||||
<NestedSugarCounter :multiplier="2" />
|
<NestedSugarCounter :multiplier="2" />
|
||||||
<CustomComponent />
|
<CustomComponent />
|
||||||
<Spin>Test</Spin>
|
<Spin>Test</Spin>
|
||||||
|
3
test/fixtures/basic/plugins/chunk-error.ts
vendored
Normal file
3
test/fixtures/basic/plugins/chunk-error.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
nuxtApp.hook('app:chunkError', () => console.log('caught chunk load error'))
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user