From 42850928794e1f05216e7c9502cf7f64d54f9d54 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 11 Apr 2023 12:58:43 +0100 Subject: [PATCH] feat(nuxt): support object-syntax plugins (#20003) --- .../2.directory-structure/1.plugins.md | 25 ++ docs/3.api/1.composables/use-nuxt-app.md | 22 +- packages/nuxt/src/app/nuxt.ts | 104 +++++- .../src/app/plugins/chunk-reload.client.ts | 29 +- .../plugins/cross-origin-prefetch.client.ts | 57 ++-- packages/nuxt/src/app/plugins/debug.ts | 8 +- .../nuxt/src/app/plugins/payload.client.ts | 41 +-- .../nuxt/src/app/plugins/preload.server.ts | 19 +- .../src/app/plugins/restore-state.client.ts | 26 +- .../src/app/plugins/revive-payload.client.ts | 16 +- .../src/app/plugins/revive-payload.server.ts | 9 +- packages/nuxt/src/app/plugins/router.ts | 316 +++++++++--------- packages/nuxt/src/components/templates.ts | 11 +- packages/nuxt/src/core/nuxt.ts | 6 +- .../nuxt/src/head/runtime/plugins/unhead.ts | 57 ++-- .../runtime/plugins/vueuse-head-polyfill.ts | 9 +- packages/nuxt/src/imports/presets.ts | 1 + .../pages/runtime/plugins/prefetch.client.ts | 60 ++-- .../nuxt/src/pages/runtime/plugins/router.ts | 278 +++++++-------- packages/schema/src/config/build.ts | 1 + test/bundle.test.ts | 4 +- test/fixtures/basic/nuxt.config.ts | 6 - .../basic/plugins/custom-type-registration.ts | 2 +- 23 files changed, 621 insertions(+), 486 deletions(-) diff --git a/docs/2.guide/2.directory-structure/1.plugins.md b/docs/2.guide/2.directory-structure/1.plugins.md index e5147b23d1..091b3e68a4 100644 --- a/docs/2.guide/2.directory-structure/1.plugins.md +++ b/docs/2.guide/2.directory-structure/1.plugins.md @@ -40,6 +40,31 @@ export default defineNuxtPlugin(nuxtApp => { }) ``` +### Object Syntax Plugins + +It is also possible to define a plugin using an object syntax, for more advanced use cases. For example: + +```ts +export default defineNuxtPlugin({ + name: 'my-plugin', + enforce: 'pre', // or 'post' + async setup (nuxtApp) { + // this is the equivalent of a normal functional plugin + }, + hooks: { + // You can directly register Nuxt app hooks here + 'app:created'() { + const nuxtApp = useNuxtApp() + // + } + } +}) +``` + +::alert +If you are using an object-syntax plugin, the properties may be statically analyzed in future to produce a more optimized build. So you should not define them at runtime. For example, setting `enforce: process.server ? 'pre' : 'post'` would defeat any future optimization Nuxt is able to do for your plugins. +:: + ## Plugin Registration Order You can control the order in which plugins are registered by prefixing a number to the file names. diff --git a/docs/3.api/1.composables/use-nuxt-app.md b/docs/3.api/1.composables/use-nuxt-app.md index d962cca4d0..70d4777d43 100644 --- a/docs/3.api/1.composables/use-nuxt-app.md +++ b/docs/3.api/1.composables/use-nuxt-app.md @@ -116,19 +116,19 @@ export default defineNuxtPlugin((nuxtApp) => { ::alert Normally `payload` must contain only plain JavaScript objects. But by setting `experimental.renderJsonPayloads`, it is possible to use more advanced types, such as `ref`, `reactive`, `shallowRef`, `shallowReactive` and `NuxtError`. -You can also add your own types. In future you will be able to add your own types easily with [object-syntax plugins](https://github.com/nuxt/nuxt/issues/14628). For now, you must add your plugin which calls both `definePayloadReducer` and `definePayloadReviver` via a custom module: +You can also add your own types, with a special plugin helper: -```ts -export default defineNuxtConfig({ - modules: [ - function (_options, nuxt) { - // TODO: support directly via object syntax plugins: https://github.com/nuxt/nuxt/issues/14628 - nuxt.hook('modules:done', () => { - nuxt.options.plugins.unshift('~/plugins/custom-type-plugin') - }) - }, - ] +```ts [plugins/custom-payload.ts] + /** + * This kind of plugin runs very early in the Nuxt lifecycle, before we revive the payload. + * You will not have access to the router or other Nuxt-injected properties. + */ +export default definePayloadPlugin((nuxtApp) => { + definePayloadReducer('BlinkingText', data => data === '' && '_') + definePayloadReviver('BlinkingText', () => '') }) +``` + :: ### `isHydrating` diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index 41812dd231..1fa4c2ab88 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -139,9 +139,31 @@ interface _NuxtApp { export interface NuxtApp extends _NuxtApp {} export const NuxtPluginIndicator = '__nuxt_plugin' + +export interface PluginMeta { + name?: string + enforce?: 'pre' | 'default' | 'post' + /** + * This allows more granular control over plugin order and should only be used by advanced users. + * It overrides the value of `enforce` and is used to sort plugins. + */ + order?: number +} + +export interface ResolvedPluginMeta { + name?: string + order: number +} + export interface Plugin = Record> { (nuxt: _NuxtApp): Promise | Promise<{ provide?: Injections }> | void | { provide?: Injections } [NuxtPluginIndicator]?: true + meta?: ResolvedPluginMeta +} + +export interface ObjectPluginInput = Record> extends PluginMeta { + hooks?: Partial + setup?: Plugin } export interface CreateOptions { @@ -306,25 +328,30 @@ export function normalizePlugins (_plugins: Plugin[]) { const legacyInjectPlugins: Plugin[] = [] const invalidPlugins: Plugin[] = [] - const plugins = _plugins.map((plugin) => { + const plugins: Plugin[] = [] + + for (const plugin of _plugins) { if (typeof plugin !== 'function') { - invalidPlugins.push(plugin) - return null + if (process.dev) { invalidPlugins.push(plugin) } + continue } + + // TODO: Skip invalid plugins in next releases + let _plugin = plugin if (plugin.length > 1) { - legacyInjectPlugins.push(plugin) // Allow usage without wrapper but warn - // TODO: Skip invalid in next releases - // @ts-ignore - return (nuxtApp: NuxtApp) => plugin(nuxtApp, nuxtApp.provide) - // return null + if (process.dev) { legacyInjectPlugins.push(plugin) } + // @ts-expect-error deliberate invalid second argument + _plugin = (nuxtApp: NuxtApp) => plugin(nuxtApp, nuxtApp.provide) } - if (!isNuxtPlugin(plugin)) { - unwrappedPlugins.push(plugin) - // Allow usage without wrapper but warn - } - return plugin - }).filter(Boolean) + + // Allow usage without wrapper but warn + if (process.dev && !isNuxtPlugin(_plugin)) { unwrappedPlugins.push(_plugin) } + + plugins.push(_plugin) + } + + plugins.sort((a, b) => (a.meta?.order || orderMap.default) - (b.meta?.order || orderMap.default)) if (process.dev && legacyInjectPlugins.length) { console.warn('[warn] [nuxt] You are using a plugin with legacy Nuxt 2 format (context, inject) which is likely to be broken. In the future they will be ignored:', legacyInjectPlugins.map(p => p.name || p).join(',')) @@ -336,12 +363,53 @@ export function normalizePlugins (_plugins: Plugin[]) { console.warn('[warn] [nuxt] You are using a plugin that has not been wrapped in `defineNuxtPlugin`. It is advised to wrap your plugins as in the future this may enable enhancements:', unwrappedPlugins.map(p => p.name || p).join(',')) } - return plugins as Plugin[] + return plugins } -export function defineNuxtPlugin> (plugin: Plugin) { - plugin[NuxtPluginIndicator] = true - return plugin +// -50: pre-all (nuxt) +// -40: custom payload revivers (user) +// -30: payload reviving (nuxt) +// -20: pre (user) <-- pre mapped to this +// -10: default (nuxt) +// 0: default (user) <-- default behavior +// +10: post (nuxt) +// +20: post (user) <-- post mapped to this +// +30: post-all (nuxt) + +const orderMap: Record, number> = { + pre: -20, + default: 0, + post: 20 +} + +export function definePayloadPlugin> (plugin: Plugin | ObjectPluginInput) { + return defineNuxtPlugin(plugin, { order: -40 }) +} + +export function defineNuxtPlugin> (plugin: Plugin | ObjectPluginInput, meta?: PluginMeta): Plugin { + if (typeof plugin === 'function') { return defineNuxtPlugin({ setup: plugin }, meta) } + + const wrapper: Plugin = (nuxtApp) => { + if (plugin.hooks) { + nuxtApp.hooks.addHooks(plugin.hooks) + } + if (plugin.setup) { + return plugin.setup(nuxtApp) + } + } + + wrapper.meta = { + name: meta?.name || plugin.name || plugin.setup?.name, + order: + meta?.order || + plugin.order || + orderMap[plugin.enforce || 'default'] || + orderMap.default + } + + wrapper[NuxtPluginIndicator] = true + + return wrapper } export function isNuxtPlugin (plugin: unknown) { diff --git a/packages/nuxt/src/app/plugins/chunk-reload.client.ts b/packages/nuxt/src/app/plugins/chunk-reload.client.ts index 55a1211e3f..d90677688e 100644 --- a/packages/nuxt/src/app/plugins/chunk-reload.client.ts +++ b/packages/nuxt/src/app/plugins/chunk-reload.client.ts @@ -3,20 +3,23 @@ import { defineNuxtPlugin, useRuntimeConfig } from '#app/nuxt' import { useRouter } from '#app/composables/router' import { reloadNuxtApp } from '#app/composables/chunk' -export default defineNuxtPlugin((nuxtApp) => { - const router = useRouter() - const config = useRuntimeConfig() +export default defineNuxtPlugin({ + name: 'nuxt:chunk-reload', + setup (nuxtApp) { + const router = useRouter() + const config = useRuntimeConfig() - const chunkErrors = new Set() + const chunkErrors = new Set() - router.beforeEach(() => { chunkErrors.clear() }) - nuxtApp.hook('app:chunkError', ({ error }) => { chunkErrors.add(error) }) + router.beforeEach(() => { chunkErrors.clear() }) + nuxtApp.hook('app:chunkError', ({ error }) => { chunkErrors.add(error) }) - router.onError((error, to) => { - if (chunkErrors.has(error)) { - const isHash = 'href' in to && (to.href as string).startsWith('#') - const path = isHash ? config.app.baseURL + (to as any).href : joinURL(config.app.baseURL, to.fullPath) - reloadNuxtApp({ path, persistState: true }) - } - }) + router.onError((error, to) => { + if (chunkErrors.has(error)) { + const isHash = 'href' in to && (to.href as string).startsWith('#') + const path = isHash ? config.app.baseURL + (to as any).href : joinURL(config.app.baseURL, to.fullPath) + reloadNuxtApp({ path, persistState: true }) + } + }) + } }) diff --git a/packages/nuxt/src/app/plugins/cross-origin-prefetch.client.ts b/packages/nuxt/src/app/plugins/cross-origin-prefetch.client.ts index 08e58dd695..191f69133b 100644 --- a/packages/nuxt/src/app/plugins/cross-origin-prefetch.client.ts +++ b/packages/nuxt/src/app/plugins/cross-origin-prefetch.client.ts @@ -3,33 +3,36 @@ import { parseURL } from 'ufo' import { useHead } from '@unhead/vue' import { defineNuxtPlugin } from '#app/nuxt' -export default defineNuxtPlugin((nuxtApp) => { - const externalURLs = ref(new Set()) - function generateRules () { - return { - type: 'speculationrules', - key: 'speculationrules', - innerHTML: JSON.stringify({ - prefetch: [ - { - source: 'list', - urls: [...externalURLs.value], - requires: ['anonymous-client-ip-when-cross-origin'] - } - ] - }) +export default defineNuxtPlugin({ + name: 'nuxt:cross-origin-prefetch', + setup (nuxtApp) { + const externalURLs = ref(new Set()) + function generateRules () { + return { + type: 'speculationrules', + key: 'speculationrules', + innerHTML: JSON.stringify({ + prefetch: [ + { + source: 'list', + urls: [...externalURLs.value], + requires: ['anonymous-client-ip-when-cross-origin'] + } + ] + }) + } } + const head = useHead({ + script: [generateRules()] + }) + nuxtApp.hook('link:prefetch', (url) => { + const { protocol } = parseURL(url) + if (protocol && ['http:', 'https:'].includes(protocol)) { + externalURLs.value.add(url) + head?.patch({ + script: [generateRules()] + }) + } + }) } - const head = useHead({ - script: [generateRules()] - }) - nuxtApp.hook('link:prefetch', (url) => { - const { protocol } = parseURL(url) - if (protocol && ['http:', 'https:'].includes(protocol)) { - externalURLs.value.add(url) - head?.patch({ - script: [generateRules()] - }) - } - }) }) diff --git a/packages/nuxt/src/app/plugins/debug.ts b/packages/nuxt/src/app/plugins/debug.ts index f2909d069e..5479a84d45 100644 --- a/packages/nuxt/src/app/plugins/debug.ts +++ b/packages/nuxt/src/app/plugins/debug.ts @@ -1,6 +1,10 @@ import { createDebugger } from 'hookable' import { defineNuxtPlugin } from '#app/nuxt' -export default defineNuxtPlugin((nuxtApp) => { - createDebugger(nuxtApp.hooks, { tag: 'nuxt-app' }) +export default defineNuxtPlugin({ + name: 'nuxt:debug', + enforce: 'pre', + setup (nuxtApp) { + createDebugger(nuxtApp.hooks, { tag: 'nuxt-app' }) + } }) diff --git a/packages/nuxt/src/app/plugins/payload.client.ts b/packages/nuxt/src/app/plugins/payload.client.ts index 55d6417c31..d080ea944d 100644 --- a/packages/nuxt/src/app/plugins/payload.client.ts +++ b/packages/nuxt/src/app/plugins/payload.client.ts @@ -3,25 +3,28 @@ import { defineNuxtPlugin } from '#app/nuxt' import { isPrerendered, loadPayload } from '#app/composables/payload' import { useRouter } from '#app/composables/router' -export default defineNuxtPlugin((nuxtApp) => { - // Only enable behavior if initial page is prerendered - // TODO: Support hybrid and dev - if (!isPrerendered()) { - return - } - - // Load payload into cache - nuxtApp.hooks.hook('link:prefetch', async (url) => { - if (!parseURL(url).protocol) { - await loadPayload(url) +export default defineNuxtPlugin({ + name: 'nuxt:payload', + setup (nuxtApp) { + // Only enable behavior if initial page is prerendered + // TODO: Support hybrid and dev + if (!isPrerendered()) { + return } - }) - // Load payload after middleware & once final route is resolved - useRouter().beforeResolve(async (to, from) => { - if (to.path === from.path) { return } - const payload = await loadPayload(to.path) - if (!payload) { return } - Object.assign(nuxtApp.static.data, payload.data) - }) + // Load payload into cache + nuxtApp.hooks.hook('link:prefetch', async (url) => { + if (!parseURL(url).protocol) { + await loadPayload(url) + } + }) + + // Load payload after middleware & once final route is resolved + useRouter().beforeResolve(async (to, from) => { + if (to.path === from.path) { return } + const payload = await loadPayload(to.path) + if (!payload) { return } + Object.assign(nuxtApp.static.data, payload.data) + }) + } }) diff --git a/packages/nuxt/src/app/plugins/preload.server.ts b/packages/nuxt/src/app/plugins/preload.server.ts index 9f6b25d62d..ec9501988b 100644 --- a/packages/nuxt/src/app/plugins/preload.server.ts +++ b/packages/nuxt/src/app/plugins/preload.server.ts @@ -1,11 +1,14 @@ import { defineNuxtPlugin } from '#app/nuxt' -export default defineNuxtPlugin((nuxtApp) => { - nuxtApp.vueApp.mixin({ - beforeCreate () { - const { _registeredComponents } = this.$nuxt.ssrContext - const { __moduleIdentifier } = this.$options - _registeredComponents.add(__moduleIdentifier) - } - }) +export default defineNuxtPlugin({ + name: 'nuxt:webpack-preload', + setup (nuxtApp) { + nuxtApp.vueApp.mixin({ + beforeCreate () { + const { _registeredComponents } = this.$nuxt.ssrContext + const { __moduleIdentifier } = this.$options + _registeredComponents.add(__moduleIdentifier) + } + }) + } }) diff --git a/packages/nuxt/src/app/plugins/restore-state.client.ts b/packages/nuxt/src/app/plugins/restore-state.client.ts index 6533650ce9..e6786e8cea 100644 --- a/packages/nuxt/src/app/plugins/restore-state.client.ts +++ b/packages/nuxt/src/app/plugins/restore-state.client.ts @@ -1,13 +1,17 @@ -import { defineNuxtPlugin } from '#app/nuxt' +import { defineNuxtPlugin, useNuxtApp } from '#app/nuxt' -export default defineNuxtPlugin((nuxtApp) => { - nuxtApp.hook('app:mounted', () => { - try { - const state = sessionStorage.getItem('nuxt:reload:state') - if (state) { - sessionStorage.removeItem('nuxt:reload:state') - Object.assign(nuxtApp.payload.state, JSON.parse(state)?.state) - } - } catch {} - }) +export default defineNuxtPlugin({ + name: 'nuxt:restore-state', + hooks: { + 'app:mounted' () { + const nuxtApp = useNuxtApp() + try { + const state = sessionStorage.getItem('nuxt:reload:state') + if (state) { + sessionStorage.removeItem('nuxt:reload:state') + Object.assign(nuxtApp.payload.state, JSON.parse(state)?.state) + } + } catch {} + } + } }) diff --git a/packages/nuxt/src/app/plugins/revive-payload.client.ts b/packages/nuxt/src/app/plugins/revive-payload.client.ts index e7fa0eaa4c..ae58308cc2 100644 --- a/packages/nuxt/src/app/plugins/revive-payload.client.ts +++ b/packages/nuxt/src/app/plugins/revive-payload.client.ts @@ -13,11 +13,15 @@ const revivers = { Reactive: (data: any) => reactive(data) } -export default defineNuxtPlugin(async (nuxtApp) => { - for (const reviver in revivers) { - definePayloadReviver(reviver, revivers[reviver as keyof typeof revivers]) +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]) + } + Object.assign(nuxtApp.payload, await callWithNuxt(nuxtApp, getNuxtClientPayload, [])) + // For backwards compatibility - TODO: remove later + window.__NUXT__ = nuxtApp.payload } - Object.assign(nuxtApp.payload, await callWithNuxt(nuxtApp, getNuxtClientPayload, [])) - // For backwards compatibility - TODO: remove later - window.__NUXT__ = nuxtApp.payload }) diff --git a/packages/nuxt/src/app/plugins/revive-payload.server.ts b/packages/nuxt/src/app/plugins/revive-payload.server.ts index d04c4ac4b0..b8e54569f6 100644 --- a/packages/nuxt/src/app/plugins/revive-payload.server.ts +++ b/packages/nuxt/src/app/plugins/revive-payload.server.ts @@ -14,8 +14,11 @@ const reducers = { Reactive: (data: any) => isReactive(data) && toRaw(data) } -export default defineNuxtPlugin(() => { - for (const reducer in reducers) { - definePayloadReducer(reducer, reducers[reducer as keyof typeof reducers]) +export default defineNuxtPlugin({ + name: 'nuxt:revive-payload:server', + setup () { + for (const reducer in reducers) { + definePayloadReducer(reducer, reducers[reducer as keyof typeof reducers]) + } } }) diff --git a/packages/nuxt/src/app/plugins/router.ts b/packages/nuxt/src/app/plugins/router.ts index 8b61b0f245..0863c5d5d7 100644 --- a/packages/nuxt/src/app/plugins/router.ts +++ b/packages/nuxt/src/app/plugins/router.ts @@ -96,176 +96,180 @@ interface Router { removeRoute: (name: string) => void } -export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => { - const initialURL = process.client - ? withoutBase(window.location.pathname, useRuntimeConfig().app.baseURL) + window.location.search + window.location.hash - : nuxtApp.ssrContext!.url +export default defineNuxtPlugin<{ route: Route, router: Router }>({ + name: 'nuxt:router', + enforce: 'pre', + setup (nuxtApp) { + const initialURL = process.client + ? withoutBase(window.location.pathname, useRuntimeConfig().app.baseURL) + window.location.search + window.location.hash + : nuxtApp.ssrContext!.url - const routes: Route[] = [] + const routes: Route[] = [] - const hooks: { [key in keyof RouterHooks]: RouterHooks[key][] } = { - 'navigate:before': [], - 'resolve:before': [], - 'navigate:after': [], - error: [] - } + const hooks: { [key in keyof RouterHooks]: RouterHooks[key][] } = { + 'navigate:before': [], + 'resolve:before': [], + 'navigate:after': [], + error: [] + } - const registerHook = (hook: T, guard: RouterHooks[T]) => { - hooks[hook].push(guard) - return () => hooks[hook].splice(hooks[hook].indexOf(guard), 1) - } - const baseURL = useRuntimeConfig().app.baseURL + const registerHook = (hook: T, guard: RouterHooks[T]) => { + hooks[hook].push(guard) + return () => hooks[hook].splice(hooks[hook].indexOf(guard), 1) + } + const baseURL = useRuntimeConfig().app.baseURL - const route: Route = reactive(getRouteFromPath(initialURL)) - async function handleNavigation (url: string | Partial, replace?: boolean): Promise { - try { - // Resolve route - const to = getRouteFromPath(url) + const route: Route = reactive(getRouteFromPath(initialURL)) + async function handleNavigation (url: string | Partial, replace?: boolean): Promise { + try { + // Resolve route + const to = getRouteFromPath(url) - // Run beforeEach hooks - for (const middleware of hooks['navigate:before']) { - const result = await middleware(to, route) - // Cancel navigation - if (result === false || result instanceof Error) { return } - // Redirect - if (result) { return handleNavigation(result, true) } - } - - for (const handler of hooks['resolve:before']) { - await handler(to, route) - } - // Perform navigation - Object.assign(route, to) - if (process.client) { - window.history[replace ? 'replaceState' : 'pushState']({}, '', joinURL(baseURL, to.fullPath)) - if (!nuxtApp.isHydrating) { - // Clear any existing errors - await callWithNuxt(nuxtApp, clearError) + // Run beforeEach hooks + for (const middleware of hooks['navigate:before']) { + const result = await middleware(to, route) + // Cancel navigation + if (result === false || result instanceof Error) { return } + // Redirect + if (result) { return handleNavigation(result, true) } } - } - // Run afterEach hooks - for (const middleware of hooks['navigate:after']) { - await middleware(to, route) - } - } catch (err) { - if (process.dev && !hooks.error.length) { - console.warn('No error handlers registered to handle middleware errors. You can register an error handler with `router.onError()`', err) - } - for (const handler of hooks.error) { - await handler(err) - } - } - } - const router: Router = { - currentRoute: route, - isReady: () => Promise.resolve(), - // These options provide a similar API to vue-router but have no effect - options: {}, - install: () => Promise.resolve(), - // Navigation - push: (url: string) => handleNavigation(url, false), - replace: (url: string) => handleNavigation(url, true), - back: () => window.history.go(-1), - go: (delta: number) => window.history.go(delta), - forward: () => window.history.go(1), - // Guards - beforeResolve: (guard: RouterHooks['resolve:before']) => registerHook('resolve:before', guard), - beforeEach: (guard: RouterHooks['navigate:before']) => registerHook('navigate:before', guard), - afterEach: (guard: RouterHooks['navigate:after']) => registerHook('navigate:after', guard), - onError: (handler: RouterHooks['error']) => registerHook('error', handler), - // Routes - resolve: getRouteFromPath, - addRoute: (parentName: string, route: Route) => { routes.push(route) }, - getRoutes: () => routes, - hasRoute: (name: string) => routes.some(route => route.name === name), - removeRoute: (name: string) => { - const index = routes.findIndex(route => route.name === name) - if (index !== -1) { - routes.splice(index, 1) - } - } - } - - nuxtApp.vueApp.component('RouterLink', { - functional: true, - props: { - to: String, - custom: Boolean, - replace: Boolean, - // Not implemented - activeClass: String, - exactActiveClass: String, - ariaCurrentValue: String - }, - setup: (props, { slots }) => { - const navigate = () => handleNavigation(props.to, props.replace) - return () => { - const route = router.resolve(props.to) - return props.custom - ? slots.default?.({ href: props.to, navigate, route }) - : h('a', { href: props.to, onClick: (e: MouseEvent) => { e.preventDefault(); return navigate() } }, slots) - } - } - }) - - if (process.client) { - window.addEventListener('popstate', (event) => { - const location = (event.target as Window).location - router.replace(location.href.replace(location.origin, '')) - }) - } - - nuxtApp._route = route - - // Handle middleware - nuxtApp._middleware = nuxtApp._middleware || { - global: [], - named: {} - } - - const initialLayout = useState('_layout') - nuxtApp.hooks.hookOnce('app:created', async () => { - router.beforeEach(async (to, from) => { - to.meta = reactive(to.meta || {}) - if (nuxtApp.isHydrating && initialLayout.value && !isReadonly(to.meta.layout)) { - to.meta.layout = initialLayout.value - } - nuxtApp._processingMiddleware = true - - const middlewareEntries = new Set([...globalMiddleware, ...nuxtApp._middleware.global]) - - for (const middleware of middlewareEntries) { - const result = await callWithNuxt(nuxtApp, middleware, [to, from]) - if (process.server) { - if (result === false || result instanceof Error) { - const error = result || createError({ - statusCode: 404, - statusMessage: `Page Not Found: ${initialURL}` - }) - return callWithNuxt(nuxtApp, showError, [error]) + for (const handler of hooks['resolve:before']) { + await handler(to, route) + } + // Perform navigation + Object.assign(route, to) + if (process.client) { + window.history[replace ? 'replaceState' : 'pushState']({}, '', joinURL(baseURL, to.fullPath)) + if (!nuxtApp.isHydrating) { + // Clear any existing errors + await callWithNuxt(nuxtApp, clearError) } } - if (result || result === false) { return result } + // Run afterEach hooks + for (const middleware of hooks['navigate:after']) { + await middleware(to, route) + } + } catch (err) { + if (process.dev && !hooks.error.length) { + console.warn('No error handlers registered to handle middleware errors. You can register an error handler with `router.onError()`', err) + } + for (const handler of hooks.error) { + await handler(err) + } + } + } + + const router: Router = { + currentRoute: route, + isReady: () => Promise.resolve(), + // These options provide a similar API to vue-router but have no effect + options: {}, + install: () => Promise.resolve(), + // Navigation + push: (url: string) => handleNavigation(url, false), + replace: (url: string) => handleNavigation(url, true), + back: () => window.history.go(-1), + go: (delta: number) => window.history.go(delta), + forward: () => window.history.go(1), + // Guards + beforeResolve: (guard: RouterHooks['resolve:before']) => registerHook('resolve:before', guard), + beforeEach: (guard: RouterHooks['navigate:before']) => registerHook('navigate:before', guard), + afterEach: (guard: RouterHooks['navigate:after']) => registerHook('navigate:after', guard), + onError: (handler: RouterHooks['error']) => registerHook('error', handler), + // Routes + resolve: getRouteFromPath, + addRoute: (parentName: string, route: Route) => { routes.push(route) }, + getRoutes: () => routes, + hasRoute: (name: string) => routes.some(route => route.name === name), + removeRoute: (name: string) => { + const index = routes.findIndex(route => route.name === name) + if (index !== -1) { + routes.splice(index, 1) + } + } + } + + nuxtApp.vueApp.component('RouterLink', { + functional: true, + props: { + to: String, + custom: Boolean, + replace: Boolean, + // Not implemented + activeClass: String, + exactActiveClass: String, + ariaCurrentValue: String + }, + setup: (props, { slots }) => { + const navigate = () => handleNavigation(props.to, props.replace) + return () => { + const route = router.resolve(props.to) + return props.custom + ? slots.default?.({ href: props.to, navigate, route }) + : h('a', { href: props.to, onClick: (e: MouseEvent) => { e.preventDefault(); return navigate() } }, slots) + } } }) - router.afterEach(() => { - delete nuxtApp._processingMiddleware + if (process.client) { + window.addEventListener('popstate', (event) => { + const location = (event.target as Window).location + router.replace(location.href.replace(location.origin, '')) + }) + } + + nuxtApp._route = route + + // Handle middleware + nuxtApp._middleware = nuxtApp._middleware || { + global: [], + named: {} + } + + const initialLayout = useState('_layout') + nuxtApp.hooks.hookOnce('app:created', async () => { + router.beforeEach(async (to, from) => { + to.meta = reactive(to.meta || {}) + if (nuxtApp.isHydrating && initialLayout.value && !isReadonly(to.meta.layout)) { + to.meta.layout = initialLayout.value + } + nuxtApp._processingMiddleware = true + + const middlewareEntries = new Set([...globalMiddleware, ...nuxtApp._middleware.global]) + + for (const middleware of middlewareEntries) { + const result = await callWithNuxt(nuxtApp, middleware, [to, from]) + if (process.server) { + if (result === false || result instanceof Error) { + const error = result || createError({ + statusCode: 404, + statusMessage: `Page Not Found: ${initialURL}` + }) + return callWithNuxt(nuxtApp, showError, [error]) + } + } + if (result || result === false) { return result } + } + }) + + router.afterEach(() => { + delete nuxtApp._processingMiddleware + }) + + await router.replace(initialURL) + if (!isEqual(route.fullPath, initialURL)) { + const event = await callWithNuxt(nuxtApp, useRequestEvent) + const options = { redirectCode: event.node.res.statusCode !== 200 ? event.node.res.statusCode || 302 : 302 } + await callWithNuxt(nuxtApp, navigateTo, [route.fullPath, options]) + } }) - await router.replace(initialURL) - if (!isEqual(route.fullPath, initialURL)) { - const event = await callWithNuxt(nuxtApp, useRequestEvent) - const options = { redirectCode: event.node.res.statusCode !== 200 ? event.node.res.statusCode || 302 : 302 } - await callWithNuxt(nuxtApp, navigateTo, [route.fullPath, options]) - } - }) - - return { - provide: { - route, - router + return { + provide: { + route, + router + } } } }) diff --git a/packages/nuxt/src/components/templates.ts b/packages/nuxt/src/components/templates.ts index 3d1705cf72..19dd42eb24 100644 --- a/packages/nuxt/src/components/templates.ts +++ b/packages/nuxt/src/components/templates.ts @@ -40,10 +40,13 @@ const components = ${genObjectFromRawEntries(globalComponents.map((c) => { return [c.pascalName, `defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`] }))} -export default defineNuxtPlugin(nuxtApp => { - for (const name in components) { - nuxtApp.vueApp.component(name, components[name]) - nuxtApp.vueApp.component('Lazy' + name, components[name]) +export default defineNuxtPlugin({ + name: 'nuxt:global-components', + setup (nuxtApp) { + for (const name in components) { + nuxtApp.vueApp.component(name, components[name]) + nuxtApp.vueApp.component('Lazy' + name, components[name]) + } } }) ` diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index 6659fab06f..638fd6a29b 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -292,9 +292,9 @@ async function initNuxt (nuxt: Nuxt) { // Add experimental support for custom types in JSON payload if (nuxt.options.experimental.renderJsonPayloads) { - nuxt.hook('modules:done', () => { - nuxt.options.plugins.unshift(resolve(nuxt.options.appDir, 'plugins/revive-payload.client')) - nuxt.options.plugins.unshift(resolve(nuxt.options.appDir, 'plugins/revive-payload.server')) + nuxt.hooks.hook('modules:done', () => { + addPlugin(resolve(nuxt.options.appDir, 'plugins/revive-payload.client')) + addPlugin(resolve(nuxt.options.appDir, 'plugins/revive-payload.server')) }) } diff --git a/packages/nuxt/src/head/runtime/plugins/unhead.ts b/packages/nuxt/src/head/runtime/plugins/unhead.ts index 527be0586f..3fd5e0c330 100644 --- a/packages/nuxt/src/head/runtime/plugins/unhead.ts +++ b/packages/nuxt/src/head/runtime/plugins/unhead.ts @@ -4,37 +4,40 @@ import { defineNuxtPlugin } from '#app/nuxt' // @ts-expect-error untyped import { appHead } from '#build/nuxt.config.mjs' -export default defineNuxtPlugin((nuxtApp) => { - const createHead = process.server ? createServerHead : createClientHead - const head = createHead() - head.push(appHead) +export default defineNuxtPlugin({ + name: 'nuxt:head', + setup (nuxtApp) { + const createHead = process.server ? createServerHead : createClientHead + const head = createHead() + head.push(appHead) - nuxtApp.vueApp.use(head) + nuxtApp.vueApp.use(head) - if (process.client) { - // pause dom updates until page is ready and between page transitions - let pauseDOMUpdates = true - const unpauseDom = () => { - pauseDOMUpdates = false - // trigger the debounced DOM update - head.hooks.callHook('entries:updated', head) + if (process.client) { + // pause dom updates until page is ready and between page transitions + let pauseDOMUpdates = true + const unpauseDom = () => { + pauseDOMUpdates = false + // trigger the debounced DOM update + head.hooks.callHook('entries:updated', head) + } + head.hooks.hook('dom:beforeRender', (context) => { context.shouldRender = !pauseDOMUpdates }) + nuxtApp.hooks.hook('page:start', () => { pauseDOMUpdates = true }) + // wait for new page before unpausing dom updates (triggered after suspense resolved) + nuxtApp.hooks.hook('page:finish', unpauseDom) + // unpause the DOM once the mount suspense is resolved + nuxtApp.hooks.hook('app:suspense:resolve', unpauseDom) } - head.hooks.hook('dom:beforeRender', (context) => { context.shouldRender = !pauseDOMUpdates }) - nuxtApp.hooks.hook('page:start', () => { pauseDOMUpdates = true }) - // wait for new page before unpausing dom updates (triggered after suspense resolved) - nuxtApp.hooks.hook('page:finish', unpauseDom) - // unpause the DOM once the mount suspense is resolved - nuxtApp.hooks.hook('app:suspense:resolve', unpauseDom) - } - if (process.server) { - nuxtApp.ssrContext!.renderMeta = async () => { - const meta = await renderSSRHead(head) - return { - ...meta, - bodyScriptsPrepend: meta.bodyTagsOpen, - // resolves naming difference with NuxtMeta and Unhead - bodyScripts: meta.bodyTags + if (process.server) { + nuxtApp.ssrContext!.renderMeta = async () => { + const meta = await renderSSRHead(head) + return { + ...meta, + bodyScriptsPrepend: meta.bodyTagsOpen, + // resolves naming difference with NuxtMeta and Unhead + bodyScripts: meta.bodyTags + } } } } diff --git a/packages/nuxt/src/head/runtime/plugins/vueuse-head-polyfill.ts b/packages/nuxt/src/head/runtime/plugins/vueuse-head-polyfill.ts index 57d1a15bc4..d3dfb3be67 100644 --- a/packages/nuxt/src/head/runtime/plugins/vueuse-head-polyfill.ts +++ b/packages/nuxt/src/head/runtime/plugins/vueuse-head-polyfill.ts @@ -2,7 +2,10 @@ import { polyfillAsVueUseHead } from '@unhead/vue/polyfill' import { defineNuxtPlugin } from '#app/nuxt' -export default defineNuxtPlugin((nuxtApp) => { - // avoid breaking ecosystem dependencies using low-level @vueuse/head APIs - polyfillAsVueUseHead(nuxtApp.vueApp._context.provides.usehead) +export default defineNuxtPlugin({ + name: 'nuxt:vueuse-head-polyfill', + setup (nuxtApp) { + // avoid breaking ecosystem dependencies using low-level @vueuse/head APIs + polyfillAsVueUseHead(nuxtApp.vueApp._context.provides.usehead) + } }) diff --git a/packages/nuxt/src/imports/presets.ts b/packages/nuxt/src/imports/presets.ts index c8fb0c7368..89518467eb 100644 --- a/packages/nuxt/src/imports/presets.ts +++ b/packages/nuxt/src/imports/presets.ts @@ -23,6 +23,7 @@ const appPreset = defineUnimportPreset({ 'defineNuxtComponent', 'useNuxtApp', 'defineNuxtPlugin', + 'definePayloadPlugin', 'reloadNuxtApp', 'useRuntimeConfig', 'useState', diff --git a/packages/nuxt/src/pages/runtime/plugins/prefetch.client.ts b/packages/nuxt/src/pages/runtime/plugins/prefetch.client.ts index b258812103..d946f14ea8 100644 --- a/packages/nuxt/src/pages/runtime/plugins/prefetch.client.ts +++ b/packages/nuxt/src/pages/runtime/plugins/prefetch.client.ts @@ -1,41 +1,43 @@ import { hasProtocol } from 'ufo' -import { defineNuxtPlugin, useNuxtApp } from '#app/nuxt' +import { defineNuxtPlugin } from '#app/nuxt' import { useRouter } from '#app/composables/router' // @ts-ignore import layouts from '#build/layouts' // @ts-ignore import { namedMiddleware } from '#build/middleware' -export default defineNuxtPlugin(() => { - const nuxtApp = useNuxtApp() - const router = useRouter() +export default defineNuxtPlugin({ + name: 'nuxt:prefetch', + setup (nuxtApp) { + const router = useRouter() + + // Force layout prefetch on route changes + nuxtApp.hooks.hook('app:mounted', () => { + router.beforeEach(async (to) => { + const layout = to?.meta?.layout + if (layout && typeof layouts[layout] === 'function') { + await layouts[layout]() + } + }) + }) + // Prefetch layouts & middleware + nuxtApp.hooks.hook('link:prefetch', (url) => { + if (hasProtocol(url)) { return } + const route = router.resolve(url) + if (!route) { return } + const layout = route?.meta?.layout + let middleware = Array.isArray(route?.meta?.middleware) ? route?.meta?.middleware : [route?.meta?.middleware] + middleware = middleware.filter(m => typeof m === 'string') + + for (const name of middleware) { + if (typeof namedMiddleware[name] === 'function') { + namedMiddleware[name]() + } + } - // Force layout prefetch on route changes - nuxtApp.hooks.hook('app:mounted', () => { - router.beforeEach(async (to) => { - const layout = to?.meta?.layout if (layout && typeof layouts[layout] === 'function') { - await layouts[layout]() + layouts[layout]() } }) - }) - // Prefetch layouts & middleware - nuxtApp.hooks.hook('link:prefetch', (url) => { - if (hasProtocol(url)) { return } - const route = router.resolve(url) - if (!route) { return } - const layout = route?.meta?.layout - let middleware = Array.isArray(route?.meta?.middleware) ? route?.meta?.middleware : [route?.meta?.middleware] - middleware = middleware.filter(m => typeof m === 'string') - - for (const name of middleware) { - if (typeof namedMiddleware[name] === 'function') { - namedMiddleware[name]() - } - } - - if (layout && typeof layouts[layout] === 'function') { - layouts[layout]() - } - }) + } }) diff --git a/packages/nuxt/src/pages/runtime/plugins/router.ts b/packages/nuxt/src/pages/runtime/plugins/router.ts index c76405d4d2..5e48553154 100644 --- a/packages/nuxt/src/pages/runtime/plugins/router.ts +++ b/packages/nuxt/src/pages/runtime/plugins/router.ts @@ -45,157 +45,161 @@ function createCurrentLocation ( return path + search + hash } -export default defineNuxtPlugin(async (nuxtApp) => { - let routerBase = useRuntimeConfig().app.baseURL - if (routerOptions.hashMode && !routerBase.includes('#')) { - // allow the user to provide a `#` in the middle: `/base/#/app` - routerBase += '#' - } - - const history = routerOptions.history?.(routerBase) ?? (process.client - ? (routerOptions.hashMode ? createWebHashHistory(routerBase) : createWebHistory(routerBase)) - : createMemoryHistory(routerBase) - ) - - const routes = routerOptions.routes?.(_routes) ?? _routes - - const initialURL = process.server ? nuxtApp.ssrContext!.url : createCurrentLocation(routerBase, window.location) - const router = createRouter({ - ...routerOptions, - history, - routes - }) - nuxtApp.vueApp.use(router) - - const previousRoute = shallowRef(router.currentRoute.value) - router.afterEach((_to, from) => { - previousRoute.value = from - }) - - Object.defineProperty(nuxtApp.vueApp.config.globalProperties, 'previousRoute', { - get: () => previousRoute.value - }) - - // Allows suspending the route object until page navigation completes - const _route = shallowRef(router.resolve(initialURL) as RouteLocation) - const syncCurrentRoute = () => { _route.value = router.currentRoute.value } - nuxtApp.hook('page:finish', syncCurrentRoute) - router.afterEach((to, from) => { - // We won't trigger suspense if the component is reused between routes - // so we need to update the route manually - if (to.matched[0]?.components?.default === from.matched[0]?.components?.default) { - syncCurrentRoute() - } - }) - - // https://github.com/vuejs/router/blob/main/packages/router/src/router.ts#L1225-L1233 - const route = {} as RouteLocation - for (const key in _route.value) { - (route as any)[key] = computed(() => _route.value[key as keyof RouteLocation]) - } - - nuxtApp._route = reactive(route) - - nuxtApp._middleware = nuxtApp._middleware || { - global: [], - named: {} - } - - const error = useError() - - try { - if (process.server) { - await router.push(initialURL) +export default defineNuxtPlugin({ + name: 'nuxt:router', + enforce: 'pre', + async setup (nuxtApp) { + let routerBase = useRuntimeConfig().app.baseURL + if (routerOptions.hashMode && !routerBase.includes('#')) { + // allow the user to provide a `#` in the middle: `/base/#/app` + routerBase += '#' } - await router.isReady() - } catch (error: any) { - // We'll catch 404s here - await callWithNuxt(nuxtApp, showError, [error]) - } + const history = routerOptions.history?.(routerBase) ?? (process.client + ? (routerOptions.hashMode ? createWebHashHistory(routerBase) : createWebHistory(routerBase)) + : createMemoryHistory(routerBase) + ) - const initialLayout = useState('_layout') - router.beforeEach(async (to, from) => { - to.meta = reactive(to.meta) - if (nuxtApp.isHydrating && initialLayout.value && !isReadonly(to.meta.layout)) { - to.meta.layout = initialLayout.value as Exclude - } - nuxtApp._processingMiddleware = true + const routes = routerOptions.routes?.(_routes) ?? _routes - type MiddlewareDef = string | RouteMiddleware - const middlewareEntries = new Set([...globalMiddleware, ...nuxtApp._middleware.global]) - for (const component of to.matched) { - const componentMiddleware = component.meta.middleware as MiddlewareDef | MiddlewareDef[] - if (!componentMiddleware) { continue } - if (Array.isArray(componentMiddleware)) { - for (const entry of componentMiddleware) { - middlewareEntries.add(entry) - } - } else { - middlewareEntries.add(componentMiddleware) + const initialURL = process.server ? nuxtApp.ssrContext!.url : createCurrentLocation(routerBase, window.location) + const router = createRouter({ + ...routerOptions, + history, + routes + }) + nuxtApp.vueApp.use(router) + + const previousRoute = shallowRef(router.currentRoute.value) + router.afterEach((_to, from) => { + previousRoute.value = from + }) + + Object.defineProperty(nuxtApp.vueApp.config.globalProperties, 'previousRoute', { + get: () => previousRoute.value + }) + + // Allows suspending the route object until page navigation completes + const _route = shallowRef(router.resolve(initialURL) as RouteLocation) + const syncCurrentRoute = () => { _route.value = router.currentRoute.value } + nuxtApp.hook('page:finish', syncCurrentRoute) + router.afterEach((to, from) => { + // We won't trigger suspense if the component is reused between routes + // so we need to update the route manually + if (to.matched[0]?.components?.default === from.matched[0]?.components?.default) { + syncCurrentRoute() } + }) + + // https://github.com/vuejs/router/blob/main/packages/router/src/router.ts#L1225-L1233 + const route = {} as RouteLocation + for (const key in _route.value) { + (route as any)[key] = computed(() => _route.value[key as keyof RouteLocation]) } - for (const entry of middlewareEntries) { - const middleware = typeof entry === 'string' ? nuxtApp._middleware.named[entry] || await namedMiddleware[entry]?.().then((r: any) => r.default || r) : entry + nuxtApp._route = reactive(route) - if (!middleware) { - if (process.dev) { - throw new Error(`Unknown route middleware: '${entry}'. Valid middleware: ${Object.keys(namedMiddleware).map(mw => `'${mw}'`).join(', ')}.`) - } - throw new Error(`Unknown route middleware: '${entry}'.`) - } - - const result = await callWithNuxt(nuxtApp, middleware, [to, from]) - if (process.server || (!nuxtApp.payload.serverRendered && nuxtApp.isHydrating)) { - if (result === false || result instanceof Error) { - const error = result || createError({ - statusCode: 404, - statusMessage: `Page Not Found: ${initialURL}` - }) - await callWithNuxt(nuxtApp, showError, [error]) - return false - } - } - if (result || result === false) { return result } + nuxtApp._middleware = nuxtApp._middleware || { + global: [], + named: {} } - }) - router.afterEach(async (to) => { - delete nuxtApp._processingMiddleware + const error = useError() - if (process.client && !nuxtApp.isHydrating && error.value) { - // Clear any existing errors - await callWithNuxt(nuxtApp, clearError) - } - if (to.matched.length === 0) { - await callWithNuxt(nuxtApp, showError, [createError({ - statusCode: 404, - fatal: false, - statusMessage: `Page not found: ${to.fullPath}` - })]) - } else if (process.server) { - const currentURL = to.fullPath || '/' - if (!isEqual(currentURL, initialURL, { trailingSlash: true })) { - const event = await callWithNuxt(nuxtApp, useRequestEvent) - const options = { redirectCode: event.node.res.statusCode !== 200 ? event.node.res.statusCode || 302 : 302 } - await callWithNuxt(nuxtApp, navigateTo, [currentURL, options]) - } - } - }) - - nuxtApp.hooks.hookOnce('app:created', async () => { try { - await router.replace({ - ...router.resolve(initialURL), - name: undefined, // #4920, #$4982 - force: true - }) + if (process.server) { + await router.push(initialURL) + } + + await router.isReady() } catch (error: any) { - // We'll catch middleware errors or deliberate exceptions here + // We'll catch 404s here await callWithNuxt(nuxtApp, showError, [error]) } - }) - return { provide: { router } } + const initialLayout = useState('_layout') + router.beforeEach(async (to, from) => { + to.meta = reactive(to.meta) + if (nuxtApp.isHydrating && initialLayout.value && !isReadonly(to.meta.layout)) { + to.meta.layout = initialLayout.value as Exclude + } + nuxtApp._processingMiddleware = true + + type MiddlewareDef = string | RouteMiddleware + const middlewareEntries = new Set([...globalMiddleware, ...nuxtApp._middleware.global]) + for (const component of to.matched) { + const componentMiddleware = component.meta.middleware as MiddlewareDef | MiddlewareDef[] + if (!componentMiddleware) { continue } + if (Array.isArray(componentMiddleware)) { + for (const entry of componentMiddleware) { + middlewareEntries.add(entry) + } + } else { + middlewareEntries.add(componentMiddleware) + } + } + + for (const entry of middlewareEntries) { + const middleware = typeof entry === 'string' ? nuxtApp._middleware.named[entry] || await namedMiddleware[entry]?.().then((r: any) => r.default || r) : entry + + if (!middleware) { + if (process.dev) { + throw new Error(`Unknown route middleware: '${entry}'. Valid middleware: ${Object.keys(namedMiddleware).map(mw => `'${mw}'`).join(', ')}.`) + } + throw new Error(`Unknown route middleware: '${entry}'.`) + } + + const result = await callWithNuxt(nuxtApp, middleware, [to, from]) + if (process.server || (!nuxtApp.payload.serverRendered && nuxtApp.isHydrating)) { + if (result === false || result instanceof Error) { + const error = result || createError({ + statusCode: 404, + statusMessage: `Page Not Found: ${initialURL}` + }) + await callWithNuxt(nuxtApp, showError, [error]) + return false + } + } + if (result || result === false) { return result } + } + }) + + router.afterEach(async (to) => { + delete nuxtApp._processingMiddleware + + if (process.client && !nuxtApp.isHydrating && error.value) { + // Clear any existing errors + await callWithNuxt(nuxtApp, clearError) + } + if (to.matched.length === 0) { + await callWithNuxt(nuxtApp, showError, [createError({ + statusCode: 404, + fatal: false, + statusMessage: `Page not found: ${to.fullPath}` + })]) + } else if (process.server) { + const currentURL = to.fullPath || '/' + if (!isEqual(currentURL, initialURL, { trailingSlash: true })) { + const event = await callWithNuxt(nuxtApp, useRequestEvent) + const options = { redirectCode: event.node.res.statusCode !== 200 ? event.node.res.statusCode || 302 : 302 } + await callWithNuxt(nuxtApp, navigateTo, [currentURL, options]) + } + } + }) + + nuxtApp.hooks.hookOnce('app:created', async () => { + try { + await router.replace({ + ...router.resolve(initialURL), + name: undefined, // #4920, #$4982 + force: true + }) + } catch (error: any) { + // We'll catch middleware errors or deliberate exceptions here + await callWithNuxt(nuxtApp, showError, [error]) + } + }) + + return { provide: { router } } + } }) as Plugin<{ router: Router }> diff --git a/packages/schema/src/config/build.ts b/packages/schema/src/config/build.ts index 076146b50a..c905fae4ce 100644 --- a/packages/schema/src/config/build.ts +++ b/packages/schema/src/config/build.ts @@ -195,6 +195,7 @@ export default defineUntypedSchema({ asyncFunctions: ['defineNuxtPlugin', 'defineNuxtRouteMiddleware'], objectDefinitions: { defineNuxtComponent: ['asyncData', 'setup'], + defineNuxtPlugin: ['setup'], definePageMeta: ['middleware', 'validate'] } } diff --git a/test/bundle.test.ts b/test/bundle.test.ts index a6603d6629..cf9cd47f5e 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -26,7 +26,7 @@ describe.skipIf(isWindows || process.env.ECOSYSTEM_CI)('minimal nuxt application it('default client bundle size', async () => { stats.client = await analyzeSizes('**/*.js', publicDir) - expect(roundToKilobytes(stats.client.totalBytes)).toMatchInlineSnapshot('"104k"') + expect(roundToKilobytes(stats.client.totalBytes)).toMatchInlineSnapshot('"105k"') expect(stats.client.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(` [ "_nuxt/_plugin-vue_export-helper.js", @@ -40,7 +40,7 @@ describe.skipIf(isWindows || process.env.ECOSYSTEM_CI)('minimal nuxt application it('default server bundle size', async () => { stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir) - expect(roundToKilobytes(stats.server.totalBytes)).toMatchInlineSnapshot('"91k"') + expect(roundToKilobytes(stats.server.totalBytes)).toMatchInlineSnapshot('"92k"') const modules = await analyzeSizes('node_modules/**/*', serverDir) expect(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2650k"') diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts index a1e40b709d..e625264c02 100644 --- a/test/fixtures/basic/nuxt.config.ts +++ b/test/fixtures/basic/nuxt.config.ts @@ -108,12 +108,6 @@ export default defineNuxtConfig({ addVitePlugin(plugin.vite()) addWebpackPlugin(plugin.webpack()) }, - function (_options, nuxt) { - // TODO: support directly via object syntax plugins: https://github.com/nuxt/nuxt/issues/14628 - nuxt.hook('modules:done', () => { - nuxt.options.plugins.unshift('~/plugins/custom-type-registration') - }) - }, function (_options, nuxt) { const routesToDuplicate = ['/async-parent', '/fixed-keyed-child-parent', '/keyed-child-parent', '/with-layout', '/with-layout2'] const stripLayout = (page: NuxtPage): NuxtPage => ({ diff --git a/test/fixtures/basic/plugins/custom-type-registration.ts b/test/fixtures/basic/plugins/custom-type-registration.ts index b7ef9b110f..ec37ab6ce9 100644 --- a/test/fixtures/basic/plugins/custom-type-registration.ts +++ b/test/fixtures/basic/plugins/custom-type-registration.ts @@ -1,4 +1,4 @@ -export default defineNuxtPlugin((nuxtApp) => { +export default definePayloadPlugin((nuxtApp) => { definePayloadReducer('BlinkingText', data => data === '' && '_') definePayloadReviver('BlinkingText', () => '') if (process.server) {