mirror of
https://github.com/nuxt/nuxt.git
synced 2024-12-01 18:07:22 +00:00
feat(nuxt): support object-syntax plugins (#20003)
This commit is contained in:
parent
f951a15232
commit
4285092879
@ -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
|
## Plugin Registration Order
|
||||||
|
|
||||||
You can control the order in which plugins are registered by prefixing a number to the file names.
|
You can control the order in which plugins are registered by prefixing a number to the file names.
|
||||||
|
@ -116,19 +116,19 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|||||||
::alert
|
::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`.
|
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
|
```ts [plugins/custom-payload.ts]
|
||||||
export default defineNuxtConfig({
|
/**
|
||||||
modules: [
|
* This kind of plugin runs very early in the Nuxt lifecycle, before we revive the payload.
|
||||||
function (_options, nuxt) {
|
* You will not have access to the router or other Nuxt-injected properties.
|
||||||
// TODO: support directly via object syntax plugins: https://github.com/nuxt/nuxt/issues/14628
|
*/
|
||||||
nuxt.hook('modules:done', () => {
|
export default definePayloadPlugin((nuxtApp) => {
|
||||||
nuxt.options.plugins.unshift('~/plugins/custom-type-plugin')
|
definePayloadReducer('BlinkingText', data => data === '<blink>' && '_')
|
||||||
})
|
definePayloadReviver('BlinkingText', () => '<blink>')
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
```
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
### `isHydrating`
|
### `isHydrating`
|
||||||
|
@ -139,9 +139,31 @@ interface _NuxtApp {
|
|||||||
export interface NuxtApp extends _NuxtApp {}
|
export interface NuxtApp extends _NuxtApp {}
|
||||||
|
|
||||||
export const NuxtPluginIndicator = '__nuxt_plugin'
|
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<Injections extends Record<string, unknown> = Record<string, unknown>> {
|
export interface Plugin<Injections extends Record<string, unknown> = Record<string, unknown>> {
|
||||||
(nuxt: _NuxtApp): Promise<void> | Promise<{ provide?: Injections }> | void | { provide?: Injections }
|
(nuxt: _NuxtApp): Promise<void> | Promise<{ provide?: Injections }> | void | { provide?: Injections }
|
||||||
[NuxtPluginIndicator]?: true
|
[NuxtPluginIndicator]?: true
|
||||||
|
meta?: ResolvedPluginMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ObjectPluginInput<Injections extends Record<string, unknown> = Record<string, unknown>> extends PluginMeta {
|
||||||
|
hooks?: Partial<RuntimeNuxtHooks>
|
||||||
|
setup?: Plugin<Injections>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateOptions {
|
export interface CreateOptions {
|
||||||
@ -306,25 +328,30 @@ export function normalizePlugins (_plugins: Plugin[]) {
|
|||||||
const legacyInjectPlugins: Plugin[] = []
|
const legacyInjectPlugins: Plugin[] = []
|
||||||
const invalidPlugins: Plugin[] = []
|
const invalidPlugins: Plugin[] = []
|
||||||
|
|
||||||
const plugins = _plugins.map((plugin) => {
|
const plugins: Plugin[] = []
|
||||||
|
|
||||||
|
for (const plugin of _plugins) {
|
||||||
if (typeof plugin !== 'function') {
|
if (typeof plugin !== 'function') {
|
||||||
invalidPlugins.push(plugin)
|
if (process.dev) { invalidPlugins.push(plugin) }
|
||||||
return null
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Skip invalid plugins in next releases
|
||||||
|
let _plugin = plugin
|
||||||
if (plugin.length > 1) {
|
if (plugin.length > 1) {
|
||||||
legacyInjectPlugins.push(plugin)
|
|
||||||
// Allow usage without wrapper but warn
|
// Allow usage without wrapper but warn
|
||||||
// TODO: Skip invalid in next releases
|
if (process.dev) { legacyInjectPlugins.push(plugin) }
|
||||||
// @ts-ignore
|
// @ts-expect-error deliberate invalid second argument
|
||||||
return (nuxtApp: NuxtApp) => plugin(nuxtApp, nuxtApp.provide)
|
_plugin = (nuxtApp: NuxtApp) => plugin(nuxtApp, nuxtApp.provide)
|
||||||
// return null
|
|
||||||
}
|
}
|
||||||
if (!isNuxtPlugin(plugin)) {
|
|
||||||
unwrappedPlugins.push(plugin)
|
// Allow usage without wrapper but warn
|
||||||
// Allow usage without wrapper but warn
|
if (process.dev && !isNuxtPlugin(_plugin)) { unwrappedPlugins.push(_plugin) }
|
||||||
}
|
|
||||||
return plugin
|
plugins.push(_plugin)
|
||||||
}).filter(Boolean)
|
}
|
||||||
|
|
||||||
|
plugins.sort((a, b) => (a.meta?.order || orderMap.default) - (b.meta?.order || orderMap.default))
|
||||||
|
|
||||||
if (process.dev && legacyInjectPlugins.length) {
|
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(','))
|
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(','))
|
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<T extends Record<string, unknown>> (plugin: Plugin<T>) {
|
// -50: pre-all (nuxt)
|
||||||
plugin[NuxtPluginIndicator] = true
|
// -40: custom payload revivers (user)
|
||||||
return plugin
|
// -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<NonNullable<ObjectPluginInput['enforce']>, number> = {
|
||||||
|
pre: -20,
|
||||||
|
default: 0,
|
||||||
|
post: 20
|
||||||
|
}
|
||||||
|
|
||||||
|
export function definePayloadPlugin<T extends Record<string, unknown>> (plugin: Plugin<T> | ObjectPluginInput<T>) {
|
||||||
|
return defineNuxtPlugin(plugin, { order: -40 })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defineNuxtPlugin<T extends Record<string, unknown>> (plugin: Plugin<T> | ObjectPluginInput<T>, meta?: PluginMeta): Plugin<T> {
|
||||||
|
if (typeof plugin === 'function') { return defineNuxtPlugin({ setup: plugin }, meta) }
|
||||||
|
|
||||||
|
const wrapper: Plugin<T> = (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) {
|
export function isNuxtPlugin (plugin: unknown) {
|
||||||
|
@ -3,20 +3,23 @@ import { defineNuxtPlugin, useRuntimeConfig } from '#app/nuxt'
|
|||||||
import { useRouter } from '#app/composables/router'
|
import { useRouter } from '#app/composables/router'
|
||||||
import { reloadNuxtApp } from '#app/composables/chunk'
|
import { reloadNuxtApp } from '#app/composables/chunk'
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
export default defineNuxtPlugin({
|
||||||
const router = useRouter()
|
name: 'nuxt:chunk-reload',
|
||||||
const config = useRuntimeConfig()
|
setup (nuxtApp) {
|
||||||
|
const router = useRouter()
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
|
||||||
const chunkErrors = new Set()
|
const chunkErrors = new Set()
|
||||||
|
|
||||||
router.beforeEach(() => { chunkErrors.clear() })
|
router.beforeEach(() => { chunkErrors.clear() })
|
||||||
nuxtApp.hook('app:chunkError', ({ error }) => { chunkErrors.add(error) })
|
nuxtApp.hook('app:chunkError', ({ error }) => { chunkErrors.add(error) })
|
||||||
|
|
||||||
router.onError((error, to) => {
|
router.onError((error, to) => {
|
||||||
if (chunkErrors.has(error)) {
|
if (chunkErrors.has(error)) {
|
||||||
const isHash = 'href' in to && (to.href as string).startsWith('#')
|
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)
|
const path = isHash ? config.app.baseURL + (to as any).href : joinURL(config.app.baseURL, to.fullPath)
|
||||||
reloadNuxtApp({ path, persistState: true })
|
reloadNuxtApp({ path, persistState: true })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
@ -3,33 +3,36 @@ import { parseURL } from 'ufo'
|
|||||||
import { useHead } from '@unhead/vue'
|
import { useHead } from '@unhead/vue'
|
||||||
import { defineNuxtPlugin } from '#app/nuxt'
|
import { defineNuxtPlugin } from '#app/nuxt'
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
export default defineNuxtPlugin({
|
||||||
const externalURLs = ref(new Set<string>())
|
name: 'nuxt:cross-origin-prefetch',
|
||||||
function generateRules () {
|
setup (nuxtApp) {
|
||||||
return {
|
const externalURLs = ref(new Set<string>())
|
||||||
type: 'speculationrules',
|
function generateRules () {
|
||||||
key: 'speculationrules',
|
return {
|
||||||
innerHTML: JSON.stringify({
|
type: 'speculationrules',
|
||||||
prefetch: [
|
key: 'speculationrules',
|
||||||
{
|
innerHTML: JSON.stringify({
|
||||||
source: 'list',
|
prefetch: [
|
||||||
urls: [...externalURLs.value],
|
{
|
||||||
requires: ['anonymous-client-ip-when-cross-origin']
|
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()]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { createDebugger } from 'hookable'
|
import { createDebugger } from 'hookable'
|
||||||
import { defineNuxtPlugin } from '#app/nuxt'
|
import { defineNuxtPlugin } from '#app/nuxt'
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
export default defineNuxtPlugin({
|
||||||
createDebugger(nuxtApp.hooks, { tag: 'nuxt-app' })
|
name: 'nuxt:debug',
|
||||||
|
enforce: 'pre',
|
||||||
|
setup (nuxtApp) {
|
||||||
|
createDebugger(nuxtApp.hooks, { tag: 'nuxt-app' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
@ -3,25 +3,28 @@ import { defineNuxtPlugin } from '#app/nuxt'
|
|||||||
import { isPrerendered, loadPayload } from '#app/composables/payload'
|
import { isPrerendered, loadPayload } from '#app/composables/payload'
|
||||||
import { useRouter } from '#app/composables/router'
|
import { useRouter } from '#app/composables/router'
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
export default defineNuxtPlugin({
|
||||||
// Only enable behavior if initial page is prerendered
|
name: 'nuxt:payload',
|
||||||
// TODO: Support hybrid and dev
|
setup (nuxtApp) {
|
||||||
if (!isPrerendered()) {
|
// Only enable behavior if initial page is prerendered
|
||||||
return
|
// 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)
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
// Load payload after middleware & once final route is resolved
|
// Load payload into cache
|
||||||
useRouter().beforeResolve(async (to, from) => {
|
nuxtApp.hooks.hook('link:prefetch', async (url) => {
|
||||||
if (to.path === from.path) { return }
|
if (!parseURL(url).protocol) {
|
||||||
const payload = await loadPayload(to.path)
|
await loadPayload(url)
|
||||||
if (!payload) { return }
|
}
|
||||||
Object.assign(nuxtApp.static.data, payload.data)
|
})
|
||||||
})
|
|
||||||
|
// 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import { defineNuxtPlugin } from '#app/nuxt'
|
import { defineNuxtPlugin } from '#app/nuxt'
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
export default defineNuxtPlugin({
|
||||||
nuxtApp.vueApp.mixin({
|
name: 'nuxt:webpack-preload',
|
||||||
beforeCreate () {
|
setup (nuxtApp) {
|
||||||
const { _registeredComponents } = this.$nuxt.ssrContext
|
nuxtApp.vueApp.mixin({
|
||||||
const { __moduleIdentifier } = this.$options
|
beforeCreate () {
|
||||||
_registeredComponents.add(__moduleIdentifier)
|
const { _registeredComponents } = this.$nuxt.ssrContext
|
||||||
}
|
const { __moduleIdentifier } = this.$options
|
||||||
})
|
_registeredComponents.add(__moduleIdentifier)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import { defineNuxtPlugin } from '#app/nuxt'
|
import { defineNuxtPlugin, useNuxtApp } from '#app/nuxt'
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
export default defineNuxtPlugin({
|
||||||
nuxtApp.hook('app:mounted', () => {
|
name: 'nuxt:restore-state',
|
||||||
try {
|
hooks: {
|
||||||
const state = sessionStorage.getItem('nuxt:reload:state')
|
'app:mounted' () {
|
||||||
if (state) {
|
const nuxtApp = useNuxtApp()
|
||||||
sessionStorage.removeItem('nuxt:reload:state')
|
try {
|
||||||
Object.assign(nuxtApp.payload.state, JSON.parse(state)?.state)
|
const state = sessionStorage.getItem('nuxt:reload:state')
|
||||||
}
|
if (state) {
|
||||||
} catch {}
|
sessionStorage.removeItem('nuxt:reload:state')
|
||||||
})
|
Object.assign(nuxtApp.payload.state, JSON.parse(state)?.state)
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
@ -13,11 +13,15 @@ const revivers = {
|
|||||||
Reactive: (data: any) => reactive(data)
|
Reactive: (data: any) => reactive(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
export default defineNuxtPlugin({
|
||||||
for (const reviver in revivers) {
|
name: 'nuxt:revive-payload:client',
|
||||||
definePayloadReviver(reviver, revivers[reviver as keyof typeof revivers])
|
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
|
|
||||||
})
|
})
|
||||||
|
@ -14,8 +14,11 @@ const reducers = {
|
|||||||
Reactive: (data: any) => isReactive(data) && toRaw(data)
|
Reactive: (data: any) => isReactive(data) && toRaw(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
export default defineNuxtPlugin({
|
||||||
for (const reducer in reducers) {
|
name: 'nuxt:revive-payload:server',
|
||||||
definePayloadReducer(reducer, reducers[reducer as keyof typeof reducers])
|
setup () {
|
||||||
|
for (const reducer in reducers) {
|
||||||
|
definePayloadReducer(reducer, reducers[reducer as keyof typeof reducers])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -96,176 +96,180 @@ interface Router {
|
|||||||
removeRoute: (name: string) => void
|
removeRoute: (name: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => {
|
export default defineNuxtPlugin<{ route: Route, router: Router }>({
|
||||||
const initialURL = process.client
|
name: 'nuxt:router',
|
||||||
? withoutBase(window.location.pathname, useRuntimeConfig().app.baseURL) + window.location.search + window.location.hash
|
enforce: 'pre',
|
||||||
: nuxtApp.ssrContext!.url
|
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][] } = {
|
const hooks: { [key in keyof RouterHooks]: RouterHooks[key][] } = {
|
||||||
'navigate:before': [],
|
'navigate:before': [],
|
||||||
'resolve:before': [],
|
'resolve:before': [],
|
||||||
'navigate:after': [],
|
'navigate:after': [],
|
||||||
error: []
|
error: []
|
||||||
}
|
}
|
||||||
|
|
||||||
const registerHook = <T extends keyof RouterHooks>(hook: T, guard: RouterHooks[T]) => {
|
const registerHook = <T extends keyof RouterHooks> (hook: T, guard: RouterHooks[T]) => {
|
||||||
hooks[hook].push(guard)
|
hooks[hook].push(guard)
|
||||||
return () => hooks[hook].splice(hooks[hook].indexOf(guard), 1)
|
return () => hooks[hook].splice(hooks[hook].indexOf(guard), 1)
|
||||||
}
|
}
|
||||||
const baseURL = useRuntimeConfig().app.baseURL
|
const baseURL = useRuntimeConfig().app.baseURL
|
||||||
|
|
||||||
const route: Route = reactive(getRouteFromPath(initialURL))
|
const route: Route = reactive(getRouteFromPath(initialURL))
|
||||||
async function handleNavigation (url: string | Partial<Route>, replace?: boolean): Promise<void> {
|
async function handleNavigation (url: string | Partial<Route>, replace?: boolean): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Resolve route
|
// Resolve route
|
||||||
const to = getRouteFromPath(url)
|
const to = getRouteFromPath(url)
|
||||||
|
|
||||||
// Run beforeEach hooks
|
// Run beforeEach hooks
|
||||||
for (const middleware of hooks['navigate:before']) {
|
for (const middleware of hooks['navigate:before']) {
|
||||||
const result = await middleware(to, route)
|
const result = await middleware(to, route)
|
||||||
// Cancel navigation
|
// Cancel navigation
|
||||||
if (result === false || result instanceof Error) { return }
|
if (result === false || result instanceof Error) { return }
|
||||||
// Redirect
|
// Redirect
|
||||||
if (result) { return handleNavigation(result, true) }
|
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 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 = {
|
for (const handler of hooks['resolve:before']) {
|
||||||
currentRoute: route,
|
await handler(to, route)
|
||||||
isReady: () => Promise.resolve(),
|
}
|
||||||
// These options provide a similar API to vue-router but have no effect
|
// Perform navigation
|
||||||
options: {},
|
Object.assign(route, to)
|
||||||
install: () => Promise.resolve(),
|
if (process.client) {
|
||||||
// Navigation
|
window.history[replace ? 'replaceState' : 'pushState']({}, '', joinURL(baseURL, to.fullPath))
|
||||||
push: (url: string) => handleNavigation(url, false),
|
if (!nuxtApp.isHydrating) {
|
||||||
replace: (url: string) => handleNavigation(url, true),
|
// Clear any existing errors
|
||||||
back: () => window.history.go(-1),
|
await callWithNuxt(nuxtApp, clearError)
|
||||||
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<RouteGuard>([...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 }
|
// 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(() => {
|
if (process.client) {
|
||||||
delete nuxtApp._processingMiddleware
|
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<RouteGuard>([...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)
|
return {
|
||||||
if (!isEqual(route.fullPath, initialURL)) {
|
provide: {
|
||||||
const event = await callWithNuxt(nuxtApp, useRequestEvent)
|
route,
|
||||||
const options = { redirectCode: event.node.res.statusCode !== 200 ? event.node.res.statusCode || 302 : 302 }
|
router
|
||||||
await callWithNuxt(nuxtApp, navigateTo, [route.fullPath, options])
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
provide: {
|
|
||||||
route,
|
|
||||||
router
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -40,10 +40,13 @@ const components = ${genObjectFromRawEntries(globalComponents.map((c) => {
|
|||||||
return [c.pascalName, `defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`]
|
return [c.pascalName, `defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`]
|
||||||
}))}
|
}))}
|
||||||
|
|
||||||
export default defineNuxtPlugin(nuxtApp => {
|
export default defineNuxtPlugin({
|
||||||
for (const name in components) {
|
name: 'nuxt:global-components',
|
||||||
nuxtApp.vueApp.component(name, components[name])
|
setup (nuxtApp) {
|
||||||
nuxtApp.vueApp.component('Lazy' + name, components[name])
|
for (const name in components) {
|
||||||
|
nuxtApp.vueApp.component(name, components[name])
|
||||||
|
nuxtApp.vueApp.component('Lazy' + name, components[name])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
`
|
`
|
||||||
|
@ -292,9 +292,9 @@ async function initNuxt (nuxt: Nuxt) {
|
|||||||
|
|
||||||
// Add experimental support for custom types in JSON payload
|
// Add experimental support for custom types in JSON payload
|
||||||
if (nuxt.options.experimental.renderJsonPayloads) {
|
if (nuxt.options.experimental.renderJsonPayloads) {
|
||||||
nuxt.hook('modules:done', () => {
|
nuxt.hooks.hook('modules:done', () => {
|
||||||
nuxt.options.plugins.unshift(resolve(nuxt.options.appDir, 'plugins/revive-payload.client'))
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/revive-payload.client'))
|
||||||
nuxt.options.plugins.unshift(resolve(nuxt.options.appDir, 'plugins/revive-payload.server'))
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/revive-payload.server'))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,37 +4,40 @@ import { defineNuxtPlugin } from '#app/nuxt'
|
|||||||
// @ts-expect-error untyped
|
// @ts-expect-error untyped
|
||||||
import { appHead } from '#build/nuxt.config.mjs'
|
import { appHead } from '#build/nuxt.config.mjs'
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
export default defineNuxtPlugin({
|
||||||
const createHead = process.server ? createServerHead : createClientHead
|
name: 'nuxt:head',
|
||||||
const head = createHead()
|
setup (nuxtApp) {
|
||||||
head.push(appHead)
|
const createHead = process.server ? createServerHead : createClientHead
|
||||||
|
const head = createHead()
|
||||||
|
head.push(appHead)
|
||||||
|
|
||||||
nuxtApp.vueApp.use(head)
|
nuxtApp.vueApp.use(head)
|
||||||
|
|
||||||
if (process.client) {
|
if (process.client) {
|
||||||
// pause dom updates until page is ready and between page transitions
|
// pause dom updates until page is ready and between page transitions
|
||||||
let pauseDOMUpdates = true
|
let pauseDOMUpdates = true
|
||||||
const unpauseDom = () => {
|
const unpauseDom = () => {
|
||||||
pauseDOMUpdates = false
|
pauseDOMUpdates = false
|
||||||
// trigger the debounced DOM update
|
// trigger the debounced DOM update
|
||||||
head.hooks.callHook('entries:updated', head)
|
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) {
|
if (process.server) {
|
||||||
nuxtApp.ssrContext!.renderMeta = async () => {
|
nuxtApp.ssrContext!.renderMeta = async () => {
|
||||||
const meta = await renderSSRHead(head)
|
const meta = await renderSSRHead(head)
|
||||||
return {
|
return {
|
||||||
...meta,
|
...meta,
|
||||||
bodyScriptsPrepend: meta.bodyTagsOpen,
|
bodyScriptsPrepend: meta.bodyTagsOpen,
|
||||||
// resolves naming difference with NuxtMeta and Unhead
|
// resolves naming difference with NuxtMeta and Unhead
|
||||||
bodyScripts: meta.bodyTags
|
bodyScripts: meta.bodyTags
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,10 @@
|
|||||||
import { polyfillAsVueUseHead } from '@unhead/vue/polyfill'
|
import { polyfillAsVueUseHead } from '@unhead/vue/polyfill'
|
||||||
import { defineNuxtPlugin } from '#app/nuxt'
|
import { defineNuxtPlugin } from '#app/nuxt'
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
export default defineNuxtPlugin({
|
||||||
// avoid breaking ecosystem dependencies using low-level @vueuse/head APIs
|
name: 'nuxt:vueuse-head-polyfill',
|
||||||
polyfillAsVueUseHead(nuxtApp.vueApp._context.provides.usehead)
|
setup (nuxtApp) {
|
||||||
|
// avoid breaking ecosystem dependencies using low-level @vueuse/head APIs
|
||||||
|
polyfillAsVueUseHead(nuxtApp.vueApp._context.provides.usehead)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
@ -23,6 +23,7 @@ const appPreset = defineUnimportPreset({
|
|||||||
'defineNuxtComponent',
|
'defineNuxtComponent',
|
||||||
'useNuxtApp',
|
'useNuxtApp',
|
||||||
'defineNuxtPlugin',
|
'defineNuxtPlugin',
|
||||||
|
'definePayloadPlugin',
|
||||||
'reloadNuxtApp',
|
'reloadNuxtApp',
|
||||||
'useRuntimeConfig',
|
'useRuntimeConfig',
|
||||||
'useState',
|
'useState',
|
||||||
|
@ -1,41 +1,43 @@
|
|||||||
import { hasProtocol } from 'ufo'
|
import { hasProtocol } from 'ufo'
|
||||||
import { defineNuxtPlugin, useNuxtApp } from '#app/nuxt'
|
import { defineNuxtPlugin } from '#app/nuxt'
|
||||||
import { useRouter } from '#app/composables/router'
|
import { useRouter } from '#app/composables/router'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import layouts from '#build/layouts'
|
import layouts from '#build/layouts'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { namedMiddleware } from '#build/middleware'
|
import { namedMiddleware } from '#build/middleware'
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
export default defineNuxtPlugin({
|
||||||
const nuxtApp = useNuxtApp()
|
name: 'nuxt:prefetch',
|
||||||
const router = useRouter()
|
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') {
|
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]()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
@ -45,157 +45,161 @@ function createCurrentLocation (
|
|||||||
return path + search + hash
|
return path + search + hash
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
export default defineNuxtPlugin({
|
||||||
let routerBase = useRuntimeConfig().app.baseURL
|
name: 'nuxt:router',
|
||||||
if (routerOptions.hashMode && !routerBase.includes('#')) {
|
enforce: 'pre',
|
||||||
// allow the user to provide a `#` in the middle: `/base/#/app`
|
async setup (nuxtApp) {
|
||||||
routerBase += '#'
|
let routerBase = useRuntimeConfig().app.baseURL
|
||||||
}
|
if (routerOptions.hashMode && !routerBase.includes('#')) {
|
||||||
|
// allow the user to provide a `#` in the middle: `/base/#/app`
|
||||||
const history = routerOptions.history?.(routerBase) ?? (process.client
|
routerBase += '#'
|
||||||
? (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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await router.isReady()
|
const history = routerOptions.history?.(routerBase) ?? (process.client
|
||||||
} catch (error: any) {
|
? (routerOptions.hashMode ? createWebHashHistory(routerBase) : createWebHistory(routerBase))
|
||||||
// We'll catch 404s here
|
: createMemoryHistory(routerBase)
|
||||||
await callWithNuxt(nuxtApp, showError, [error])
|
)
|
||||||
}
|
|
||||||
|
|
||||||
const initialLayout = useState('_layout')
|
const routes = routerOptions.routes?.(_routes) ?? _routes
|
||||||
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<PageMeta['layout'], Ref | false>
|
|
||||||
}
|
|
||||||
nuxtApp._processingMiddleware = true
|
|
||||||
|
|
||||||
type MiddlewareDef = string | RouteMiddleware
|
const initialURL = process.server ? nuxtApp.ssrContext!.url : createCurrentLocation(routerBase, window.location)
|
||||||
const middlewareEntries = new Set<MiddlewareDef>([...globalMiddleware, ...nuxtApp._middleware.global])
|
const router = createRouter({
|
||||||
for (const component of to.matched) {
|
...routerOptions,
|
||||||
const componentMiddleware = component.meta.middleware as MiddlewareDef | MiddlewareDef[]
|
history,
|
||||||
if (!componentMiddleware) { continue }
|
routes
|
||||||
if (Array.isArray(componentMiddleware)) {
|
})
|
||||||
for (const entry of componentMiddleware) {
|
nuxtApp.vueApp.use(router)
|
||||||
middlewareEntries.add(entry)
|
|
||||||
}
|
const previousRoute = shallowRef(router.currentRoute.value)
|
||||||
} else {
|
router.afterEach((_to, from) => {
|
||||||
middlewareEntries.add(componentMiddleware)
|
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) {
|
nuxtApp._route = reactive(route)
|
||||||
const middleware = typeof entry === 'string' ? nuxtApp._middleware.named[entry] || await namedMiddleware[entry]?.().then((r: any) => r.default || r) : entry
|
|
||||||
|
|
||||||
if (!middleware) {
|
nuxtApp._middleware = nuxtApp._middleware || {
|
||||||
if (process.dev) {
|
global: [],
|
||||||
throw new Error(`Unknown route middleware: '${entry}'. Valid middleware: ${Object.keys(namedMiddleware).map(mw => `'${mw}'`).join(', ')}.`)
|
named: {}
|
||||||
}
|
|
||||||
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) => {
|
const error = useError()
|
||||||
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 {
|
try {
|
||||||
await router.replace({
|
if (process.server) {
|
||||||
...router.resolve(initialURL),
|
await router.push(initialURL)
|
||||||
name: undefined, // #4920, #$4982
|
}
|
||||||
force: true
|
|
||||||
})
|
await router.isReady()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// We'll catch middleware errors or deliberate exceptions here
|
// We'll catch 404s here
|
||||||
await callWithNuxt(nuxtApp, showError, [error])
|
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<PageMeta['layout'], Ref | false>
|
||||||
|
}
|
||||||
|
nuxtApp._processingMiddleware = true
|
||||||
|
|
||||||
|
type MiddlewareDef = string | RouteMiddleware
|
||||||
|
const middlewareEntries = new Set<MiddlewareDef>([...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 }>
|
}) as Plugin<{ router: Router }>
|
||||||
|
@ -195,6 +195,7 @@ export default defineUntypedSchema({
|
|||||||
asyncFunctions: ['defineNuxtPlugin', 'defineNuxtRouteMiddleware'],
|
asyncFunctions: ['defineNuxtPlugin', 'defineNuxtRouteMiddleware'],
|
||||||
objectDefinitions: {
|
objectDefinitions: {
|
||||||
defineNuxtComponent: ['asyncData', 'setup'],
|
defineNuxtComponent: ['asyncData', 'setup'],
|
||||||
|
defineNuxtPlugin: ['setup'],
|
||||||
definePageMeta: ['middleware', 'validate']
|
definePageMeta: ['middleware', 'validate']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ describe.skipIf(isWindows || process.env.ECOSYSTEM_CI)('minimal nuxt application
|
|||||||
|
|
||||||
it('default client bundle size', async () => {
|
it('default client bundle size', async () => {
|
||||||
stats.client = await analyzeSizes('**/*.js', publicDir)
|
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(`
|
expect(stats.client.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
"_nuxt/_plugin-vue_export-helper.js",
|
"_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 () => {
|
it('default server bundle size', async () => {
|
||||||
stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
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)
|
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||||
expect(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2650k"')
|
expect(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2650k"')
|
||||||
|
6
test/fixtures/basic/nuxt.config.ts
vendored
6
test/fixtures/basic/nuxt.config.ts
vendored
@ -108,12 +108,6 @@ export default defineNuxtConfig({
|
|||||||
addVitePlugin(plugin.vite())
|
addVitePlugin(plugin.vite())
|
||||||
addWebpackPlugin(plugin.webpack())
|
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) {
|
function (_options, nuxt) {
|
||||||
const routesToDuplicate = ['/async-parent', '/fixed-keyed-child-parent', '/keyed-child-parent', '/with-layout', '/with-layout2']
|
const routesToDuplicate = ['/async-parent', '/fixed-keyed-child-parent', '/keyed-child-parent', '/with-layout', '/with-layout2']
|
||||||
const stripLayout = (page: NuxtPage): NuxtPage => ({
|
const stripLayout = (page: NuxtPage): NuxtPage => ({
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export default defineNuxtPlugin((nuxtApp) => {
|
export default definePayloadPlugin((nuxtApp) => {
|
||||||
definePayloadReducer('BlinkingText', data => data === '<original-blink>' && '_')
|
definePayloadReducer('BlinkingText', data => data === '<original-blink>' && '_')
|
||||||
definePayloadReviver('BlinkingText', () => '<revivified-blink>')
|
definePayloadReviver('BlinkingText', () => '<revivified-blink>')
|
||||||
if (process.server) {
|
if (process.server) {
|
||||||
|
Loading…
Reference in New Issue
Block a user