mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 07:05:11 +00:00
feat: experimental config schema (#18410)
This commit is contained in:
parent
3c715ac729
commit
1af319e0fa
@ -4,6 +4,7 @@ export * from './module/install'
|
||||
|
||||
// Loader
|
||||
export * from './loader/config'
|
||||
export * from './loader/schema'
|
||||
export * from './loader/nuxt'
|
||||
|
||||
// Utils
|
||||
|
9
packages/kit/src/loader/schema.ts
Normal file
9
packages/kit/src/loader/schema.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import type { SchemaDefinition } from '@nuxt/schema'
|
||||
import { useNuxt } from '../context'
|
||||
|
||||
export function extendNuxtSchema (def: SchemaDefinition | (() => SchemaDefinition)) {
|
||||
const nuxt = useNuxt()
|
||||
nuxt.hook('schema:extend', (schemas) => {
|
||||
schemas.push(typeof def === 'function' ? def() : def)
|
||||
})
|
||||
}
|
@ -42,11 +42,10 @@
|
||||
"@nuxt/telemetry": "^2.1.9",
|
||||
"@nuxt/ui-templates": "^1.1.0",
|
||||
"@nuxt/vite-builder": "3.0.0",
|
||||
"@unhead/ssr": "^1.0.18",
|
||||
"@vue/reactivity": "^3.2.45",
|
||||
"@vue/shared": "^3.2.45",
|
||||
"@vueuse/head": "^1.0.23",
|
||||
"unhead": "^1.0.18",
|
||||
"@unhead/ssr": "^1.0.18",
|
||||
"chokidar": "^3.5.3",
|
||||
"cookie-es": "^0.5.0",
|
||||
"defu": "^6.1.1",
|
||||
@ -58,13 +57,14 @@
|
||||
"h3": "^1.0.2",
|
||||
"hash-sum": "^2.0.0",
|
||||
"hookable": "^5.4.2",
|
||||
"jiti": "^1.16.2",
|
||||
"knitwork": "^1.0.0",
|
||||
"magic-string": "^0.27.0",
|
||||
"mlly": "^1.1.0",
|
||||
"nitropack": "^2.0.0-rc.0",
|
||||
"nuxi": "3.0.0",
|
||||
"ohash": "^1.0.0",
|
||||
"ofetch": "^1.0.0",
|
||||
"ohash": "^1.0.0",
|
||||
"pathe": "^1.0.0",
|
||||
"perfect-debounce": "^0.1.3",
|
||||
"scule": "^1.0.0",
|
||||
@ -73,6 +73,7 @@
|
||||
"ultrahtml": "^1.2.0",
|
||||
"unctx": "^2.1.1",
|
||||
"unenv": "^1.0.1",
|
||||
"unhead": "^1.0.18",
|
||||
"unimport": "^1.3.0",
|
||||
"unplugin": "^1.0.1",
|
||||
"untyped": "^1.2.2",
|
||||
|
@ -21,6 +21,7 @@ import { TreeShakePlugin } from './plugins/tree-shake'
|
||||
import { DevOnlyPlugin } from './plugins/dev-only'
|
||||
import { addModuleTranspiles } from './modules'
|
||||
import { initNitro } from './nitro'
|
||||
import schemaModule from './schema'
|
||||
|
||||
export function createNuxt (options: NuxtOptions): Nuxt {
|
||||
const hooks = createHooks<NuxtHooks>()
|
||||
@ -231,6 +232,7 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
|
||||
.map(i => new RegExp(`(^|\\/)${escapeRE(i.cwd!.split('node_modules/').pop()!)}(\\/|$)(?!node_modules\\/)`))
|
||||
}
|
||||
}])
|
||||
options._modules.push(schemaModule)
|
||||
options.modulesDir.push(resolve(options.workspaceDir, 'node_modules'))
|
||||
options.modulesDir.push(resolve(pkgDir, 'node_modules'))
|
||||
options.build.transpile.push('@nuxt/ui-templates')
|
||||
|
168
packages/nuxt/src/core/schema.ts
Normal file
168
packages/nuxt/src/core/schema.ts
Normal file
@ -0,0 +1,168 @@
|
||||
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')
|
||||
}
|
||||
}
|
||||
})
|
3
packages/nuxt/types.d.ts
vendored
3
packages/nuxt/types.d.ts
vendored
@ -1,8 +1,11 @@
|
||||
/// <reference types="nitropack" />
|
||||
export * from './dist/index'
|
||||
|
||||
import type { Schema, SchemaDefinition } from '@nuxt/schema'
|
||||
|
||||
declare global {
|
||||
const defineNuxtConfig: typeof import('nuxt/config')['defineNuxtConfig']
|
||||
const defineNuxtSchema: (schema: SchemaDefinition) => SchemaDefinition
|
||||
}
|
||||
|
||||
declare module 'nitropack' {
|
||||
|
@ -442,4 +442,6 @@ export default defineUntypedSchema({
|
||||
* @type {typeof import('../src/types/config').AppConfig}
|
||||
*/
|
||||
appConfig: {},
|
||||
|
||||
$schema: {}
|
||||
})
|
||||
|
@ -100,6 +100,13 @@ export default defineUntypedSchema({
|
||||
/**
|
||||
* Experimental component islands support with <NuxtIsland> and .island.vue files.
|
||||
*/
|
||||
componentIslands: false
|
||||
componentIslands: false,
|
||||
|
||||
/**
|
||||
* Enable experimental config schema support
|
||||
*
|
||||
* @see https://github.com/nuxt/nuxt/issues/15592
|
||||
*/
|
||||
configSchema: false
|
||||
}
|
||||
})
|
||||
|
@ -4,6 +4,8 @@ import type { ServerOptions as ViteServerOptions, UserConfig as ViteUserConfig }
|
||||
import type { Options as VuePluginOptions } from '@vitejs/plugin-vue'
|
||||
import type { AppHeadMetaObject } from './meta'
|
||||
import type { Nuxt } from './nuxt'
|
||||
import type { SchemaDefinition } from 'untyped'
|
||||
export type { SchemaDefinition } from 'untyped'
|
||||
|
||||
type DeepPartial<T> = T extends Function ? T : T extends Record<string, any> ? { [P in keyof T]?: DeepPartial<T[P]> } : T
|
||||
|
||||
@ -11,6 +13,13 @@ type DeepPartial<T> = T extends Function ? T : T extends Record<string, any> ? {
|
||||
export interface NuxtConfig extends DeepPartial<Omit<ConfigSchema, 'vite'>> {
|
||||
// Avoid DeepPartial for vite config interface (#4772)
|
||||
vite?: ConfigSchema['vite']
|
||||
|
||||
/**
|
||||
* Experimental custom config schema
|
||||
*
|
||||
* @see https://github.com/nuxt/nuxt/issues/15592
|
||||
*/
|
||||
$schema?: SchemaDefinition
|
||||
}
|
||||
|
||||
// TODO: Expose ConfigLayer<T> from c12
|
||||
@ -29,6 +38,7 @@ export interface NuxtOptions extends Omit<ConfigSchema, 'builder'> {
|
||||
sourcemap: Required<Exclude<ConfigSchema['sourcemap'], boolean>>
|
||||
builder: '@nuxt/vite-builder' | '@nuxt/webpack-builder' | { bundle: (nuxt: Nuxt) => Promise<void> }
|
||||
_layers: NuxtConfigLayer[]
|
||||
$schema: SchemaDefinition
|
||||
}
|
||||
|
||||
export interface ViteConfig extends ViteUserConfig {
|
||||
|
@ -10,6 +10,7 @@ import type { Nuxt, NuxtApp, ResolvedNuxtTemplate } from './nuxt'
|
||||
import type { Nitro, NitroConfig } from 'nitropack'
|
||||
import type { Component, ComponentsOptions } from './components'
|
||||
import type { NuxtCompatibility, NuxtCompatibilityIssues } from '..'
|
||||
import type { Schema, SchemaDefinition } from 'untyped'
|
||||
|
||||
export type HookResult = Promise<void> | void
|
||||
|
||||
@ -92,6 +93,12 @@ export interface NuxtHooks {
|
||||
'prepare:types': (options: { references: TSReference[], declarations: string[], tsConfig: TSConfig }) => HookResult
|
||||
'listen': (listenerServer: HttpServer | HttpsServer, listener: any) => HookResult
|
||||
|
||||
// Schema
|
||||
'schema:extend': (schemas: SchemaDefinition[]) => void
|
||||
'schema:resolved': (schema: Schema) => void
|
||||
'schema:beforeWrite': (schema: Schema) => void
|
||||
'schema:written': () => void
|
||||
|
||||
// Vite
|
||||
'vite:extend': (viteBuildContext: { nuxt: Nuxt, config: ViteInlineConfig }) => HookResult
|
||||
'vite:extendConfig': (viteInlineConfig: ViteInlineConfig, env: { isClient: boolean, isServer: boolean }) => HookResult
|
||||
|
@ -432,6 +432,7 @@ importers:
|
||||
h3: ^1.0.2
|
||||
hash-sum: ^2.0.0
|
||||
hookable: ^5.4.2
|
||||
jiti: ^1.16.2
|
||||
knitwork: ^1.0.0
|
||||
magic-string: ^0.27.0
|
||||
mlly: ^1.1.0
|
||||
@ -478,6 +479,7 @@ importers:
|
||||
h3: 1.0.2
|
||||
hash-sum: 2.0.0
|
||||
hookable: 5.4.2
|
||||
jiti: 1.16.2
|
||||
knitwork: 1.0.0
|
||||
magic-string: 0.27.0
|
||||
mlly: 1.1.0
|
||||
|
3
test/fixtures/basic/nuxt.config.ts
vendored
3
test/fixtures/basic/nuxt.config.ts
vendored
@ -146,7 +146,8 @@ export default defineNuxtConfig({
|
||||
componentIslands: true,
|
||||
reactivityTransform: true,
|
||||
treeshakeClientOnly: true,
|
||||
payloadExtraction: true
|
||||
payloadExtraction: true,
|
||||
configSchema: true
|
||||
},
|
||||
appConfig: {
|
||||
fromNuxtConfig: true,
|
||||
|
6
test/fixtures/basic/nuxt.schema.ts
vendored
Normal file
6
test/fixtures/basic/nuxt.schema.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
export default defineNuxtSchema({
|
||||
appConfig: {
|
||||
/** This is an example app config defined in custom schema */
|
||||
userConfig: 123
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue
Block a user