mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 05:35:13 +00:00
feat(nuxt): allow plugins to specify dependencies (#24127)
This commit is contained in:
parent
02306fd13d
commit
5877e11c89
@ -101,9 +101,11 @@ In case you're new to 'alphabetical' numbering, remember that filenames are sort
|
|||||||
|
|
||||||
## Loading Strategy
|
## Loading Strategy
|
||||||
|
|
||||||
|
### Parallel Plugins
|
||||||
|
|
||||||
By default, Nuxt loads plugins sequentially. You can define a plugin as `parallel` so Nuxt won't wait the end of the plugin's execution before loading the next plugin.
|
By default, Nuxt loads plugins sequentially. You can define a plugin as `parallel` so Nuxt won't wait the end of the plugin's execution before loading the next plugin.
|
||||||
|
|
||||||
```ts [plugins/hello.ts]
|
```ts [plugins/my-plugin.ts]
|
||||||
export default defineNuxtPlugin({
|
export default defineNuxtPlugin({
|
||||||
name: 'my-plugin',
|
name: 'my-plugin',
|
||||||
parallel: true,
|
parallel: true,
|
||||||
@ -113,6 +115,20 @@ export default defineNuxtPlugin({
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Plugins With Dependencies
|
||||||
|
|
||||||
|
If a plugin needs to await a parallel plugin before it runs, you can add the plugin's name to the `dependsOn` array.
|
||||||
|
|
||||||
|
```ts [plugins/depending-on-my-plugin.ts]
|
||||||
|
export default defineNuxtPlugin({
|
||||||
|
name: 'depends-on-my-plugin',
|
||||||
|
dependsOn: ['my-plugin']
|
||||||
|
async setup (nuxtApp) {
|
||||||
|
// this plugin will wait for the end of `my-plugin`'s execution before it runs
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
## Using Composables
|
## Using Composables
|
||||||
|
|
||||||
You can use [composables](/docs/guide/directory-structure/composables) as well as [utils](/docs/guide/directory-structure/utils) within Nuxt plugins:
|
You can use [composables](/docs/guide/directory-structure/composables) as well as [utils](/docs/guide/directory-structure/utils) within Nuxt plugins:
|
||||||
|
@ -7,9 +7,7 @@ export * from './composables/index'
|
|||||||
export * from './components/index'
|
export * from './components/index'
|
||||||
export * from './config'
|
export * from './config'
|
||||||
export * from './compat/idle-callback'
|
export * from './compat/idle-callback'
|
||||||
|
export * from './types'
|
||||||
// eslint-disable-next-line import/no-restricted-paths
|
|
||||||
export type { PageMeta } from '../pages/runtime/index'
|
|
||||||
|
|
||||||
export const isVue2 = false
|
export const isVue2 = false
|
||||||
export const isVue3 = true
|
export const isVue3 = true
|
||||||
|
@ -18,6 +18,8 @@ import type { NuxtError } from '../app/composables/error'
|
|||||||
import type { AsyncDataRequestStatus } from '../app/composables/asyncData'
|
import type { AsyncDataRequestStatus } from '../app/composables/asyncData'
|
||||||
import type { NuxtAppManifestMeta } from '../app/composables/manifest'
|
import type { NuxtAppManifestMeta } from '../app/composables/manifest'
|
||||||
|
|
||||||
|
import type { NuxtAppLiterals } from '#app'
|
||||||
|
|
||||||
const nuxtAppCtx = /*@__PURE__*/ getContext<NuxtApp>('nuxt-app', {
|
const nuxtAppCtx = /*@__PURE__*/ getContext<NuxtApp>('nuxt-app', {
|
||||||
asyncContext: !!__NUXT_ASYNC_CONTEXT__ && import.meta.server
|
asyncContext: !!__NUXT_ASYNC_CONTEXT__ && import.meta.server
|
||||||
})
|
})
|
||||||
@ -154,6 +156,10 @@ export const NuxtPluginIndicator = '__nuxt_plugin'
|
|||||||
export interface PluginMeta {
|
export interface PluginMeta {
|
||||||
name?: string
|
name?: string
|
||||||
enforce?: 'pre' | 'default' | 'post'
|
enforce?: 'pre' | 'default' | 'post'
|
||||||
|
/**
|
||||||
|
* Await for other named plugins to finish before running this plugin.
|
||||||
|
*/
|
||||||
|
dependsOn?: NuxtAppLiterals['pluginName'][]
|
||||||
/**
|
/**
|
||||||
* This allows more granular control over plugin order and should only be used by advanced users.
|
* This allows more granular control over plugin order and should only be used by advanced users.
|
||||||
* It overrides the value of `enforce` and is used to sort plugins.
|
* It overrides the value of `enforce` and is used to sort plugins.
|
||||||
@ -190,6 +196,10 @@ export interface ObjectPlugin<Injections extends Record<string, unknown> = Recor
|
|||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
parallel?: boolean
|
parallel?: boolean
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_name?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated Use `ObjectPlugin` */
|
/** @deprecated Use `ObjectPlugin` */
|
||||||
@ -331,26 +341,61 @@ export async function applyPlugin (nuxtApp: NuxtApp, plugin: Plugin & ObjectPlug
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function applyPlugins (nuxtApp: NuxtApp, plugins: Array<Plugin & ObjectPlugin<any>>) {
|
export async function applyPlugins (nuxtApp: NuxtApp, plugins: Array<Plugin & ObjectPlugin<any>>) {
|
||||||
|
const resolvedPlugins: string[] = []
|
||||||
|
const unresolvedPlugins: [Set<string>, Plugin & ObjectPlugin<any>][] = []
|
||||||
const parallels: Promise<any>[] = []
|
const parallels: Promise<any>[] = []
|
||||||
const errors: Error[] = []
|
const errors: Error[] = []
|
||||||
for (const plugin of plugins) {
|
let promiseDepth = 0
|
||||||
if (import.meta.server && nuxtApp.ssrContext?.islandContext && plugin.env?.islands === false) { continue }
|
|
||||||
const promise = applyPlugin(nuxtApp, plugin)
|
async function executePlugin (plugin: Plugin & ObjectPlugin<any>) {
|
||||||
|
if (plugin.dependsOn && !plugin.dependsOn.every(name => resolvedPlugins.includes(name))) {
|
||||||
|
unresolvedPlugins.push([new Set(plugin.dependsOn), plugin])
|
||||||
|
} else {
|
||||||
|
const promise = applyPlugin(nuxtApp, plugin).then(async () => {
|
||||||
|
if (plugin._name) {
|
||||||
|
resolvedPlugins.push(plugin._name)
|
||||||
|
await Promise.all(unresolvedPlugins.map(async ([dependsOn, unexecutedPlugin]) => {
|
||||||
|
if (dependsOn.has(plugin._name!)) {
|
||||||
|
dependsOn.delete(plugin._name!)
|
||||||
|
if (dependsOn.size === 0) {
|
||||||
|
promiseDepth++
|
||||||
|
await executePlugin(unexecutedPlugin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (plugin.parallel) {
|
if (plugin.parallel) {
|
||||||
parallels.push(promise.catch(e => errors.push(e)))
|
parallels.push(promise.catch(e => errors.push(e)))
|
||||||
} else {
|
} else {
|
||||||
await promise
|
await promise
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const plugin of plugins) {
|
||||||
|
if (import.meta.server && nuxtApp.ssrContext?.islandContext && plugin.env?.islands === false) { continue }
|
||||||
|
await executePlugin(plugin)
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(parallels)
|
await Promise.all(parallels)
|
||||||
|
if (promiseDepth) {
|
||||||
|
for (let i = 0; i < promiseDepth; i++) {
|
||||||
|
await Promise.all(parallels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (errors.length) { throw errors[0] }
|
if (errors.length) { throw errors[0] }
|
||||||
}
|
}
|
||||||
|
|
||||||
/*@__NO_SIDE_EFFECTS__*/
|
/*@__NO_SIDE_EFFECTS__*/
|
||||||
export function defineNuxtPlugin<T extends Record<string, unknown>> (plugin: Plugin<T> | ObjectPlugin<T>): Plugin<T> & ObjectPlugin<T> {
|
export function defineNuxtPlugin<T extends Record<string, unknown>> (plugin: Plugin<T> | ObjectPlugin<T>): Plugin<T> & ObjectPlugin<T> {
|
||||||
if (typeof plugin === 'function') { return plugin }
|
if (typeof plugin === 'function') { return plugin }
|
||||||
|
|
||||||
|
const _name = plugin._name || plugin.name
|
||||||
delete plugin.name
|
delete plugin.name
|
||||||
return Object.assign(plugin.setup || (() => {}), plugin, { [NuxtPluginIndicator]: true } as const)
|
return Object.assign(plugin.setup || (() => {}), plugin, { [NuxtPluginIndicator]: true, _name } as const)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*@__NO_SIDE_EFFECTS__*/
|
/*@__NO_SIDE_EFFECTS__*/
|
||||||
|
6
packages/nuxt/src/app/types.ts
Normal file
6
packages/nuxt/src/app/types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// eslint-disable-next-line import/no-restricted-paths
|
||||||
|
export type { PageMeta } from '../pages/runtime/index'
|
||||||
|
|
||||||
|
export interface NuxtAppLiterals {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
@ -8,6 +8,8 @@ import * as defaultTemplates from './templates'
|
|||||||
import { getNameFromPath, hasSuffix, uniqueBy } from './utils'
|
import { getNameFromPath, hasSuffix, uniqueBy } from './utils'
|
||||||
import { extractMetadata, orderMap } from './plugins/plugin-metadata'
|
import { extractMetadata, orderMap } from './plugins/plugin-metadata'
|
||||||
|
|
||||||
|
import type { PluginMeta } from '#app'
|
||||||
|
|
||||||
export function createApp (nuxt: Nuxt, options: Partial<NuxtApp> = {}): NuxtApp {
|
export function createApp (nuxt: Nuxt, options: Partial<NuxtApp> = {}): NuxtApp {
|
||||||
return defu(options, {
|
return defu(options, {
|
||||||
dir: nuxt.options.srcDir,
|
dir: nuxt.options.srcDir,
|
||||||
@ -185,7 +187,7 @@ function resolvePaths<Item extends Record<string, any>> (items: Item[], key: { [
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function annotatePlugins (nuxt: Nuxt, plugins: NuxtPlugin[]) {
|
export async function annotatePlugins (nuxt: Nuxt, plugins: NuxtPlugin[]) {
|
||||||
const _plugins: NuxtPlugin[] = []
|
const _plugins: Array<NuxtPlugin & Omit<PluginMeta, 'enforce'>> = []
|
||||||
for (const plugin of plugins) {
|
for (const plugin of plugins) {
|
||||||
try {
|
try {
|
||||||
const code = plugin.src in nuxt.vfs ? nuxt.vfs[plugin.src] : await fsp.readFile(plugin.src!, 'utf-8')
|
const code = plugin.src in nuxt.vfs ? nuxt.vfs[plugin.src] : await fsp.readFile(plugin.src!, 'utf-8')
|
||||||
@ -201,3 +203,29 @@ export async function annotatePlugins (nuxt: Nuxt, plugins: NuxtPlugin[]) {
|
|||||||
|
|
||||||
return _plugins.sort((a, b) => (a.order ?? orderMap.default) - (b.order ?? orderMap.default))
|
return _plugins.sort((a, b) => (a.order ?? orderMap.default) - (b.order ?? orderMap.default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkForCircularDependencies (_plugins: Array<NuxtPlugin & Omit<PluginMeta, 'enforce'>>) {
|
||||||
|
const deps: Record<string, string[]> = Object.create(null)
|
||||||
|
const pluginNames = _plugins.map(plugin => plugin.name)
|
||||||
|
for (const plugin of _plugins) {
|
||||||
|
// Make sure dependency plugins are registered
|
||||||
|
if (plugin.dependsOn && plugin.dependsOn.some(name => !pluginNames.includes(name))) {
|
||||||
|
console.error(`Plugin \`${plugin.name}\` depends on \`${plugin.dependsOn.filter(name => !pluginNames.includes(name)).join(', ')}\` but they are not registered.`)
|
||||||
|
}
|
||||||
|
// Make graph to detect circular dependencies
|
||||||
|
if (plugin.name) {
|
||||||
|
deps[plugin.name] = plugin.dependsOn || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const checkDeps = (name: string, visited: string[] = []): string[] => {
|
||||||
|
if (visited.includes(name)) {
|
||||||
|
console.error(`Circular dependency detected in plugins: ${visited.join(' -> ')} -> ${name}`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
visited.push(name)
|
||||||
|
return (deps[name] || []).flatMap(dep => checkDeps(dep, [...visited]))
|
||||||
|
}
|
||||||
|
for (const name in deps) {
|
||||||
|
checkDeps(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { CallExpression, Property, SpreadElement } from 'estree'
|
import type { CallExpression, Literal, Property, SpreadElement } from 'estree'
|
||||||
import type { Node } from 'estree-walker'
|
import type { Node } from 'estree-walker'
|
||||||
import { walk } from 'estree-walker'
|
import { walk } from 'estree-walker'
|
||||||
import { transform } from 'esbuild'
|
import { transform } from 'esbuild'
|
||||||
@ -87,7 +87,8 @@ type PluginMetaKey = keyof PluginMeta
|
|||||||
const keys: Record<PluginMetaKey, string> = {
|
const keys: Record<PluginMetaKey, string> = {
|
||||||
name: 'name',
|
name: 'name',
|
||||||
order: 'order',
|
order: 'order',
|
||||||
enforce: 'enforce'
|
enforce: 'enforce',
|
||||||
|
dependsOn: 'dependsOn'
|
||||||
}
|
}
|
||||||
function isMetadataKey (key: string): key is PluginMetaKey {
|
function isMetadataKey (key: string): key is PluginMetaKey {
|
||||||
return key in keys
|
return key in keys
|
||||||
@ -107,6 +108,12 @@ function extractMetaFromObject (properties: Array<Property | SpreadElement>) {
|
|||||||
if (property.value.type === 'UnaryExpression' && property.value.argument.type === 'Literal') {
|
if (property.value.type === 'UnaryExpression' && property.value.argument.type === 'Literal') {
|
||||||
meta[propertyKey] = JSON.parse(property.value.operator + property.value.argument.raw!)
|
meta[propertyKey] = JSON.parse(property.value.operator + property.value.argument.raw!)
|
||||||
}
|
}
|
||||||
|
if (propertyKey === 'dependsOn' && property.value.type === 'ArrayExpression') {
|
||||||
|
if (property.value.elements.some(e => !e || e.type !== 'Literal' || typeof e.value !== 'string')) {
|
||||||
|
throw new Error('dependsOn must take an array of string literals')
|
||||||
|
}
|
||||||
|
meta[propertyKey] = property.value.elements.map(e => (e as Literal)!.value as string)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return meta
|
return meta
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import { hash } from 'ohash'
|
|||||||
import { camelCase } from 'scule'
|
import { camelCase } from 'scule'
|
||||||
import { filename } from 'pathe/utils'
|
import { filename } from 'pathe/utils'
|
||||||
import type { Nuxt, NuxtApp, NuxtTemplate } from 'nuxt/schema'
|
import type { Nuxt, NuxtApp, NuxtTemplate } from 'nuxt/schema'
|
||||||
import { annotatePlugins } from './app'
|
import { annotatePlugins, checkForCircularDependencies } from './app'
|
||||||
|
|
||||||
interface TemplateContext {
|
interface TemplateContext {
|
||||||
nuxt: Nuxt
|
nuxt: Nuxt
|
||||||
@ -62,7 +62,7 @@ export const clientPluginTemplate: NuxtTemplate<TemplateContext> = {
|
|||||||
filename: 'plugins/client.mjs',
|
filename: 'plugins/client.mjs',
|
||||||
async getContents (ctx) {
|
async getContents (ctx) {
|
||||||
const clientPlugins = await annotatePlugins(ctx.nuxt, ctx.app.plugins.filter(p => !p.mode || p.mode !== 'server'))
|
const clientPlugins = await annotatePlugins(ctx.nuxt, ctx.app.plugins.filter(p => !p.mode || p.mode !== 'server'))
|
||||||
await annotatePlugins(ctx.nuxt, clientPlugins)
|
checkForCircularDependencies(clientPlugins)
|
||||||
const exports: string[] = []
|
const exports: string[] = []
|
||||||
const imports: string[] = []
|
const imports: string[] = []
|
||||||
for (const plugin of clientPlugins) {
|
for (const plugin of clientPlugins) {
|
||||||
@ -82,6 +82,7 @@ export const serverPluginTemplate: NuxtTemplate<TemplateContext> = {
|
|||||||
filename: 'plugins/server.mjs',
|
filename: 'plugins/server.mjs',
|
||||||
async getContents (ctx) {
|
async getContents (ctx) {
|
||||||
const serverPlugins = await annotatePlugins(ctx.nuxt, ctx.app.plugins.filter(p => !p.mode || p.mode !== 'client'))
|
const serverPlugins = await annotatePlugins(ctx.nuxt, ctx.app.plugins.filter(p => !p.mode || p.mode !== 'client'))
|
||||||
|
checkForCircularDependencies(serverPlugins)
|
||||||
const exports: string[] = []
|
const exports: string[] = []
|
||||||
const imports: string[] = []
|
const imports: string[] = []
|
||||||
for (const plugin of serverPlugins) {
|
for (const plugin of serverPlugins) {
|
||||||
@ -99,7 +100,7 @@ export const serverPluginTemplate: NuxtTemplate<TemplateContext> = {
|
|||||||
|
|
||||||
export const pluginsDeclaration: NuxtTemplate<TemplateContext> = {
|
export const pluginsDeclaration: NuxtTemplate<TemplateContext> = {
|
||||||
filename: 'types/plugins.d.ts',
|
filename: 'types/plugins.d.ts',
|
||||||
getContents: (ctx) => {
|
getContents: async (ctx) => {
|
||||||
const EXTENSION_RE = new RegExp(`(?<=\\w)(${ctx.nuxt.options.extensions.map(e => escapeRE(e)).join('|')})$`, 'g')
|
const EXTENSION_RE = new RegExp(`(?<=\\w)(${ctx.nuxt.options.extensions.map(e => escapeRE(e)).join('|')})$`, 'g')
|
||||||
const tsImports: string[] = []
|
const tsImports: string[] = []
|
||||||
for (const p of ctx.app.plugins) {
|
for (const p of ctx.app.plugins) {
|
||||||
@ -111,6 +112,8 @@ export const pluginsDeclaration: NuxtTemplate<TemplateContext> = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pluginsName = (await annotatePlugins(ctx.nuxt, ctx.app.plugins)).filter(p => p.name).map(p => `'${p.name}'`)
|
||||||
|
|
||||||
return `// Generated by Nuxt'
|
return `// Generated by Nuxt'
|
||||||
import type { Plugin } from '#app'
|
import type { Plugin } from '#app'
|
||||||
|
|
||||||
@ -122,6 +125,10 @@ type NuxtAppInjections = \n ${tsImports.map(p => `InjectionType<typeof ${genDyn
|
|||||||
|
|
||||||
declare module '#app' {
|
declare module '#app' {
|
||||||
interface NuxtApp extends NuxtAppInjections { }
|
interface NuxtApp extends NuxtAppInjections { }
|
||||||
|
|
||||||
|
interface NuxtAppLiterals {
|
||||||
|
pluginName: ${pluginsName.join(' | ')}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import { parse } from 'acorn'
|
import { parse } from 'acorn'
|
||||||
|
|
||||||
import { RemovePluginMetadataPlugin, extractMetadata } from '../src/core/plugins/plugin-metadata'
|
import { RemovePluginMetadataPlugin, extractMetadata } from '../src/core/plugins/plugin-metadata'
|
||||||
|
import { checkForCircularDependencies } from '../src/core/app'
|
||||||
|
|
||||||
describe('plugin-metadata', () => {
|
describe('plugin-metadata', () => {
|
||||||
it('should extract metadata from object-syntax plugins', async () => {
|
it('should extract metadata from object-syntax plugins', async () => {
|
||||||
@ -63,3 +64,51 @@ describe('plugin-metadata', () => {
|
|||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('plugin sanity checking', () => {
|
||||||
|
it('non-existent depends are warned', () => {
|
||||||
|
vi.spyOn(console, 'error')
|
||||||
|
checkForCircularDependencies([
|
||||||
|
{
|
||||||
|
name: 'A',
|
||||||
|
src: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'B',
|
||||||
|
dependsOn: ['D'],
|
||||||
|
src: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'C',
|
||||||
|
src: ''
|
||||||
|
}
|
||||||
|
])
|
||||||
|
expect(console.error).toBeCalledWith('Plugin `B` depends on `D` but they are not registered.')
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('circular dependencies are warned', () => {
|
||||||
|
vi.spyOn(console, 'error')
|
||||||
|
checkForCircularDependencies([
|
||||||
|
{
|
||||||
|
name: 'A',
|
||||||
|
dependsOn: ['B'],
|
||||||
|
src: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'B',
|
||||||
|
dependsOn: ['C'],
|
||||||
|
src: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'C',
|
||||||
|
dependsOn: ['A'],
|
||||||
|
src: ''
|
||||||
|
}
|
||||||
|
])
|
||||||
|
expect(console.error).toBeCalledWith('Circular dependency detected in plugins: A -> B -> C -> A')
|
||||||
|
expect(console.error).toBeCalledWith('Circular dependency detected in plugins: B -> C -> A -> B')
|
||||||
|
expect(console.error).toBeCalledWith('Circular dependency detected in plugins: C -> A -> B -> C')
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -16,6 +16,10 @@ export interface NuxtPlugin {
|
|||||||
* Default Nuxt priorities can be seen at [here](https://github.com/nuxt/nuxt/blob/9904849bc87c53dfbd3ea3528140a5684c63c8d8/packages/nuxt/src/core/plugins/plugin-metadata.ts#L15-L34).
|
* Default Nuxt priorities can be seen at [here](https://github.com/nuxt/nuxt/blob/9904849bc87c53dfbd3ea3528140a5684c63c8d8/packages/nuxt/src/core/plugins/plugin-metadata.ts#L15-L34).
|
||||||
*/
|
*/
|
||||||
order?: number
|
order?: number
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
name?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal type for simpler NuxtTemplate interface extension
|
// Internal type for simpler NuxtTemplate interface extension
|
||||||
|
@ -32,7 +32,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
|||||||
const serverDir = join(rootDir, '.output/server')
|
const serverDir = join(rootDir, '.output/server')
|
||||||
|
|
||||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"199k"')
|
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"200k"`)
|
||||||
|
|
||||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"1847k"')
|
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"1847k"')
|
||||||
@ -71,7 +71,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
|||||||
const serverDir = join(rootDir, '.output-inline/server')
|
const serverDir = join(rootDir, '.output-inline/server')
|
||||||
|
|
||||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"510k"')
|
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"511k"`)
|
||||||
|
|
||||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"77.0k"')
|
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"77.0k"')
|
||||||
|
12
test/fixtures/basic-types/types.ts
vendored
12
test/fixtures/basic-types/types.ts
vendored
@ -242,6 +242,18 @@ describe('nuxtApp', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('plugins', () => {
|
||||||
|
it('dependsOn is strongly typed', () => {
|
||||||
|
defineNuxtPlugin({
|
||||||
|
// @ts-expect-error invalid plugin name
|
||||||
|
dependsOn: ['something']
|
||||||
|
})
|
||||||
|
defineNuxtPlugin({
|
||||||
|
dependsOn: ['nuxt:router']
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('runtimeConfig', () => {
|
describe('runtimeConfig', () => {
|
||||||
it('generated runtimeConfig types', () => {
|
it('generated runtimeConfig types', () => {
|
||||||
const runtimeConfig = useRuntimeConfig()
|
const runtimeConfig = useRuntimeConfig()
|
||||||
|
6
test/fixtures/basic/plugins/async-plugin.ts
vendored
6
test/fixtures/basic/plugins/async-plugin.ts
vendored
@ -1,4 +1,6 @@
|
|||||||
export default defineNuxtPlugin(async (/* nuxtApp */) => {
|
export default defineNuxtPlugin({
|
||||||
|
name: 'async-plugin',
|
||||||
|
async setup (/* nuxtApp */) {
|
||||||
const config1 = useRuntimeConfig()
|
const config1 = useRuntimeConfig()
|
||||||
await new Promise(resolve => setTimeout(resolve, 100))
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
const { data } = useFetch('/api/hey', { key: 'hey' })
|
const { data } = useFetch('/api/hey', { key: 'hey' })
|
||||||
@ -10,4 +12,6 @@ export default defineNuxtPlugin(async (/* nuxtApp */) => {
|
|||||||
: 'Async plugin failed!'
|
: 'Async plugin failed!'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
parallel: true
|
||||||
})
|
})
|
||||||
|
12
test/fixtures/basic/plugins/dependsOnPlugin.ts
vendored
Normal file
12
test/fixtures/basic/plugins/dependsOnPlugin.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export default defineNuxtPlugin({
|
||||||
|
name: 'depends-on-plugin',
|
||||||
|
dependsOn: ['async-plugin'],
|
||||||
|
async setup () {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
if (!nuxtApp.$asyncPlugin) {
|
||||||
|
throw new Error('$asyncPlugin is not defined')
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
|
},
|
||||||
|
parallel: true
|
||||||
|
})
|
247
test/nuxt/plugin.test.ts
Normal file
247
test/nuxt/plugin.test.ts
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import { applyPlugins } from '#app/nuxt'
|
||||||
|
import { defineNuxtPlugin } from '#app'
|
||||||
|
|
||||||
|
vi.mock('#app', async (original) => {
|
||||||
|
return {
|
||||||
|
...(await original<typeof import('#app')>()),
|
||||||
|
applyPlugin: vi.fn(async (_nuxtApp, plugin) => {
|
||||||
|
await plugin()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function pluginFactory (name: string, dependsOn?: string[], sequence: string[], parallel = true) {
|
||||||
|
return defineNuxtPlugin({
|
||||||
|
name,
|
||||||
|
dependsOn,
|
||||||
|
async setup () {
|
||||||
|
sequence.push(`start ${name}`)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10))
|
||||||
|
sequence.push(`end ${name}`)
|
||||||
|
},
|
||||||
|
parallel
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('plugin dependsOn', () => {
|
||||||
|
it('expect B to await A to finish before being run', async () => {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
const sequence: string[] = []
|
||||||
|
const plugins = [
|
||||||
|
pluginFactory('A', undefined, sequence),
|
||||||
|
pluginFactory('B', ['A'], sequence)
|
||||||
|
]
|
||||||
|
|
||||||
|
await applyPlugins(nuxtApp, plugins)
|
||||||
|
|
||||||
|
expect(sequence).toMatchObject([
|
||||||
|
'start A',
|
||||||
|
'end A',
|
||||||
|
'start B',
|
||||||
|
'end B'
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('expect C to await A and B to finish before being run', async () => {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
const sequence: string[] = []
|
||||||
|
const plugins = [
|
||||||
|
pluginFactory('A', undefined, sequence),
|
||||||
|
pluginFactory('B', ['A'], sequence),
|
||||||
|
pluginFactory('C', ['A', 'B'], sequence)
|
||||||
|
]
|
||||||
|
|
||||||
|
await applyPlugins(nuxtApp, plugins)
|
||||||
|
|
||||||
|
expect(sequence).toMatchObject([
|
||||||
|
'start A',
|
||||||
|
'end A',
|
||||||
|
'start B',
|
||||||
|
'end B',
|
||||||
|
'start C',
|
||||||
|
'end C'
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('expect C to not wait for A to finish before being run', async () => {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
const sequence: string[] = []
|
||||||
|
const plugins = [
|
||||||
|
pluginFactory('A', undefined, sequence),
|
||||||
|
pluginFactory('B', ['A'], sequence),
|
||||||
|
defineNuxtPlugin({
|
||||||
|
name,
|
||||||
|
async setup () {
|
||||||
|
sequence.push('start C')
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5))
|
||||||
|
sequence.push('end C')
|
||||||
|
},
|
||||||
|
parallel: true
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
await applyPlugins(nuxtApp, plugins)
|
||||||
|
|
||||||
|
expect(sequence).toMatchObject([
|
||||||
|
'start A',
|
||||||
|
'start C',
|
||||||
|
'end C',
|
||||||
|
'end A',
|
||||||
|
'start B',
|
||||||
|
'end B'
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('expect C to block the depends on of A-B since C is sequential', async () => {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
const sequence: string[] = []
|
||||||
|
const plugins = [
|
||||||
|
pluginFactory('A', undefined, sequence),
|
||||||
|
defineNuxtPlugin({
|
||||||
|
name,
|
||||||
|
async setup () {
|
||||||
|
sequence.push('start C')
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50))
|
||||||
|
sequence.push('end C')
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
pluginFactory('B', ['A'], sequence)
|
||||||
|
]
|
||||||
|
|
||||||
|
await applyPlugins(nuxtApp, plugins)
|
||||||
|
|
||||||
|
expect(sequence).toMatchObject([
|
||||||
|
'start A',
|
||||||
|
'start C',
|
||||||
|
'end A',
|
||||||
|
'end C',
|
||||||
|
'start B',
|
||||||
|
'end B'
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('relying on plugin not registed yet', async () => {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
const sequence: string[] = []
|
||||||
|
const plugins = [
|
||||||
|
pluginFactory('C', ['A'], sequence),
|
||||||
|
pluginFactory('A', undefined, sequence, true),
|
||||||
|
pluginFactory('E', ['B', 'C'], sequence, false),
|
||||||
|
pluginFactory('B', undefined, sequence),
|
||||||
|
pluginFactory('D', ['C'], sequence, false)
|
||||||
|
]
|
||||||
|
await applyPlugins(nuxtApp, plugins)
|
||||||
|
|
||||||
|
expect(sequence).toMatchObject([
|
||||||
|
'start A',
|
||||||
|
'start B',
|
||||||
|
'end A',
|
||||||
|
'start C',
|
||||||
|
'end B',
|
||||||
|
'end C',
|
||||||
|
'start E',
|
||||||
|
'start D',
|
||||||
|
'end E',
|
||||||
|
'end D'
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('test depending on not yet registered plugin and already resolved plugin', async () => {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
const sequence: string[] = []
|
||||||
|
const plugins = [
|
||||||
|
pluginFactory('A', undefined, sequence),
|
||||||
|
pluginFactory('B', ['A', 'C'], sequence),
|
||||||
|
pluginFactory('C', undefined, sequence, false),
|
||||||
|
pluginFactory('D', undefined, sequence, false),
|
||||||
|
pluginFactory('E', ['C'], sequence, false)
|
||||||
|
]
|
||||||
|
await applyPlugins(nuxtApp, plugins)
|
||||||
|
|
||||||
|
expect(sequence).toMatchObject([
|
||||||
|
'start A',
|
||||||
|
'start C',
|
||||||
|
'end A',
|
||||||
|
'end C',
|
||||||
|
'start B',
|
||||||
|
'start D',
|
||||||
|
'end B',
|
||||||
|
'end D',
|
||||||
|
'start E',
|
||||||
|
'end E'
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple depth of plugin dependency', async () => {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
const sequence: string[] = []
|
||||||
|
const plugins = [
|
||||||
|
pluginFactory('A', undefined, sequence),
|
||||||
|
pluginFactory('C', ['B', 'A'], sequence),
|
||||||
|
pluginFactory('B', undefined, sequence, false),
|
||||||
|
pluginFactory('E', ['D'], sequence, false),
|
||||||
|
pluginFactory('D', ['C'], sequence, false)
|
||||||
|
]
|
||||||
|
await applyPlugins(nuxtApp, plugins)
|
||||||
|
|
||||||
|
expect(sequence).toMatchObject([
|
||||||
|
'start A',
|
||||||
|
'start B',
|
||||||
|
'end A',
|
||||||
|
'end B',
|
||||||
|
'start C',
|
||||||
|
'end C',
|
||||||
|
'start D',
|
||||||
|
'end D',
|
||||||
|
'start E',
|
||||||
|
'end E'
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not throw when circular dependency is not a problem', async () => {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
const sequence: string[] = []
|
||||||
|
const plugins = [
|
||||||
|
pluginFactory('A', ['B'], sequence),
|
||||||
|
pluginFactory('B', ['C'], sequence),
|
||||||
|
pluginFactory('C', ['D'], sequence),
|
||||||
|
pluginFactory('D', [], sequence),
|
||||||
|
]
|
||||||
|
|
||||||
|
await applyPlugins(nuxtApp, plugins)
|
||||||
|
expect(sequence).toMatchObject([
|
||||||
|
'start D',
|
||||||
|
'end D',
|
||||||
|
'start C',
|
||||||
|
'end C',
|
||||||
|
'start B',
|
||||||
|
'end B',
|
||||||
|
'start A',
|
||||||
|
'end A'
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('function plugin', async () => {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
const sequence: string[] = []
|
||||||
|
const plugins = [
|
||||||
|
pluginFactory('A', undefined, sequence),
|
||||||
|
defineNuxtPlugin(() => {
|
||||||
|
sequence.push('start C')
|
||||||
|
sequence.push('end C')
|
||||||
|
}),
|
||||||
|
pluginFactory('B', undefined, sequence, false)
|
||||||
|
]
|
||||||
|
await applyPlugins(nuxtApp, plugins)
|
||||||
|
|
||||||
|
expect(sequence).toMatchObject([
|
||||||
|
'start A',
|
||||||
|
'start C',
|
||||||
|
'end C',
|
||||||
|
'start B',
|
||||||
|
'end A',
|
||||||
|
'end B'
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user