-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
/
schema.ts
178 lines (159 loc) · 5.78 KB
/
schema.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
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-granular` 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')
}
}
})