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

perf(nuxt): extract and apply plugin order at build time #21611

Merged
merged 16 commits into from Jun 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions packages/nuxt/package.json
Expand Up @@ -89,6 +89,7 @@
"prompts": "^2.4.2",
"scule": "^1.0.0",
"strip-literal": "^1.0.1",
"typescript-estree": "^18.1.0",
"ufo": "^1.1.2",
"ultrahtml": "^1.2.0",
"uncrypto": "^0.1.3",
Expand Down
6 changes: 2 additions & 4 deletions packages/nuxt/src/app/entry.ts
Expand Up @@ -8,11 +8,11 @@ import type { $Fetch, NitroFetchRequest } from 'nitropack'
import { baseURL } from '#build/paths.mjs'

import type { CreateOptions } from '#app'
import { applyPlugins, createNuxtApp, normalizePlugins } from '#app/nuxt'
import { applyPlugins, createNuxtApp } from '#app/nuxt'

import '#build/css'
// @ts-expect-error virtual file
import _plugins from '#build/plugins'
import plugins from '#build/plugins'
// @ts-expect-error virtual file
import RootComponent from '#build/root-component.mjs'
// @ts-expect-error virtual file
Expand All @@ -26,8 +26,6 @@ if (!globalThis.$fetch) {

let entry: Function

const plugins = normalizePlugins(_plugins)

if (process.server) {
entry = async function createNuxtAppServer (ssrContext: CreateOptions['ssrContext']) {
const vueApp = createApp(RootComponent)
Expand Down
118 changes: 21 additions & 97 deletions packages/nuxt/src/app/nuxt.ts
Expand Up @@ -160,7 +160,6 @@ export interface PluginMeta {

export interface ResolvedPluginMeta {
name?: string
order: number
parallel?: boolean
}

Expand All @@ -170,7 +169,7 @@ export interface Plugin<Injections extends Record<string, unknown> = Record<stri
meta?: ResolvedPluginMeta
}

export interface ObjectPluginInput<Injections extends Record<string, unknown> = Record<string, unknown>> extends PluginMeta {
export interface ObjectPlugin<Injections extends Record<string, unknown> = Record<string, unknown>> extends PluginMeta {
hooks?: Partial<RuntimeNuxtHooks>
setup?: Plugin<Injections>
/**
Expand All @@ -181,6 +180,9 @@ export interface ObjectPluginInput<Injections extends Record<string, unknown> =
parallel?: boolean
}

/** @deprecated Use `ObjectPlugin` */
export type ObjectPluginInput<Injections extends Record<string, unknown> = Record<string, unknown>> = ObjectPlugin<Injections>

export interface CreateOptions {
vueApp: NuxtApp['vueApp']
ssrContext?: NuxtApp['ssrContext']
Expand Down Expand Up @@ -301,22 +303,26 @@ export function createNuxtApp (options: CreateOptions) {
return nuxtApp
}

export async function applyPlugin (nuxtApp: NuxtApp, plugin: Plugin) {
if (typeof plugin !== 'function') { return }
const { provide } = await nuxtApp.runWithContext(() => plugin(nuxtApp)) || {}
if (provide && typeof provide === 'object') {
for (const key in provide) {
nuxtApp.provide(key, provide[key])
export async function applyPlugin (nuxtApp: NuxtApp, plugin: Plugin & ObjectPlugin<any>) {
if (plugin.hooks) {
nuxtApp.hooks.addHooks(plugin.hooks)
}
if (typeof plugin === 'function') {
const { provide } = await nuxtApp.runWithContext(() => plugin(nuxtApp)) || {}
if (provide && typeof provide === 'object') {
for (const key in provide) {
nuxtApp.provide(key, provide[key])
}
}
}
}

export async function applyPlugins (nuxtApp: NuxtApp, plugins: Plugin[]) {
export async function applyPlugins (nuxtApp: NuxtApp, plugins: Array<Plugin & ObjectPlugin<any>>) {
const parallels: Promise<any>[] = []
const errors: Error[] = []
for (const plugin of plugins) {
const promise = applyPlugin(nuxtApp, plugin)
if (plugin.meta?.parallel) {
if (plugin.parallel) {
parallels.push(promise.catch(e => errors.push(e)))
} else {
await promise
Expand All @@ -326,97 +332,15 @@ export async function applyPlugins (nuxtApp: NuxtApp, plugins: Plugin[]) {
if (errors.length) { throw errors[0] }
}

export function normalizePlugins (_plugins: Plugin[]) {
const unwrappedPlugins: Plugin[] = []
const legacyInjectPlugins: Plugin[] = []
const invalidPlugins: Plugin[] = []

const plugins: Plugin[] = []

for (const plugin of _plugins) {
if (typeof plugin !== 'function') {
if (process.dev) { invalidPlugins.push(plugin) }
continue
}

// TODO: Skip invalid plugins in next releases
let _plugin = plugin
if (plugin.length > 1) {
// Allow usage without wrapper but warn
if (process.dev) { legacyInjectPlugins.push(plugin) }
// @ts-expect-error deliberate invalid second argument
_plugin = (nuxtApp: NuxtApp) => plugin(nuxtApp, nuxtApp.provide)
}

// Allow usage without wrapper but warn
if (process.dev && !isNuxtPlugin(_plugin)) { unwrappedPlugins.push(_plugin) }

plugins.push(_plugin)
}

plugins.sort((a, b) => (a.meta?.order || orderMap.default) - (b.meta?.order || orderMap.default))

if (process.dev && legacyInjectPlugins.length) {
console.warn('[warn] [nuxt] You are using a plugin with legacy Nuxt 2 format (context, inject) which is likely to be broken. In the future they will be ignored:', legacyInjectPlugins.map(p => p.name || p).join(','))
}
if (process.dev && invalidPlugins.length) {
console.warn('[warn] [nuxt] Some plugins are not exposing a function and skipped:', invalidPlugins)
}
if (process.dev && unwrappedPlugins.length) {
console.warn('[warn] [nuxt] You are using a plugin that has not been wrapped in `defineNuxtPlugin`. It is advised to wrap your plugins as in the future this may enable enhancements:', unwrappedPlugins.map(p => p.name || p).join(','))
}

return plugins
}

// -50: pre-all (nuxt)
// -40: custom payload revivers (user)
// -30: payload reviving (nuxt)
// -20: pre (user) <-- pre mapped to this
// -10: default (nuxt)
// 0: default (user) <-- default behavior
// +10: post (nuxt)
// +20: post (user) <-- post mapped to this
// +30: post-all (nuxt)

const orderMap: Record<NonNullable<ObjectPluginInput['enforce']>, number> = {
pre: -20,
default: 0,
post: 20
}

/*! @__NO_SIDE_EFFECTS__ */
export function definePayloadPlugin<T extends Record<string, unknown>> (plugin: Plugin<T> | ObjectPluginInput<T>) {
return defineNuxtPlugin(plugin, { order: -40 })
export function defineNuxtPlugin<T extends Record<string, unknown>> (plugin: Plugin<T> | ObjectPlugin<T>): Plugin<T> & ObjectPlugin<T> {
if (typeof plugin === 'function') { return plugin }
delete plugin.name
return Object.assign(plugin.setup || (() => {}), plugin, { [NuxtPluginIndicator]: true } as const)
}

/*! @__NO_SIDE_EFFECTS__ */
export function defineNuxtPlugin<T extends Record<string, unknown>> (plugin: Plugin<T> | ObjectPluginInput<T>, meta?: PluginMeta): Plugin<T> {
if (typeof plugin === 'function') { return defineNuxtPlugin({ setup: plugin }, meta) }

const wrapper: Plugin<T> = (nuxtApp) => {
if (plugin.hooks) {
nuxtApp.hooks.addHooks(plugin.hooks)
}
if (plugin.setup) {
return plugin.setup(nuxtApp)
}
}

wrapper.meta = {
name: meta?.name || plugin.name || plugin.setup?.name,
parallel: plugin.parallel,
order:
meta?.order ||
plugin.order ||
orderMap[plugin.enforce || 'default'] ||
orderMap.default
}

wrapper[NuxtPluginIndicator] = true

return wrapper
}
export const definePayloadPlugin = defineNuxtPlugin

export function isNuxtPlugin (plugin: unknown) {
return typeof plugin === 'function' && NuxtPluginIndicator in plugin
Expand Down
19 changes: 19 additions & 0 deletions packages/nuxt/src/core/app.ts
Expand Up @@ -6,6 +6,7 @@ import type { Nuxt, NuxtApp, NuxtPlugin, NuxtTemplate, ResolvedNuxtTemplate } fr

import * as defaultTemplates from './templates'
import { getNameFromPath, hasSuffix, uniqueBy } from './utils'
import { extractMetadata, orderMap } from './plugins/plugin-metadata'

export function createApp (nuxt: Nuxt, options: Partial<NuxtApp> = {}): NuxtApp {
return defu(options, {
Expand Down Expand Up @@ -149,3 +150,21 @@ function resolvePaths<Item extends Record<string, any>> (items: Item[], key: { [
}
}))
}

export async function annotatePlugins (nuxt: Nuxt, plugins: NuxtPlugin[]) {
const _plugins: NuxtPlugin[] = []
for (const plugin of plugins) {
try {
const code = plugin.src in nuxt.vfs ? nuxt.vfs[plugin.src] : await fsp.readFile(plugin.src!, 'utf-8')
_plugins.push({
...extractMetadata(code),
...plugin
})
} catch (e) {
console.warn(`[nuxt] Could not resolve \`${plugin.src}\`.`)
_plugins.push(plugin)
}
}

return _plugins.sort((a, b) => (a.order ?? orderMap.default) - (b.order ?? orderMap.default))
}
6 changes: 5 additions & 1 deletion packages/nuxt/src/core/nuxt.ts
@@ -1,7 +1,7 @@
import { join, normalize, relative, resolve } from 'pathe'
import { createDebugger, createHooks } from 'hookable'
import type { LoadNuxtOptions } from '@nuxt/kit'
import { addComponent, addPlugin, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, logger, nuxtCtx, resolveAlias, resolveFiles, resolvePath, tryResolveModule } from '@nuxt/kit'
import { addBuildPlugin, addComponent, addPlugin, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, logger, nuxtCtx, resolveAlias, resolveFiles, resolvePath, tryResolveModule } from '@nuxt/kit'
import type { Nuxt, NuxtHooks, NuxtOptions } from 'nuxt/schema'

import escapeRE from 'escape-string-regexp'
Expand All @@ -24,6 +24,7 @@ import { LayerAliasingPlugin } from './plugins/layer-aliasing'
import { addModuleTranspiles } from './modules'
import { initNitro } from './nitro'
import schemaModule from './schema'
import { RemovePluginMetadataPlugin } from './plugins/plugin-metadata'

export function createNuxt (options: NuxtOptions): Nuxt {
const hooks = createHooks<NuxtHooks>()
Expand Down Expand Up @@ -72,6 +73,9 @@ async function initNuxt (nuxt: Nuxt) {
}
})

// Add plugin normalisation plugin
addBuildPlugin(RemovePluginMetadataPlugin(nuxt))

// Add import protection
const config = {
rootDir: nuxt.options.rootDir,
Expand Down