diff --git a/packages/kit/src/module/define.ts b/packages/kit/src/module/define.ts index e0986ef59a..e5a771cff5 100644 --- a/packages/kit/src/module/define.ts +++ b/packages/kit/src/module/define.ts @@ -1,7 +1,7 @@ import { performance } from 'node:perf_hooks' import { defu } from 'defu' import { applyDefaults } from 'untyped' -import type { ModuleDefinition, ModuleOptions, ModuleSetupReturn, Nuxt, NuxtModule, NuxtOptions } from '@nuxt/schema' +import type { ModuleDefinition, ModuleOptions, ModuleSetupInstallResult, ModuleSetupReturn, Nuxt, NuxtModule, NuxtOptions, ResolvedModuleOptions } from '@nuxt/schema' import { logger } from '../logger' import { tryUseNuxt, useNuxt } from '../context' import { checkNuxtCompatibility } from '../compatibility' @@ -10,28 +10,76 @@ import { checkNuxtCompatibility } from '../compatibility' * 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 (definition: ModuleDefinition | NuxtModule): NuxtModule { - if (typeof definition === 'function') { return defineNuxtModule({ setup: definition }) } +export function defineNuxtModule ( + definition: ModuleDefinition, false> | NuxtModule, false> +): NuxtModule - // Normalize definition and meta - const module: ModuleDefinition & Required, 'meta'>> = defu(definition, { meta: {} }) - if (module.meta.configKey === undefined) { - module.meta.configKey = module.meta.name +export function defineNuxtModule (): { + with: > ( + definition: ModuleDefinition | NuxtModule + ) => NuxtModule +} + +export function defineNuxtModule ( + definition?: ModuleDefinition, false> | NuxtModule, false>, +) { + if (definition) { + return _defineNuxtModule(definition) } + return { + with: >( + definition: ModuleDefinition | NuxtModule, + ) => _defineNuxtModule(definition), + } +} + +function _defineNuxtModule< + TOptions extends ModuleOptions, + TOptionsDefaults extends Partial, + TWith extends boolean, +> ( + definition: ModuleDefinition | NuxtModule, +): NuxtModule { + if (typeof definition === 'function') { + return _defineNuxtModule({ setup: definition }) + } + + // Normalize definition and meta + const module: ModuleDefinition & Required, 'meta'>> = defu(definition, { meta: {} }) + + module.meta.configKey ||= module.meta.name + // Resolves module options from inline options, [configKey] in nuxt.config, defaults and schema - async function getOptions (inlineOptions?: OptionsT, nuxt: Nuxt = useNuxt()) { - const configKey = module.meta.configKey || module.meta.name! - const _defaults = module.defaults instanceof Function ? module.defaults(nuxt) : module.defaults - let _options = defu(inlineOptions, nuxt.options[configKey as keyof NuxtOptions], _defaults) as OptionsT + async function getOptions ( + inlineOptions?: Partial, + nuxt: Nuxt = useNuxt(), + ): Promise< + TWith extends true + ? ResolvedModuleOptions + : TOptions + > { + const nuxtConfigOptionsKey = module.meta.configKey || module.meta.name + + const nuxtConfigOptions: Partial = nuxtConfigOptionsKey && nuxtConfigOptionsKey in nuxt.options ? nuxt.options[ nuxtConfigOptionsKey] : {} + + const optionsDefaults: TOptionsDefaults = + module.defaults instanceof Function + ? module.defaults(nuxt) + : module.defaults ?? {} + + let options = defu(inlineOptions, nuxtConfigOptions, optionsDefaults) + if (module.schema) { - _options = await applyDefaults(module.schema, _options) as OptionsT + options = await applyDefaults(module.schema, options) as any } - return Promise.resolve(_options) + + // @ts-expect-error ignore type mismatch when calling `defineNuxtModule` without `.with()` + return Promise.resolve(options) } // Module format is always a simple function - async function normalizedModule (this: any, inlineOptions: OptionsT, nuxt: Nuxt) { + async function normalizedModule (this: any, inlineOptions: Partial, nuxt: Nuxt): Promise { if (!nuxt) { nuxt = tryUseNuxt() || this.nuxt /* invoked by nuxt 2 */ } @@ -81,7 +129,7 @@ export function defineNuxtModule (definition: Mo if (res === false) { return false } // Return module install result - return defu(res, { + return defu(res, { timings: { setup: setupTime, }, @@ -92,5 +140,5 @@ export function defineNuxtModule (definition: Mo normalizedModule.getMeta = () => Promise.resolve(module.meta) normalizedModule.getOptions = getOptions - return normalizedModule as NuxtModule + return > normalizedModule } diff --git a/packages/kit/src/module/install.ts b/packages/kit/src/module/install.ts index 6d04cef352..7a0488258e 100644 --- a/packages/kit/src/module/install.ts +++ b/packages/kit/src/module/install.ts @@ -26,7 +26,7 @@ export async function installModule< } // Call module - const res = await nuxtModule(inlineOptions, nuxt) ?? {} + const res = await nuxtModule(inlineOptions || {}, nuxt) ?? {} if (res === false /* setup aborted */) { return } diff --git a/packages/schema/src/types/module.ts b/packages/schema/src/types/module.ts index 9b92d6a93e..1879678923 100644 --- a/packages/schema/src/types/module.ts +++ b/packages/schema/src/types/module.ts @@ -1,3 +1,4 @@ +import type { Defu } from 'defu' import type { NuxtHooks } from './hooks' import type { Nuxt } from './nuxt' import type { NuxtCompatibility } from './compatibility' @@ -26,8 +27,7 @@ export interface ModuleMeta { /** The options received. */ export type ModuleOptions = Record -/** Optional result for nuxt modules */ -export interface ModuleSetupReturn { +export type ModuleSetupInstallResult = { /** * Timing information for the initial setup */ @@ -39,19 +39,62 @@ export interface ModuleSetupReturn { } type Awaitable = T | Promise -type _ModuleSetupReturn = Awaitable -/** Input module passed to defineNuxtModule. */ -export interface ModuleDefinition { +type Prettify = { + [K in keyof T]: T[K]; +} & {} + +export type ModuleSetupReturn = Awaitable + +export type ResolvedModuleOptions< + TOptions extends ModuleOptions, + TOptionsDefaults extends Partial, +> = + Prettify< + Defu< + Partial, + [Partial, TOptionsDefaults] + > + > + +/** Module definition passed to 'defineNuxtModule(...)' or 'defineNuxtModule().with(...)'. */ +export interface ModuleDefinition< + TOptions extends ModuleOptions, + TOptionsDefaults extends Partial, + TWith extends boolean, +> { meta?: ModuleMeta - defaults?: T | ((nuxt: Nuxt) => T) - schema?: T + defaults?: TOptionsDefaults | ((nuxt: Nuxt) => TOptionsDefaults) + schema?: TOptions hooks?: Partial - setup?: (this: void, resolvedOptions: T, nuxt: Nuxt) => _ModuleSetupReturn + setup?: ( + this: void, + resolvedOptions: TWith extends true + ? ResolvedModuleOptions + : TOptions, + nuxt: Nuxt + ) => ModuleSetupReturn } -export interface NuxtModule { - (this: void, inlineOptions: T, nuxt: Nuxt): _ModuleSetupReturn - getOptions?: (inlineOptions?: T, nuxt?: Nuxt) => Promise +export interface NuxtModule< + TOptions extends ModuleOptions = ModuleOptions, + TOptionsDefaults extends Partial = Partial, + TWith extends boolean = false, +> { + ( + this: void, + resolvedOptions: TWith extends true + ? ResolvedModuleOptions + : TOptions, + nuxt: Nuxt + ): ModuleSetupReturn + getOptions?: ( + inlineOptions?: Partial, + nuxt?: Nuxt + ) => Promise< + TWith extends true + ? ResolvedModuleOptions + : TOptions + > getMeta?: () => Promise } diff --git a/test/fixtures/basic-types/types.ts b/test/fixtures/basic-types/types.ts index a6dc98d189..a3338af471 100644 --- a/test/fixtures/basic-types/types.ts +++ b/test/fixtures/basic-types/types.ts @@ -254,6 +254,23 @@ describe('modules', () => { }, }) }) + + it('correctly typed resolved options in defineNuxtModule setup using `.with()`', () => { + defineNuxtModule<{ + foo?: string + baz: number + }>().with({ + defaults: { + foo: 'bar', + }, + setup: (resolvedOptions) => { + expectTypeOf(resolvedOptions).toEqualTypeOf<{ + foo: string + baz?: number | undefined + }>() + }, + }) + }) }) describe('nuxtApp', () => {