feat(nuxt)!: support universal global middleware (#5038)

This commit is contained in:
Daniel Roe 2022-06-27 13:10:29 +01:00 committed by GitHub
parent 7efdb486db
commit 8c2c80e43e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 75 additions and 88 deletions

View File

@ -0,0 +1,3 @@
export default defineNuxtRouteMiddleware(() => {
console.log('running global middleware')
})

View File

@ -4,6 +4,8 @@ import { createError } from 'h3'
import { defineNuxtPlugin } from '..' import { defineNuxtPlugin } from '..'
import { callWithNuxt } from '../nuxt' import { callWithNuxt } from '../nuxt'
import { clearError, navigateTo, throwError, useRuntimeConfig } from '#app' import { clearError, navigateTo, throwError, useRuntimeConfig } from '#app'
// @ts-ignore
import { globalMiddleware } from '#build/middleware'
interface Route { interface Route {
/** Percentage encoded pathname section of the URL. */ /** Percentage encoded pathname section of the URL. */
@ -201,7 +203,7 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => {
to.meta = reactive(to.meta || {}) to.meta = reactive(to.meta || {})
nuxtApp._processingMiddleware = true nuxtApp._processingMiddleware = true
const middlewareEntries = new Set<RouteGuard>(nuxtApp._middleware.global) const middlewareEntries = new Set<RouteGuard>([...globalMiddleware, ...nuxtApp._middleware.global])
for (const middleware of middlewareEntries) { for (const middleware of middlewareEntries) {
const result = await callWithNuxt(nuxtApp, middleware, [to, from]) const result = await callWithNuxt(nuxtApp, middleware, [to, from])

View File

@ -1,11 +1,11 @@
import { promises as fsp } from 'node:fs' import { promises as fsp } from 'node:fs'
import { dirname, resolve, basename, extname } from 'pathe' import { dirname, resolve } from 'pathe'
import defu from 'defu' import defu from 'defu'
import { kebabCase } from 'scule'
import type { Nuxt, NuxtApp, NuxtPlugin } from '@nuxt/schema' import type { Nuxt, NuxtApp, NuxtPlugin } from '@nuxt/schema'
import { findPath, resolveFiles, normalizePlugin, normalizeTemplate, compileTemplate, templateUtils, tryResolveModule } from '@nuxt/kit' import { findPath, resolveFiles, normalizePlugin, normalizeTemplate, compileTemplate, templateUtils, tryResolveModule } from '@nuxt/kit'
import * as defaultTemplates from './templates' import * as defaultTemplates from './templates'
import { getNameFromPath, hasSuffix, uniqueBy } from './utils'
export function createApp (nuxt: Nuxt, options: Partial<NuxtApp> = {}): NuxtApp { export function createApp (nuxt: Nuxt, options: Partial<NuxtApp> = {}): NuxtApp {
return defu(options, { return defu(options, {
@ -83,6 +83,17 @@ export async function resolveApp (nuxt: Nuxt, app: NuxtApp) {
} }
} }
// Resolve middleware/ from all config layers
app.middleware = []
for (const config of nuxt.options._layers.map(layer => layer.config)) {
const middlewareFiles = await resolveFiles(config.srcDir, `${config.dir?.middleware || 'middleware'}/*{${nuxt.options.extensions.join(',')}}`)
app.middleware.push(...middlewareFiles.map((file) => {
const name = getNameFromPath(file)
return { name, path: file, global: hasSuffix(file, '.global') }
}))
}
app.middleware = uniqueBy(app.middleware, 'name')
// Resolve plugins // Resolve plugins
app.plugins = [ app.plugins = [
...nuxt.options.plugins.map(normalizePlugin) ...nuxt.options.plugins.map(normalizePlugin)
@ -101,18 +112,3 @@ export async function resolveApp (nuxt: Nuxt, app: NuxtApp) {
// Extend app // Extend app
await nuxt.callHook('app:resolve', app) await nuxt.callHook('app:resolve', app)
} }
function getNameFromPath (path: string) {
return kebabCase(basename(path).replace(extname(path), '')).replace(/["']/g, '')
}
function uniqueBy <T, K extends keyof T> (arr: T[], key: K) {
const res: T[] = []
const seen = new Set<T[K]>()
for (const item of arr) {
if (seen.has(item[key])) { continue }
seen.add(item[key])
res.push(item)
}
return res
}

View File

@ -13,7 +13,7 @@ export async function build (nuxt: Nuxt) {
if (nuxt.options.dev) { if (nuxt.options.dev) {
watch(nuxt) watch(nuxt)
nuxt.hook('builder:watch', async (event, path) => { nuxt.hook('builder:watch', async (event, path) => {
if (event !== 'change' && /^(app\.|error\.|plugins\/|layouts\/)/i.test(path)) { if (event !== 'change' && /^(app\.|error\.|plugins\/|middleware\/|layouts\/)/i.test(path)) {
if (path.startsWith('app')) { if (path.startsWith('app')) {
app.mainComponent = null app.mainComponent = null
} }

View File

@ -1,4 +1,4 @@
import { Nuxt, NuxtApp, NuxtMiddleware } from '@nuxt/schema' import { Nuxt, NuxtApp } from '@nuxt/schema'
import { createTransformer } from 'unctx/transform' import { createTransformer } from 'unctx/transform'
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
@ -8,15 +8,13 @@ export const UnctxTransformPlugin = (nuxt: Nuxt) => {
}) })
let app: NuxtApp | undefined let app: NuxtApp | undefined
let middleware: NuxtMiddleware[] = []
nuxt.hook('app:resolve', (_app) => { app = _app }) nuxt.hook('app:resolve', (_app) => { app = _app })
nuxt.hook('pages:middleware:extend', (_middlewares) => { middleware = _middlewares })
return createUnplugin((options: { sourcemap?: boolean } = {}) => ({ return createUnplugin((options: { sourcemap?: boolean } = {}) => ({
name: 'unctx:transfrom', name: 'unctx:transfrom',
enforce: 'post', enforce: 'post',
transformInclude (id) { transformInclude (id) {
return Boolean(app?.plugins.find(i => i.src === id) || middleware.find(m => m.path === id)) return Boolean(app?.plugins.find(i => i.src === id) || app.middleware.find(m => m.path === id))
}, },
transform (code, id) { transform (code, id) {
const result = transformer.transform(code) const result = transformer.transform(code)

View File

@ -159,7 +159,7 @@ export const schemaTemplate = {
// Add layouts template // Add layouts template
export const layoutTemplate: NuxtTemplate = { export const layoutTemplate: NuxtTemplate = {
filename: 'layouts.mjs', filename: 'layouts.mjs',
getContents ({ app }) { getContents ({ app }: TemplateContext) {
const layoutsObject = genObjectFromRawEntries(Object.values(app.layouts).map(({ name, file }) => { const layoutsObject = genObjectFromRawEntries(Object.values(app.layouts).map(({ name, file }) => {
return [name, `defineAsyncComponent(${genDynamicImport(file)})`] return [name, `defineAsyncComponent(${genDynamicImport(file)})`]
})) }))
@ -170,6 +170,21 @@ export const layoutTemplate: NuxtTemplate = {
} }
} }
// Add middleware template
export const middlewareTemplate: NuxtTemplate = {
filename: 'middleware.mjs',
getContents ({ app }: TemplateContext) {
const globalMiddleware = app.middleware.filter(mw => mw.global)
const namedMiddleware = app.middleware.filter(mw => !mw.global)
const namedMiddlewareObject = genObjectFromRawEntries(namedMiddleware.map(mw => [mw.name, genDynamicImport(mw.path)]))
return [
...globalMiddleware.map(mw => genImport(mw.path, genSafeVariableName(mw.name))),
`export const globalMiddleware = ${genArrayFromRaw(globalMiddleware.map(mw => genSafeVariableName(mw.name)))}`,
`export const namedMiddleware = ${namedMiddlewareObject}`
].join('\n')
}
}
export const clientConfigTemplate: NuxtTemplate = { export const clientConfigTemplate: NuxtTemplate = {
filename: 'nitro.client.mjs', filename: 'nitro.client.mjs',
getContents: () => ` getContents: () => `

View File

@ -0,0 +1,25 @@
import { basename, extname } from 'pathe'
import { kebabCase, pascalCase } from 'scule'
export function getNameFromPath (path: string) {
return kebabCase(basename(path).replace(extname(path), '')).replace(/["']/g, '')
}
export function uniqueBy <T, K extends keyof T> (arr: T[], key: K) {
const res: T[] = []
const seen = new Set<T[K]>()
for (const item of arr) {
if (seen.has(item[key])) { continue }
seen.add(item[key])
res.push(item)
}
return res
}
export function hasSuffix (path: string, suffix: string) {
return basename(path).replace(extname(path), '').endsWith(suffix)
}
export function getImportName (name: string) {
return pascalCase(name).replace(/[^\w]/g, r => '_' + r.charCodeAt(0))
}

View File

@ -1,10 +1,11 @@
import { existsSync } from 'node:fs' import { existsSync } from 'node:fs'
import { defineNuxtModule, addTemplate, addPlugin, addVitePlugin, addWebpackPlugin, findPath } from '@nuxt/kit' import { defineNuxtModule, addTemplate, addPlugin, addVitePlugin, addWebpackPlugin, findPath } from '@nuxt/kit'
import { resolve } from 'pathe' import { resolve } from 'pathe'
import { genDynamicImport, genString, genArrayFromRaw, genImport, genObjectFromRawEntries, genSafeVariableName } from 'knitwork' import { genString, genImport, genObjectFromRawEntries } from 'knitwork'
import escapeRE from 'escape-string-regexp' import escapeRE from 'escape-string-regexp'
import { NuxtApp } from '@nuxt/schema'
import { distDir } from '../dirs' import { distDir } from '../dirs'
import { resolvePagesRoutes, normalizeRoutes, resolveMiddleware } from './utils' import { resolvePagesRoutes, normalizeRoutes } from './utils'
import { TransformMacroPlugin, TransformMacroPluginOptions } from './macros' import { TransformMacroPlugin, TransformMacroPluginOptions } from './macros'
export default defineNuxtModule({ export default defineNuxtModule({
@ -33,6 +34,7 @@ export default defineNuxtModule({
nuxt.hook('builder:watch', async (event, path) => { nuxt.hook('builder:watch', async (event, path) => {
const dirs = [ const dirs = [
nuxt.options.dir.pages, nuxt.options.dir.pages,
nuxt.options.dir.layouts,
nuxt.options.dir.middleware nuxt.options.dir.middleware
].filter(Boolean) ].filter(Boolean)
@ -102,29 +104,11 @@ export default defineNuxtModule({
} }
}) })
// Add middleware template
addTemplate({
filename: 'middleware.mjs',
async getContents () {
const middleware = await resolveMiddleware()
await nuxt.callHook('pages:middleware:extend', middleware)
const globalMiddleware = middleware.filter(mw => mw.global)
const namedMiddleware = middleware.filter(mw => !mw.global)
const namedMiddlewareObject = genObjectFromRawEntries(namedMiddleware.map(mw => [mw.name, genDynamicImport(mw.path)]))
return [
...globalMiddleware.map(mw => genImport(mw.path, genSafeVariableName(mw.name))),
`export const globalMiddleware = ${genArrayFromRaw(globalMiddleware.map(mw => genSafeVariableName(mw.name)))}`,
`export const namedMiddleware = ${namedMiddlewareObject}`
].join('\n')
}
})
addTemplate({ addTemplate({
filename: 'types/middleware.d.ts', filename: 'types/middleware.d.ts',
getContents: async () => { getContents: ({ app }: { app: NuxtApp }) => {
const composablesFile = resolve(runtimeDir, 'composables') const composablesFile = resolve(runtimeDir, 'composables')
const middleware = await resolveMiddleware() const namedMiddleware = app.middleware.filter(mw => !mw.global)
const namedMiddleware = middleware.filter(mw => !mw.global)
return [ return [
'import type { NavigationGuard } from \'vue-router\'', 'import type { NavigationGuard } from \'vue-router\'',
`export type MiddlewareKey = ${namedMiddleware.map(mw => genString(mw.name)).join(' | ') || 'string'}`, `export type MiddlewareKey = ${namedMiddleware.map(mw => genString(mw.name)).join(' | ') || 'string'}`,
@ -139,7 +123,7 @@ export default defineNuxtModule({
addTemplate({ addTemplate({
filename: 'types/layouts.d.ts', filename: 'types/layouts.d.ts',
getContents: ({ app }) => { getContents: ({ app }: { app: NuxtApp }) => {
const composablesFile = resolve(runtimeDir, 'composables') const composablesFile = resolve(runtimeDir, 'composables')
return [ return [
'import { ComputedRef, Ref } from \'vue\'', 'import { ComputedRef, Ref } from \'vue\'',

View File

@ -1,10 +1,10 @@
import { basename, extname, normalize, relative, resolve } from 'pathe' import { extname, normalize, relative, resolve } from 'pathe'
import { encodePath } from 'ufo' import { encodePath } from 'ufo'
import { NuxtMiddleware, NuxtPage } from '@nuxt/schema' import { NuxtPage } from '@nuxt/schema'
import { resolveFiles, useNuxt } from '@nuxt/kit' import { resolveFiles, useNuxt } from '@nuxt/kit'
import { kebabCase } from 'scule'
import { genImport, genDynamicImport, genArrayFromRaw, genSafeVariableName } from 'knitwork' import { genImport, genDynamicImport, genArrayFromRaw, genSafeVariableName } from 'knitwork'
import escapeRE from 'escape-string-regexp' import escapeRE from 'escape-string-regexp'
import { uniqueBy } from '../core/utils'
enum SegmentParserState { enum SegmentParserState {
initial, initial,
@ -241,39 +241,3 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
})) }))
} }
} }
export async function resolveMiddleware (): Promise<NuxtMiddleware[]> {
const nuxt = useNuxt()
const middlewareDirs = nuxt.options._layers.map(
layer => resolve(layer.config.srcDir, layer.config.dir?.middleware || 'middleware')
)
const allMiddlewares = (await Promise.all(
middlewareDirs.map(async (dir) => {
const files = await resolveFiles(dir, `*{${nuxt.options.extensions.join(',')}}`)
return files.map(path => ({ name: getNameFromPath(path), path, global: hasSuffix(path, '.global') }))
})
)).flat()
return uniqueBy(allMiddlewares, 'name')
}
function getNameFromPath (path: string) {
return kebabCase(basename(path).replace(extname(path), '')).replace(/["']/g, '').replace('.global', '')
}
function hasSuffix (path: string, suffix: string) {
return basename(path).replace(extname(path), '').endsWith(suffix)
}
function uniqueBy <T, K extends keyof T> (arr: T[], key: K) {
const res: T[] = []
const seen = new Set<T[K]>()
for (const item of arr) {
if (seen.has(item[key])) { continue }
seen.add(item[key])
res.push(item)
}
return res
}

View File

@ -72,7 +72,6 @@ export interface NuxtHooks {
'app:templatesGenerated': (app: NuxtApp) => HookResult 'app:templatesGenerated': (app: NuxtApp) => HookResult
'builder:generateApp': () => HookResult 'builder:generateApp': () => HookResult
'pages:extend': (pages: NuxtPage[]) => HookResult 'pages:extend': (pages: NuxtPage[]) => HookResult
'pages:middleware:extend': (middleware: NuxtMiddleware[]) => HookResult
// Auto imports // Auto imports
'autoImports:sources': (presets: ImportPresetWithDeprecation[]) => HookResult 'autoImports:sources': (presets: ImportPresetWithDeprecation[]) => HookResult

View File

@ -1,6 +1,6 @@
import type { Hookable } from 'hookable' import type { Hookable } from 'hookable'
import type { Ignore } from 'ignore' import type { Ignore } from 'ignore'
import type { NuxtHooks, NuxtLayout } from './hooks' import type { NuxtHooks, NuxtLayout, NuxtMiddleware } from './hooks'
import type { NuxtOptions } from './config' import type { NuxtOptions } from './config'
export interface Nuxt { export interface Nuxt {
@ -58,6 +58,7 @@ export interface NuxtApp {
extensions: string[] extensions: string[]
plugins: NuxtPlugin[] plugins: NuxtPlugin[]
layouts: Record<string, NuxtLayout> layouts: Record<string, NuxtLayout>
middleware: NuxtMiddleware[]
templates: NuxtTemplate[] templates: NuxtTemplate[]
} }