diff --git a/packages/bridge/package.json b/packages/bridge/package.json index c960bb91fb..29442de7b8 100644 --- a/packages/bridge/package.json +++ b/packages/bridge/package.json @@ -34,6 +34,7 @@ "defu": "^5.0.1", "destr": "^1.1.0", "enhanced-resolve": "^5.9.0", + "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.1", "externality": "^0.1.6", "fs-extra": "^10.0.0", diff --git a/packages/bridge/src/app.ts b/packages/bridge/src/app.ts index baa22d00fd..4cfc4a9c56 100644 --- a/packages/bridge/src/app.ts +++ b/packages/bridge/src/app.ts @@ -1,6 +1,8 @@ -import { useNuxt, resolveModule, addTemplate } from '@nuxt/kit' +import { useNuxt, resolveModule, addTemplate, resolveAlias } from '@nuxt/kit' +import { NuxtModule } from '@nuxt/schema' import { resolve } from 'pathe' import { componentsTypeTemplate } from '../../nuxt3/src/components/templates' +import { schemaTemplate } from '../../nuxt3/src/core/templates' import { distDir } from './dirs' export function setupAppBridge (_options: any) { @@ -38,6 +40,15 @@ export function setupAppBridge (_options: any) { references.push({ path: resolve(nuxt.options.buildDir, 'types/components.d.ts') }) }) + // Augment schema with module types + nuxt.hook('modules:done', async (container: any) => { + nuxt.options._installedModules = await Promise.all(Object.values(container.requiredModules).map(async (m: { src: string, handler: NuxtModule }) => ({ + meta: await m.handler.getMeta?.(), + entryPath: resolveAlias(m.src, nuxt.options.alias) + }))) + addTemplate(schemaTemplate) + }) + // Alias vue to have identical vue3 exports nuxt.options.alias['vue2-bridge'] = resolve(distDir, 'runtime/vue2-bridge.mjs') for (const alias of [ diff --git a/packages/kit/src/module/container.ts b/packages/kit/src/module/container.ts index d838627725..93995da9ef 100644 --- a/packages/kit/src/module/container.ts +++ b/packages/kit/src/module/container.ts @@ -32,7 +32,7 @@ export function useModuleContainer (nuxt: Nuxt = useNuxt()): ModuleContainer { } else { src = moduleOpts } - await installModule(src, inlineOptions, nuxt) + await installModule(src, inlineOptions) } nuxt[MODULE_CONTAINER_KEY] = { diff --git a/packages/kit/src/module/install.ts b/packages/kit/src/module/install.ts index f0e088e525..f92b1d9c62 100644 --- a/packages/kit/src/module/install.ts +++ b/packages/kit/src/module/install.ts @@ -1,18 +1,34 @@ -import type { NuxtModule, Nuxt } from '@nuxt/schema' +import type { Nuxt, NuxtModule } from '@nuxt/schema' import { useNuxt } from '../context' import { resolveModule, requireModule, importModule } from '../internal/cjs' import { resolveAlias } from '../resolve' import { useModuleContainer } from './container' /** Installs a module on a Nuxt instance. */ -export async function installModule (nuxtModule: string | NuxtModule, inlineOptions?: any, nuxt: Nuxt = useNuxt()) { +export async function installModule (moduleToInstall: string | NuxtModule, _inlineOptions?: any, _nuxt?: Nuxt) { + const nuxt = useNuxt() + const { nuxtModule, inlineOptions } = await normalizeModule(moduleToInstall, _inlineOptions) + + // Call module + await nuxtModule.call(useModuleContainer(), inlineOptions, nuxt) + + nuxt.options._installedModules = nuxt.options._installedModules || [] + nuxt.options._installedModules.push({ + meta: await nuxtModule.getMeta?.(), + entryPath: typeof moduleToInstall === 'string' ? resolveAlias(moduleToInstall) : undefined + }) +} + +// --- Internal --- + +async function normalizeModule (nuxtModule: string | NuxtModule, inlineOptions?: any) { + const 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 = {} + [nuxtModule, inlineOptions] = [inlineOptions, {}] console.warn(new Error('`installModule` is being called with old signature!')) } @@ -29,6 +45,5 @@ export async function installModule (nuxtModule: string | NuxtModule, inlineOpti throw new TypeError('Nuxt module should be a function: ' + nuxtModule) } - // Call module - await nuxtModule.call(useModuleContainer(), inlineOptions, nuxt) + return { nuxtModule, inlineOptions } as { nuxtModule: NuxtModule, inlineOptions: undefined | Record } } diff --git a/packages/nuxt3/src/core/nuxt.ts b/packages/nuxt3/src/core/nuxt.ts index aeef3c4718..c1c98383c1 100644 --- a/packages/nuxt3/src/core/nuxt.ts +++ b/packages/nuxt3/src/core/nuxt.ts @@ -49,6 +49,8 @@ async function initNuxt (nuxt: Nuxt) { if (nuxt.options.typescript.shim) { opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/vue-shim.d.ts') }) } + // Add module augmentations directly to NuxtConfig + opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/schema.d.ts') }) }) // Add import protection @@ -82,9 +84,9 @@ async function initNuxt (nuxt: Nuxt) { for (const m of modulesToInstall) { if (Array.isArray(m)) { - await installModule(m[0], m[1], nuxt) + await installModule(m[0], m[1]) } else { - await installModule(m, {}, nuxt) + await installModule(m, {}) } } diff --git a/packages/nuxt3/src/core/templates.ts b/packages/nuxt3/src/core/templates.ts index 998a8994d0..c99008d6de 100644 --- a/packages/nuxt3/src/core/templates.ts +++ b/packages/nuxt3/src/core/templates.ts @@ -1,13 +1,13 @@ import { templateUtils } from '@nuxt/kit' import type { Nuxt, NuxtApp } from '@nuxt/schema' -import { genArrayFromRaw, genDynamicImport, genExport, genImport } from 'knitwork' +import { genArrayFromRaw, genDynamicImport, genExport, genImport, genString } from 'knitwork' import { isAbsolute, join, relative } from 'pathe' import escapeRE from 'escape-string-regexp' -type TemplateContext = { - nuxt: Nuxt; - app: NuxtApp; +export interface TemplateContext { + nuxt: Nuxt + app: NuxtApp } export const vueShim = { @@ -110,3 +110,25 @@ export { } ` } } + +const adHocModules = ['router', 'pages', 'auto-imports', 'meta', 'components'] +export const schemaTemplate = { + filename: 'types/schema.d.ts', + getContents: ({ nuxt }: TemplateContext) => { + const moduleInfo = nuxt.options._installedModules.map(m => ({ + ...m.meta || {}, + importName: m.entryPath || m.meta?.name + })).filter(m => m.configKey && m.name && !adHocModules.includes(m.name)) + + return [ + "import { NuxtModule } from '@nuxt/schema'", + "declare module '@nuxt/schema' {", + ' interface NuxtConfig {', + ...moduleInfo.filter(Boolean).map(meta => + ` [${genString(meta.configKey)}]?: typeof ${genDynamicImport(meta.importName, { wrapper: false })}.default extends NuxtModule ? Partial : Record` + ), + ' }', + '}' + ].join('\n') + } +} diff --git a/packages/schema/src/config/_common.ts b/packages/schema/src/config/_common.ts index a936bac5d9..dbaec827e3 100644 --- a/packages/schema/src/config/_common.ts +++ b/packages/schema/src/config/_common.ts @@ -288,6 +288,14 @@ export default { */ _modules: [], + /** + * Installed module metadata + * + * @version 3 + * @private + */ + _installedModules: [], + /** * Allows customizing the global ID used in the main HTML template as well as the main * Vue instance name and other options. @@ -678,13 +686,13 @@ export default { /** * Runtime config allows passing dynamic config and environment variables to the Nuxt app context. - * + * * The value of this object is accessible from server only using `$config` or `useRuntimeConfig`. * It will override `publicRuntimeConfig` on the server-side. * * It should hold _private_ environment variables (that should not be exposed on the frontend). * This could include a reference to your API secret tokens. - * + * * Values are automatically replaced by matching env variables at runtime, e.g. setting an environment * variable `API_SECRET=my-api-key` would overwrite the value in the example below. * Note that the env variable has to be named exactly the same as the config key. @@ -705,9 +713,9 @@ export default { /** * Runtime config allows passing dynamic config and environment variables to the Nuxt app context. - * + * * The value of this object is accessible from both client and server using `$config` or `useRuntimeConfig`. - * + * * It should hold env variables that are _public_ as they will be accessible on the frontend. This could include a * reference to your public URL. * diff --git a/yarn.lock b/yarn.lock index 2871514025..12f2c06134 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2576,6 +2576,7 @@ __metadata: defu: ^5.0.1 destr: ^1.1.0 enhanced-resolve: ^5.9.0 + escape-string-regexp: ^5.0.0 estree-walker: ^3.0.1 externality: ^0.1.6 fs-extra: ^10.0.0