Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nuxt): add nuxtMiddleware route rule #25841

Merged
merged 26 commits into from Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9a18df4
chore: last time, lukasz, /dev/urandom is not a variable name generat…
HigherOrderLogic Feb 17, 2024
edfa10d
chore: weird tests fix
HigherOrderLogic Feb 17, 2024
74e8f1e
chore: testing the test fix
HigherOrderLogic Feb 17, 2024
24998a7
fix: properly get route rules
HigherOrderLogic Mar 3, 2024
7546484
fix: use augmentation to define route rules type
danielroe Mar 9, 2024
66be202
style: small lints
danielroe Mar 9, 2024
44af5a8
fix: normalise middleware in nitro init
danielroe Mar 9, 2024
3f80bee
fix: use middleware name
danielroe Mar 9, 2024
dfff49b
fix(nuxt): support getting route rules on server
danielroe Mar 9, 2024
311a4a2
style: revert formatting change
danielroe Mar 9, 2024
8f613f9
fix: provide typed values for `nuxtMiddleware`
danielroe Mar 9, 2024
9244713
fix(nuxt): normalise route rules before starting nitro
danielroe Mar 9, 2024
cd9e441
fix: use route rules from nitro config
danielroe Mar 9, 2024
061fe38
test: get html text rather than FetchResponse
danielroe Mar 9, 2024
0a57fe1
style: remove blank line
danielroe Mar 9, 2024
dd95d76
fix: type matched route rule return
danielroe Mar 9, 2024
544e427
chore: reduce diff
danielroe Mar 16, 2024
f89c08b
Merge remote-tracking branch 'origin/main' into HigherOrderLogic/main
danielroe Mar 16, 2024
a46b8da
test: only run route rules test when app manifest is on
danielroe Mar 16, 2024
e9f06e3
fix: only add middleware if manifest is enabled
danielroe Mar 16, 2024
3369471
feat: add support in universal router
danielroe Mar 16, 2024
91f9e08
chore: remove old code
danielroe Mar 16, 2024
24745e0
test: also run composables test with appManifest off
danielroe Mar 16, 2024
fb464b9
Merge remote-tracking branch 'origin/main' into HigherOrderLogic/main
danielroe Mar 16, 2024
e1adddd
test: mock manifest before environment is set up
danielroe Mar 16, 2024
f3e3a3a
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/nuxt/src/app/composables/manifest.ts
@@ -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
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
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
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
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
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
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
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
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
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
@@ -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
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
@@ -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
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
@@ -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
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