From 045b9edb5dd9698b2c8963e2a25316703cd6f282 Mon Sep 17 00:00:00 2001 From: pooya parsa Date: Tue, 21 Dec 2021 14:57:26 +0100 Subject: [PATCH] feat(kit, schema)!: finalize nuxt 3 module spec and utils (#2275) --- packages/bridge/build.config.ts | 3 +- packages/bridge/module.cjs | 8 +- packages/bridge/src/auto-imports.ts | 2 +- packages/bridge/src/meta.ts | 2 +- packages/bridge/src/module.ts | 33 ++-- packages/bridge/src/vite/module.ts | 8 +- packages/bridge/types.d.ts | 2 +- packages/kit/src/compatibility.ts | 41 ++++- packages/kit/src/components.ts | 10 +- packages/kit/src/module/container.ts | 37 ++++- packages/kit/src/module/define.ts | 171 ++++++++++++--------- packages/kit/src/module/install.ts | 73 +++------ packages/nuxt3/build.config.ts | 3 +- packages/nuxt3/src/core/nuxt.ts | 2 +- packages/schema/build.config.ts | 2 +- packages/schema/src/config/_common.ts | 4 +- packages/schema/src/index.ts | 10 +- packages/schema/src/types/compatibility.ts | 29 ++++ packages/schema/src/types/hooks.ts | 4 + packages/schema/src/types/module.ts | 62 ++------ 20 files changed, 287 insertions(+), 219 deletions(-) create mode 100644 packages/schema/src/types/compatibility.ts diff --git a/packages/bridge/build.config.ts b/packages/bridge/build.config.ts index ecc6176d1f..5ad46c00fb 100644 --- a/packages/bridge/build.config.ts +++ b/packages/bridge/build.config.ts @@ -8,6 +8,7 @@ export default defineBuildConfig({ ], externals: [ 'webpack', - 'vite' + 'vite', + 'vue-meta' ] }) diff --git a/packages/bridge/module.cjs b/packages/bridge/module.cjs index dc11dfa3e2..f019b1253b 100644 --- a/packages/bridge/module.cjs +++ b/packages/bridge/module.cjs @@ -3,20 +3,22 @@ module.exports = function (...args) { return import('./dist/module.mjs').then(m => m.default.call(this, ...args)) } +const pkg = require('./package.json') + module.exports.defineNuxtConfig = (config = {}) => { if (config.bridge !== false) { + config.bridge = config.bridge || {} + config.bridge._version = pkg.version if (!config.buildModules) { config.buildModules = [] } if (!config.buildModules.find(m => m === '@nuxt/bridge' || m === '@nuxt/bridge-edge')) { - config.buildModules.push('@nuxt/bridge') + config.buildModules.unshift('@nuxt/bridge') } } return config } -const pkg = require('./package.json') - module.exports.meta = { pkg, name: pkg.name, diff --git a/packages/bridge/src/auto-imports.ts b/packages/bridge/src/auto-imports.ts index b5ff4093a3..6160362fac 100644 --- a/packages/bridge/src/auto-imports.ts +++ b/packages/bridge/src/auto-imports.ts @@ -35,5 +35,5 @@ export async function setupAutoImports () { autoImports.push({ name: 'useNuxt2Meta', as: 'useNuxt2Meta', from: '#app' }) }) - await installModule(nuxt, autoImports) + await installModule(autoImports) } diff --git a/packages/bridge/src/meta.ts b/packages/bridge/src/meta.ts index 516cfc76f2..deb0c88380 100644 --- a/packages/bridge/src/meta.ts +++ b/packages/bridge/src/meta.ts @@ -29,5 +29,5 @@ export const setupMeta = async (opts: SetupMetaOptions) => { const runtimeDir = resolve(distDir, 'runtime/meta') nuxt.options.alias['#meta'] = runtimeDir - await installModule(nuxt, metaModule) + await installModule(metaModule) } diff --git a/packages/bridge/src/module.ts b/packages/bridge/src/module.ts index 39640c51af..22f76b76a6 100644 --- a/packages/bridge/src/module.ts +++ b/packages/bridge/src/module.ts @@ -1,5 +1,7 @@ import { createRequire } from 'module' -import { defineNuxtModule, installModule, checkNuxtCompatibilityIssues } from '@nuxt/kit' +import { defineNuxtModule, installModule, checkNuxtCompatibility, nuxtCtx } from '@nuxt/kit' +import type { NuxtModule } from '@nuxt/schema' +import { NuxtCompatibility } from '@nuxt/schema/src/types/compatibility' import type { BridgeConfig, ScriptSetupOptions } from '../types' import { setupNitroBridge } from './nitro' import { setupAppBridge } from './app' @@ -12,8 +14,10 @@ import { setupTranspile } from './transpile' import { setupScriptSetup } from './setup' export default defineNuxtModule({ - name: 'nuxt-bridge', - configKey: 'bridge', + meta: { + name: 'nuxt-bridge', + configKey: 'bridge' + }, defaults: { nitro: true, vite: false, @@ -22,7 +26,7 @@ export default defineNuxtModule({ transpile: true, scriptSetup: true, autoImports: true, - constraints: true, + compatibility: true, meta: null, // TODO: Remove from 2.16 postcss8: true, @@ -32,6 +36,11 @@ export default defineNuxtModule({ async setup (opts, nuxt) { const _require = createRequire(import.meta.url) + // Allow using kit compasables in all modules + if (!nuxtCtx.use()) { + nuxtCtx.set(nuxt) + } + if (opts.nitro) { await setupNitroBridge() } @@ -51,11 +60,11 @@ export default defineNuxtModule({ await setupAutoImports() } if (opts.vite) { - const viteModule = await import('./vite/module').then(r => r.default || r) - await installModule(nuxt, viteModule) + const viteModule = await import('./vite/module').then(r => r.default || r) as NuxtModule + await installModule(viteModule) } if (opts.postcss8) { - await installModule(nuxt, _require.resolve('@nuxt/postcss8')) + await installModule(_require.resolve('@nuxt/postcss8')) } if (opts.typescript) { await setupTypescript() @@ -66,12 +75,12 @@ export default defineNuxtModule({ if (opts.transpile) { setupTranspile() } - if (opts.constraints) { - nuxt.hook('modules:done', (moduleContainer: any) => { + if (opts.compatibility) { + nuxt.hook('modules:done', async (moduleContainer: any) => { for (const [name, m] of Object.entries(moduleContainer.requiredModules || {})) { - const requires = (m as any)?.handler?.meta?.requires - if (requires) { - const issues = checkNuxtCompatibilityIssues(requires, nuxt) + const compat = ((m as any)?.handler?.meta?.compatibility || {}) as NuxtCompatibility + if (compat) { + const issues = await checkNuxtCompatibility(compat, nuxt) if (issues.length) { console.warn(`[bridge] Detected module incompatibility issues for \`${name}\`:\n` + issues.toString()) } diff --git a/packages/bridge/src/vite/module.ts b/packages/bridge/src/vite/module.ts index 8bcb5a2d93..3965188f71 100644 --- a/packages/bridge/src/vite/module.ts +++ b/packages/bridge/src/vite/module.ts @@ -5,10 +5,12 @@ import { middlewareTemplate, storeTemplate } from './templates' import type { ViteOptions } from './types' export default defineNuxtModule({ - name: 'nuxt-bridge:vite', + meta: { + name: 'nuxt-bridge:vite', + version, + configKey: 'vite' + }, defaults: {}, - version, - configKey: 'vite', setup (viteOptions, nuxt) { nuxt.options.cli.badgeMessages.push(`⚡ Vite Mode Enabled (v${version})`) // eslint-disable-next-line no-console diff --git a/packages/bridge/types.d.ts b/packages/bridge/types.d.ts index fe2b6e5eaa..23b83e3e89 100644 --- a/packages/bridge/types.d.ts +++ b/packages/bridge/types.d.ts @@ -15,7 +15,7 @@ export interface BridgeConfig { scriptSetup: boolean | ScriptSetupOptions autoImports: boolean transpile: boolean - constraints: boolean + compatibility: boolean postcss8: boolean resolve: boolean typescript: boolean diff --git a/packages/kit/src/compatibility.ts b/packages/kit/src/compatibility.ts index d17eb549ba..e2f5f5a0b2 100644 --- a/packages/kit/src/compatibility.ts +++ b/packages/kit/src/compatibility.ts @@ -1,12 +1,14 @@ import satisfies from 'semver/functions/satisfies.js' // npm/node-semver#381 -import type { Nuxt, NuxtCompatibilityConstraints, NuxtCompatibilityIssues } from '@nuxt/schema' +import type { Nuxt, NuxtCompatibility, NuxtCompatibilityIssues } from '@nuxt/schema' import { useNuxt } from './context' /** * Check version constraints and return incompatibility issues as an array */ -export function checkNuxtCompatibilityIssues (constraints: NuxtCompatibilityConstraints, nuxt: Nuxt = useNuxt()): NuxtCompatibilityIssues { +export async function checkNuxtCompatibility (constraints: NuxtCompatibility, nuxt: Nuxt = useNuxt()): Promise { const issues: NuxtCompatibilityIssues = [] + + // Nuxt version check if (constraints.nuxt) { const nuxtVersion = getNuxtVersion(nuxt) const nuxtSemanticVersion = nuxtVersion.split('-').shift() @@ -17,15 +19,39 @@ export function checkNuxtCompatibilityIssues (constraints: NuxtCompatibilityCons }) } } - issues.toString = () => issues.map(issue => ` - [${issue.name}] ${issue.message}`).join('\n') + + // Bridge compatibility check + if (isNuxt2(nuxt)) { + const bridgeRequirement = constraints?.bridge + const hasBridge = !!(nuxt.options as any).bridge + if (bridgeRequirement === true && !hasBridge) { + issues.push({ + name: 'bridge', + message: 'Nuxt bridge is required' + }) + } else if (bridgeRequirement === false && hasBridge) { + issues.push({ + name: 'bridge', + message: 'Nuxt bridge is not supported' + }) + } + } + + // Allow extending compatibility checks + await nuxt.callHook('kit:compatibility', constraints, issues) + + // Issues formatter + issues.toString = () => + issues.map(issue => ` - [${issue.name}] ${issue.message}`).join('\n') + return issues } /** * Check version constraints and throw a detailed error if has any, otherwise returns true */ -export function ensureNuxtCompatibility (constraints: NuxtCompatibilityConstraints, nuxt: Nuxt = useNuxt()): true { - const issues = checkNuxtCompatibilityIssues(constraints, nuxt) +export async function assertNuxtCompatibility (constraints: NuxtCompatibility, nuxt: Nuxt = useNuxt()): Promise { + const issues = await checkNuxtCompatibility(constraints, nuxt) if (issues.length) { throw new Error('Nuxt compatibility issues found:\n' + issues.toString()) } @@ -35,8 +61,9 @@ export function ensureNuxtCompatibility (constraints: NuxtCompatibilityConstrain /** * Check version constraints and return true if passed, otherwise returns false */ -export function hasNuxtCompatibility (constraints: NuxtCompatibilityConstraints, nuxt: Nuxt = useNuxt()) { - return !checkNuxtCompatibilityIssues(constraints, nuxt).length +export async function hasNuxtCompatibility (constraints: NuxtCompatibility, nuxt: Nuxt = useNuxt()): Promise { + const issues = await checkNuxtCompatibility(constraints, nuxt) + return !issues.length } /** diff --git a/packages/kit/src/components.ts b/packages/kit/src/components.ts index 5465598caa..6b8486ca23 100644 --- a/packages/kit/src/components.ts +++ b/packages/kit/src/components.ts @@ -1,16 +1,16 @@ import { pascalCase, kebabCase } from 'scule' import type { ComponentsDir, Component } from '@nuxt/schema' import { useNuxt } from './context' -import { ensureNuxtCompatibility } from './compatibility' +import { assertNuxtCompatibility } from './compatibility' /** * Register a directory to be scanned for components and imported only when used. * * Requires Nuxt 2.13+ */ -export function addComponentsDir (dir: ComponentsDir) { +export async function addComponentsDir (dir: ComponentsDir) { const nuxt = useNuxt() - ensureNuxtCompatibility({ nuxt: '>=2.13' }, nuxt) + await assertNuxtCompatibility({ nuxt: '>=2.13' }, nuxt) nuxt.options.components = nuxt.options.components || [] nuxt.hook('components:dirs', (dirs) => { dirs.push(dir) }) } @@ -24,9 +24,9 @@ export type AddComponentOptions = { name: string, filePath: string } & Partial=2.13' }, nuxt) + await assertNuxtCompatibility({ nuxt: '>=2.13' }, nuxt) nuxt.options.components = nuxt.options.components || [] // Apply defaults diff --git a/packages/kit/src/module/container.ts b/packages/kit/src/module/container.ts index fd3e65aa8e..d838627725 100644 --- a/packages/kit/src/module/container.ts +++ b/packages/kit/src/module/container.ts @@ -6,19 +6,44 @@ import { addTemplate } from '../template' import { addServerMiddleware } from '../server' import { isNuxt2 } from '../compatibility' import { addPluginTemplate } from '../plugin' - +import { useNuxt } from '../context' import { installModule } from './install' -export function createModuleContainer (nuxt: Nuxt): ModuleContainer { - return { +const MODULE_CONTAINER_KEY = '__module_container__' + +export function useModuleContainer (nuxt: Nuxt = useNuxt()): ModuleContainer { + if (nuxt[MODULE_CONTAINER_KEY]) { + return nuxt[MODULE_CONTAINER_KEY] + } + + async function requireModule (moduleOpts) { + let src, inlineOptions + if (typeof moduleOpts === 'string') { + src = moduleOpts + } else if (Array.isArray(moduleOpts)) { + [src, inlineOptions] = moduleOpts + } else if (typeof moduleOpts === 'object') { + if (moduleOpts.src || moduleOpts.handler) { + src = moduleOpts.src || moduleOpts.handler + inlineOptions = moduleOpts.options + } else { + src = moduleOpts + } + } else { + src = moduleOpts + } + await installModule(src, inlineOptions, nuxt) + } + + nuxt[MODULE_CONTAINER_KEY] = { nuxt, options: nuxt.options, ready () { return Promise.resolve() }, addVendor () {}, - requireModule: installModule, - addModule: installModule, + requireModule, + addModule: requireModule, addServerMiddleware, @@ -73,4 +98,6 @@ export function createModuleContainer (nuxt: Nuxt): ModuleContainer { } } } + + return nuxt[MODULE_CONTAINER_KEY] } diff --git a/packages/kit/src/module/define.ts b/packages/kit/src/module/define.ts index 0129b57da1..0ae88e614f 100644 --- a/packages/kit/src/module/define.ts +++ b/packages/kit/src/module/define.ts @@ -3,105 +3,128 @@ import defu from 'defu' import { applyDefaults } from 'untyped' import consola from 'consola' import { dirname } from 'pathe' -import type { Nuxt, NuxtTemplate, NuxtModule, LegacyNuxtModule, ModuleOptions } from '@nuxt/schema' +import type { Nuxt, NuxtTemplate, NuxtModule, ModuleOptions, ModuleDefinition } from '@nuxt/schema' import { useNuxt, nuxtCtx } from '../context' -import { isNuxt2, checkNuxtCompatibilityIssues } from '../compatibility' +import { isNuxt2, checkNuxtCompatibility } from '../compatibility' import { templateUtils, compileTemplate } from '../internal/template' /** * Define a Nuxt module, automatically merging defaults with user provided options, installing * any hooks that are provided, and calling an optional setup function for full control. */ -export function defineNuxtModule (input: NuxtModule | ((nuxt: Nuxt) => NuxtModule)): LegacyNuxtModule { - let mod: NuxtModule +export function defineNuxtModule (definition: ModuleDefinition): NuxtModule { + // Normalize definition and meta + if (!definition.meta) { definition.meta = {} } + if (!definition.meta.configKey) { + // @ts-ignore TODO: Remove non-meta fallbacks in RC + definition.meta.name = definition.meta.name || definition.name + // @ts-ignore + definition.meta.configKey = definition.meta.configKey || definition.configKey || definition.meta.name + } - function wrappedModule (inlineOptions: OptionsT) { - // Get nuxt context - const nuxt: Nuxt = this.nuxt || useNuxt() + // Resolves module options from inline options, [configKey] in nuxt.config, defaults and schema + function getOptions (inlineOptions?: OptionsT, nuxt: Nuxt = useNuxt()) { + const configKey = definition.meta.configKey || definition.meta.name + const _defaults = typeof definition.defaults === 'function' ? definition.defaults(nuxt) : definition.defaults + let _options = defu(inlineOptions, nuxt.options[configKey], _defaults) as OptionsT + if (definition.schema) { + _options = applyDefaults(definition.schema, _options) as OptionsT + } + return Promise.resolve(_options) + } - // Resolve function - if (typeof input === 'function') { - mod = input(nuxt) - } else { - mod = input + // Module format is always a simple function + async function normalizedModule (inlineOptions: OptionsT, nuxt: Nuxt) { + if (!nuxt) { + nuxt = useNuxt() || this.nuxt /* invoked by nuxt 2 */ } - // Install hooks - if (mod.hooks) { - if (isNuxt2(nuxt)) { - nuxt.addHooks(mod.hooks) - } else { - nuxt.hooks.addHooks(mod.hooks) + // Avoid duplicate installs + const uniqueKey = definition.meta.name || definition.meta.configKey + if (uniqueKey) { + nuxt.options._requiredModules = nuxt.options._requiredModules || {} + if (nuxt.options._requiredModules[uniqueKey]) { + // TODO: Notify user if inline options is provided since will be ignored! + return } + nuxt.options._requiredModules[uniqueKey] = true } - // Stop if no install provided - if (typeof mod.setup !== 'function') { - return - } - - // check nuxt version range - if (mod.requires) { - const issues = checkNuxtCompatibilityIssues(mod.requires, nuxt) + // Check compatibility contraints + if (definition.meta.compatibility) { + const issues = await checkNuxtCompatibility(definition.meta.compatibility, nuxt) if (issues.length) { - consola.warn(`Module \`${mod.name}\` is disabled due to incompatibility issues:\n${issues.toString()}`) + consola.warn(`Module \`${definition.meta.name}\` is disabled due to incompatibility issues:\n${issues.toString()}`) return } } - // Resolve options - const configKey = mod.configKey || mod.name - const userOptions = defu(inlineOptions, nuxt.options[configKey]) as OptionsT - const resolvedOptions = applyDefaults(mod.defaults as any, userOptions) as OptionsT + // Prepare + nuxt2Shims(nuxt) - // Ensure nuxt instance exists (nuxt2 compatibility) - if (!nuxtCtx.use()) { - nuxtCtx.set(nuxt) - // @ts-ignore - if (!nuxt.__nuxtkit_close__) { - nuxt.hook('close', () => nuxtCtx.unset()) - // @ts-ignore - nuxt.__nuxtkit_close__ = true - } - } + // Resolve module and options + const _options = await getOptions(inlineOptions, nuxt) - if (isNuxt2()) { - // Support virtual templates with getContents() by writing them to .nuxt directory - let virtualTemplates: NuxtTemplate[] - nuxt.hook('builder:prepared', (_builder, buildOptions) => { - virtualTemplates = buildOptions.templates.filter(t => t.getContents) - for (const template of virtualTemplates) { - buildOptions.templates.splice(buildOptions.templates.indexOf(template), 1) - } - }) - nuxt.hook('build:templates', async (templates) => { - const context = { - nuxt, - utils: templateUtils, - app: { - dir: nuxt.options.srcDir, - extensions: nuxt.options.extensions, - plugins: nuxt.options.plugins, - templates: [ - ...templates.templatesFiles, - ...virtualTemplates - ], - templateVars: templates.templateVars - } - } - for await (const template of virtualTemplates) { - const contents = await compileTemplate({ ...template, src: '' }, context) - await fsp.mkdir(dirname(template.dst), { recursive: true }) - await fsp.writeFile(template.dst, contents) - } - }) + // Register hooks + if (definition.hooks) { + nuxt.hooks.addHooks(definition.hooks) } // Call setup - return mod.setup.call(null, resolvedOptions, nuxt) + await definition.setup?.call(null, _options, nuxt) } - wrappedModule.meta = mod + // Define getters for options and meta + normalizedModule.getMeta = () => Promise.resolve(definition.meta) + normalizedModule.getOptions = getOptions - return wrappedModule + return normalizedModule as NuxtModule +} + +// -- Nuxt 2 compatibility shims -- +const NUXT2_SHIMS_KEY = '__nuxt2_shims_key__' +function nuxt2Shims (nuxt: Nuxt) { + // Avoid duplicate install and only apply to Nuxt2 + if (!isNuxt2(nuxt) || nuxt[NUXT2_SHIMS_KEY]) { return } + nuxt[NUXT2_SHIMS_KEY] = true + + // Allow using nuxt.hooks + // @ts-ignore Nuxt 2 extends hookable + nuxt.hooks = nuxt + + // Allow using useNuxt() + if (!nuxtCtx.use()) { + nuxtCtx.set(nuxt) + nuxt.hook('close', () => nuxtCtx.unset()) + } + + // Support virtual templates with getContents() by writing them to .nuxt directory + let virtualTemplates: NuxtTemplate[] + nuxt.hook('builder:prepared', (_builder, buildOptions) => { + virtualTemplates = buildOptions.templates.filter(t => t.getContents) + for (const template of virtualTemplates) { + buildOptions.templates.splice(buildOptions.templates.indexOf(template), 1) + } + }) + nuxt.hook('build:templates', async (templates) => { + const context = { + nuxt, + utils: templateUtils, + app: { + dir: nuxt.options.srcDir, + extensions: nuxt.options.extensions, + plugins: nuxt.options.plugins, + templates: [ + ...templates.templatesFiles, + ...virtualTemplates + ], + templateVars: templates.templateVars + } + } + for await (const template of virtualTemplates) { + const contents = await compileTemplate({ ...template, src: '' }, context) + await fsp.mkdir(dirname(template.dst), { recursive: true }) + await fsp.writeFile(template.dst, contents) + } + }) } diff --git a/packages/kit/src/module/install.ts b/packages/kit/src/module/install.ts index fb53656963..c6415c407c 100644 --- a/packages/kit/src/module/install.ts +++ b/packages/kit/src/module/install.ts @@ -1,63 +1,34 @@ -import type { LegacyNuxtModule, NuxtModule, ModuleMeta, ModuleInstallOptions, ModuleOptions, ModuleSrc, Nuxt } from '@nuxt/schema' +import type { NuxtModule, Nuxt } from '@nuxt/schema' +import { useNuxt } from '../context' import { resolveModule, requireModule, importModule } from '../internal/cjs' import { resolveAlias } from '../resolve' -import { defineNuxtModule } from './define' -import { createModuleContainer } from './container' +import { useModuleContainer } from './container' /** Installs a module on a Nuxt instance. */ -export async function installModule (nuxt: Nuxt, installOpts: ModuleInstallOptions) { - let src: ModuleSrc - let options: ModuleOptions = {} - const meta: ModuleMeta = {} - - // Extract src, meta and options - if (typeof installOpts === 'string') { - src = installOpts - } else if (Array.isArray(installOpts)) { - [src, options] = installOpts - } else if (typeof installOpts === 'object') { - if (installOpts.src || installOpts.handler) { - src = installOpts.src || installOpts.handler - options = installOpts.options - Object.assign(meta, installOpts.meta) - } else { - src = installOpts as NuxtModule - } - } else { - src = installOpts +export async function installModule (nuxtModule: string | NuxtModule, inlineOptions?: any, nuxt: Nuxt = useNuxt()) { + // Detect if `installModule` used with older signuture (nuxt, nuxtModule) + // TODO: Remove in RC + // @ts-ignore + if (nuxtModule?._version || nuxtModule?.version || nuxtModule?.constructor?.version || '') { + // @ts-ignore + [nuxt, nuxtModule] = [nuxtModule, inlineOptions] + inlineOptions = {} + console.warn(new Error('`installModule` is being called with old signature!')) } - // Resolve as legacy handler - let handler: LegacyNuxtModule - if (typeof src === 'string') { - const _src = resolveModule(resolveAlias(src, nuxt.options.alias), { paths: nuxt.options.modulesDir }) + // Import if input is string + if (typeof nuxtModule === 'string') { + const _src = resolveModule(resolveAlias(nuxtModule, nuxt.options.alias), { paths: nuxt.options.modulesDir }) // TODO: also check with type: 'module' in closest `package.json` - const isESM = _src.endsWith('.mjs') || meta.isESM - handler = isESM ? await importModule(_src) : requireModule(_src) - if (!meta.name) { - meta.name = src - } - } else if (typeof src === 'function') { - handler = src - } else { - handler = defineNuxtModule(src) + const isESM = _src.endsWith('.mjs') + nuxtModule = isESM ? await importModule(_src) : requireModule(_src) } - // Merge meta - if (handler.meta) { - Object.assign(meta, handler.meta) + // Throw error if input is not a function + if (typeof nuxtModule !== 'function') { + throw new TypeError('Nuxt module should be a function: ' + nuxtModule) } - // Ensure module is required once - if (typeof meta.name === 'string') { - nuxt.options._requiredModules = nuxt.options._requiredModules || {} - if (nuxt.options._requiredModules[meta.name]) { - return - } - nuxt.options._requiredModules[meta.name] = true - } - - // Execute in legacy container - const container = createModuleContainer(nuxt) - await handler.call(container, options) + // Call module + await nuxtModule.call(useModuleContainer(), inlineOptions, nuxt) } diff --git a/packages/nuxt3/build.config.ts b/packages/nuxt3/build.config.ts index d6b5010333..4ee95be909 100644 --- a/packages/nuxt3/build.config.ts +++ b/packages/nuxt3/build.config.ts @@ -21,6 +21,7 @@ export default defineBuildConfig({ externals: [ '@vue/reactivity', '@vue/shared', - '@vueuse/head' + '@vueuse/head', + 'vue-meta' ] }) diff --git a/packages/nuxt3/src/core/nuxt.ts b/packages/nuxt3/src/core/nuxt.ts index acb7b89cd6..f384dc02da 100644 --- a/packages/nuxt3/src/core/nuxt.ts +++ b/packages/nuxt3/src/core/nuxt.ts @@ -67,7 +67,7 @@ async function initNuxt (nuxt: Nuxt) { }) for (const m of modulesToInstall) { - await installModule(nuxt, m) + await installModule(m, nuxt) } await nuxt.callHook('modules:done', { nuxt } as ModuleContainer) diff --git a/packages/schema/build.config.ts b/packages/schema/build.config.ts index b95a7330b1..f1f4910add 100644 --- a/packages/schema/build.config.ts +++ b/packages/schema/build.config.ts @@ -26,6 +26,6 @@ export default defineBuildConfig({ 'vite', // Implicit '@vue/compiler-core', - '@vue/shared ', + '@vue/shared' ] }) diff --git a/packages/schema/src/config/_common.ts b/packages/schema/src/config/_common.ts index ffa99215f1..d3fc8ac22e 100644 --- a/packages/schema/src/config/_common.ts +++ b/packages/schema/src/config/_common.ts @@ -225,7 +225,7 @@ export default { * function () {} * ] * ``` - * @type {typeof import('../src/types/module').ModuleInstallOptions[]} + * @type {(typeof import('../src/types/module').NuxtModule | string)[]} * @version 2 * @version 3 */ @@ -262,7 +262,7 @@ export default { * decreases the size of `node_modules` in production deployments. Please refer to each * module's documentation to see if it is recommended to use `modules` or `buildModules`. * - * @type {typeof import('../src/types/module').ModuleInstallOptions[]} + * @type {(typeof import('../src/types/module').NuxtModule | string)[]} * @version 2 * @version 3 */ diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index e06425f2a2..9f753138ef 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -1,13 +1,15 @@ // Types import './types/global' + +export * from './types/compatibility' +export * from './types/components' export * from './types/config' export * from './types/hooks' -export * from './types/module' -export * from './types/meta' -export * from './types/nuxt' -export * from './types/components' export * from './types/imports' +export * from './types/meta' +export * from './types/module' +export * from './types/nuxt' export * from './types/pages' // Schema diff --git a/packages/schema/src/types/compatibility.ts b/packages/schema/src/types/compatibility.ts new file mode 100644 index 0000000000..adb23019c9 --- /dev/null +++ b/packages/schema/src/types/compatibility.ts @@ -0,0 +1,29 @@ +export interface NuxtCompatibility { + /** + * Required nuxt version in semver format. + * + * @example `^2.14.0` or `>=3.0.0-27219851.6e49637`. + * + */ + nuxt?: string + + /** + * Bridge constraint for Nuxt 2 support. + * + * - `true`: When using Nuxt 2, using bridge module is required + * - `false`: When using Nuxt 2, using bridge module is not supported + */ + bridge?: Boolean +} + +export interface NuxtCompatibilityIssue { + name: string + message: string +} + +export interface NuxtCompatibilityIssues extends Array { + /** + * Return formatted error message + */ + toString(): string +} diff --git a/packages/schema/src/types/hooks.ts b/packages/schema/src/types/hooks.ts index 22b69ae2a6..4e9d3ab5e4 100644 --- a/packages/schema/src/types/hooks.ts +++ b/packages/schema/src/types/hooks.ts @@ -6,6 +6,7 @@ import type { NuxtTemplate, Nuxt, NuxtApp } from './nuxt' import type { AutoImport, AutoImportSource } from './imports' import type { NuxtConfig, NuxtOptions } from './config' import type { Component, ComponentsDir, ScanDir, ComponentsOptions } from './components' +import { NuxtCompatibility, NuxtCompatibilityIssues } from '..' type HookResult = Promise | void @@ -39,6 +40,9 @@ export type NuxtPage = { } export interface NuxtHooks { + // Kit + 'kit:compatibility': (compatibility: NuxtCompatibility, issues: NuxtCompatibilityIssues) => HookResult + // nuxt3 'app:resolve': (app: NuxtApp) => HookResult 'app:templates': (app: NuxtApp) => HookResult diff --git a/packages/schema/src/types/module.ts b/packages/schema/src/types/module.ts index a34ed4fd70..fb8a2ba761 100644 --- a/packages/schema/src/types/module.ts +++ b/packages/schema/src/types/module.ts @@ -1,25 +1,6 @@ import { NuxtHooks } from './hooks' -import type { Nuxt, NuxtTemplate } from "./nuxt"; - - -export interface NuxtCompatibilityConstraints { - /** - * Required nuxt version. for example, `^2.14.0` or `>=3.0.0-27219851.6e49637`. - */ - nuxt?: string -} - -export interface NuxtCompatibilityIssue { - name: string - message: string -} - -export interface NuxtCompatibilityIssues extends Array { - /** - * Return formatted error message - */ - toString(): string -} +import type { Nuxt, NuxtTemplate } from "./nuxt" +import type { NuxtCompatibility } from './compatibility' export interface ModuleMeta { /** Module name */ @@ -35,9 +16,9 @@ export interface ModuleMeta { configKey?: string /** - * Semver constraints for the versions of Nuxt or features this module are supported. + * Constraints for the versions of Nuxt or features this module requires */ - requires?: NuxtCompatibilityConstraints + compatibility?: NuxtCompatibility [key: string]: any } @@ -45,33 +26,22 @@ export interface ModuleMeta { /** The options received */ export type ModuleOptions = Record -/** A pre-kit Nuxt module */ -export interface LegacyNuxtModule { - (this: ModuleContainer, inlineOptions?: ModuleOptions): void | Promise +/** Input module passed to defineNuxtModule */ +export interface ModuleDefinition { meta?: ModuleMeta -} - -/** A Nuxt module definition */ -export interface NuxtModule extends ModuleMeta { - defaults?: T - setup?: (this: null, resolvedOptions: T, nuxt: Nuxt) => void | Promise + defaults?: T | ((nuxt: Nuxt) => T) + schema?: T hooks?: Partial + setup?: (this: void, resolvedOptions: T, nuxt: Nuxt) => void | Promise } -export type ModuleSrc = string | NuxtModule | LegacyNuxtModule - -export interface ModuleInstallOptionsObj { - src: ModuleSrc, - meta: ModuleMeta - options: ModuleOptions - handler: LegacyNuxtModule +/** Nuxt modules are always a simple function */ +export interface NuxtModule { + (this: void, inlineOptions: T, nuxt: Nuxt): void | Promise + getOptions?: (inlineOptions?: T, nuxt?: Nuxt) => Promise + getMeta?: () => Promise } -export type ModuleInstallOptions = - ModuleSrc | - [ModuleSrc, ModuleOptions?] | - Partial - /** * Legacy ModuleContainer for backwards compatibility with Nuxt 2 module format. */ @@ -104,8 +74,8 @@ export interface ModuleContainer { extendRoutes(fn): void /** Registers a module */ - requireModule(nuxt: Nuxt, opts: any): Promise + requireModule(installOptions: any, opts: any): Promise /** Registers a module */ - addModule(nuxt: Nuxt, opts: any): Promise + addModule(installOptions: any, opts: any): Promise }