@kubb/core 5.0.0-alpha.4 → 5.0.0-alpha.41

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.
Files changed (71) hide show
  1. package/dist/PluginDriver-BQwm8hDd.cjs +1729 -0
  2. package/dist/PluginDriver-BQwm8hDd.cjs.map +1 -0
  3. package/dist/PluginDriver-CgXFtmNP.js +1617 -0
  4. package/dist/PluginDriver-CgXFtmNP.js.map +1 -0
  5. package/dist/index.cjs +915 -1901
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.ts +268 -264
  8. package/dist/index.js +894 -1863
  9. package/dist/index.js.map +1 -1
  10. package/dist/mocks.cjs +164 -0
  11. package/dist/mocks.cjs.map +1 -0
  12. package/dist/mocks.d.ts +74 -0
  13. package/dist/mocks.js +159 -0
  14. package/dist/mocks.js.map +1 -0
  15. package/dist/types-C6NCtNqM.d.ts +2151 -0
  16. package/package.json +11 -14
  17. package/src/FileManager.ts +131 -0
  18. package/src/FileProcessor.ts +84 -0
  19. package/src/Kubb.ts +174 -85
  20. package/src/PluginDriver.ts +941 -0
  21. package/src/constants.ts +33 -43
  22. package/src/createAdapter.ts +25 -0
  23. package/src/createKubb.ts +605 -0
  24. package/src/createPlugin.ts +31 -0
  25. package/src/createRenderer.ts +57 -0
  26. package/src/createStorage.ts +58 -0
  27. package/src/defineGenerator.ts +88 -100
  28. package/src/defineLogger.ts +13 -3
  29. package/src/defineParser.ts +45 -0
  30. package/src/definePlugin.ts +90 -7
  31. package/src/defineResolver.ts +453 -0
  32. package/src/devtools.ts +14 -14
  33. package/src/index.ts +12 -17
  34. package/src/mocks.ts +234 -0
  35. package/src/renderNode.ts +35 -0
  36. package/src/storages/fsStorage.ts +29 -9
  37. package/src/storages/memoryStorage.ts +2 -2
  38. package/src/types.ts +821 -152
  39. package/src/utils/TreeNode.ts +47 -9
  40. package/src/utils/diagnostics.ts +4 -1
  41. package/src/utils/executeStrategies.ts +16 -13
  42. package/src/utils/getBarrelFiles.ts +88 -15
  43. package/src/utils/isInputPath.ts +10 -0
  44. package/src/utils/packageJSON.ts +75 -0
  45. package/dist/chunk-ByKO4r7w.cjs +0 -38
  46. package/dist/hooks.cjs +0 -50
  47. package/dist/hooks.cjs.map +0 -1
  48. package/dist/hooks.d.ts +0 -49
  49. package/dist/hooks.js +0 -46
  50. package/dist/hooks.js.map +0 -1
  51. package/dist/types-Bbh1o0yW.d.ts +0 -1057
  52. package/src/BarrelManager.ts +0 -74
  53. package/src/PackageManager.ts +0 -180
  54. package/src/PluginManager.ts +0 -668
  55. package/src/PromiseManager.ts +0 -40
  56. package/src/build.ts +0 -420
  57. package/src/config.ts +0 -56
  58. package/src/defineAdapter.ts +0 -22
  59. package/src/defineStorage.ts +0 -56
  60. package/src/errors.ts +0 -1
  61. package/src/hooks/index.ts +0 -8
  62. package/src/hooks/useKubb.ts +0 -22
  63. package/src/hooks/useMode.ts +0 -11
  64. package/src/hooks/usePlugin.ts +0 -11
  65. package/src/hooks/usePluginManager.ts +0 -11
  66. package/src/utils/FunctionParams.ts +0 -155
  67. package/src/utils/formatters.ts +0 -56
  68. package/src/utils/getConfigs.ts +0 -30
  69. package/src/utils/getPlugins.ts +0 -23
  70. package/src/utils/linters.ts +0 -25
  71. package/src/utils/resolveOptions.ts +0 -93
@@ -0,0 +1,941 @@
1
+ import { basename, extname, resolve } from 'node:path'
2
+ import { performance } from 'node:perf_hooks'
3
+ import type { AsyncEventEmitter } from '@internals/utils'
4
+ import { isPromiseRejectedResult, transformReservedWord } from '@internals/utils'
5
+ import type { FileNode, InputNode } from '@kubb/ast'
6
+ import { createFile } from '@kubb/ast'
7
+ import { DEFAULT_STUDIO_URL } from './constants.ts'
8
+ import type { Generator } from './defineGenerator.ts'
9
+ import { type HookStylePlugin, isHookStylePlugin } from './definePlugin.ts'
10
+ import { defineResolver } from './defineResolver.ts'
11
+ import { openInStudio as openInStudioFn } from './devtools.ts'
12
+ import { FileManager } from './FileManager.ts'
13
+ import { applyHookResult } from './renderNode.ts'
14
+
15
+ import type {
16
+ Adapter,
17
+ Config,
18
+ DevtoolsOptions,
19
+ Group,
20
+ KubbHooks,
21
+ KubbPluginSetupContext,
22
+ Output,
23
+ Plugin,
24
+ PluginContext,
25
+ PluginFactoryOptions,
26
+ PluginLifecycle,
27
+ PluginLifecycleHooks,
28
+ PluginParameter,
29
+ PluginWithLifeCycle,
30
+ ResolveNameParams,
31
+ ResolvePathParams,
32
+ Resolver,
33
+ } from './types.ts'
34
+ import { hookFirst, hookParallel, hookSeq } from './utils/executeStrategies.ts'
35
+
36
+ type RequiredPluginLifecycle = Required<PluginLifecycle>
37
+
38
+ /**
39
+ * Hook dispatch strategy used by the `PluginDriver`.
40
+ *
41
+ * - `hookFirst` — stops at the first non-null result.
42
+ * - `hookForPlugin` — calls only the matching plugin.
43
+ * - `hookParallel` — calls all plugins concurrently.
44
+ * - `hookSeq` — calls all plugins in order, threading the result.
45
+ */
46
+ export type Strategy = 'hookFirst' | 'hookForPlugin' | 'hookParallel' | 'hookSeq'
47
+
48
+ type ParseResult<H extends PluginLifecycleHooks> = RequiredPluginLifecycle[H]
49
+
50
+ type SafeParseResult<H extends PluginLifecycleHooks, Result = ReturnType<ParseResult<H>>> = {
51
+ result: Result
52
+ plugin: Plugin
53
+ }
54
+
55
+ // inspired by: https://github.com/rollup/rollup/blob/master/src/utils/PluginDriver.ts#
56
+
57
+ type Options = {
58
+ hooks?: AsyncEventEmitter<KubbHooks>
59
+ /**
60
+ * @default Number.POSITIVE_INFINITY
61
+ */
62
+ concurrency?: number
63
+ }
64
+
65
+ /**
66
+ * Parameters accepted by `PluginDriver.getFile` to resolve a generated file descriptor.
67
+ */
68
+ export type GetFileOptions<TOptions = object> = {
69
+ name: string
70
+ mode?: 'single' | 'split'
71
+ extname: FileNode['extname']
72
+ pluginName: string
73
+ options?: TOptions
74
+ }
75
+
76
+
77
+ const hookFirstNullCheck = (state: unknown) => !!(state as SafeParseResult<'resolveName'> | null)?.result
78
+
79
+ export class PluginDriver {
80
+ readonly config: Config
81
+ readonly options: Options
82
+
83
+ /**
84
+ * Returns `'single'` when `fileOrFolder` has a file extension, `'split'` otherwise.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * PluginDriver.getMode('src/gen/types.ts') // 'single'
89
+ * PluginDriver.getMode('src/gen/types') // 'split'
90
+ * ```
91
+ */
92
+ static getMode(fileOrFolder: string | undefined | null): 'single' | 'split' {
93
+ if (!fileOrFolder) {
94
+ return 'split'
95
+ }
96
+ return extname(fileOrFolder) ? 'single' : 'split'
97
+ }
98
+
99
+ /**
100
+ * The universal `@kubb/ast` `InputNode` produced by the adapter, set by
101
+ * the build pipeline after the adapter's `parse()` resolves.
102
+ */
103
+ inputNode: InputNode | undefined = undefined
104
+ adapter: Adapter | undefined = undefined
105
+ #studioIsOpen = false
106
+
107
+ /**
108
+ * Central file store for all generated files.
109
+ * Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to
110
+ * add files; this property gives direct read/write access when needed.
111
+ */
112
+ readonly fileManager = new FileManager()
113
+
114
+ readonly plugins = new Map<string, Plugin>()
115
+
116
+ /**
117
+ * Tracks which plugins have generators registered via `addGenerator()` (event-based path).
118
+ * Used by the build loop to decide whether to emit generator events for a given plugin.
119
+ */
120
+ readonly #pluginsWithEventGenerators = new Set<string>()
121
+ readonly #resolvers = new Map<string, Resolver>()
122
+ readonly #defaultResolvers = new Map<string, Resolver>()
123
+ readonly #hookListeners = new Map<keyof KubbHooks, Set<(...args: never[]) => void | Promise<void>>>()
124
+
125
+ constructor(config: Config, options: Options) {
126
+ this.config = config
127
+ this.options = {
128
+ ...options,
129
+ hooks: options.hooks,
130
+ }
131
+ config.plugins
132
+ .map((rawPlugin) => {
133
+ if (isHookStylePlugin(rawPlugin)) {
134
+ return this.#normalizeHookStylePlugin(rawPlugin as HookStylePlugin)
135
+ }
136
+ return { ...rawPlugin, buildStart: rawPlugin.buildStart ?? (() => {}), buildEnd: rawPlugin.buildEnd ?? (() => {}) } as unknown as Plugin
137
+ })
138
+ .filter((plugin) => {
139
+ if (typeof plugin.apply === 'function') {
140
+ return plugin.apply(config)
141
+ }
142
+ return true
143
+ })
144
+ .sort((a, b) => {
145
+ if (b.dependencies?.includes(a.name)) return -1
146
+ if (a.dependencies?.includes(b.name)) return 1
147
+ return 0
148
+ })
149
+ .forEach((plugin) => {
150
+ this.plugins.set(plugin.name, plugin)
151
+ })
152
+ }
153
+
154
+ get hooks() {
155
+ if (!this.options.hooks) {
156
+ throw new Error('hooks are not defined')
157
+ }
158
+ return this.options.hooks
159
+ }
160
+
161
+ /**
162
+ * Creates a `Plugin`-compatible object from a hook-style plugin and registers
163
+ * its lifecycle handlers on the `AsyncEventEmitter`.
164
+ *
165
+ * The normalized plugin has an empty `buildStart` — generators registered via
166
+ * `addGenerator()` in `kubb:plugin:setup` are stored on `normalizedPlugin.generators`
167
+ * and used by `runPluginAstHooks` during the build.
168
+ */
169
+ #normalizeHookStylePlugin(hookPlugin: HookStylePlugin): Plugin {
170
+ const generators: Plugin['generators'] = []
171
+ const driver = this
172
+ // The options shape is the minimal struct required by Plugin. Hook-style plugins
173
+ // use generators registered via addGenerator() and resolvers set via setResolver().
174
+ // `inject` and `resolver` are required by the Plugin type but are irrelevant for hook-style
175
+ // plugins: inject is a no-op and resolver is set dynamically via setResolver() in kubb:plugin:setup.
176
+ //
177
+ // `resolveName` and `resolvePath` bridge the legacy PluginDriver.resolveName/resolvePath
178
+ // lifecycle so that other plugins calling `driver.resolveName({ pluginName })` or
179
+ // `driver.getFile({ pluginName })` still get correct results from hook-style plugins.
180
+ const normalizedPlugin = {
181
+ name: hookPlugin.name,
182
+ dependencies: hookPlugin.dependencies,
183
+ options: { output: { path: '.' }, exclude: [], override: [] },
184
+ generators,
185
+ inject: () => undefined,
186
+ resolveName(name: string, type?: ResolveNameParams['type']) {
187
+ const resolver = driver.getResolver(hookPlugin.name)
188
+ return resolver.default(name, type)
189
+ },
190
+ resolvePath(baseName: FileNode['baseName'], pathMode?: 'single' | 'split', resolveOptions?: Record<string, unknown>) {
191
+ const resolver = driver.getResolver(hookPlugin.name)
192
+ const opts = normalizedPlugin.options as Record<string, unknown>
193
+ const group = resolveOptions?.group as Record<string, string> | undefined
194
+ return resolver.resolvePath(
195
+ { baseName, pathMode, tag: group?.tag, path: group?.path },
196
+ { root: resolve(driver.config.root, driver.config.output.path), output: opts.output as Output, group: opts.group as Group | undefined },
197
+ )
198
+ },
199
+ buildStart() {},
200
+ buildEnd() {},
201
+ } as unknown as Plugin
202
+ this.registerPluginHooks(hookPlugin, normalizedPlugin)
203
+ return normalizedPlugin
204
+ }
205
+
206
+ /**
207
+ * Registers a hook-style plugin's lifecycle handlers on the shared `AsyncEventEmitter`.
208
+ *
209
+ * For `kubb:plugin:setup`, the registered listener wraps the globally emitted context with a
210
+ * plugin-specific one so that `addGenerator`, `setResolver`, `setTransformer`, and
211
+ * `setRenderer` all target the correct `normalizedPlugin` entry in the plugins map.
212
+ *
213
+ * All other hooks are iterated and registered directly as pass-through listeners.
214
+ * Any event key present in the global `KubbHooks` interface can be subscribed to.
215
+ *
216
+ * External tooling can subscribe to any of these events via `hooks.on(...)` to observe
217
+ * the plugin lifecycle without modifying plugin behavior.
218
+ */
219
+ registerPluginHooks(hookPlugin: HookStylePlugin, normalizedPlugin: Plugin): void {
220
+ const { hooks } = hookPlugin
221
+
222
+ // kubb:plugin:setup gets special treatment: the globally emitted context is wrapped with
223
+ // plugin-specific implementations so that addGenerator / setResolver / etc. target
224
+ // this plugin's normalizedPlugin entry rather than being no-ops.
225
+ if (hooks['kubb:plugin:setup']) {
226
+ const setupHandler = (globalCtx: KubbPluginSetupContext) => {
227
+ const pluginCtx: KubbPluginSetupContext = {
228
+ ...globalCtx,
229
+ options: hookPlugin.options ?? {},
230
+ addGenerator: (gen) => {
231
+ this.registerGenerator(normalizedPlugin.name, gen)
232
+ },
233
+ setResolver: (resolver) => {
234
+ this.setPluginResolver(normalizedPlugin.name, resolver)
235
+ },
236
+ setTransformer: (visitor) => {
237
+ normalizedPlugin.transformer = visitor
238
+ },
239
+ setRenderer: (renderer) => {
240
+ normalizedPlugin.renderer = renderer
241
+ },
242
+ setOptions: (opts) => {
243
+ normalizedPlugin.options = { ...normalizedPlugin.options, ...opts }
244
+ },
245
+ injectFile: (file) => {
246
+ const fileNode = createFile({
247
+ baseName: file.baseName,
248
+ path: file.path,
249
+ sources: file.sources ?? [],
250
+ imports: [],
251
+ exports: [],
252
+ })
253
+ this.fileManager.add(fileNode)
254
+ },
255
+ }
256
+ return hooks['kubb:plugin:setup']!(pluginCtx)
257
+ }
258
+
259
+ this.hooks.on('kubb:plugin:setup', setupHandler)
260
+ this.#trackHookListener('kubb:plugin:setup', setupHandler as (...args: never[]) => void | Promise<void>)
261
+ }
262
+
263
+ // All other hooks are registered as direct pass-through listeners on the shared emitter.
264
+ for (const [event, handler] of Object.entries(hooks) as Array<[keyof KubbHooks, ((...args: never[]) => void | Promise<void>) | undefined]>) {
265
+ if (event === 'kubb:plugin:setup' || !handler) continue
266
+ this.hooks.on(event, handler as never)
267
+ this.#trackHookListener(event, handler as (...args: never[]) => void | Promise<void>)
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Emits the `kubb:plugin:setup` event so that all registered hook-style plugin listeners
273
+ * can configure generators, resolvers, transformers and renderers before `buildStart` runs.
274
+ *
275
+ * Call this once from `safeBuild` before the plugin execution loop begins.
276
+ */
277
+ async emitSetupHooks(): Promise<void> {
278
+ await this.hooks.emit('kubb:plugin:setup', {
279
+ config: this.config,
280
+ addGenerator: () => {},
281
+ setResolver: () => {},
282
+ setTransformer: () => {},
283
+ setRenderer: () => {},
284
+ setOptions: () => {},
285
+ injectFile: () => {},
286
+ updateConfig: () => {},
287
+ options: {},
288
+ })
289
+ }
290
+
291
+ /**
292
+ * Registers a generator for the given plugin on the shared event emitter.
293
+ *
294
+ * The generator's `schema`, `operation`, and `operations` methods are registered as
295
+ * listeners on `kubb:generate:schema`, `kubb:generate:operation`, and `kubb:generate:operations`
296
+ * respectively. Each listener is scoped to the owning plugin via a `ctx.plugin.name` check
297
+ * so that generators from different plugins do not cross-fire.
298
+ *
299
+ * The renderer resolution chain is: `generator.renderer → plugin.renderer → config.renderer`.
300
+ * Set `generator.renderer = null` to explicitly opt out of rendering even when the plugin
301
+ * declares a renderer.
302
+ *
303
+ * Call this method inside `addGenerator()` (in `kubb:plugin:setup`) to wire up a generator.
304
+ */
305
+ registerGenerator(pluginName: string, gen: Generator): void {
306
+ const resolveRenderer = () => {
307
+ const plugin = this.plugins.get(pluginName)
308
+ return gen.renderer === null ? undefined : (gen.renderer ?? plugin?.renderer ?? this.config.renderer)
309
+ }
310
+
311
+ if (gen.schema) {
312
+ const schemaHandler = async (node: Parameters<NonNullable<typeof gen.schema>>[0], ctx: Parameters<NonNullable<typeof gen.schema>>[1]) => {
313
+ if (ctx.plugin.name !== pluginName) return
314
+ const result = await gen.schema!(node, ctx)
315
+ await applyHookResult(result, this, resolveRenderer())
316
+ }
317
+
318
+ this.hooks.on('kubb:generate:schema', schemaHandler)
319
+ this.#trackHookListener('kubb:generate:schema', schemaHandler as (...args: never[]) => void | Promise<void>)
320
+ }
321
+
322
+ if (gen.operation) {
323
+ const operationHandler = async (node: Parameters<NonNullable<typeof gen.operation>>[0], ctx: Parameters<NonNullable<typeof gen.operation>>[1]) => {
324
+ if (ctx.plugin.name !== pluginName) return
325
+ const result = await gen.operation!(node, ctx)
326
+ await applyHookResult(result, this, resolveRenderer())
327
+ }
328
+
329
+ this.hooks.on('kubb:generate:operation', operationHandler)
330
+ this.#trackHookListener('kubb:generate:operation', operationHandler as (...args: never[]) => void | Promise<void>)
331
+ }
332
+
333
+ if (gen.operations) {
334
+ const operationsHandler = async (nodes: Parameters<NonNullable<typeof gen.operations>>[0], ctx: Parameters<NonNullable<typeof gen.operations>>[1]) => {
335
+ if (ctx.plugin.name !== pluginName) return
336
+ const result = await gen.operations!(nodes, ctx)
337
+ await applyHookResult(result, this, resolveRenderer())
338
+ }
339
+
340
+ this.hooks.on('kubb:generate:operations', operationsHandler)
341
+ this.#trackHookListener('kubb:generate:operations', operationsHandler as (...args: never[]) => void | Promise<void>)
342
+ }
343
+
344
+ this.#pluginsWithEventGenerators.add(pluginName)
345
+ }
346
+
347
+ /**
348
+ * Returns `true` when at least one generator was registered for the given plugin
349
+ * via `addGenerator()` in `kubb:plugin:setup` (event-based path).
350
+ *
351
+ * Used by the build loop to decide whether to walk the AST and emit generator events
352
+ * for a plugin that has no static `plugin.generators`.
353
+ */
354
+ hasRegisteredGenerators(pluginName: string): boolean {
355
+ return this.#pluginsWithEventGenerators.has(pluginName)
356
+ }
357
+
358
+ dispose(): void {
359
+ for (const [event, handlers] of this.#hookListeners) {
360
+ for (const handler of handlers) {
361
+ this.hooks.off(event, handler as never)
362
+ }
363
+ }
364
+ this.#hookListeners.clear()
365
+ this.#pluginsWithEventGenerators.clear()
366
+ }
367
+
368
+ #trackHookListener(event: keyof KubbHooks, handler: (...args: never[]) => void | Promise<void>): void {
369
+ let handlers = this.#hookListeners.get(event)
370
+ if (!handlers) {
371
+ handlers = new Set()
372
+ this.#hookListeners.set(event, handlers)
373
+ }
374
+ handlers.add(handler)
375
+ }
376
+
377
+ #createDefaultResolver(pluginName: string): Resolver {
378
+ const existingResolver = this.#defaultResolvers.get(pluginName)
379
+ if (existingResolver) {
380
+ return existingResolver
381
+ }
382
+
383
+ const resolver = defineResolver<PluginFactoryOptions>(() => ({
384
+ name: 'default',
385
+ pluginName,
386
+ }))
387
+ this.#defaultResolvers.set(pluginName, resolver)
388
+ return resolver
389
+ }
390
+
391
+ setPluginResolver(pluginName: string, partial: Partial<Resolver>): void {
392
+ const defaultResolver = this.#createDefaultResolver(pluginName)
393
+ const merged = { ...defaultResolver, ...partial }
394
+ this.#resolvers.set(pluginName, merged)
395
+ // Mirror the resolved resolver onto the plugin so that consumers using
396
+ // `getPlugin(name).resolver` get the correct resolver without going through getResolver().
397
+ const plugin = this.plugins.get(pluginName)
398
+ if (plugin) {
399
+ plugin.resolver = merged
400
+ }
401
+ }
402
+
403
+ getResolver(pluginName: string): Resolver {
404
+ const dynamicResolver = this.#resolvers.get(pluginName)
405
+ if (dynamicResolver) {
406
+ return dynamicResolver
407
+ }
408
+
409
+ const pluginResolver = this.plugins.get(pluginName)?.resolver
410
+ if (pluginResolver) {
411
+ return pluginResolver
412
+ }
413
+
414
+ return this.#createDefaultResolver(pluginName)
415
+ }
416
+
417
+ getContext<TOptions extends PluginFactoryOptions>(plugin: Plugin<TOptions>): PluginContext<TOptions> & Record<string, unknown> {
418
+ const driver = this
419
+
420
+ const baseContext = {
421
+ config: driver.config,
422
+ get root(): string {
423
+ return resolve(driver.config.root, driver.config.output.path)
424
+ },
425
+ getMode(output: { path: string }): 'single' | 'split' {
426
+ return PluginDriver.getMode(resolve(driver.config.root, driver.config.output.path, output.path))
427
+ },
428
+ hooks: driver.hooks,
429
+ plugin,
430
+ getPlugin: driver.getPlugin.bind(driver),
431
+ requirePlugin: driver.requirePlugin.bind(driver),
432
+ driver: driver,
433
+ addFile: async (...files: Array<FileNode>) => {
434
+ driver.fileManager.add(...files)
435
+ },
436
+ upsertFile: async (...files: Array<FileNode>) => {
437
+ driver.fileManager.upsert(...files)
438
+ },
439
+ get inputNode(): InputNode | undefined {
440
+ return driver.inputNode
441
+ },
442
+ get adapter(): Adapter | undefined {
443
+ return driver.adapter
444
+ },
445
+ get resolver() {
446
+ return driver.getResolver(plugin.name)
447
+ },
448
+ get transformer() {
449
+ return plugin.transformer
450
+ },
451
+ warn(message: string) {
452
+ driver.hooks.emit('kubb:warn', message)
453
+ },
454
+ error(error: string | Error) {
455
+ driver.hooks.emit('kubb:error', typeof error === 'string' ? new Error(error) : error)
456
+ },
457
+ info(message: string) {
458
+ driver.hooks.emit('kubb:info', message)
459
+ },
460
+ openInStudio(options?: DevtoolsOptions) {
461
+ if (!driver.config.devtools || driver.#studioIsOpen) {
462
+ return
463
+ }
464
+
465
+ if (typeof driver.config.devtools !== 'object') {
466
+ throw new Error('Devtools must be an object')
467
+ }
468
+
469
+ if (!driver.inputNode || !driver.adapter) {
470
+ throw new Error('adapter is not defined, make sure you have set the parser in kubb.config.ts')
471
+ }
472
+
473
+ driver.#studioIsOpen = true
474
+
475
+ const studioUrl = driver.config.devtools?.studioUrl ?? DEFAULT_STUDIO_URL
476
+
477
+ return openInStudioFn(driver.inputNode, studioUrl, options)
478
+ },
479
+ } as unknown as PluginContext<TOptions>
480
+
481
+ let mergedExtras: Record<string, unknown> = {}
482
+
483
+ for (const p of this.plugins.values()) {
484
+ if (typeof p.inject === 'function') {
485
+ const result = (p.inject as (this: PluginContext) => unknown).call(baseContext as unknown as PluginContext)
486
+ if (result !== null && typeof result === 'object') {
487
+ mergedExtras = { ...mergedExtras, ...(result as Record<string, unknown>) }
488
+ }
489
+ }
490
+ }
491
+
492
+ return {
493
+ ...baseContext,
494
+ ...mergedExtras,
495
+ }
496
+ }
497
+ /**
498
+ * @deprecated use resolvers context instead
499
+ */
500
+ getFile<TOptions = object>({ name, mode, extname, pluginName, options }: GetFileOptions<TOptions>): FileNode<{ pluginName: string }> {
501
+ const resolvedName = mode ? (mode === 'single' ? '' : this.resolveName({ name, pluginName, type: 'file' })) : name
502
+
503
+ const path = this.resolvePath({
504
+ baseName: `${resolvedName}${extname}` as const,
505
+ mode,
506
+ pluginName,
507
+ options,
508
+ })
509
+
510
+ if (!path) {
511
+ throw new Error(`Filepath should be defined for resolvedName "${resolvedName}" and pluginName "${pluginName}"`)
512
+ }
513
+
514
+ return createFile<{ pluginName: string }>({
515
+ path,
516
+ baseName: basename(path) as `${string}.${string}`,
517
+ meta: {
518
+ pluginName,
519
+ },
520
+ sources: [],
521
+ imports: [],
522
+ exports: [],
523
+ })
524
+ }
525
+
526
+ /**
527
+ * @deprecated use resolvers context instead
528
+ */
529
+ resolvePath = <TOptions = object>(params: ResolvePathParams<TOptions>): string => {
530
+ const root = resolve(this.config.root, this.config.output.path)
531
+ const defaultPath = resolve(root, params.baseName)
532
+
533
+ if (params.pluginName) {
534
+ const paths = this.hookForPluginSync({
535
+ pluginName: params.pluginName,
536
+ hookName: 'resolvePath',
537
+ parameters: [params.baseName, params.mode, params.options as object],
538
+ })
539
+
540
+ return paths?.at(0) || defaultPath
541
+ }
542
+
543
+ const firstResult = this.hookFirstSync({
544
+ hookName: 'resolvePath',
545
+ parameters: [params.baseName, params.mode, params.options as object],
546
+ })
547
+
548
+ return firstResult?.result || defaultPath
549
+ }
550
+ /**
551
+ * @deprecated use resolvers context instead
552
+ */
553
+ resolveName = (params: ResolveNameParams): string => {
554
+ if (params.pluginName) {
555
+ const names = this.hookForPluginSync({
556
+ pluginName: params.pluginName,
557
+ hookName: 'resolveName',
558
+ parameters: [params.name.trim(), params.type],
559
+ })
560
+
561
+ return transformReservedWord(names?.at(0) ?? params.name)
562
+ }
563
+
564
+ const name = this.hookFirstSync({
565
+ hookName: 'resolveName',
566
+ parameters: [params.name.trim(), params.type],
567
+ })?.result
568
+
569
+ return transformReservedWord(name ?? params.name)
570
+ }
571
+
572
+ /**
573
+ * Run a specific hookName for plugin x.
574
+ */
575
+ async hookForPlugin<H extends PluginLifecycleHooks>({
576
+ pluginName,
577
+ hookName,
578
+ parameters,
579
+ }: {
580
+ pluginName: string
581
+ hookName: H
582
+ parameters: PluginParameter<H>
583
+ }): Promise<Array<ReturnType<ParseResult<H>> | null>> {
584
+ const plugin = this.plugins.get(pluginName)
585
+
586
+ if (!plugin) {
587
+ return [null]
588
+ }
589
+
590
+ this.hooks.emit('kubb:plugins:hook:progress:start', {
591
+ hookName,
592
+ plugins: [plugin],
593
+ })
594
+
595
+ const result = await this.#execute<H>({
596
+ strategy: 'hookFirst',
597
+ hookName,
598
+ parameters,
599
+ plugin,
600
+ })
601
+
602
+ this.hooks.emit('kubb:plugins:hook:progress:end', { hookName })
603
+
604
+ return [result]
605
+ }
606
+
607
+ /**
608
+ * Run a specific hookName for plugin x.
609
+ */
610
+ hookForPluginSync<H extends PluginLifecycleHooks>({
611
+ pluginName,
612
+ hookName,
613
+ parameters,
614
+ }: {
615
+ pluginName: string
616
+ hookName: H
617
+ parameters: PluginParameter<H>
618
+ }): Array<ReturnType<ParseResult<H>>> | null {
619
+ const plugin = this.plugins.get(pluginName)
620
+
621
+ if (!plugin) {
622
+ return null
623
+ }
624
+
625
+ const result = this.#executeSync<H>({
626
+ strategy: 'hookFirst',
627
+ hookName,
628
+ parameters,
629
+ plugin,
630
+ })
631
+
632
+ return result !== null ? [result] : []
633
+ }
634
+
635
+ /**
636
+ * Returns the first non-null result.
637
+ */
638
+ async hookFirst<H extends PluginLifecycleHooks>({
639
+ hookName,
640
+ parameters,
641
+ skipped,
642
+ }: {
643
+ hookName: H
644
+ parameters: PluginParameter<H>
645
+ skipped?: ReadonlySet<Plugin> | null
646
+ }): Promise<SafeParseResult<H>> {
647
+ const plugins: Array<Plugin> = []
648
+ for (const plugin of this.plugins.values()) {
649
+ if (hookName in plugin && (skipped ? !skipped.has(plugin) : true)) plugins.push(plugin)
650
+ }
651
+
652
+ this.hooks.emit('kubb:plugins:hook:progress:start', { hookName, plugins })
653
+
654
+ const promises = plugins.map((plugin) => {
655
+ return async () => {
656
+ const value = await this.#execute<H>({
657
+ strategy: 'hookFirst',
658
+ hookName,
659
+ parameters,
660
+ plugin,
661
+ })
662
+
663
+ return Promise.resolve({
664
+ plugin,
665
+ result: value,
666
+ } as SafeParseResult<H>)
667
+ }
668
+ })
669
+
670
+ const result = await hookFirst(promises, hookFirstNullCheck)
671
+
672
+ this.hooks.emit('kubb:plugins:hook:progress:end', { hookName })
673
+
674
+ return result
675
+ }
676
+
677
+ /**
678
+ * Returns the first non-null result.
679
+ */
680
+ hookFirstSync<H extends PluginLifecycleHooks>({
681
+ hookName,
682
+ parameters,
683
+ skipped,
684
+ }: {
685
+ hookName: H
686
+ parameters: PluginParameter<H>
687
+ skipped?: ReadonlySet<Plugin> | null
688
+ }): SafeParseResult<H> | null {
689
+ let parseResult: SafeParseResult<H> | null = null
690
+
691
+ for (const plugin of this.plugins.values()) {
692
+ if (!(hookName in plugin)) continue
693
+ if (skipped?.has(plugin)) continue
694
+
695
+ parseResult = {
696
+ result: this.#executeSync<H>({
697
+ strategy: 'hookFirst',
698
+ hookName,
699
+ parameters,
700
+ plugin,
701
+ }),
702
+ plugin,
703
+ } as SafeParseResult<H>
704
+
705
+ if (parseResult.result != null) break
706
+ }
707
+
708
+ return parseResult
709
+ }
710
+
711
+ /**
712
+ * Runs all plugins in parallel based on `this.plugin` order and `dependencies` settings.
713
+ */
714
+ async hookParallel<H extends PluginLifecycleHooks, TOutput = void>({
715
+ hookName,
716
+ parameters,
717
+ }: {
718
+ hookName: H
719
+ parameters?: Parameters<RequiredPluginLifecycle[H]> | undefined
720
+ }): Promise<Awaited<TOutput>[]> {
721
+ const plugins: Array<Plugin> = []
722
+ for (const plugin of this.plugins.values()) {
723
+ if (hookName in plugin) plugins.push(plugin)
724
+ }
725
+ this.hooks.emit('kubb:plugins:hook:progress:start', { hookName, plugins })
726
+
727
+ const pluginStartTimes = new Map<Plugin, number>()
728
+
729
+ const promises = plugins.map((plugin) => {
730
+ return () => {
731
+ pluginStartTimes.set(plugin, performance.now())
732
+ return this.#execute({
733
+ strategy: 'hookParallel',
734
+ hookName,
735
+ parameters,
736
+ plugin,
737
+ }) as Promise<TOutput>
738
+ }
739
+ })
740
+
741
+ const results = await hookParallel(promises, this.options.concurrency)
742
+
743
+ results.forEach((result, index) => {
744
+ if (isPromiseRejectedResult<Error>(result)) {
745
+ const plugin = plugins[index]
746
+
747
+ if (plugin) {
748
+ const startTime = pluginStartTimes.get(plugin) ?? performance.now()
749
+ this.hooks.emit('kubb:error', result.reason, {
750
+ plugin,
751
+ hookName,
752
+ strategy: 'hookParallel',
753
+ duration: Math.round(performance.now() - startTime),
754
+ parameters,
755
+ })
756
+ }
757
+ }
758
+ })
759
+
760
+ this.hooks.emit('kubb:plugins:hook:progress:end', { hookName })
761
+
762
+ return results.reduce((acc, result) => {
763
+ if (result.status === 'fulfilled') {
764
+ acc.push(result.value)
765
+ }
766
+ return acc
767
+ }, [] as Awaited<TOutput>[])
768
+ }
769
+
770
+ /**
771
+ * Execute a lifecycle hook sequentially for all plugins that implement it.
772
+ */
773
+ async hookSeq<H extends PluginLifecycleHooks>({ hookName, parameters }: { hookName: H; parameters?: PluginParameter<H> }): Promise<void> {
774
+ const plugins: Array<Plugin> = []
775
+ for (const plugin of this.plugins.values()) {
776
+ if (hookName in plugin) plugins.push(plugin)
777
+ }
778
+ this.hooks.emit('kubb:plugins:hook:progress:start', { hookName, plugins })
779
+
780
+ const promises = plugins.map((plugin) => {
781
+ return () =>
782
+ this.#execute({
783
+ strategy: 'hookSeq',
784
+ hookName,
785
+ parameters,
786
+ plugin,
787
+ })
788
+ })
789
+
790
+ await hookSeq(promises)
791
+
792
+ this.hooks.emit('kubb:plugins:hook:progress:end', { hookName })
793
+ }
794
+
795
+ getPlugin<TName extends keyof Kubb.PluginRegistry>(pluginName: TName): Plugin<Kubb.PluginRegistry[TName]> | undefined
796
+ getPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(pluginName: string): Plugin<TOptions> | undefined
797
+ getPlugin(pluginName: string): Plugin | undefined {
798
+ return this.plugins.get(pluginName) as Plugin | undefined
799
+ }
800
+
801
+ /**
802
+ * Like `getPlugin` but throws a descriptive error when the plugin is not found.
803
+ */
804
+ requirePlugin<TName extends keyof Kubb.PluginRegistry>(pluginName: TName): Plugin<Kubb.PluginRegistry[TName]>
805
+ requirePlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(pluginName: string): Plugin<TOptions>
806
+ requirePlugin(pluginName: string): Plugin {
807
+ const plugin = this.plugins.get(pluginName)
808
+ if (!plugin) {
809
+ throw new Error(`[kubb] Plugin "${pluginName}" is required but not found. Make sure it is included in your Kubb config.`)
810
+ }
811
+ return plugin
812
+ }
813
+
814
+ /**
815
+ * Emit hook-processing completion metadata after a plugin hook resolves.
816
+ */
817
+ #emitProcessingEnd<H extends PluginLifecycleHooks>({
818
+ startTime,
819
+ output,
820
+ strategy,
821
+ hookName,
822
+ plugin,
823
+ parameters,
824
+ }: {
825
+ startTime: number
826
+ output: unknown
827
+ strategy: Strategy
828
+ hookName: H
829
+ plugin: PluginWithLifeCycle
830
+ parameters: unknown[] | undefined
831
+ }): void {
832
+ this.hooks.emit('kubb:plugins:hook:processing:end', {
833
+ duration: Math.round(performance.now() - startTime),
834
+ parameters,
835
+ output,
836
+ strategy,
837
+ hookName,
838
+ plugin,
839
+ })
840
+ }
841
+
842
+ // Implementation signature
843
+ #execute<H extends PluginLifecycleHooks>({
844
+ strategy,
845
+ hookName,
846
+ parameters,
847
+ plugin,
848
+ }: {
849
+ strategy: Strategy
850
+ hookName: H
851
+ parameters: unknown[] | undefined
852
+ plugin: PluginWithLifeCycle
853
+ }): Promise<ReturnType<ParseResult<H>> | null> | null {
854
+ const hook = plugin[hookName]
855
+
856
+ if (!hook) {
857
+ return null
858
+ }
859
+
860
+ this.hooks.emit('kubb:plugins:hook:processing:start', {
861
+ strategy,
862
+ hookName,
863
+ parameters,
864
+ plugin,
865
+ })
866
+
867
+ const startTime = performance.now()
868
+
869
+ const task = (async () => {
870
+ try {
871
+ const output =
872
+ typeof hook === 'function' ? await Promise.resolve((hook as (...args: unknown[]) => unknown).apply(this.getContext(plugin), parameters ?? [])) : hook
873
+
874
+ this.#emitProcessingEnd({ startTime, output, strategy, hookName, plugin, parameters })
875
+
876
+ return output as ReturnType<ParseResult<H>>
877
+ } catch (error) {
878
+ this.hooks.emit('kubb:error', error as Error, {
879
+ plugin,
880
+ hookName,
881
+ strategy,
882
+ duration: Math.round(performance.now() - startTime),
883
+ })
884
+
885
+ return null
886
+ }
887
+ })()
888
+
889
+ return task
890
+ }
891
+
892
+ /**
893
+ * Execute a plugin lifecycle hook synchronously and return its output.
894
+ */
895
+ #executeSync<H extends PluginLifecycleHooks>({
896
+ strategy,
897
+ hookName,
898
+ parameters,
899
+ plugin,
900
+ }: {
901
+ strategy: Strategy
902
+ hookName: H
903
+ parameters: PluginParameter<H>
904
+ plugin: PluginWithLifeCycle
905
+ }): ReturnType<ParseResult<H>> | null {
906
+ const hook = plugin[hookName]
907
+
908
+ if (!hook) {
909
+ return null
910
+ }
911
+
912
+ this.hooks.emit('kubb:plugins:hook:processing:start', {
913
+ strategy,
914
+ hookName,
915
+ parameters,
916
+ plugin,
917
+ })
918
+
919
+ const startTime = performance.now()
920
+
921
+ try {
922
+ const output =
923
+ typeof hook === 'function'
924
+ ? ((hook as (...args: unknown[]) => unknown).apply(this.getContext(plugin), parameters) as ReturnType<ParseResult<H>>)
925
+ : (hook as ReturnType<ParseResult<H>>)
926
+
927
+ this.#emitProcessingEnd({ startTime, output, strategy, hookName, plugin, parameters })
928
+
929
+ return output
930
+ } catch (error) {
931
+ this.hooks.emit('kubb:error', error as Error, {
932
+ plugin,
933
+ hookName,
934
+ strategy,
935
+ duration: Math.round(performance.now() - startTime),
936
+ })
937
+
938
+ return null
939
+ }
940
+ }
941
+ }