mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-11 08:33:53 +00:00
feat(nuxt): add nuxtMiddleware
route rule (#25841)
This commit is contained in:
parent
79ea75e72a
commit
f9fe282506
@ -1,7 +1,8 @@
|
||||
import type { MatcherExport, RouteMatcher } from 'radix3'
|
||||
import { createMatcherFromExport } from 'radix3'
|
||||
import { createMatcherFromExport, createRouter as createRadixRouter, toRouteMatcher } from 'radix3'
|
||||
import { defu } from 'defu'
|
||||
import { useAppConfig } from '../config'
|
||||
import { useRuntimeConfig } from '../nuxt'
|
||||
// @ts-expect-error virtual file
|
||||
import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs'
|
||||
// @ts-expect-error virtual file
|
||||
@ -43,6 +44,12 @@ export function getAppManifest (): Promise<NuxtAppManifest> {
|
||||
|
||||
/** @since 3.7.4 */
|
||||
export async function getRouteRules (url: string) {
|
||||
if (import.meta.server) {
|
||||
const _routeRulesMatcher = toRouteMatcher(
|
||||
createRadixRouter({ routes: useRuntimeConfig().nitro!.routeRules })
|
||||
)
|
||||
return defu({} as Record<string, any>, ..._routeRulesMatcher.matchAll(url).reverse())
|
||||
}
|
||||
await getAppManifest()
|
||||
return defu({} as Record<string, any>, ...matcher.matchAll(url).reverse())
|
||||
}
|
||||
|
@ -3,11 +3,14 @@ import { computed, defineComponent, h, isReadonly, reactive } from 'vue'
|
||||
import { isEqual, joinURL, parseQuery, parseURL, stringifyParsedURL, stringifyQuery, withoutBase } from 'ufo'
|
||||
import { createError } from 'h3'
|
||||
import { defineNuxtPlugin, useRuntimeConfig } from '../nuxt'
|
||||
import { getRouteRules } from '../composables/manifest'
|
||||
import { clearError, showError } from '../composables/error'
|
||||
import { navigateTo } from '../composables/router'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import { globalMiddleware } from '#build/middleware'
|
||||
// @ts-expect-error virtual file
|
||||
import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs'
|
||||
|
||||
interface Route {
|
||||
/** Percentage encoded pathname section of the URL. */
|
||||
@ -243,6 +246,23 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>({
|
||||
if (import.meta.client || !nuxtApp.ssrContext?.islandContext) {
|
||||
const middlewareEntries = new Set<RouteGuard>([...globalMiddleware, ...nuxtApp._middleware.global])
|
||||
|
||||
if (isAppManifestEnabled) {
|
||||
const routeRules = await nuxtApp.runWithContext(() => getRouteRules(to.path))
|
||||
|
||||
if (routeRules.nuxtMiddleware) {
|
||||
for (const key in routeRules.nuxtMiddleware) {
|
||||
const guard = nuxtApp._middleware.named[key] as RouteGuard | undefined
|
||||
if (!guard) { return }
|
||||
|
||||
if (routeRules.nuxtMiddleware[key]) {
|
||||
middlewareEntries.add(guard)
|
||||
} else {
|
||||
middlewareEntries.delete(guard)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const middleware of middlewareEntries) {
|
||||
const result = await nuxtApp.runWithContext(() => middleware(to, from))
|
||||
if (import.meta.server) {
|
||||
|
@ -6,7 +6,7 @@ import { createRouter as createRadixRouter, exportMatcher, toRouteMatcher } from
|
||||
import { randomUUID } from 'uncrypto'
|
||||
import { joinURL, withTrailingSlash } from 'ufo'
|
||||
import { build, copyPublicAssets, createDevServer, createNitro, prepare, prerender, scanHandlers, writeTypes } from 'nitropack'
|
||||
import type { Nitro, NitroConfig } from 'nitropack'
|
||||
import type { Nitro, NitroConfig, NitroOptions } from 'nitropack'
|
||||
import { findPath, logger, resolveIgnorePatterns, resolveNuxtModule, resolvePath } from '@nuxt/kit'
|
||||
import escapeRE from 'escape-string-regexp'
|
||||
import { defu } from 'defu'
|
||||
@ -262,6 +262,25 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
||||
}
|
||||
)
|
||||
|
||||
nuxt.options.alias['#app-manifest'] = join(tempDir, `meta/${buildId}.json`)
|
||||
|
||||
nuxt.hook('nitro:config', (config) => {
|
||||
const rules = config.routeRules
|
||||
for (const rule in rules) {
|
||||
if (!(rules[rule] as any).nuxtMiddleware) { continue }
|
||||
const value = (rules[rule] as any).nuxtMiddleware
|
||||
if (typeof value === 'string') {
|
||||
(rules[rule] as NitroOptions['routeRules']).nuxtMiddleware = { [value]: true }
|
||||
} else if (Array.isArray(value)) {
|
||||
const normalizedRules: Record<string, boolean> = {}
|
||||
for (const middleware of value) {
|
||||
normalizedRules[middleware] = true
|
||||
}
|
||||
(rules[rule] as NitroOptions['routeRules']).nuxtMiddleware = normalizedRules
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
nuxt.hook('nitro:init', (nitro) => {
|
||||
nitro.hooks.hook('rollup:before', async (nitro) => {
|
||||
const routeRules = {} as Record<string, any>
|
||||
@ -272,8 +291,12 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
||||
const filteredRules = {} as Record<string, any>
|
||||
for (const routeKey in _routeRules[key]) {
|
||||
const value = (_routeRules as any)[key][routeKey]
|
||||
if (['prerender', 'redirect'].includes(routeKey) && value) {
|
||||
filteredRules[routeKey] = routeKey === 'redirect' ? typeof value === 'string' ? value : value.to : value
|
||||
if (['prerender', 'redirect', 'nuxtMiddleware'].includes(routeKey) && value) {
|
||||
if (routeKey === 'redirect') {
|
||||
filteredRules[routeKey] = typeof value === 'string' ? value : value.to
|
||||
} else {
|
||||
filteredRules[routeKey] = value
|
||||
}
|
||||
hasRules = true
|
||||
}
|
||||
}
|
||||
|
@ -244,6 +244,7 @@ declare module 'nitropack' {
|
||||
interface NitroRouteRules {
|
||||
ssr?: boolean
|
||||
experimentalNoScripts?: boolean
|
||||
nuxtMiddleware?: Record<string, boolean>
|
||||
}
|
||||
interface NitroRuntimeHooks {
|
||||
'dev:ssr-logs': (ctx: { logs: LogObject[], path: string }) => void | Promise<void>
|
||||
|
@ -474,6 +474,11 @@ export default defineNuxtModule({
|
||||
' interface PageMeta {',
|
||||
' middleware?: MiddlewareKey | NavigationGuard | Array<MiddlewareKey | NavigationGuard>',
|
||||
' }',
|
||||
'}',
|
||||
'declare module \'nitropack\' {',
|
||||
' interface NitroRouteConfig {',
|
||||
' nuxtMiddleware?: MiddlewareKey | MiddlewareKey[] | Record<MiddlewareKey, boolean>',
|
||||
' }',
|
||||
'}'
|
||||
].join('\n')
|
||||
}
|
||||
|
@ -15,10 +15,13 @@ import type { PageMeta } from '../composables'
|
||||
|
||||
import { toArray } from '../utils'
|
||||
import type { Plugin, RouteMiddleware } from '#app'
|
||||
import { getRouteRules } from '#app/composables/manifest'
|
||||
import { defineNuxtPlugin, useRuntimeConfig } from '#app/nuxt'
|
||||
import { clearError, showError, useError } from '#app/composables/error'
|
||||
import { navigateTo } from '#app/composables/router'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs'
|
||||
// @ts-expect-error virtual file
|
||||
import _routes from '#build/routes'
|
||||
// @ts-expect-error virtual file
|
||||
@ -173,6 +176,20 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
|
||||
}
|
||||
}
|
||||
|
||||
if (isAppManifestEnabled) {
|
||||
const routeRules = await nuxtApp.runWithContext(() => getRouteRules(to.path))
|
||||
|
||||
if (routeRules.nuxtMiddleware) {
|
||||
for (const key in routeRules.nuxtMiddleware) {
|
||||
if (routeRules.nuxtMiddleware[key]) {
|
||||
middlewareEntries.add(key)
|
||||
} else {
|
||||
middlewareEntries.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of middlewareEntries) {
|
||||
const middleware = typeof entry === 'string' ? nuxtApp._middleware.named[entry] || await namedMiddleware[entry]?.().then((r: any) => r.default || r) : entry
|
||||
|
||||
|
@ -26,6 +26,7 @@ declare module 'nitropack' {
|
||||
interface NitroRouteRules {
|
||||
ssr?: boolean
|
||||
experimentalNoScripts?: boolean
|
||||
nuxtMiddleware?: Record<string, boolean>
|
||||
}
|
||||
interface NitroRuntimeHooks {
|
||||
'dev:ssr-logs': (ctx: { logs: LogObject[], path: string }) => void | Promise<void>
|
||||
|
1
packages/nuxt/types.d.ts
vendored
1
packages/nuxt/types.d.ts
vendored
@ -26,6 +26,7 @@ declare module 'nitropack' {
|
||||
interface NitroRouteRules {
|
||||
ssr?: boolean
|
||||
experimentalNoScripts?: boolean
|
||||
nuxtMiddleware?: Record<string, boolean>
|
||||
}
|
||||
interface NitroRuntimeHooks {
|
||||
'dev:ssr-logs': (ctx: { logs: LogObject[], path: string }) => void | Promise<void>
|
||||
|
@ -80,6 +80,11 @@ describe('route rules', () => {
|
||||
const html = await $fetch('/no-scripts')
|
||||
expect(html).not.toContain('<script')
|
||||
})
|
||||
|
||||
it.runIf(isTestingAppManifest)('should run middleware defined in routeRules config', async () => {
|
||||
const html = await $fetch('/route-rules/middleware')
|
||||
expect(html).toContain('Hello from routeRules!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('modules', () => {
|
||||
|
@ -19,7 +19,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
for (const outputDir of ['.output', '.output-inline']) {
|
||||
it('default client bundle size', async () => {
|
||||
const clientStats = await analyzeSizes('**/*.js', join(rootDir, outputDir, 'public'))
|
||||
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot('"105k"')
|
||||
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot('"106k"')
|
||||
expect(clientStats.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
|
||||
[
|
||||
"_nuxt/entry.js",
|
||||
@ -32,7 +32,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
const serverDir = join(rootDir, '.output/server')
|
||||
|
||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"205k"')
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"206k"')
|
||||
|
||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"1336k"')
|
||||
@ -72,7 +72,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
const serverDir = join(rootDir, '.output-inline/server')
|
||||
|
||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"524k"')
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"525k"')
|
||||
|
||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"77.8k"')
|
||||
|
3
test/fixtures/basic/middleware/routeRulesMiddleware.ts
vendored
Normal file
3
test/fixtures/basic/middleware/routeRulesMiddleware.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
export default defineNuxtRouteMiddleware((to) => {
|
||||
to.meta.hello = 'Hello from routeRules!'
|
||||
})
|
1
test/fixtures/basic/nuxt.config.ts
vendored
1
test/fixtures/basic/nuxt.config.ts
vendored
@ -60,6 +60,7 @@ export default defineNuxtConfig({
|
||||
},
|
||||
routeRules: {
|
||||
'/route-rules/spa': { ssr: false },
|
||||
'/route-rules/middleware': { nuxtMiddleware: 'route-rules-middleware' },
|
||||
'/hydration/spa-redirection/**': { ssr: false },
|
||||
'/no-scripts': { experimentalNoScripts: true }
|
||||
},
|
||||
|
5
test/fixtures/basic/pages/route-rules/middleware.vue
vendored
Normal file
5
test/fixtures/basic/pages/route-rules/middleware.vue
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>Greeting: {{ $route.meta.hello }}</div>
|
||||
</div>
|
||||
</template>
|
@ -19,29 +19,6 @@ import { useId } from '#app/composables/id'
|
||||
import { callOnce } from '#app/composables/once'
|
||||
import { useLoadingIndicator } from '#app/composables/loading-indicator'
|
||||
|
||||
vi.mock('#app/compat/idle-callback', () => ({
|
||||
requestIdleCallback: (cb: Function) => cb()
|
||||
}))
|
||||
|
||||
const timestamp = Date.now()
|
||||
registerEndpoint('/_nuxt/builds/latest.json', defineEventHandler(() => ({
|
||||
id: 'override',
|
||||
timestamp
|
||||
})))
|
||||
registerEndpoint('/_nuxt/builds/meta/override.json', defineEventHandler(() => ({
|
||||
id: 'override',
|
||||
timestamp,
|
||||
matcher: {
|
||||
static: {
|
||||
'/': null,
|
||||
'/pre': null,
|
||||
'/pre/test': { redirect: true }
|
||||
},
|
||||
wildcard: { '/pre': { prerender: true } },
|
||||
dynamic: {}
|
||||
},
|
||||
prerendered: ['/specific-prerendered']
|
||||
})))
|
||||
registerEndpoint('/api/test', defineEventHandler(event => ({
|
||||
method: event.method,
|
||||
headers: Object.fromEntries(event.headers.entries())
|
||||
|
28
test/setup-runtime.ts
Normal file
28
test/setup-runtime.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { vi } from 'vitest'
|
||||
import { defineEventHandler } from 'h3'
|
||||
|
||||
import { registerEndpoint } from '@nuxt/test-utils/runtime'
|
||||
|
||||
vi.mock('#app/compat/idle-callback', () => ({
|
||||
requestIdleCallback: (cb: Function) => cb()
|
||||
}))
|
||||
|
||||
const timestamp = Date.now()
|
||||
registerEndpoint('/_nuxt/builds/latest.json', defineEventHandler(() => ({
|
||||
id: 'override',
|
||||
timestamp
|
||||
})))
|
||||
registerEndpoint('/_nuxt/builds/meta/override.json', defineEventHandler(() => ({
|
||||
id: 'override',
|
||||
timestamp,
|
||||
matcher: {
|
||||
static: {
|
||||
'/': null,
|
||||
'/pre': null,
|
||||
'/pre/test': { redirect: true }
|
||||
},
|
||||
wildcard: { '/pre': { prerender: true } },
|
||||
dynamic: {}
|
||||
},
|
||||
prerendered: ['/specific-prerendered']
|
||||
})))
|
@ -7,6 +7,9 @@ export default defineVitestConfig({
|
||||
include: ['packages/nuxt/src/app']
|
||||
},
|
||||
environment: 'nuxt',
|
||||
setupFiles: [
|
||||
'./test/setup-runtime.ts'
|
||||
],
|
||||
environmentOptions: {
|
||||
nuxt: {
|
||||
overrides: {
|
||||
|
Loading…
Reference in New Issue
Block a user