Skip to content

Commit

Permalink
feat(nuxt): add nuxtMiddleware route rule (#25841)
Browse files Browse the repository at this point in the history
  • Loading branch information
HigherOrderLogic committed Mar 16, 2024
1 parent 79ea75e commit f9fe282
Show file tree
Hide file tree
Showing 16 changed files with 127 additions and 30 deletions.
9 changes: 8 additions & 1 deletion packages/nuxt/src/app/composables/manifest.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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())
}
20 changes: 20 additions & 0 deletions packages/nuxt/src/app/plugins/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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) {
Expand Down
29 changes: 26 additions & 3 deletions packages/nuxt/src/core/nitro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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>
Expand All @@ -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
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/nuxt/src/core/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
5 changes: 5 additions & 0 deletions packages/nuxt/src/pages/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
Expand Down
17 changes: 17 additions & 0 deletions packages/nuxt/src/pages/runtime/plugins/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions packages/nuxt/types.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
1 change: 1 addition & 0 deletions packages/nuxt/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
5 changes: 5 additions & 0 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
6 changes: 3 additions & 3 deletions test/bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"')
Expand Down Expand Up @@ -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"')
Expand Down
3 changes: 3 additions & 0 deletions test/fixtures/basic/middleware/routeRulesMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default defineNuxtRouteMiddleware((to) => {
to.meta.hello = 'Hello from routeRules!'
})
1 change: 1 addition & 0 deletions test/fixtures/basic/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
},
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/basic/pages/route-rules/middleware.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<div>
<div>Greeting: {{ $route.meta.hello }}</div>
</div>
</template>
23 changes: 0 additions & 23 deletions test/nuxt/composables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
28 changes: 28 additions & 0 deletions test/setup-runtime.ts
Original file line number Diff line number Diff line change
@@ -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']
})))
3 changes: 3 additions & 0 deletions vitest.nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export default defineVitestConfig({
include: ['packages/nuxt/src/app']
},
environment: 'nuxt',
setupFiles: [
'./test/setup-runtime.ts'
],
environmentOptions: {
nuxt: {
overrides: {
Expand Down

0 comments on commit f9fe282

Please sign in to comment.