schema-dsl 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +130 -113
- package/LICENSE +21 -21
- package/README.md +628 -628
- package/dist/{DslBuilder-DkLaOo9Q.d.ts → DslBuilder-BIgQOAXp.d.ts} +2 -0
- package/dist/{DslBuilder-DQDN0ZxZ.d.cts → DslBuilder-CjHTucNQ.d.cts} +2 -0
- package/dist/{Validator-hFWKGxir.d.ts → Validator-CllRdrY0.d.ts} +1 -1
- package/dist/{Validator-C7GsVQOH.d.cts → Validator-D6okG9tr.d.cts} +1 -1
- package/dist/index.cjs +75 -29
- package/dist/index.d.cts +10 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.js +75 -29
- package/dist/plugins/custom-format.cjs +33 -17
- package/dist/plugins/custom-format.d.cts +1 -1
- package/dist/plugins/custom-format.d.ts +1 -1
- package/dist/plugins/custom-format.js +33 -17
- package/dist/plugins/custom-type-example.cjs +33 -17
- package/dist/plugins/custom-type-example.d.cts +1 -1
- package/dist/plugins/custom-type-example.d.ts +1 -1
- package/dist/plugins/custom-type-example.js +33 -17
- package/dist/plugins/custom-validator.cjs +0 -2
- package/dist/plugins/custom-validator.d.cts +1 -1
- package/dist/plugins/custom-validator.d.ts +1 -1
- package/dist/plugins/custom-validator.js +0 -2
- package/docs/FEATURE-INDEX.md +553 -553
- package/docs/add-custom-locale.md +496 -496
- package/docs/add-keyword.md +24 -24
- package/docs/api-reference.md +1047 -1047
- package/docs/api.md +13 -13
- package/docs/best-practices-project-structure.md +417 -417
- package/docs/best-practices.md +712 -712
- package/docs/cache-manager.md +344 -344
- package/docs/compile.md +45 -45
- package/docs/conditional-api.md +1307 -1307
- package/docs/custom-extensions-guide.md +339 -339
- package/docs/design-philosophy.md +606 -606
- package/docs/doc-index.md +324 -324
- package/docs/dsl-syntax.md +714 -714
- package/docs/dynamic-locale.md +608 -608
- package/docs/enum.md +482 -482
- package/docs/error-handling.md +1975 -1975
- package/docs/export-guide.md +501 -501
- package/docs/export-limitations.md +567 -567
- package/docs/faq.md +596 -596
- package/docs/frontend-i18n-guide.md +307 -307
- package/docs/i18n-user-guide.md +487 -487
- package/docs/i18n.md +476 -476
- package/docs/index.md +48 -48
- package/docs/json-schema-basics.md +40 -40
- package/docs/label-vs-description.md +271 -271
- package/docs/markdown-exporter.md +406 -406
- package/docs/mongodb-exporter.md +302 -302
- package/docs/multi-language.md +26 -26
- package/docs/multi-type-support.md +322 -322
- package/docs/mysql-exporter.md +280 -280
- package/docs/number-operators.md +449 -449
- package/docs/optional-marker-guide.md +326 -326
- package/docs/performance-guide.md +49 -49
- package/docs/plugin-system.md +381 -381
- package/docs/plugin-type-registration.md +34 -34
- package/docs/postgresql-exporter.md +311 -311
- package/docs/public/favicon.svg +4 -4
- package/docs/quick-start.md +435 -435
- package/docs/runtime-locale-support.md +532 -532
- package/docs/schema-helper.md +345 -345
- package/docs/schema-utils-advanced-issues.md +23 -23
- package/docs/schema-utils-best-practices.md +20 -20
- package/docs/schema-utils-chaining.md +150 -150
- package/docs/schema-utils.md +524 -524
- package/docs/security-checklist.md +20 -20
- package/docs/string-extensions.md +488 -488
- package/docs/troubleshooting.md +486 -486
- package/docs/type-converter.md +310 -310
- package/docs/type-reference.md +242 -242
- package/docs/typescript-guide.md +584 -584
- package/docs/union-type-guide.md +157 -157
- package/docs/union-types.md +284 -284
- package/docs/validate-async.md +491 -491
- package/docs/validate-batch.md +49 -49
- package/docs/validate-dsl-object-support.md +578 -578
- package/docs/validate.md +506 -506
- package/docs/validation-guide.md +502 -502
- package/docs/validator.md +39 -39
- package/package.json +131 -131
- package/plugins/custom-format.cjs +8 -8
- package/plugins/custom-type-example.cjs +8 -8
- package/plugins/custom-validator.cjs +8 -8
- package/src/adapters/DslAdapter.ts +111 -111
- package/src/adapters/index.ts +1 -1
- package/src/config/constants.ts +83 -83
- package/src/config/index.ts +2 -2
- package/src/config/patterns.ts +77 -77
- package/src/core/CacheManager.ts +169 -159
- package/src/core/ConditionalBuilder.ts +382 -382
- package/src/core/ConditionalRuntime.ts +27 -27
- package/src/core/ConditionalValidator.ts +254 -254
- package/src/core/DslBuilder.ts +687 -677
- package/src/core/ErrorCodes.ts +38 -38
- package/src/core/ErrorFormatter.ts +271 -271
- package/src/core/JSONSchemaCore.ts +65 -65
- package/src/core/Locale.ts +187 -187
- package/src/core/MessageTemplate.ts +42 -42
- package/src/core/ObjectDslBuilder.ts +64 -64
- package/src/core/PluginManager.ts +326 -326
- package/src/core/StringExtensions.ts +140 -140
- package/src/core/TemplateEngine.ts +44 -44
- package/src/core/Validator.ts +448 -448
- package/src/errors/I18nError.ts +159 -159
- package/src/errors/ValidationError.ts +105 -105
- package/src/exporters/BaseExporter.ts +60 -60
- package/src/exporters/MarkdownExporter.ts +305 -305
- package/src/exporters/MongoDBExporter.ts +126 -126
- package/src/exporters/MySQLExporter.ts +156 -155
- package/src/exporters/PostgreSQLExporter.ts +222 -222
- package/src/exporters/index.ts +18 -18
- package/src/index.ts +651 -633
- package/src/locales/en-US.ts +160 -160
- package/src/locales/es-ES.ts +160 -160
- package/src/locales/fr-FR.ts +160 -160
- package/src/locales/index.ts +103 -103
- package/src/locales/ja-JP.ts +160 -160
- package/src/locales/types.ts +156 -156
- package/src/locales/zh-CN.ts +160 -160
- package/src/parser/ConstraintParser.ts +101 -101
- package/src/parser/DslParser.ts +470 -470
- package/src/parser/SchemaCompiler.ts +66 -66
- package/src/parser/TypeRegistry.ts +250 -250
- package/src/parser/index.ts +6 -6
- package/src/plugins/custom-format.ts +124 -126
- package/src/plugins/custom-type-example.ts +106 -108
- package/src/plugins/custom-validator.ts +138 -140
- package/src/types/conditional.ts +28 -28
- package/src/types/config.ts +59 -59
- package/src/types/dsl.ts +131 -131
- package/src/types/error.ts +60 -60
- package/src/types/index.ts +17 -17
- package/src/types/infer.ts +127 -127
- package/src/types/plugin.ts +58 -58
- package/src/types/safe-regex.d.ts +9 -9
- package/src/types/schema.ts +66 -66
- package/src/types/validate.ts +71 -71
- package/src/utils/SchemaHelper.ts +196 -196
- package/src/utils/SchemaUtils.ts +365 -346
- package/src/utils/TypeConverter.ts +215 -215
- package/src/utils/index.ts +10 -10
- package/src/validators/CustomKeywords.ts +477 -477
|
@@ -1,326 +1,326 @@
|
|
|
1
|
-
import { EventEmitter } from 'node:events'
|
|
2
|
-
import type { Plugin, HookName, HookFn } from '../types/plugin.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* PluginManager — plugin registration and hook execution.
|
|
6
|
-
*
|
|
7
|
-
* Full v1 compatible API:
|
|
8
|
-
* - hooks : public hooks Map; supports any hook name
|
|
9
|
-
* - EventEmitter compat: on / once / off / emit / removeListener / removeAllListeners
|
|
10
|
-
* - unhook(name, fn): remove a specific hook handler
|
|
11
|
-
* - runHook(name, ...args): pass arbitrary args, collect return values
|
|
12
|
-
* - install(core, name?, opts?): supports named + options install for a single plugin
|
|
13
|
-
* - install / uninstall pass through context
|
|
14
|
-
* - has / get / list / clear / size / uninstall (alias)
|
|
15
|
-
*/
|
|
16
|
-
export class PluginManager extends EventEmitter {
|
|
17
|
-
readonly plugins: Map<string, Plugin> = new Map()
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Public hooks Map (v1 compat: pluginManager.hooks.get('hookName')).
|
|
21
|
-
* Supports any string name, not limited to built-in HookName values.
|
|
22
|
-
*/
|
|
23
|
-
readonly hooks: Map<string, Array<HookFn>> = new Map()
|
|
24
|
-
|
|
25
|
-
/** v1 compat context (passed to plugin install / uninstall). */
|
|
26
|
-
readonly context: { plugins: Map<string, Plugin>; hooks: Map<string, Array<HookFn>> } = {
|
|
27
|
-
plugins: this.plugins,
|
|
28
|
-
hooks: this.hooks,
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Per-plugin hook references (used for automatic cleanup on unregister). */
|
|
32
|
-
private readonly _pluginHooks: Map<string, Map<string, Set<HookFn>>> = new Map()
|
|
33
|
-
|
|
34
|
-
private _installedCore: unknown = undefined
|
|
35
|
-
|
|
36
|
-
/** Built-in hook names (pre-initialized on construction). */
|
|
37
|
-
private static readonly BUILTIN_HOOKS: ReadonlyArray<HookName> = [
|
|
38
|
-
'beforeParse',
|
|
39
|
-
'afterParse',
|
|
40
|
-
'beforeCompile',
|
|
41
|
-
'afterCompile',
|
|
42
|
-
'beforeValidate',
|
|
43
|
-
'afterValidate',
|
|
44
|
-
'onError',
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
/** Legacy v1 lifecycle hook names. */
|
|
48
|
-
private static readonly LEGACY_HOOKS: ReadonlyArray<string> = [
|
|
49
|
-
'onBeforeRegister',
|
|
50
|
-
'onAfterRegister',
|
|
51
|
-
'onBeforeValidate',
|
|
52
|
-
'onAfterValidate',
|
|
53
|
-
'onBeforeExport',
|
|
54
|
-
'onAfterExport',
|
|
55
|
-
'onBeforeCompile',
|
|
56
|
-
'onAfterCompile',
|
|
57
|
-
'onError',
|
|
58
|
-
]
|
|
59
|
-
|
|
60
|
-
constructor() {
|
|
61
|
-
super()
|
|
62
|
-
this.setMaxListeners(0)
|
|
63
|
-
for (const name of [...PluginManager.BUILTIN_HOOKS, ...PluginManager.LEGACY_HOOKS]) {
|
|
64
|
-
this._ensureHook(name)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
69
|
-
// Core API
|
|
70
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Register a plugin.
|
|
74
|
-
*/
|
|
75
|
-
register(plugin: Plugin): this {
|
|
76
|
-
if (!plugin || typeof plugin !== 'object') {
|
|
77
|
-
throw new Error('[schema-dsl] Plugin must be an object')
|
|
78
|
-
}
|
|
79
|
-
if (!plugin.name) {
|
|
80
|
-
throw new Error('[schema-dsl] Plugin must have a valid name')
|
|
81
|
-
}
|
|
82
|
-
if (!plugin.install) {
|
|
83
|
-
throw new Error(`[schema-dsl] Plugin must have an install function`)
|
|
84
|
-
}
|
|
85
|
-
if (this.plugins.has(plugin.name)) {
|
|
86
|
-
throw new Error(`[schema-dsl] Plugin "${plugin.name}" is already registered`)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
this._runHookSync('onBeforeRegister', plugin)
|
|
90
|
-
|
|
91
|
-
this.plugins.set(plugin.name, plugin)
|
|
92
|
-
|
|
93
|
-
// Auto-register hooks declared by the plugin
|
|
94
|
-
if (plugin.hooks) {
|
|
95
|
-
for (const [hookName, fn] of Object.entries(plugin.hooks)) {
|
|
96
|
-
if (fn) {
|
|
97
|
-
this._addPluginHook(plugin.name, hookName, fn)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
this._runHookSync('onAfterRegister', plugin)
|
|
103
|
-
this.emit('plugin:registered', plugin)
|
|
104
|
-
return this
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Add a hook callback (supports any name, not limited to built-in HookName).
|
|
109
|
-
*/
|
|
110
|
-
hook(name: string, fn: HookFn): this {
|
|
111
|
-
this._ensureHook(name)
|
|
112
|
-
this.hooks.get(name)!.push(fn)
|
|
113
|
-
return this
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Remove a specific hook handler (v1 compat: unhook).
|
|
118
|
-
*/
|
|
119
|
-
unhook(name: string, fn: HookFn): this {
|
|
120
|
-
const list = this.hooks.get(name)
|
|
121
|
-
if (list) {
|
|
122
|
-
const idx = list.indexOf(fn)
|
|
123
|
-
if (idx !== -1) list.splice(idx, 1)
|
|
124
|
-
}
|
|
125
|
-
return this
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Run the specified hook.
|
|
130
|
-
* - Args are passed directly to each handler (v1 compat: runHook(name, arg1, arg2, ...)).
|
|
131
|
-
* - Returns an array of all handler return values.
|
|
132
|
-
* - If a handler throws, emits 'hook:error' and runs onError (does not interrupt remaining handlers).
|
|
133
|
-
*/
|
|
134
|
-
async runHook(name: string, ...args: unknown[]): Promise<unknown[]> {
|
|
135
|
-
const list = this.hooks.get(name)
|
|
136
|
-
if (!list || list.length === 0) return []
|
|
137
|
-
|
|
138
|
-
const results: unknown[] = []
|
|
139
|
-
for (const fn of list) {
|
|
140
|
-
try {
|
|
141
|
-
const result = await fn(...args)
|
|
142
|
-
results.push(result)
|
|
143
|
-
} catch (error) {
|
|
144
|
-
const payload = { hookName: name, handler: fn, error }
|
|
145
|
-
this.emit('hook:error', payload)
|
|
146
|
-
this._runHookSync('onError', error, { hookName: name, handler: fn })
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return results
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Install plugins.
|
|
154
|
-
*
|
|
155
|
-
* Supports two call forms (v1 compat):
|
|
156
|
-
* install(core) — install all registered plugins
|
|
157
|
-
* install(core, pluginName, options?) — install the named plugin, merging plugin.options + options
|
|
158
|
-
*/
|
|
159
|
-
install(core: unknown, pluginName?: string, extraOptions?: Record<string, unknown>): this {
|
|
160
|
-
this._installedCore = core
|
|
161
|
-
|
|
162
|
-
if (pluginName !== undefined) {
|
|
163
|
-
// Install the named plugin
|
|
164
|
-
const plugin = this.plugins.get(pluginName)
|
|
165
|
-
if (!plugin) {
|
|
166
|
-
throw new Error(`[schema-dsl] Plugin "${pluginName}" is not registered`)
|
|
167
|
-
}
|
|
168
|
-
this._installPlugin(core, plugin, extraOptions)
|
|
169
|
-
} else {
|
|
170
|
-
// Install all plugins
|
|
171
|
-
for (const plugin of this.plugins.values()) {
|
|
172
|
-
this._installPlugin(core, plugin, extraOptions)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return this
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
private _installPlugin(core: unknown, plugin: Plugin, extraOptions?: Record<string, unknown>): void {
|
|
179
|
-
const mergedOptions = { ...(plugin.options ?? {}), ...(extraOptions ?? {}) }
|
|
180
|
-
try {
|
|
181
|
-
plugin.install?.(core, mergedOptions, this.context)
|
|
182
|
-
this.emit('plugin:installed', plugin)
|
|
183
|
-
} catch (error) {
|
|
184
|
-
this.emit('plugin:error', { plugin, error })
|
|
185
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
186
|
-
throw new Error(`[schema-dsl] Failed to install plugin "${plugin.name}": ${msg}`)
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Unregister a plugin (v2 primary method).
|
|
192
|
-
* - Automatically cleans up all hooks registered by this plugin.
|
|
193
|
-
* - Emits 'plugin:uninstalled' event.
|
|
194
|
-
*/
|
|
195
|
-
unregister(name: string, coreInstance?: unknown): this {
|
|
196
|
-
const plugin = this.plugins.get(name)
|
|
197
|
-
if (!plugin) return this
|
|
198
|
-
|
|
199
|
-
const effectiveCore = coreInstance === undefined ? this._installedCore : coreInstance
|
|
200
|
-
|
|
201
|
-
if (typeof plugin.uninstall === 'function') {
|
|
202
|
-
try {
|
|
203
|
-
plugin.uninstall(effectiveCore, this.context)
|
|
204
|
-
} catch (error) {
|
|
205
|
-
this.emit('plugin:error', { plugin, error })
|
|
206
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
207
|
-
throw new Error(`[schema-dsl] Failed to uninstall plugin "${name}": ${msg}`)
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Clean up hooks registered by this plugin
|
|
212
|
-
const pluginHookMap = this._pluginHooks.get(name)
|
|
213
|
-
if (pluginHookMap) {
|
|
214
|
-
for (const [hookName, fns] of pluginHookMap) {
|
|
215
|
-
const list = this.hooks.get(hookName)
|
|
216
|
-
if (list) {
|
|
217
|
-
for (const fn of fns) {
|
|
218
|
-
const idx = list.indexOf(fn)
|
|
219
|
-
if (idx !== -1) list.splice(idx, 1)
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
this._pluginHooks.delete(name)
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
this.plugins.delete(name)
|
|
227
|
-
|
|
228
|
-
this.emit('plugin:uninstalled', plugin)
|
|
229
|
-
return this
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
233
|
-
// v1 Compat API
|
|
234
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
235
|
-
|
|
236
|
-
/** v1 compat: check whether a plugin is registered. */
|
|
237
|
-
has(name: string): boolean {
|
|
238
|
-
return this.plugins.has(name)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** v1 compat: get a single plugin or the full plugins Map. */
|
|
242
|
-
get(name?: string): Plugin | Map<string, Plugin> | undefined {
|
|
243
|
-
if (name === undefined) return this.plugins
|
|
244
|
-
return this.plugins.get(name)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** v1 compat: list all plugin metadata. */
|
|
248
|
-
list(): Array<{ name: string; version?: string; description?: string }> {
|
|
249
|
-
return Array.from(this.plugins.values()).map(p => ({
|
|
250
|
-
name: p.name,
|
|
251
|
-
...(p.version !== undefined ? { version: p.version } : {}),
|
|
252
|
-
...(p.description !== undefined ? { description: p.description } : {}),
|
|
253
|
-
}))
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** v1 compat: clear all plugins (including hook cleanup). */
|
|
257
|
-
clear(coreInstance?: unknown): this {
|
|
258
|
-
for (const name of Array.from(this.plugins.keys())) {
|
|
259
|
-
try {
|
|
260
|
-
this.unregister(name, coreInstance)
|
|
261
|
-
} catch {
|
|
262
|
-
// v1 behavior: clear() ignores individual plugin uninstall errors and continues
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
this.plugins.clear()
|
|
266
|
-
this._pluginHooks.clear()
|
|
267
|
-
// Clear all custom hooks (keep built-in pre-initialized entries but empty their handler arrays)
|
|
268
|
-
for (const [, list] of this.hooks) {
|
|
269
|
-
list.length = 0
|
|
270
|
-
}
|
|
271
|
-
this.emit('plugins:cleared')
|
|
272
|
-
return this
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** v1 compat: uninstall a plugin (alias for unregister). */
|
|
276
|
-
uninstall(name: string, coreInstance?: unknown): this {
|
|
277
|
-
return this.unregister(name, coreInstance)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
get pluginCount(): number {
|
|
281
|
-
return this.plugins.size
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/** v1 compat: plugin count (alias for pluginCount). */
|
|
285
|
-
get size(): number {
|
|
286
|
-
return this.plugins.size
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
290
|
-
// Private Helpers
|
|
291
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
292
|
-
|
|
293
|
-
private _ensureHook(name: string): void {
|
|
294
|
-
if (!this.hooks.has(name)) {
|
|
295
|
-
this.hooks.set(name, [])
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
private _addPluginHook(pluginName: string, hookName: string, fn: HookFn): void {
|
|
300
|
-
this._ensureHook(hookName)
|
|
301
|
-
this.hooks.get(hookName)!.push(fn)
|
|
302
|
-
|
|
303
|
-
// Record the mapping (for cleanup on unregister)
|
|
304
|
-
if (!this._pluginHooks.has(pluginName)) {
|
|
305
|
-
this._pluginHooks.set(pluginName, new Map())
|
|
306
|
-
}
|
|
307
|
-
const pluginMap = this._pluginHooks.get(pluginName)!
|
|
308
|
-
if (!pluginMap.has(hookName)) {
|
|
309
|
-
pluginMap.set(hookName, new Set())
|
|
310
|
-
}
|
|
311
|
-
pluginMap.get(hookName)!.add(fn)
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
private _runHookSync(name: string, ...args: unknown[]): void {
|
|
315
|
-
const list = this.hooks.get(name)
|
|
316
|
-
if (!list || list.length === 0) return
|
|
317
|
-
|
|
318
|
-
for (const handler of list) {
|
|
319
|
-
try {
|
|
320
|
-
handler(...args)
|
|
321
|
-
} catch (error) {
|
|
322
|
-
this.emit('hook:error', { hookName: name, handler, error })
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
import type { Plugin, HookName, HookFn } from '../types/plugin.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PluginManager — plugin registration and hook execution.
|
|
6
|
+
*
|
|
7
|
+
* Full v1 compatible API:
|
|
8
|
+
* - hooks : public hooks Map; supports any hook name
|
|
9
|
+
* - EventEmitter compat: on / once / off / emit / removeListener / removeAllListeners
|
|
10
|
+
* - unhook(name, fn): remove a specific hook handler
|
|
11
|
+
* - runHook(name, ...args): pass arbitrary args, collect return values
|
|
12
|
+
* - install(core, name?, opts?): supports named + options install for a single plugin
|
|
13
|
+
* - install / uninstall pass through context
|
|
14
|
+
* - has / get / list / clear / size / uninstall (alias)
|
|
15
|
+
*/
|
|
16
|
+
export class PluginManager extends EventEmitter {
|
|
17
|
+
readonly plugins: Map<string, Plugin> = new Map()
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Public hooks Map (v1 compat: pluginManager.hooks.get('hookName')).
|
|
21
|
+
* Supports any string name, not limited to built-in HookName values.
|
|
22
|
+
*/
|
|
23
|
+
readonly hooks: Map<string, Array<HookFn>> = new Map()
|
|
24
|
+
|
|
25
|
+
/** v1 compat context (passed to plugin install / uninstall). */
|
|
26
|
+
readonly context: { plugins: Map<string, Plugin>; hooks: Map<string, Array<HookFn>> } = {
|
|
27
|
+
plugins: this.plugins,
|
|
28
|
+
hooks: this.hooks,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Per-plugin hook references (used for automatic cleanup on unregister). */
|
|
32
|
+
private readonly _pluginHooks: Map<string, Map<string, Set<HookFn>>> = new Map()
|
|
33
|
+
|
|
34
|
+
private _installedCore: unknown = undefined
|
|
35
|
+
|
|
36
|
+
/** Built-in hook names (pre-initialized on construction). */
|
|
37
|
+
private static readonly BUILTIN_HOOKS: ReadonlyArray<HookName> = [
|
|
38
|
+
'beforeParse',
|
|
39
|
+
'afterParse',
|
|
40
|
+
'beforeCompile',
|
|
41
|
+
'afterCompile',
|
|
42
|
+
'beforeValidate',
|
|
43
|
+
'afterValidate',
|
|
44
|
+
'onError',
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
/** Legacy v1 lifecycle hook names. */
|
|
48
|
+
private static readonly LEGACY_HOOKS: ReadonlyArray<string> = [
|
|
49
|
+
'onBeforeRegister',
|
|
50
|
+
'onAfterRegister',
|
|
51
|
+
'onBeforeValidate',
|
|
52
|
+
'onAfterValidate',
|
|
53
|
+
'onBeforeExport',
|
|
54
|
+
'onAfterExport',
|
|
55
|
+
'onBeforeCompile',
|
|
56
|
+
'onAfterCompile',
|
|
57
|
+
'onError',
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
constructor() {
|
|
61
|
+
super()
|
|
62
|
+
this.setMaxListeners(0)
|
|
63
|
+
for (const name of [...PluginManager.BUILTIN_HOOKS, ...PluginManager.LEGACY_HOOKS]) {
|
|
64
|
+
this._ensureHook(name)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
69
|
+
// Core API
|
|
70
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Register a plugin.
|
|
74
|
+
*/
|
|
75
|
+
register(plugin: Plugin): this {
|
|
76
|
+
if (!plugin || typeof plugin !== 'object') {
|
|
77
|
+
throw new Error('[schema-dsl] Plugin must be an object')
|
|
78
|
+
}
|
|
79
|
+
if (!plugin.name) {
|
|
80
|
+
throw new Error('[schema-dsl] Plugin must have a valid name')
|
|
81
|
+
}
|
|
82
|
+
if (!plugin.install) {
|
|
83
|
+
throw new Error(`[schema-dsl] Plugin must have an install function`)
|
|
84
|
+
}
|
|
85
|
+
if (this.plugins.has(plugin.name)) {
|
|
86
|
+
throw new Error(`[schema-dsl] Plugin "${plugin.name}" is already registered`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this._runHookSync('onBeforeRegister', plugin)
|
|
90
|
+
|
|
91
|
+
this.plugins.set(plugin.name, plugin)
|
|
92
|
+
|
|
93
|
+
// Auto-register hooks declared by the plugin
|
|
94
|
+
if (plugin.hooks) {
|
|
95
|
+
for (const [hookName, fn] of Object.entries(plugin.hooks)) {
|
|
96
|
+
if (fn) {
|
|
97
|
+
this._addPluginHook(plugin.name, hookName, fn)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this._runHookSync('onAfterRegister', plugin)
|
|
103
|
+
this.emit('plugin:registered', plugin)
|
|
104
|
+
return this
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Add a hook callback (supports any name, not limited to built-in HookName).
|
|
109
|
+
*/
|
|
110
|
+
hook(name: string, fn: HookFn): this {
|
|
111
|
+
this._ensureHook(name)
|
|
112
|
+
this.hooks.get(name)!.push(fn)
|
|
113
|
+
return this
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Remove a specific hook handler (v1 compat: unhook).
|
|
118
|
+
*/
|
|
119
|
+
unhook(name: string, fn: HookFn): this {
|
|
120
|
+
const list = this.hooks.get(name)
|
|
121
|
+
if (list) {
|
|
122
|
+
const idx = list.indexOf(fn)
|
|
123
|
+
if (idx !== -1) list.splice(idx, 1)
|
|
124
|
+
}
|
|
125
|
+
return this
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Run the specified hook.
|
|
130
|
+
* - Args are passed directly to each handler (v1 compat: runHook(name, arg1, arg2, ...)).
|
|
131
|
+
* - Returns an array of all handler return values.
|
|
132
|
+
* - If a handler throws, emits 'hook:error' and runs onError (does not interrupt remaining handlers).
|
|
133
|
+
*/
|
|
134
|
+
async runHook(name: string, ...args: unknown[]): Promise<unknown[]> {
|
|
135
|
+
const list = this.hooks.get(name)
|
|
136
|
+
if (!list || list.length === 0) return []
|
|
137
|
+
|
|
138
|
+
const results: unknown[] = []
|
|
139
|
+
for (const fn of list) {
|
|
140
|
+
try {
|
|
141
|
+
const result = await fn(...args)
|
|
142
|
+
results.push(result)
|
|
143
|
+
} catch (error) {
|
|
144
|
+
const payload = { hookName: name, handler: fn, error }
|
|
145
|
+
this.emit('hook:error', payload)
|
|
146
|
+
this._runHookSync('onError', error, { hookName: name, handler: fn })
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return results
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Install plugins.
|
|
154
|
+
*
|
|
155
|
+
* Supports two call forms (v1 compat):
|
|
156
|
+
* install(core) — install all registered plugins
|
|
157
|
+
* install(core, pluginName, options?) — install the named plugin, merging plugin.options + options
|
|
158
|
+
*/
|
|
159
|
+
install(core: unknown, pluginName?: string, extraOptions?: Record<string, unknown>): this {
|
|
160
|
+
this._installedCore = core
|
|
161
|
+
|
|
162
|
+
if (pluginName !== undefined) {
|
|
163
|
+
// Install the named plugin
|
|
164
|
+
const plugin = this.plugins.get(pluginName)
|
|
165
|
+
if (!plugin) {
|
|
166
|
+
throw new Error(`[schema-dsl] Plugin "${pluginName}" is not registered`)
|
|
167
|
+
}
|
|
168
|
+
this._installPlugin(core, plugin, extraOptions)
|
|
169
|
+
} else {
|
|
170
|
+
// Install all plugins
|
|
171
|
+
for (const plugin of this.plugins.values()) {
|
|
172
|
+
this._installPlugin(core, plugin, extraOptions)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return this
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private _installPlugin(core: unknown, plugin: Plugin, extraOptions?: Record<string, unknown>): void {
|
|
179
|
+
const mergedOptions = { ...(plugin.options ?? {}), ...(extraOptions ?? {}) }
|
|
180
|
+
try {
|
|
181
|
+
plugin.install?.(core, mergedOptions, this.context)
|
|
182
|
+
this.emit('plugin:installed', plugin)
|
|
183
|
+
} catch (error) {
|
|
184
|
+
this.emit('plugin:error', { plugin, error })
|
|
185
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
186
|
+
throw new Error(`[schema-dsl] Failed to install plugin "${plugin.name}": ${msg}`)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Unregister a plugin (v2 primary method).
|
|
192
|
+
* - Automatically cleans up all hooks registered by this plugin.
|
|
193
|
+
* - Emits 'plugin:uninstalled' event.
|
|
194
|
+
*/
|
|
195
|
+
unregister(name: string, coreInstance?: unknown): this {
|
|
196
|
+
const plugin = this.plugins.get(name)
|
|
197
|
+
if (!plugin) return this
|
|
198
|
+
|
|
199
|
+
const effectiveCore = coreInstance === undefined ? this._installedCore : coreInstance
|
|
200
|
+
|
|
201
|
+
if (typeof plugin.uninstall === 'function') {
|
|
202
|
+
try {
|
|
203
|
+
plugin.uninstall(effectiveCore, this.context)
|
|
204
|
+
} catch (error) {
|
|
205
|
+
this.emit('plugin:error', { plugin, error })
|
|
206
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
207
|
+
throw new Error(`[schema-dsl] Failed to uninstall plugin "${name}": ${msg}`)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Clean up hooks registered by this plugin
|
|
212
|
+
const pluginHookMap = this._pluginHooks.get(name)
|
|
213
|
+
if (pluginHookMap) {
|
|
214
|
+
for (const [hookName, fns] of pluginHookMap) {
|
|
215
|
+
const list = this.hooks.get(hookName)
|
|
216
|
+
if (list) {
|
|
217
|
+
for (const fn of fns) {
|
|
218
|
+
const idx = list.indexOf(fn)
|
|
219
|
+
if (idx !== -1) list.splice(idx, 1)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
this._pluginHooks.delete(name)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.plugins.delete(name)
|
|
227
|
+
|
|
228
|
+
this.emit('plugin:uninstalled', plugin)
|
|
229
|
+
return this
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
233
|
+
// v1 Compat API
|
|
234
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
/** v1 compat: check whether a plugin is registered. */
|
|
237
|
+
has(name: string): boolean {
|
|
238
|
+
return this.plugins.has(name)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** v1 compat: get a single plugin or the full plugins Map. */
|
|
242
|
+
get(name?: string): Plugin | Map<string, Plugin> | undefined {
|
|
243
|
+
if (name === undefined) return this.plugins
|
|
244
|
+
return this.plugins.get(name)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** v1 compat: list all plugin metadata. */
|
|
248
|
+
list(): Array<{ name: string; version?: string; description?: string }> {
|
|
249
|
+
return Array.from(this.plugins.values()).map(p => ({
|
|
250
|
+
name: p.name,
|
|
251
|
+
...(p.version !== undefined ? { version: p.version } : {}),
|
|
252
|
+
...(p.description !== undefined ? { description: p.description } : {}),
|
|
253
|
+
}))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** v1 compat: clear all plugins (including hook cleanup). */
|
|
257
|
+
clear(coreInstance?: unknown): this {
|
|
258
|
+
for (const name of Array.from(this.plugins.keys())) {
|
|
259
|
+
try {
|
|
260
|
+
this.unregister(name, coreInstance)
|
|
261
|
+
} catch {
|
|
262
|
+
// v1 behavior: clear() ignores individual plugin uninstall errors and continues
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
this.plugins.clear()
|
|
266
|
+
this._pluginHooks.clear()
|
|
267
|
+
// Clear all custom hooks (keep built-in pre-initialized entries but empty their handler arrays)
|
|
268
|
+
for (const [, list] of this.hooks) {
|
|
269
|
+
list.length = 0
|
|
270
|
+
}
|
|
271
|
+
this.emit('plugins:cleared')
|
|
272
|
+
return this
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** v1 compat: uninstall a plugin (alias for unregister). */
|
|
276
|
+
uninstall(name: string, coreInstance?: unknown): this {
|
|
277
|
+
return this.unregister(name, coreInstance)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
get pluginCount(): number {
|
|
281
|
+
return this.plugins.size
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** v1 compat: plugin count (alias for pluginCount). */
|
|
285
|
+
get size(): number {
|
|
286
|
+
return this.plugins.size
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
290
|
+
// Private Helpers
|
|
291
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
private _ensureHook(name: string): void {
|
|
294
|
+
if (!this.hooks.has(name)) {
|
|
295
|
+
this.hooks.set(name, [])
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private _addPluginHook(pluginName: string, hookName: string, fn: HookFn): void {
|
|
300
|
+
this._ensureHook(hookName)
|
|
301
|
+
this.hooks.get(hookName)!.push(fn)
|
|
302
|
+
|
|
303
|
+
// Record the mapping (for cleanup on unregister)
|
|
304
|
+
if (!this._pluginHooks.has(pluginName)) {
|
|
305
|
+
this._pluginHooks.set(pluginName, new Map())
|
|
306
|
+
}
|
|
307
|
+
const pluginMap = this._pluginHooks.get(pluginName)!
|
|
308
|
+
if (!pluginMap.has(hookName)) {
|
|
309
|
+
pluginMap.set(hookName, new Set())
|
|
310
|
+
}
|
|
311
|
+
pluginMap.get(hookName)!.add(fn)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private _runHookSync(name: string, ...args: unknown[]): void {
|
|
315
|
+
const list = this.hooks.get(name)
|
|
316
|
+
if (!list || list.length === 0) return
|
|
317
|
+
|
|
318
|
+
for (const handler of list) {
|
|
319
|
+
try {
|
|
320
|
+
handler(...args)
|
|
321
|
+
} catch (error) {
|
|
322
|
+
this.emit('hook:error', { hookName: name, handler, error })
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|