Skip to content

Commit

Permalink
feat: add support for global middleware (#938)
Browse files Browse the repository at this point in the history
  • Loading branch information
wattanx authored Oct 28, 2023
1 parent d17c49d commit de16a00
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 14 deletions.
3 changes: 3 additions & 0 deletions packages/bridge/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { resolveImports } from 'mlly'
import { componentsTypeTemplate, schemaTemplate, middlewareTypeTemplate } from './type-templates'
import { distDir } from './dirs'
import { VueCompat } from './vue-compat'
import { globalMiddlewareTemplate } from './global-middleware-template'

export async function setupAppBridge (_options: any) {
const nuxt = useNuxt()
Expand Down Expand Up @@ -102,6 +103,8 @@ export async function setupAppBridge (_options: any) {
}
})

addTemplate(globalMiddlewareTemplate)

// Alias vue3 utilities to vue2
const { dst: vueCompat } = addTemplate({ src: resolve(distDir, 'runtime/vue2-bridge.mjs') })
addWebpackPlugin(VueCompat.webpack({ src: vueCompat }))
Expand Down
30 changes: 30 additions & 0 deletions packages/bridge/src/global-middleware-template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

import type { Nuxt, NuxtApp } from '@nuxt/schema'
import { genArrayFromRaw, genImport, genSafeVariableName } from 'knitwork'
import { resolveFiles } from '@nuxt/kit'
import { getNameFromPath, hasSuffix } from './utils/names'

interface TemplateContext {
nuxt: Nuxt
app: NuxtApp & { templateVars: Record<string, any> }
}

export const globalMiddlewareTemplate = {
filename: 'global-middleware.mjs',
getContents: async ({ nuxt, app }: TemplateContext) => {
app.middleware = []
const middlewareDir = nuxt.options.dir.middleware || 'middleware'
const middlewareFiles = await resolveFiles(nuxt.options.srcDir, `${middlewareDir}/*{${nuxt.options.extensions.join(',')}}`)
app.middleware.push(...middlewareFiles.map((file) => {
const name = getNameFromPath(file)
return { name, path: file, global: hasSuffix(file, '.global') }
}))

const globalMiddleware = app.middleware.filter(mw => mw.global)

return [
...globalMiddleware.map(mw => genImport(mw.path, genSafeVariableName(mw.name))),
`export const globalMiddleware = ${genArrayFromRaw(globalMiddleware.map(mw => genSafeVariableName(mw.name)))}`
].join('\n')
}
}
8 changes: 5 additions & 3 deletions packages/bridge/src/runtime/app.plugin.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Vue, { version } from 'vue'
import { createHooks } from 'hookable'
import { callWithNuxt, setNuxtAppInstance } from '#app'
import { setNuxtAppInstance } from '#app'
import { globalMiddleware } from '#build/global-middleware'

// Reshape payload to match key `useLazyAsyncData` expects
function proxiedState (state) {
Expand Down Expand Up @@ -58,16 +59,17 @@ export default async (ctx, inject) => {
nuxtApp.callHook = nuxtApp.hooks.callHook

const middleware = await import('#build/middleware').then(r => r.default)

nuxtApp._middleware = nuxtApp._middleware || {
global: [],
global: globalMiddleware,
named: middleware
}

ctx.app.router.beforeEach(async (to, from, next) => {
nuxtApp._processingMiddleware = true

for (const middleware of nuxtApp._middleware.global) {
const result = await callWithNuxt(nuxtApp, middleware, [to, from])
const result = await middleware(ctx)
if (result || result === false) { return next(result) }
}

Expand Down
43 changes: 43 additions & 0 deletions packages/bridge/src/utils/names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { basename, dirname, extname, normalize } from 'pathe'
import { kebabCase, pascalCase, splitByCase } from 'scule'
import { withTrailingSlash } from 'ufo'

export function getNameFromPath (path: string, relativeTo?: string) {
const relativePath = relativeTo
? normalize(path).replace(withTrailingSlash(normalize(relativeTo)), '')
: basename(path)
const prefixParts = splitByCase(dirname(relativePath))
const fileName = basename(relativePath, extname(relativePath))
return kebabCase(resolveComponentName(fileName.toLowerCase() === 'index' ? '' : fileName, prefixParts)).replace(/["']/g, '')
}

export function hasSuffix (path: string, suffix: string) {
return basename(path).replace(extname(path), '').endsWith(suffix)
}

export function resolveComponentName (fileName: string, prefixParts: string[]) {
/**
* Array of fileName parts splitted by case, / or -
* @example third-component -> ['third', 'component']
* @example AwesomeComponent -> ['Awesome', 'Component']
*/
const fileNameParts = splitByCase(fileName)
const fileNamePartsContent = fileNameParts.join('/').toLowerCase()
const componentNameParts: string[] = [...prefixParts]
let index = prefixParts.length - 1
const matchedSuffix: string[] = []
while (index >= 0) {
matchedSuffix.unshift(...splitByCase(prefixParts[index] || '').map(p => p.toLowerCase()))
const matchedSuffixContent = matchedSuffix.join('/')
if ((fileNamePartsContent === matchedSuffixContent || fileNamePartsContent.startsWith(matchedSuffixContent + '/')) ||
// e.g Item/Item/Item.vue -> Item
(prefixParts[index].toLowerCase() === fileNamePartsContent &&
prefixParts[index + 1] &&
prefixParts[index] === prefixParts[index + 1])) {
componentNameParts.length = index
}
index--
}

return pascalCase(componentNameParts) + pascalCase(fileNameParts)
}
16 changes: 16 additions & 0 deletions playground/middleware/redirect.global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { withoutTrailingSlash } from 'ufo'

export default defineNuxtRouteMiddleware((to) => {
if (useRequestHeaders(['trailing-slash'])['trailing-slash'] && to.fullPath.endsWith('/')) {
return navigateTo(withoutTrailingSlash(to.fullPath), { redirectCode: 307 })
}
if (to.path.startsWith('/redirect/')) {
return navigateTo('/navigation-target')
}
if (to.path === '/navigate-to-external') {
return navigateTo('/', { external: true })
}
if (to.path === '/navigate-to-false') {
return false
}
})
8 changes: 1 addition & 7 deletions playground/middleware/redirect.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineNuxtRouteMiddleware, navigateTo, abortNavigation } from '#app'
import { defineNuxtRouteMiddleware, abortNavigation } from '#app'

export default defineNuxtRouteMiddleware((to) => {
if ('abort' in to.query) {
Expand All @@ -7,10 +7,4 @@ export default defineNuxtRouteMiddleware((to) => {
fatal: true
})
}
if (to.path === '/navigate-to-external') {
return navigateTo('/', { external: true })
}
if (to.path === '/redirect') {
return navigateTo('/navigation-target')
}
})
23 changes: 19 additions & 4 deletions test/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,19 @@ describe('middleware', () => {
})

it('should redirect to navigation-target', async () => {
const html = await $fetch('/redirect')
const html = await $fetch('/redirect/')

expect(html).toContain('Navigated successfully')
})

it('should redirect to navigation-target', async () => {
const html = await $fetch('/add-route-middleware')

expect(html).toContain('Navigated successfully')
})
})

describe('navigate', () => {
it('should not overwrite headers', async () => {
const { headers, status } = await fetch('/navigate-to-external', { redirect: 'manual' })

Expand All @@ -189,10 +197,17 @@ describe('middleware', () => {
expect(res.status).toEqual(401)
})

it('should redirect to navigation-target', async () => {
const html = await $fetch('/add-route-middleware')
it('respects redirects + headers in middleware', async () => {
const res = await fetch('/navigate-some-path/', { redirect: 'manual', headers: { 'trailing-slash': 'true' } })
expect(res.headers.get('location')).toEqual('/navigate-some-path')
expect(res.status).toEqual(307)
expect(await res.text()).toMatchInlineSnapshot('"<!DOCTYPE html><html><head><meta http-equiv=\\"refresh\\" content=\\"0; url=/navigate-some-path\\"></head></html>"')
})

expect(html).toContain('Navigated successfully')
it('supports directly aborting navigation on SSR', async () => {
const { status } = await fetch('/navigate-to-false', { redirect: 'manual' })

expect(status).toEqual(404)
})
})

Expand Down

0 comments on commit de16a00

Please sign in to comment.