mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-11 08:33:53 +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
|
// Loader
|
||||||
export * from './loader/config'
|
export * from './loader/config'
|
||||||
|
export * from './loader/schema'
|
||||||
export * from './loader/nuxt'
|
export * from './loader/nuxt'
|
||||||
|
|
||||||
// Utils
|
// 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/telemetry": "^2.1.9",
|
||||||
"@nuxt/ui-templates": "^1.1.0",
|
"@nuxt/ui-templates": "^1.1.0",
|
||||||
"@nuxt/vite-builder": "3.0.0",
|
"@nuxt/vite-builder": "3.0.0",
|
||||||
|
"@unhead/ssr": "^1.0.18",
|
||||||
"@vue/reactivity": "^3.2.45",
|
"@vue/reactivity": "^3.2.45",
|
||||||
"@vue/shared": "^3.2.45",
|
"@vue/shared": "^3.2.45",
|
||||||
"@vueuse/head": "^1.0.23",
|
"@vueuse/head": "^1.0.23",
|
||||||
"unhead": "^1.0.18",
|
|
||||||
"@unhead/ssr": "^1.0.18",
|
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"cookie-es": "^0.5.0",
|
"cookie-es": "^0.5.0",
|
||||||
"defu": "^6.1.1",
|
"defu": "^6.1.1",
|
||||||
@ -58,13 +57,14 @@
|
|||||||
"h3": "^1.0.2",
|
"h3": "^1.0.2",
|
||||||
"hash-sum": "^2.0.0",
|
"hash-sum": "^2.0.0",
|
||||||
"hookable": "^5.4.2",
|
"hookable": "^5.4.2",
|
||||||
|
"jiti": "^1.16.2",
|
||||||
"knitwork": "^1.0.0",
|
"knitwork": "^1.0.0",
|
||||||
"magic-string": "^0.27.0",
|
"magic-string": "^0.27.0",
|
||||||
"mlly": "^1.1.0",
|
"mlly": "^1.1.0",
|
||||||
"nitropack": "^2.0.0-rc.0",
|
"nitropack": "^2.0.0-rc.0",
|
||||||
"nuxi": "3.0.0",
|
"nuxi": "3.0.0",
|
||||||
"ohash": "^1.0.0",
|
|
||||||
"ofetch": "^1.0.0",
|
"ofetch": "^1.0.0",
|
||||||
|
"ohash": "^1.0.0",
|
||||||
"pathe": "^1.0.0",
|
"pathe": "^1.0.0",
|
||||||
"perfect-debounce": "^0.1.3",
|
"perfect-debounce": "^0.1.3",
|
||||||
"scule": "^1.0.0",
|
"scule": "^1.0.0",
|
||||||
@ -73,6 +73,7 @@
|
|||||||
"ultrahtml": "^1.2.0",
|
"ultrahtml": "^1.2.0",
|
||||||
"unctx": "^2.1.1",
|
"unctx": "^2.1.1",
|
||||||
"unenv": "^1.0.1",
|
"unenv": "^1.0.1",
|
||||||
|
"unhead": "^1.0.18",
|
||||||
"unimport": "^1.3.0",
|
"unimport": "^1.3.0",
|
||||||
"unplugin": "^1.0.1",
|
"unplugin": "^1.0.1",
|
||||||
"untyped": "^1.2.2",
|
"untyped": "^1.2.2",
|
||||||
|
@ -21,6 +21,7 @@ import { TreeShakePlugin } from './plugins/tree-shake'
|
|||||||
import { DevOnlyPlugin } from './plugins/dev-only'
|
import { DevOnlyPlugin } from './plugins/dev-only'
|
||||||
import { addModuleTranspiles } from './modules'
|
import { addModuleTranspiles } from './modules'
|
||||||
import { initNitro } from './nitro'
|
import { initNitro } from './nitro'
|
||||||
|
import schemaModule from './schema'
|
||||||
|
|
||||||
export function createNuxt (options: NuxtOptions): Nuxt {
|
export function createNuxt (options: NuxtOptions): Nuxt {
|
||||||
const hooks = createHooks<NuxtHooks>()
|
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\\/)`))
|
.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(options.workspaceDir, 'node_modules'))
|
||||||
options.modulesDir.push(resolve(pkgDir, 'node_modules'))
|
options.modulesDir.push(resolve(pkgDir, 'node_modules'))
|
||||||
options.build.transpile.push('@nuxt/ui-templates')
|
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" />
|
/// <reference types="nitropack" />
|
||||||
export * from './dist/index'
|
export * from './dist/index'
|
||||||
|
|
||||||
|
import type { Schema, SchemaDefinition } from '@nuxt/schema'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
const defineNuxtConfig: typeof import('nuxt/config')['defineNuxtConfig']
|
const defineNuxtConfig: typeof import('nuxt/config')['defineNuxtConfig']
|
||||||
|
const defineNuxtSchema: (schema: SchemaDefinition) => SchemaDefinition
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'nitropack' {
|
declare module 'nitropack' {
|
||||||
|
@ -442,4 +442,6 @@ export default defineUntypedSchema({
|
|||||||
* @type {typeof import('../src/types/config').AppConfig}
|
* @type {typeof import('../src/types/config').AppConfig}
|
||||||
*/
|
*/
|
||||||
appConfig: {},
|
appConfig: {},
|
||||||
|
|
||||||
|
$schema: {}
|
||||||
})
|
})
|
||||||
|
@ -100,6 +100,13 @@ export default defineUntypedSchema({
|
|||||||
/**
|
/**
|
||||||
* Experimental component islands support with <NuxtIsland> and .island.vue files.
|
* 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 { Options as VuePluginOptions } from '@vitejs/plugin-vue'
|
||||||
import type { AppHeadMetaObject } from './meta'
|
import type { AppHeadMetaObject } from './meta'
|
||||||
import type { Nuxt } from './nuxt'
|
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
|
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'>> {
|
export interface NuxtConfig extends DeepPartial<Omit<ConfigSchema, 'vite'>> {
|
||||||
// Avoid DeepPartial for vite config interface (#4772)
|
// Avoid DeepPartial for vite config interface (#4772)
|
||||||
vite?: ConfigSchema['vite']
|
vite?: ConfigSchema['vite']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Experimental custom config schema
|
||||||
|
*
|
||||||
|
* @see https://github.com/nuxt/nuxt/issues/15592
|
||||||
|
*/
|
||||||
|
$schema?: SchemaDefinition
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Expose ConfigLayer<T> from c12
|
// TODO: Expose ConfigLayer<T> from c12
|
||||||
@ -29,6 +38,7 @@ export interface NuxtOptions extends Omit<ConfigSchema, 'builder'> {
|
|||||||
sourcemap: Required<Exclude<ConfigSchema['sourcemap'], boolean>>
|
sourcemap: Required<Exclude<ConfigSchema['sourcemap'], boolean>>
|
||||||
builder: '@nuxt/vite-builder' | '@nuxt/webpack-builder' | { bundle: (nuxt: Nuxt) => Promise<void> }
|
builder: '@nuxt/vite-builder' | '@nuxt/webpack-builder' | { bundle: (nuxt: Nuxt) => Promise<void> }
|
||||||
_layers: NuxtConfigLayer[]
|
_layers: NuxtConfigLayer[]
|
||||||
|
$schema: SchemaDefinition
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViteConfig extends ViteUserConfig {
|
export interface ViteConfig extends ViteUserConfig {
|
||||||
|
@ -10,6 +10,7 @@ import type { Nuxt, NuxtApp, ResolvedNuxtTemplate } from './nuxt'
|
|||||||
import type { Nitro, NitroConfig } from 'nitropack'
|
import type { Nitro, NitroConfig } from 'nitropack'
|
||||||
import type { Component, ComponentsOptions } from './components'
|
import type { Component, ComponentsOptions } from './components'
|
||||||
import type { NuxtCompatibility, NuxtCompatibilityIssues } from '..'
|
import type { NuxtCompatibility, NuxtCompatibilityIssues } from '..'
|
||||||
|
import type { Schema, SchemaDefinition } from 'untyped'
|
||||||
|
|
||||||
export type HookResult = Promise<void> | void
|
export type HookResult = Promise<void> | void
|
||||||
|
|
||||||
@ -92,6 +93,12 @@ export interface NuxtHooks {
|
|||||||
'prepare:types': (options: { references: TSReference[], declarations: string[], tsConfig: TSConfig }) => HookResult
|
'prepare:types': (options: { references: TSReference[], declarations: string[], tsConfig: TSConfig }) => HookResult
|
||||||
'listen': (listenerServer: HttpServer | HttpsServer, listener: any) => 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
|
||||||
'vite:extend': (viteBuildContext: { nuxt: Nuxt, config: ViteInlineConfig }) => HookResult
|
'vite:extend': (viteBuildContext: { nuxt: Nuxt, config: ViteInlineConfig }) => HookResult
|
||||||
'vite:extendConfig': (viteInlineConfig: ViteInlineConfig, env: { isClient: boolean, isServer: boolean }) => HookResult
|
'vite:extendConfig': (viteInlineConfig: ViteInlineConfig, env: { isClient: boolean, isServer: boolean }) => HookResult
|
||||||
|
@ -432,6 +432,7 @@ importers:
|
|||||||
h3: ^1.0.2
|
h3: ^1.0.2
|
||||||
hash-sum: ^2.0.0
|
hash-sum: ^2.0.0
|
||||||
hookable: ^5.4.2
|
hookable: ^5.4.2
|
||||||
|
jiti: ^1.16.2
|
||||||
knitwork: ^1.0.0
|
knitwork: ^1.0.0
|
||||||
magic-string: ^0.27.0
|
magic-string: ^0.27.0
|
||||||
mlly: ^1.1.0
|
mlly: ^1.1.0
|
||||||
@ -478,6 +479,7 @@ importers:
|
|||||||
h3: 1.0.2
|
h3: 1.0.2
|
||||||
hash-sum: 2.0.0
|
hash-sum: 2.0.0
|
||||||
hookable: 5.4.2
|
hookable: 5.4.2
|
||||||
|
jiti: 1.16.2
|
||||||
knitwork: 1.0.0
|
knitwork: 1.0.0
|
||||||
magic-string: 0.27.0
|
magic-string: 0.27.0
|
||||||
mlly: 1.1.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,
|
componentIslands: true,
|
||||||
reactivityTransform: true,
|
reactivityTransform: true,
|
||||||
treeshakeClientOnly: true,
|
treeshakeClientOnly: true,
|
||||||
payloadExtraction: true
|
payloadExtraction: true,
|
||||||
|
configSchema: true
|
||||||
},
|
},
|
||||||
appConfig: {
|
appConfig: {
|
||||||
fromNuxtConfig: true,
|
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