feat(kit,schema): add `.with` for better module options types (#27520)

This commit is contained in:
Damian Głowala 2024-06-21 10:33:42 +02:00 committed by GitHub
parent 3a59c02cfd
commit e374474df3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 136 additions and 28 deletions

View File

@ -1,7 +1,7 @@
import { performance } from 'node:perf_hooks' import { performance } from 'node:perf_hooks'
import { defu } from 'defu' import { defu } from 'defu'
import { applyDefaults } from 'untyped' 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 { logger } from '../logger'
import { tryUseNuxt, useNuxt } from '../context' import { tryUseNuxt, useNuxt } from '../context'
import { checkNuxtCompatibility } from '../compatibility' import { checkNuxtCompatibility } from '../compatibility'
@ -10,28 +10,76 @@ import { checkNuxtCompatibility } from '../compatibility'
* Define a Nuxt module, automatically merging defaults with user provided options, installing * 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. * any hooks that are provided, and calling an optional setup function for full control.
*/ */
export function defineNuxtModule<OptionsT extends ModuleOptions> (definition: ModuleDefinition<OptionsT> | NuxtModule<OptionsT>): NuxtModule<OptionsT> { export function defineNuxtModule<TOptions extends ModuleOptions> (
if (typeof definition === 'function') { return defineNuxtModule({ setup: definition }) } definition: ModuleDefinition<TOptions, Partial<TOptions>, false> | NuxtModule<TOptions, Partial<TOptions>, false>
): NuxtModule<TOptions, TOptions, false>
// Normalize definition and meta export function defineNuxtModule<TOptions extends ModuleOptions> (): {
const module: ModuleDefinition<OptionsT> & Required<Pick<ModuleDefinition<OptionsT>, 'meta'>> = defu(definition, { meta: {} }) with: <TOptionsDefaults extends Partial<TOptions>> (
if (module.meta.configKey === undefined) { definition: ModuleDefinition<TOptions, TOptionsDefaults, true> | NuxtModule<TOptions, TOptionsDefaults, true>
module.meta.configKey = module.meta.name ) => NuxtModule<TOptions, TOptionsDefaults, true>
}
export function defineNuxtModule<TOptions extends ModuleOptions> (
definition?: ModuleDefinition<TOptions, Partial<TOptions>, false> | NuxtModule<TOptions, Partial<TOptions>, false>,
) {
if (definition) {
return _defineNuxtModule(definition)
} }
return {
with: <TOptionsDefaults extends Partial<TOptions>>(
definition: ModuleDefinition<TOptions, TOptionsDefaults, true> | NuxtModule<TOptions, TOptionsDefaults, true>,
) => _defineNuxtModule(definition),
}
}
function _defineNuxtModule<
TOptions extends ModuleOptions,
TOptionsDefaults extends Partial<TOptions>,
TWith extends boolean,
> (
definition: ModuleDefinition<TOptions, TOptionsDefaults, TWith> | NuxtModule<TOptions, TOptionsDefaults, TWith>,
): NuxtModule<TOptions, TOptionsDefaults, TWith> {
if (typeof definition === 'function') {
return _defineNuxtModule<TOptions, TOptionsDefaults, TWith>({ setup: definition })
}
// Normalize definition and meta
const module: ModuleDefinition<TOptions, TOptionsDefaults, TWith> & Required<Pick<ModuleDefinition<TOptions, TOptionsDefaults, TWith>, 'meta'>> = defu(definition, { meta: {} })
module.meta.configKey ||= module.meta.name
// Resolves module options from inline options, [configKey] in nuxt.config, defaults and schema // Resolves module options from inline options, [configKey] in nuxt.config, defaults and schema
async function getOptions (inlineOptions?: OptionsT, nuxt: Nuxt = useNuxt()) { async function getOptions (
const configKey = module.meta.configKey || module.meta.name! inlineOptions?: Partial<TOptions>,
const _defaults = module.defaults instanceof Function ? module.defaults(nuxt) : module.defaults nuxt: Nuxt = useNuxt(),
let _options = defu(inlineOptions, nuxt.options[configKey as keyof NuxtOptions], _defaults) as OptionsT ): Promise<
TWith extends true
? ResolvedModuleOptions<TOptions, TOptionsDefaults>
: TOptions
> {
const nuxtConfigOptionsKey = module.meta.configKey || module.meta.name
const nuxtConfigOptions: Partial<TOptions> = nuxtConfigOptionsKey && nuxtConfigOptionsKey in nuxt.options ? nuxt.options[<keyof NuxtOptions> nuxtConfigOptionsKey] : {}
const optionsDefaults: TOptionsDefaults =
module.defaults instanceof Function
? module.defaults(nuxt)
: module.defaults ?? <TOptionsDefaults> {}
let options = defu(inlineOptions, nuxtConfigOptions, optionsDefaults)
if (module.schema) { 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 // Module format is always a simple function
async function normalizedModule (this: any, inlineOptions: OptionsT, nuxt: Nuxt) { async function normalizedModule (this: any, inlineOptions: Partial<TOptions>, nuxt: Nuxt): Promise<ModuleSetupReturn> {
if (!nuxt) { if (!nuxt) {
nuxt = tryUseNuxt() || this.nuxt /* invoked by nuxt 2 */ nuxt = tryUseNuxt() || this.nuxt /* invoked by nuxt 2 */
} }
@ -81,7 +129,7 @@ export function defineNuxtModule<OptionsT extends ModuleOptions> (definition: Mo
if (res === false) { return false } if (res === false) { return false }
// Return module install result // Return module install result
return defu(res, <ModuleSetupReturn> { return defu(res, <ModuleSetupInstallResult> {
timings: { timings: {
setup: setupTime, setup: setupTime,
}, },
@ -92,5 +140,5 @@ export function defineNuxtModule<OptionsT extends ModuleOptions> (definition: Mo
normalizedModule.getMeta = () => Promise.resolve(module.meta) normalizedModule.getMeta = () => Promise.resolve(module.meta)
normalizedModule.getOptions = getOptions normalizedModule.getOptions = getOptions
return normalizedModule as NuxtModule<OptionsT> return <NuxtModule<TOptions, TOptionsDefaults, TWith>> normalizedModule
} }

View File

@ -26,7 +26,7 @@ export async function installModule<
} }
// Call module // Call module
const res = await nuxtModule(inlineOptions, nuxt) ?? {} const res = await nuxtModule(inlineOptions || {}, nuxt) ?? {}
if (res === false /* setup aborted */) { if (res === false /* setup aborted */) {
return return
} }

View File

@ -1,3 +1,4 @@
import type { Defu } from 'defu'
import type { NuxtHooks } from './hooks' import type { NuxtHooks } from './hooks'
import type { Nuxt } from './nuxt' import type { Nuxt } from './nuxt'
import type { NuxtCompatibility } from './compatibility' import type { NuxtCompatibility } from './compatibility'
@ -26,8 +27,7 @@ export interface ModuleMeta {
/** The options received. */ /** The options received. */
export type ModuleOptions = Record<string, any> export type ModuleOptions = Record<string, any>
/** Optional result for nuxt modules */ export type ModuleSetupInstallResult = {
export interface ModuleSetupReturn {
/** /**
* Timing information for the initial setup * Timing information for the initial setup
*/ */
@ -39,19 +39,62 @@ export interface ModuleSetupReturn {
} }
type Awaitable<T> = T | Promise<T> type Awaitable<T> = T | Promise<T>
type _ModuleSetupReturn = Awaitable<void | false | ModuleSetupReturn>
/** Input module passed to defineNuxtModule. */ type Prettify<T> = {
export interface ModuleDefinition<T extends ModuleOptions = ModuleOptions> { [K in keyof T]: T[K];
} & {}
export type ModuleSetupReturn = Awaitable<false | void | ModuleSetupInstallResult>
export type ResolvedModuleOptions<
TOptions extends ModuleOptions,
TOptionsDefaults extends Partial<TOptions>,
> =
Prettify<
Defu<
Partial<TOptions>,
[Partial<TOptions>, TOptionsDefaults]
>
>
/** Module definition passed to 'defineNuxtModule(...)' or 'defineNuxtModule().with(...)'. */
export interface ModuleDefinition<
TOptions extends ModuleOptions,
TOptionsDefaults extends Partial<TOptions>,
TWith extends boolean,
> {
meta?: ModuleMeta meta?: ModuleMeta
defaults?: T | ((nuxt: Nuxt) => T) defaults?: TOptionsDefaults | ((nuxt: Nuxt) => TOptionsDefaults)
schema?: T schema?: TOptions
hooks?: Partial<NuxtHooks> hooks?: Partial<NuxtHooks>
setup?: (this: void, resolvedOptions: T, nuxt: Nuxt) => _ModuleSetupReturn setup?: (
this: void,
resolvedOptions: TWith extends true
? ResolvedModuleOptions<TOptions, TOptionsDefaults>
: TOptions,
nuxt: Nuxt
) => ModuleSetupReturn
} }
export interface NuxtModule<T extends ModuleOptions = ModuleOptions> { export interface NuxtModule<
(this: void, inlineOptions: T, nuxt: Nuxt): _ModuleSetupReturn TOptions extends ModuleOptions = ModuleOptions,
getOptions?: (inlineOptions?: T, nuxt?: Nuxt) => Promise<T> TOptionsDefaults extends Partial<TOptions> = Partial<TOptions>,
TWith extends boolean = false,
> {
(
this: void,
resolvedOptions: TWith extends true
? ResolvedModuleOptions<TOptions, TOptionsDefaults>
: TOptions,
nuxt: Nuxt
): ModuleSetupReturn
getOptions?: (
inlineOptions?: Partial<TOptions>,
nuxt?: Nuxt
) => Promise<
TWith extends true
? ResolvedModuleOptions<TOptions, TOptionsDefaults>
: TOptions
>
getMeta?: () => Promise<ModuleMeta> getMeta?: () => Promise<ModuleMeta>
} }

View File

@ -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', () => { describe('nuxtApp', () => {