Nuxt/packages/nuxt/src/core/schema.ts

169 lines
5.1 KiB
TypeScript
Raw Normal View History

import { existsSync } from 'node:fs'
import { writeFile, mkdir, rm } from 'node:fs/promises'
import { dirname, resolve } from 'pathe'
import chokidar from 'chokidar'
import { defu } from 'defu'
import { debounce } from 'perfect-debounce'
import { defineNuxtModule, createResolver } from '@nuxt/kit'
import {
resolveSchema as resolveUntypedSchema,
generateMarkdown,
generateTypes
} from 'untyped'
import type { Schema, SchemaDefinition } from 'untyped'
// @ts-ignore
import untypedPlugin from 'untyped/babel-plugin'
import jiti from 'jiti'
export default defineNuxtModule({
meta: {
name: 'nuxt-config-schema'
},
async setup (_, nuxt) {
if (!nuxt.options.experimental.configSchema) {
return
}
const resolver = createResolver(import.meta.url)
// Initialize untyped/jiti loader
const _resolveSchema = jiti(dirname(import.meta.url), {
esmResolve: true,
interopDefault: true,
cache: false,
requireCache: false,
transformOptions: {
babel: {
plugins: [untypedPlugin]
}
}
})
// Register module types
nuxt.hook('prepare:types', (ctx) => {
ctx.references.push({ path: 'nuxt-config-schema' })
ctx.references.push({ path: 'schema/nuxt.schema.d.ts' })
})
// Resolve schema after all modules initialized
let schema: Schema
nuxt.hook('modules:done', async () => {
schema = await resolveSchema()
})
// Writie schema after build to allow further modifications
nuxt.hooks.hook('build:done', async () => {
await nuxt.hooks.callHook('schema:beforeWrite', schema)
await writeSchema(schema)
await nuxt.hooks.callHook('schema:written')
})
// Watch for schema changes in development mode
if (nuxt.options.dev) {
const filesToWatch = await Promise.all(nuxt.options._layers.map(layer =>
resolver.resolve(layer.config.rootDir, 'nuxt.schema.*')
))
const watcher = chokidar.watch(filesToWatch, {
...nuxt.options.watchers.chokidar,
ignoreInitial: true
})
const onChange = debounce(async () => {
schema = await resolveSchema()
await nuxt.hooks.callHook('schema:beforeWrite', schema)
await writeSchema(schema)
await nuxt.hooks.callHook('schema:written')
})
watcher.on('all', onChange)
nuxt.hook('close', () => watcher.close())
}
// --- utils ---
async function resolveSchema () {
// Global import
// @ts-ignore
globalThis.defineNuxtSchema = (val: any) => val
// Load schema from layers
const schemaDefs: SchemaDefinition[] = [nuxt.options.$schema]
for (const layer of nuxt.options._layers) {
const filePath = await resolver.resolvePath(resolve(layer.config.rootDir, 'nuxt.schema'))
if (filePath && existsSync(filePath)) {
let loadedConfig: SchemaDefinition
try {
loadedConfig = _resolveSchema(filePath)
} catch (err) {
console.warn(
'[nuxt-config-schema] Unable to load schema from',
filePath,
err
)
continue
}
schemaDefs.push(loadedConfig)
}
}
// Allow hooking to extend custom schemas
await nuxt.hooks.callHook('schema:extend', schemaDefs)
// Resolve and merge schemas
const schemas = await Promise.all(
schemaDefs.map(schemaDef => resolveUntypedSchema(schemaDef))
)
// @ts-expect-error
// Merge after normalazation
const schema = defu(...schemas)
// Allow hooking to extend resolved schema
await nuxt.hooks.callHook('schema:resolved', schema)
return schema
}
async function writeSchema (schema: Schema) {
// Avoid writing empty schema
const isEmptySchema = !schema.properties || Object.keys(schema.properties).length === 0
if (isEmptySchema) {
await rm(resolve(nuxt.options.buildDir, 'schema'), { recursive: true }).catch(() => { })
return
}
// Write it to build dir
await mkdir(resolve(nuxt.options.buildDir, 'schema'), { recursive: true })
await writeFile(
resolve(nuxt.options.buildDir, 'schema/nuxt.schema.json'),
JSON.stringify(schema, null, 2),
'utf8'
)
const markdown = '# Nuxt Custom Config Schema' + generateMarkdown(schema)
await writeFile(
resolve(nuxt.options.buildDir, 'schema/nuxt.schema.md'),
markdown,
'utf8'
)
const _types = generateTypes(schema, {
addExport: true,
interfaceName: 'NuxtCustomSchema',
partial: true
})
const types =
_types +
`
export type CustomAppConfig = Exclude<NuxtCustomSchema['appConfig'], undefined>
declare module '@nuxt/schema' {
interface NuxtConfig extends NuxtCustomSchema {}
interface NuxtOptions extends NuxtCustomSchema {}
interface AppConfigInput extends CustomAppConfig {}
interface AppConfig extends CustomAppConfig {}
}`
const typesPath = resolve(
nuxt.options.buildDir,
'schema/nuxt.schema.d.ts'
)
await writeFile(typesPath, types, 'utf8')
}
}
})