feat: support async plugins and middlewares (#3884)

This commit is contained in:
Anthony Fu 2022-04-01 17:55:23 +08:00 committed by GitHub
parent 9a2fc45a11
commit 4c77c88325
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 82 additions and 18 deletions

View File

@ -59,6 +59,7 @@
"perfect-debounce": "^0.1.3",
"scule": "^0.2.1",
"ufo": "^0.8.3",
"unctx": "^1.1.4",
"unimport": "^0.1.3",
"unplugin": "^0.6.1",
"untyped": "^0.4.4",

View File

@ -3,8 +3,11 @@ import { getCurrentInstance, reactive } from 'vue'
import type { App, onErrorCaptured, VNode } from 'vue'
import { createHooks, Hookable } from 'hookable'
import type { RuntimeConfig } from '@nuxt/schema'
import { getContext } from 'unctx'
import { legacyPlugin, LegacyContext } from './compat/legacy-app'
const nuxtAppCtx = getContext<NuxtApp>('nuxt-app')
type NuxtMeta = {
htmlAttrs?: string
headAttrs?: string
@ -172,12 +175,6 @@ export function isLegacyPlugin (plugin: unknown): plugin is LegacyPlugin {
return !plugin[NuxtPluginIndicator]
}
let currentNuxtAppInstance: NuxtApp | null
export const setNuxtAppInstance = (nuxt: NuxtApp | null) => {
currentNuxtAppInstance = nuxt
}
/**
* Ensures that the setup function passed in has access to the Nuxt instance via `useNuxt`.
*
@ -185,13 +182,14 @@ export const setNuxtAppInstance = (nuxt: NuxtApp | null) => {
* @param setup The function to call
*/
export function callWithNuxt<T extends (...args: any[]) => any> (nuxt: NuxtApp | _NuxtApp, setup: T, args?: Parameters<T>) {
setNuxtAppInstance(nuxt as NuxtApp)
const p: ReturnType<T> = args ? setup(...args as Parameters<T>) : setup()
const fn = () => args ? setup(...args as Parameters<T>) : setup()
if (process.server) {
// Unset nuxt instance to prevent context-sharing in server-side
setNuxtAppInstance(null)
return nuxtAppCtx.callAsync<ReturnType<T>>(nuxt, fn)
} else {
// In client side we could assume nuxt app is singleton
nuxtAppCtx.set(nuxt)
return fn()
}
return p
}
/**
@ -201,10 +199,11 @@ export function useNuxtApp () {
const vm = getCurrentInstance()
if (!vm) {
if (!currentNuxtAppInstance) {
const nuxtAppInstance = nuxtAppCtx.use()
if (!nuxtAppInstance) {
throw new Error('nuxt instance unavailable')
}
return currentNuxtAppInstance
return nuxtAppInstance
}
return vm.appContext.app.$nuxt as NuxtApp

View File

@ -12,6 +12,7 @@ import autoImportsModule from '../auto-imports/module'
import { distDir, pkgDir } from '../dirs'
import { version } from '../../package.json'
import { ImportProtectionPlugin, vueAppPatterns } from './plugins/import-protection'
import { UnctxTransformPlugin } from './plugins/unctx'
import { addModuleTranspiles } from './modules'
export function createNuxt (options: NuxtOptions): Nuxt {
@ -57,7 +58,6 @@ async function initNuxt (nuxt: Nuxt) {
})
// Add import protection
const config = {
rootDir: nuxt.options.rootDir,
patterns: vueAppPatterns(nuxt)
@ -65,6 +65,10 @@ async function initNuxt (nuxt: Nuxt) {
addVitePlugin(ImportProtectionPlugin.vite(config))
addWebpackPlugin(ImportProtectionPlugin.webpack(config))
// Add unctx transform
addVitePlugin(UnctxTransformPlugin(nuxt).vite())
addWebpackPlugin(UnctxTransformPlugin(nuxt).webpack())
// Init user modules
await nuxt.callHook('modules:before', { nuxt } as ModuleContainer)
const modulesToInstall = [

View File

@ -0,0 +1,31 @@
import { Nuxt, NuxtApp, NuxtMiddleware } from '@nuxt/schema'
import { createTransformer } from 'unctx/transform'
import { createUnplugin } from 'unplugin'
export const UnctxTransformPlugin = (nuxt: Nuxt) => {
const transformer = createTransformer({
asyncFunctions: ['defineNuxtPlugin', 'defineNuxtRouteMiddleware']
})
let app: NuxtApp | undefined
let middleware: NuxtMiddleware[] = []
nuxt.hook('app:resolve', (_app) => { app = _app })
nuxt.hook('pages:middleware:extend', (_middlewares) => { middleware = _middlewares })
return createUnplugin(() => ({
name: 'unctx:transfrom',
enforce: 'post',
transformInclude (id) {
return Boolean(app?.plugins.find(i => i.src === id) || middleware.find(m => m.path === id))
},
transform (code, id) {
const result = transformer.transform(code)
if (result) {
return {
code: result.code,
map: result.magicString.generateMap({ source: id, includeContent: true })
}
}
}
}))
}

View File

@ -40,8 +40,6 @@ describe('pages', () => {
// composables auto import
expect(html).toContain('Composable | foo: auto imported from ~/components/foo.ts')
expect(html).toContain('Composable | bar: auto imported from ~/components/useBar.ts')
// plugins
expect(html).toContain('Plugin | myPlugin: Injected by my-plugin')
// should import components
expect(html).toContain('This is a custom component with a named export.')
})
@ -146,6 +144,18 @@ describe('middlewares', () => {
})
})
describe('plugins', () => {
it('basic plugin', async () => {
const html = await $fetch('/plugins')
expect(html).toContain('myPlugin: Injected by my-plugin')
})
it('async plugin', async () => {
const html = await $fetch('/plugins')
expect(html).toContain('asyncPlugin: Async plugin works! 123')
})
})
describe('layouts', () => {
it('should apply custom layout', async () => {
const html = await $fetch('/with-layout')

View File

@ -1,5 +1,6 @@
export default defineNuxtRouteMiddleware((to) => {
export default defineNuxtRouteMiddleware(async (to) => {
if (to.path.startsWith('/redirect/')) {
await new Promise(resolve => setTimeout(resolve, 100))
return navigateTo(to.path.slice('/redirect/'.length - 1))
}
})

View File

@ -7,7 +7,6 @@
<div>RuntimeConfig | testConfig: {{ config.testConfig }}</div>
<div>Composable | foo: {{ foo }}</div>
<div>Composable | bar: {{ bar }}</div>
<div>Plugin | myPlugin: {{ $myPlugin() }}</div>
<SugarCounter :count="12" />
<CustomComponent />
</div>

6
test/fixtures/basic/pages/plugins.vue vendored Normal file
View File

@ -0,0 +1,6 @@
<template>
<div>
<div>myPlugin: {{ $myPlugin() }}</div>
<div>asyncPlugin: {{ $asyncPlugin() }}</div>
</div>
</template>

View File

@ -0,0 +1,12 @@
export default defineNuxtPlugin(async (/* nuxtApp */) => {
const config1 = useRuntimeConfig()
await new Promise(resolve => setTimeout(resolve, 100))
const config2 = useRuntimeConfig()
return {
provide: {
asyncPlugin: () => config1 && config1 === config2
? 'Async plugin works! ' + config1.testConfig
: 'Async plugin failed!'
}
}
})

View File

@ -15500,6 +15500,7 @@ __metadata:
scule: ^0.2.1
ufo: ^0.8.3
unbuild: latest
unctx: ^1.1.4
unimport: ^0.1.3
unplugin: ^0.6.1
untyped: ^0.4.4