mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-15 18:34:50 +00:00
179 lines
5.8 KiB
TypeScript
179 lines
5.8 KiB
TypeScript
import { existsSync } from 'node:fs'
|
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
import { pathToFileURL } from 'node:url'
|
|
import { dirname, resolve } from 'pathe'
|
|
import chokidar from 'chokidar'
|
|
import { interopDefault } from 'mlly'
|
|
import { defu } from 'defu'
|
|
import { debounce } from 'perfect-debounce'
|
|
import { createResolver, defineNuxtModule, tryResolveModule } from '@nuxt/kit'
|
|
import {
|
|
generateTypes,
|
|
resolveSchema as resolveUntypedSchema
|
|
} from 'untyped'
|
|
import type { Schema, SchemaDefinition } from 'untyped'
|
|
// @ts-expect-error TODO: add upstream type
|
|
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', async (ctx) => {
|
|
ctx.references.push({ path: 'nuxt-config-schema' })
|
|
ctx.references.push({ path: 'schema/nuxt.schema.d.ts' })
|
|
if (nuxt.options._prepare) {
|
|
await writeSchema(schema)
|
|
}
|
|
})
|
|
|
|
// Resolve schema after all modules initialized
|
|
let schema: Schema
|
|
nuxt.hook('modules:done', async () => {
|
|
schema = await resolveSchema()
|
|
})
|
|
|
|
// Write schema after build to allow further modifications
|
|
nuxt.hooks.hook('build:done', () => writeSchema(schema))
|
|
|
|
// Watch for schema changes in development mode
|
|
if (nuxt.options.dev) {
|
|
const onChange = debounce(async () => {
|
|
schema = await resolveSchema()
|
|
await writeSchema(schema)
|
|
})
|
|
|
|
if (nuxt.options.experimental.watcher === 'parcel') {
|
|
const watcherPath = await tryResolveModule('@parcel/watcher', [nuxt.options.rootDir, ...nuxt.options.modulesDir])
|
|
if (watcherPath) {
|
|
const { subscribe } = await import(pathToFileURL(watcherPath).href).then(interopDefault) as typeof import('@parcel/watcher')
|
|
for (const layer of nuxt.options._layers) {
|
|
const subscription = await subscribe(layer.config.rootDir, onChange, {
|
|
ignore: ['!nuxt.schema.*']
|
|
})
|
|
nuxt.hook('close', () => subscription.unsubscribe())
|
|
}
|
|
return
|
|
}
|
|
console.warn('[nuxt] falling back to `chokidar` as `@parcel/watcher` cannot be resolved in your project.')
|
|
}
|
|
|
|
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
|
|
})
|
|
watcher.on('all', onChange)
|
|
nuxt.hook('close', () => watcher.close())
|
|
}
|
|
|
|
// --- utils ---
|
|
|
|
async function resolveSchema () {
|
|
// Global import
|
|
// @ts-expect-error adding to globalThis for 'auto-import' support within nuxt.config file
|
|
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))
|
|
)
|
|
|
|
// Merge after normalization
|
|
const schema = defu(...schemas as [Schema, Schema])
|
|
|
|
// Allow hooking to extend resolved schema
|
|
await nuxt.hooks.callHook('schema:resolved', schema)
|
|
|
|
return schema
|
|
}
|
|
|
|
async function writeSchema (schema: Schema) {
|
|
await nuxt.hooks.callHook('schema:beforeWrite', schema)
|
|
// 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 _types = generateTypes(schema, {
|
|
addExport: true,
|
|
interfaceName: 'NuxtCustomSchema',
|
|
partial: true,
|
|
allowExtraKeys: false
|
|
})
|
|
const types =
|
|
_types +
|
|
`
|
|
export type CustomAppConfig = Exclude<NuxtCustomSchema['appConfig'], undefined>
|
|
type _CustomAppConfig = CustomAppConfig
|
|
|
|
declare module '@nuxt/schema' {
|
|
interface NuxtConfig extends Omit<NuxtCustomSchema, 'appConfig'> {}
|
|
interface NuxtOptions extends Omit<NuxtCustomSchema, 'appConfig'> {}
|
|
interface CustomAppConfig extends _CustomAppConfig {}
|
|
}
|
|
|
|
declare module 'nuxt/schema' {
|
|
interface NuxtConfig extends Omit<NuxtCustomSchema, 'appConfig'> {}
|
|
interface NuxtOptions extends Omit<NuxtCustomSchema, 'appConfig'> {}
|
|
interface CustomAppConfig extends _CustomAppConfig {}
|
|
}
|
|
`
|
|
const typesPath = resolve(
|
|
nuxt.options.buildDir,
|
|
'schema/nuxt.schema.d.ts'
|
|
)
|
|
await writeFile(typesPath, types, 'utf8')
|
|
await nuxt.hooks.callHook('schema:written')
|
|
}
|
|
}
|
|
})
|