@kubb/core 5.0.0-beta.2 → 5.0.0-beta.21

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 (45) hide show
  1. package/README.md +8 -38
  2. package/dist/KubbDriver-BBRa5CH2.cjs +2231 -0
  3. package/dist/KubbDriver-BBRa5CH2.cjs.map +1 -0
  4. package/dist/KubbDriver-Cq1isv2P.js +2110 -0
  5. package/dist/KubbDriver-Cq1isv2P.js.map +1 -0
  6. package/dist/{types-CC09VtBt.d.ts → createKubb-CYrw_xaR.d.ts} +1414 -1255
  7. package/dist/index.cjs +221 -1074
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +2 -185
  10. package/dist/index.js +211 -1068
  11. package/dist/index.js.map +1 -1
  12. package/dist/mocks.cjs +30 -21
  13. package/dist/mocks.cjs.map +1 -1
  14. package/dist/mocks.d.ts +5 -5
  15. package/dist/mocks.js +29 -20
  16. package/dist/mocks.js.map +1 -1
  17. package/package.json +6 -18
  18. package/src/FileManager.ts +75 -58
  19. package/src/FileProcessor.ts +48 -38
  20. package/src/KubbDriver.ts +915 -0
  21. package/src/constants.ts +11 -6
  22. package/src/createAdapter.ts +84 -1
  23. package/src/createKubb.ts +1022 -485
  24. package/src/createRenderer.ts +33 -22
  25. package/src/defineGenerator.ts +96 -7
  26. package/src/defineLogger.ts +42 -3
  27. package/src/defineMiddleware.ts +1 -1
  28. package/src/defineParser.ts +1 -1
  29. package/src/definePlugin.ts +304 -8
  30. package/src/defineResolver.ts +271 -150
  31. package/src/devtools.ts +8 -1
  32. package/src/index.ts +2 -2
  33. package/src/mocks.ts +11 -14
  34. package/src/storages/fsStorage.ts +13 -37
  35. package/src/types.ts +39 -1292
  36. package/dist/PluginDriver-BXibeQk-.cjs +0 -1036
  37. package/dist/PluginDriver-BXibeQk-.cjs.map +0 -1
  38. package/dist/PluginDriver-DV3p2Hky.js +0 -945
  39. package/dist/PluginDriver-DV3p2Hky.js.map +0 -1
  40. package/src/Kubb.ts +0 -300
  41. package/src/PluginDriver.ts +0 -424
  42. package/src/renderNode.ts +0 -35
  43. package/src/utils/diagnostics.ts +0 -18
  44. package/src/utils/isInputPath.ts +0 -10
  45. package/src/utils/packageJSON.ts +0 -99
@@ -1,18 +1,160 @@
1
1
  import path from 'node:path'
2
2
  import { camelCase, pascalCase } from '@internals/utils'
3
- import type { FileNode, InputNode, Node, OperationNode, SchemaNode } from '@kubb/ast'
3
+ import type { FileNode, InputMeta, Node, OperationNode, SchemaNode } from '@kubb/ast'
4
4
  import { createFile, isOperationNode, isSchemaNode } from '@kubb/ast'
5
- import { PluginDriver } from './PluginDriver.ts'
6
- import type {
7
- Config,
8
- PluginFactoryOptions,
9
- ResolveBannerContext,
10
- ResolveOptionsContext,
11
- Resolver,
12
- ResolverContext,
13
- ResolverFileParams,
14
- ResolverPathParams,
15
- } from './types.ts'
5
+ import type { PluginFactoryOptions } from './definePlugin.ts'
6
+ import { getMode } from './definePlugin.ts'
7
+ import type { Config, Group, Output } from './types.ts'
8
+
9
+ /**
10
+ * Type/string pattern filter for include/exclude/override matching.
11
+ */
12
+ type PatternFilter = {
13
+ type: string
14
+ pattern: string | RegExp
15
+ }
16
+
17
+ /**
18
+ * Pattern filter with partial option overrides applied when the pattern matches.
19
+ */
20
+ type PatternOverride<TOptions> = PatternFilter & {
21
+ options: Omit<Partial<TOptions>, 'override'>
22
+ }
23
+
24
+ /**
25
+ * Context for resolving filtered options for a given operation or schema node.
26
+ *
27
+ * @internal
28
+ */
29
+ export type ResolveOptionsContext<TOptions> = {
30
+ options: TOptions
31
+ exclude?: Array<PatternFilter>
32
+ include?: Array<PatternFilter>
33
+ override?: Array<PatternOverride<TOptions>>
34
+ }
35
+
36
+ /**
37
+ * Base constraint for all plugin resolver objects.
38
+ *
39
+ * `default`, `resolveOptions`, `resolvePath`, `resolveFile`, `resolveBanner`, and `resolveFooter`
40
+ * are injected automatically by `defineResolver` — extend this type to add custom resolution methods.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * type MyResolver = Resolver & {
45
+ * resolveName(node: SchemaNode): string
46
+ * resolveTypedName(node: SchemaNode): string
47
+ * }
48
+ * ```
49
+ */
50
+ export type Resolver = {
51
+ name: string
52
+ pluginName: string
53
+ default(name: string, type?: 'file' | 'function' | 'type' | 'const'): string
54
+ resolveOptions<TOptions>(node: Node, context: ResolveOptionsContext<TOptions>): TOptions | null
55
+ resolvePath(params: ResolverPathParams, context: ResolverContext): string
56
+ resolveFile(params: ResolverFileParams, context: ResolverContext): FileNode
57
+ resolveBanner(meta: InputMeta | undefined, context: ResolveBannerContext): string | null
58
+ resolveFooter(meta: InputMeta | undefined, context: ResolveBannerContext): string | null
59
+ }
60
+
61
+ /**
62
+ * File-specific parameters for `Resolver.resolvePath`.
63
+ *
64
+ * Pass alongside a `ResolverContext` to identify which file to resolve.
65
+ * Provide `tag` for tag-based grouping or `path` for path-based grouping.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * resolver.resolvePath(
70
+ * { baseName: 'petTypes.ts', tag: 'pets' },
71
+ * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
72
+ * )
73
+ * // → '/src/types/petsController/petTypes.ts'
74
+ * ```
75
+ */
76
+ export type ResolverPathParams = {
77
+ baseName: FileNode['baseName']
78
+ pathMode?: 'single' | 'split'
79
+ /**
80
+ * Tag value used when `group.type === 'tag'`.
81
+ */
82
+ tag?: string
83
+ /**
84
+ * Path value used when `group.type === 'path'`.
85
+ */
86
+ path?: string
87
+ }
88
+
89
+ /**
90
+ * Shared context passed as the second argument to `Resolver.resolvePath` and `Resolver.resolveFile`.
91
+ *
92
+ * Describes where on disk output is rooted, which output config is active, and the optional
93
+ * grouping strategy that controls subdirectory layout.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const context: ResolverContext = {
98
+ * root: config.root,
99
+ * output,
100
+ * group,
101
+ * }
102
+ * ```
103
+ */
104
+ export type ResolverContext = {
105
+ root: string
106
+ output: Output
107
+ group?: Group
108
+ /**
109
+ * Plugin name used to populate `meta.pluginName` on the resolved file.
110
+ */
111
+ pluginName?: string
112
+ }
113
+
114
+ /**
115
+ * File-specific parameters for `Resolver.resolveFile`.
116
+ *
117
+ * Pass alongside a `ResolverContext` to fully describe the file to resolve.
118
+ * `tag` and `path` are used only when a matching `group` is present in the context.
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * resolver.resolveFile(
123
+ * { name: 'listPets', extname: '.ts', tag: 'pets' },
124
+ * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
125
+ * )
126
+ * // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... }
127
+ * ```
128
+ */
129
+ export type ResolverFileParams = {
130
+ name: string
131
+ extname: FileNode['extname']
132
+ /**
133
+ * Tag value used when `group.type === 'tag'`.
134
+ */
135
+ tag?: string
136
+ /**
137
+ * Path value used when `group.type === 'path'`.
138
+ */
139
+ path?: string
140
+ }
141
+
142
+ /**
143
+ * Context passed to `Resolver.resolveBanner` and `Resolver.resolveFooter`.
144
+ *
145
+ * `output` is optional — not every plugin configures a banner/footer.
146
+ * `config` carries the global Kubb config, used to derive the default Kubb banner.
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * resolver.resolveBanner(meta, { output: { banner: '// generated' }, config })
151
+ * // → '// generated'
152
+ * ```
153
+ */
154
+ export type ResolveBannerContext = {
155
+ output?: Pick<Output, 'banner' | 'footer'>
156
+ config: Config
157
+ }
16
158
 
17
159
  /**
18
160
  * Builder type for the plugin-specific resolver fields.
@@ -20,19 +162,16 @@ import type {
20
162
  * `default`, `resolveOptions`, `resolvePath`, `resolveFile`, `resolveBanner`, and `resolveFooter`
21
163
  * are optional — built-in fallbacks are injected when omitted.
22
164
  *
23
- * The builder receives `ctx` a reference to the fully assembled resolver so methods can
24
- * call sibling resolver methods without using `this`. Because `ctx` is captured by the closure
25
- * and the resolver is populated after the builder runs, `ctx` correctly reflects any overrides
26
- * that were applied by the builder itself.
165
+ * Methods in the returned object can call sibling resolver methods via `this`.
27
166
  */
28
- type ResolverBuilder<T extends PluginFactoryOptions> = (ctx: T['resolver']) => Omit<
167
+ type ResolverBuilder<T extends PluginFactoryOptions> = () => Omit<
29
168
  T['resolver'],
30
169
  'default' | 'resolveOptions' | 'resolvePath' | 'resolveFile' | 'resolveBanner' | 'resolveFooter' | 'name' | 'pluginName'
31
170
  > &
32
171
  Partial<Pick<T['resolver'], 'default' | 'resolveOptions' | 'resolvePath' | 'resolveFile' | 'resolveBanner' | 'resolveFooter'>> & {
33
172
  name: string
34
173
  pluginName: T['name']
35
- }
174
+ } & ThisType<T['resolver']>
36
175
 
37
176
  // String patterns are compiled lazily and cached — the same filter is reused for every node.
38
177
  const stringPatternCache = new Map<string, RegExp>()
@@ -54,20 +193,12 @@ function testPattern(value: string, pattern: string | RegExp): boolean {
54
193
  * Checks if an operation matches a pattern for a given filter type (`tag`, `operationId`, `path`, `method`).
55
194
  */
56
195
  function matchesOperationPattern(node: OperationNode, type: string, pattern: string | RegExp): boolean {
57
- switch (type) {
58
- case 'tag':
59
- return node.tags.some((tag) => testPattern(tag, pattern))
60
- case 'operationId':
61
- return testPattern(node.operationId, pattern)
62
- case 'path':
63
- return testPattern(node.path, pattern)
64
- case 'method':
65
- return testPattern(node.method.toLowerCase(), pattern)
66
- case 'contentType':
67
- return node.requestBody?.content?.some((c) => testPattern(c.contentType, pattern)) ?? false
68
- default:
69
- return false
70
- }
196
+ if (type === 'tag') return node.tags.some((tag) => testPattern(tag, pattern))
197
+ if (type === 'operationId') return testPattern(node.operationId, pattern)
198
+ if (type === 'path') return testPattern(node.path, pattern)
199
+ if (type === 'method') return testPattern(node.method.toLowerCase(), pattern)
200
+ if (type === 'contentType') return node.requestBody?.content?.some((c) => testPattern(c.contentType, pattern)) ?? false
201
+ return false
71
202
  }
72
203
 
73
204
  /**
@@ -76,12 +207,8 @@ function matchesOperationPattern(node: OperationNode, type: string, pattern: str
76
207
  * Returns `null` when the filter type doesn't apply to schemas.
77
208
  */
78
209
  function matchesSchemaPattern(node: SchemaNode, type: string, pattern: string | RegExp): boolean | null {
79
- switch (type) {
80
- case 'schemaName':
81
- return node.name ? testPattern(node.name, pattern) : false
82
- default:
83
- return null
84
- }
210
+ if (type === 'schemaName') return node.name ? testPattern(node.name, pattern) : false
211
+ return null
85
212
  }
86
213
 
87
214
  /**
@@ -92,19 +219,9 @@ function matchesSchemaPattern(node: SchemaNode, type: string, pattern: string |
92
219
  * - `camelCase` for everything else.
93
220
  */
94
221
  function defaultResolver(name: string, type?: 'file' | 'function' | 'type' | 'const'): string {
95
- let resolvedName = camelCase(name)
96
-
97
- if (type === 'file' || type === 'function') {
98
- resolvedName = camelCase(name, {
99
- isFile: type === 'file',
100
- })
101
- }
102
-
103
- if (type === 'type') {
104
- resolvedName = pascalCase(name)
105
- }
106
-
107
- return resolvedName
222
+ if (type === 'file' || type === 'function') return camelCase(name, { isFile: type === 'file' })
223
+ if (type === 'type') return pascalCase(name)
224
+ return camelCase(name)
108
225
  }
109
226
 
110
227
  /**
@@ -130,19 +247,18 @@ function defaultResolver(name: string, type?: 'file' | 'function' | 'type' | 'co
130
247
  * // → { enumType: 'enum' } when operationId matches
131
248
  * ```
132
249
  */
133
- export function defaultResolveOptions<TOptions>(
250
+ const resolveOptionsCache = new WeakMap<object, WeakMap<Node, { value: unknown }>>()
251
+
252
+ function computeOptions<TOptions>(
134
253
  node: Node,
135
- { options, exclude = [], include, override = [] }: ResolveOptionsContext<TOptions>,
254
+ options: TOptions,
255
+ exclude: Array<PatternFilter>,
256
+ include: Array<PatternFilter> | undefined,
257
+ override: Array<PatternOverride<TOptions>>,
136
258
  ): TOptions | null {
137
259
  if (isOperationNode(node)) {
138
- const isExcluded = exclude.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))
139
- if (isExcluded) {
140
- return null
141
- }
142
-
143
- if (include && !include.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) {
144
- return null
145
- }
260
+ if (exclude.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null
261
+ if (include && !include.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null
146
262
 
147
263
  const overrideOptions = override.find(({ type, pattern }) => matchesOperationPattern(node, type, pattern))?.options
148
264
 
@@ -150,18 +266,13 @@ export function defaultResolveOptions<TOptions>(
150
266
  }
151
267
 
152
268
  if (isSchemaNode(node)) {
153
- if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)) {
154
- return null
155
- }
156
-
269
+ if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)) return null
157
270
  if (include) {
158
271
  const results = include.map(({ type, pattern }) => matchesSchemaPattern(node, type, pattern))
159
272
  const applicable = results.filter((r) => r !== null)
160
- if (applicable.length > 0 && !applicable.includes(true)) {
161
- return null
162
- }
163
- }
164
273
 
274
+ if (applicable.length > 0 && !applicable.includes(true)) return null
275
+ }
165
276
  const overrideOptions = override.find(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)?.options
166
277
 
167
278
  return { ...options, ...overrideOptions }
@@ -170,6 +281,26 @@ export function defaultResolveOptions<TOptions>(
170
281
  return options
171
282
  }
172
283
 
284
+ export function defaultResolveOptions<TOptions>(
285
+ node: Node,
286
+ { options, exclude = [], include, override = [] }: ResolveOptionsContext<TOptions>,
287
+ ): TOptions | null {
288
+ const optionsKey = options as object
289
+ let byOptions = resolveOptionsCache.get(optionsKey)
290
+ if (!byOptions) {
291
+ byOptions = new WeakMap()
292
+ resolveOptionsCache.set(optionsKey, byOptions)
293
+ }
294
+ const cached = byOptions.get(node)
295
+ if (cached !== undefined) return cached.value as TOptions | null
296
+
297
+ const result = computeOptions(node, options, exclude, include, override)
298
+
299
+ byOptions.set(node, { value: result })
300
+
301
+ return result
302
+ }
303
+
173
304
  /**
174
305
  * Default path resolver used by `defineResolver`.
175
306
  *
@@ -215,31 +346,30 @@ export function defaultResolveOptions<TOptions>(
215
346
  * ```
216
347
  */
217
348
  export function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }: ResolverPathParams, { root, output, group }: ResolverContext): string {
218
- const mode = pathMode ?? PluginDriver.getMode(path.resolve(root, output.path))
349
+ const mode = pathMode ?? getMode(path.resolve(root, output.path))
219
350
 
220
351
  if (mode === 'single') {
221
352
  return path.resolve(root, output.path)
222
353
  }
223
354
 
224
- let result: string
225
-
226
- if (group && (groupPath || tag)) {
227
- const groupValue = group.type === 'path' ? groupPath! : tag!
228
- const defaultName =
229
- group.type === 'tag'
230
- ? ({ group: g }: { group: string }) => `${camelCase(g)}Controller`
231
- : ({ group: g }: { group: string }) => {
232
- // Strip traversal components (empty, '.', '..') before taking the first meaningful segment.
233
- // When every segment is a traversal component (e.g. '../../') we fall back to '' so the
234
- // file is placed directly in the output root the boundary check below ensures safety.
235
- const segment = g.split('/').filter((s) => s !== '' && s !== '.' && s !== '..')[0]
236
- return segment ? camelCase(segment) : ''
237
- }
238
- const resolveName = group.name ?? defaultName
239
- result = path.resolve(root, output.path, resolveName({ group: groupValue }), baseName)
240
- } else {
241
- result = path.resolve(root, output.path, baseName)
242
- }
355
+ const result: string = (() => {
356
+ if (group && (groupPath || tag)) {
357
+ const groupValue = group.type === 'path' ? groupPath! : tag!
358
+ const defaultName =
359
+ group.type === 'tag'
360
+ ? ({ group: g }: { group: string }) => `${camelCase(g)}Controller`
361
+ : ({ group: g }: { group: string }) => {
362
+ // Strip traversal components (empty, '.', '..') before taking the first meaningful segment.
363
+ // When every segment is a traversal component (e.g. '../../') we fall back to '' so the
364
+ // file is placed directly in the output root the boundary check below ensures safety.
365
+ const segment = g.split('/').filter((s) => s !== '' && s !== '.' && s !== '..')[0]
366
+ return segment ? camelCase(segment) : ''
367
+ }
368
+ const resolveName = group.name ?? defaultName
369
+ return path.resolve(root, output.path, resolveName({ group: groupValue }), baseName)
370
+ }
371
+ return path.resolve(root, output.path, baseName)
372
+ })()
243
373
 
244
374
  // Ensure the resolved path stays within the configured output directory.
245
375
  // This prevents path traversal from malicious OpenAPI specs or custom group.name functions.
@@ -268,35 +398,35 @@ export function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }:
268
398
  *
269
399
  * @example Resolve a schema file
270
400
  * ```ts
271
- * const file = defaultResolveFile(
401
+ * const file = defaultResolveFile.call(
402
+ * resolver,
272
403
  * { name: 'pet', extname: '.ts' },
273
404
  * { root: '/src', output: { path: 'types' } },
274
- * resolver,
275
405
  * )
276
406
  * // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... }
277
407
  * ```
278
408
  *
279
409
  * @example Resolve an operation file with tag grouping
280
410
  * ```ts
281
- * const file = defaultResolveFile(
411
+ * const file = defaultResolveFile.call(
412
+ * resolver,
282
413
  * { name: 'listPets', extname: '.ts', tag: 'pets' },
283
414
  * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
284
- * resolver,
285
415
  * )
286
416
  * // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... }
287
417
  * ```
288
418
  */
289
- export function defaultResolveFile({ name, extname, tag, path: groupPath }: ResolverFileParams, context: ResolverContext, ctx: Resolver): FileNode {
290
- const pathMode = PluginDriver.getMode(path.resolve(context.root, context.output.path))
291
- const resolvedName = pathMode === 'single' ? '' : ctx.default(name, 'file')
419
+ export function defaultResolveFile(this: Resolver, { name, extname, tag, path: groupPath }: ResolverFileParams, context: ResolverContext): FileNode {
420
+ const pathMode = getMode(path.resolve(context.root, context.output.path))
421
+ const resolvedName = pathMode === 'single' ? '' : this.default(name, 'file')
292
422
  const baseName = `${resolvedName}${extname}` as FileNode['baseName']
293
- const filePath = ctx.resolvePath({ baseName, pathMode, tag, path: groupPath }, context)
423
+ const filePath = this.resolvePath({ baseName, pathMode, tag, path: groupPath }, context)
294
424
 
295
425
  return createFile({
296
426
  path: filePath,
297
427
  baseName: path.basename(filePath) as `${string}.${string}`,
298
428
  meta: {
299
- pluginName: ctx.pluginName,
429
+ pluginName: this.pluginName,
300
430
  },
301
431
  sources: [],
302
432
  imports: [],
@@ -319,17 +449,16 @@ export function buildDefaultBanner({
319
449
  config: Config
320
450
  }): string {
321
451
  try {
322
- let source = ''
323
- if (Array.isArray(config.input)) {
324
- const first = config.input[0]
325
- if (first && 'path' in first) {
326
- source = path.basename(first.path)
452
+ const source = (() => {
453
+ if (Array.isArray(config.input)) {
454
+ const first = config.input[0]
455
+ if (first && 'path' in first) return path.basename(first.path)
456
+ return ''
327
457
  }
328
- } else if ('path' in config.input) {
329
- source = path.basename(config.input.path)
330
- } else if ('data' in config.input) {
331
- source = 'text content'
332
- }
458
+ if (config.input && 'path' in config.input) return path.basename(config.input.path)
459
+ if (config.input && 'data' in config.input) return 'text content'
460
+ return ''
461
+ })()
333
462
 
334
463
  let banner = '/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n'
335
464
 
@@ -367,10 +496,9 @@ export function buildDefaultBanner({
367
496
  *
368
497
  * A user-supplied `output.banner` overrides the default Kubb "Generated by Kubb" notice.
369
498
  * When no `output.banner` is set, the Kubb notice is used (including `title` and `version`
370
- * from the OAS spec when a `node` is provided).
499
+ * from the document metadata when `meta` is provided).
371
500
  *
372
- * - When `output.banner` is a function and `node` is provided, returns `output.banner(node)`.
373
- * - When `output.banner` is a function and `node` is absent, falls back to the Kubb notice.
501
+ * - When `output.banner` is a function, calls it with `meta` and returns the result.
374
502
  * - When `output.banner` is a string, returns it directly.
375
503
  * - When `config.output.defaultBanner` is `false`, returns `undefined`.
376
504
  * - Otherwise returns the Kubb "Generated by Kubb" notice.
@@ -381,27 +509,27 @@ export function buildDefaultBanner({
381
509
  * // → '// my banner'
382
510
  * ```
383
511
  *
384
- * @example Function banner with node
512
+ * @example Function banner with metadata
385
513
  * ```ts
386
- * defaultResolveBanner(inputNode, { output: { banner: (node) => `// v${node.version}` }, config })
514
+ * defaultResolveBanner(meta, { output: { banner: (m) => `// v${m?.version}` }, config })
387
515
  * // → '// v3.0.0'
388
516
  * ```
389
517
  *
390
518
  * @example No user banner — Kubb notice with OAS metadata
391
519
  * ```ts
392
- * defaultResolveBanner(inputNode, { config })
520
+ * defaultResolveBanner(meta, { config })
393
521
  * // → '/** Generated by Kubb ... Title: Pet Store ... *\/'
394
522
  * ```
395
523
  *
396
524
  * @example Disabled default banner
397
525
  * ```ts
398
526
  * defaultResolveBanner(undefined, { config: { output: { defaultBanner: false }, ...config } })
399
- * // → undefined
527
+ * // → null
400
528
  * ```
401
529
  */
402
- export function defaultResolveBanner(node: InputNode | undefined, { output, config }: ResolveBannerContext): string | undefined {
530
+ export function defaultResolveBanner(meta: InputMeta | undefined, { output, config }: ResolveBannerContext): string | null {
403
531
  if (typeof output?.banner === 'function') {
404
- return output.banner(node)
532
+ return output.banner(meta)
405
533
  }
406
534
 
407
535
  if (typeof output?.banner === 'string') {
@@ -409,12 +537,12 @@ export function defaultResolveBanner(node: InputNode | undefined, { output, conf
409
537
  }
410
538
 
411
539
  if (config.output.defaultBanner === false) {
412
- return undefined
540
+ return null
413
541
  }
414
542
 
415
543
  return buildDefaultBanner({
416
- title: node?.meta?.title,
417
- version: node?.meta?.version,
544
+ title: meta?.title,
545
+ version: meta?.version,
418
546
  config,
419
547
  })
420
548
  }
@@ -422,8 +550,7 @@ export function defaultResolveBanner(node: InputNode | undefined, { output, conf
422
550
  /**
423
551
  * Default footer resolver — returns the footer string for a generated file.
424
552
  *
425
- * - When `output.footer` is a function and `node` is provided, calls it with the node.
426
- * - When `output.footer` is a function and `node` is absent, returns `undefined`.
553
+ * - When `output.footer` is a function, calls it with `meta` and returns the result.
427
554
  * - When `output.footer` is a string, returns it directly.
428
555
  * - Otherwise returns `undefined`.
429
556
  *
@@ -433,20 +560,20 @@ export function defaultResolveBanner(node: InputNode | undefined, { output, conf
433
560
  * // → '// end of file'
434
561
  * ```
435
562
  *
436
- * @example Function footer with node
563
+ * @example Function footer with metadata
437
564
  * ```ts
438
- * defaultResolveFooter(inputNode, { output: { footer: (node) => `// ${node.title}` }, config })
565
+ * defaultResolveFooter(meta, { output: { footer: (m) => `// ${m?.title}` }, config })
439
566
  * // → '// Pet Store'
440
567
  * ```
441
568
  */
442
- export function defaultResolveFooter(node: InputNode | undefined, { output }: ResolveBannerContext): string | undefined {
569
+ export function defaultResolveFooter(meta: InputMeta | undefined, { output }: ResolveBannerContext): string | null {
443
570
  if (typeof output?.footer === 'function') {
444
- return node ? output.footer(node) : undefined
571
+ return output.footer(meta)
445
572
  }
446
573
  if (typeof output?.footer === 'string') {
447
574
  return output.footer
448
575
  }
449
- return undefined
576
+ return null
450
577
  }
451
578
 
452
579
  /**
@@ -459,25 +586,24 @@ export function defaultResolveFooter(node: InputNode | undefined, { output }: Re
459
586
  * - `resolvePath` — output path computation
460
587
  * - `resolveFile` — full `FileNode` construction
461
588
  *
462
- * The builder receives `ctx` a reference to the assembled resolver so methods can
463
- * call sibling resolver methods using `ctx` instead of `this`.
589
+ * Methods in the returned object can call sibling resolver methods via `this`.
464
590
  *
465
591
  * @example Basic resolver with naming helpers
466
592
  * ```ts
467
- * export const resolver = defineResolver<PluginTs>((ctx) => ({
593
+ * export const resolver = defineResolver<PluginTs>(() => ({
468
594
  * name: 'default',
469
595
  * resolveName(node) {
470
- * return ctx.default(node.name, 'function')
596
+ * return this.default(node.name, 'function')
471
597
  * },
472
598
  * resolveTypedName(node) {
473
- * return ctx.default(node.name, 'type')
599
+ * return this.default(node.name, 'type')
474
600
  * },
475
601
  * }))
476
602
  * ```
477
603
  *
478
604
  * @example Override resolvePath for a custom output structure
479
605
  * ```ts
480
- * export const resolver = defineResolver<PluginTs>((_ctx) => ({
606
+ * export const resolver = defineResolver<PluginTs>(() => ({
481
607
  * name: 'custom',
482
608
  * resolvePath({ baseName }, { root, output }) {
483
609
  * return path.resolve(root, output.path, 'generated', baseName)
@@ -485,37 +611,32 @@ export function defaultResolveFooter(node: InputNode | undefined, { output }: Re
485
611
  * }))
486
612
  * ```
487
613
  *
488
- * @example Use ctx.default inside a helper
614
+ * @example Use this.default inside a helper
489
615
  * ```ts
490
- * export const resolver = defineResolver<PluginTs>((ctx) => ({
616
+ * export const resolver = defineResolver<PluginTs>(() => ({
491
617
  * name: 'default',
492
618
  * resolveParamName(node, param) {
493
- * return ctx.default(`${node.operationId} ${param.in} ${param.name}`, 'type')
619
+ * return this.default(`${node.operationId} ${param.in} ${param.name}`, 'type')
494
620
  * },
495
621
  * }))
496
622
  * ```
497
623
  */
498
624
  export function defineResolver<T extends PluginFactoryOptions>(build: ResolverBuilder<T>): T['resolver'] {
499
- // Create the resolver shell first. When `build(resolver)` executes below, `resolver` is
500
- // still empty, but methods returned by the builder capture it by reference. By the time
501
- // those methods are actually called, `Object.assign` will have already populated all
502
- // properties (including any overrides from the builder itself).
503
- const resolver = {} as T['resolver']
625
+ // `resolver` is kept so the default `resolveFile` wrapper can reference the fully assembled
626
+ // object via `.call(resolver, ...)` at call-time, after the result is assigned below.
627
+ let resolver: T['resolver']
504
628
 
505
- Object.assign(resolver, {
629
+ const result = {
506
630
  default: defaultResolver,
507
631
  resolveOptions: defaultResolveOptions,
508
632
  resolvePath: defaultResolvePath,
509
- // Wire the default resolveFile implementation with a wrapper that passes resolver as ctx.
510
- // Unlike other defaults which can be assigned directly, defaultResolveFile requires the
511
- // resolver as its third parameter.
512
- resolveFile: (params: ResolverFileParams, context: ResolverContext) => defaultResolveFile(params, context, resolver as Resolver),
633
+ resolveFile: (params: ResolverFileParams, context: ResolverContext) => defaultResolveFile.call(resolver as Resolver, params, context),
513
634
  resolveBanner: defaultResolveBanner,
514
635
  resolveFooter: defaultResolveFooter,
515
- // Builder overrides are applied last. Any method in the builder can call
516
- // ctx.xxx() and will see the fully merged resolver (including its own overrides).
517
- ...build(resolver),
518
- })
636
+ ...build(),
637
+ } as T['resolver']
638
+
639
+ resolver = result
519
640
 
520
641
  return resolver
521
642
  }
package/src/devtools.ts CHANGED
@@ -1,7 +1,14 @@
1
1
  import type { InputNode } from '@kubb/ast'
2
2
  import { deflateSync, inflateSync } from 'fflate'
3
3
  import { x } from 'tinyexec'
4
- import type { DevtoolsOptions } from './types.ts'
4
+
5
+ export type DevtoolsOptions = {
6
+ /**
7
+ * Open the AST inspector in Kubb Studio (`/ast`). Defaults to the main Studio page.
8
+ * @default false
9
+ */
10
+ ast?: boolean
11
+ }
5
12
 
6
13
  /**
7
14
  * Encodes an `InputNode` as a compressed, URL-safe string.
package/src/index.ts CHANGED
@@ -13,8 +13,8 @@ export { definePlugin } from './definePlugin.ts'
13
13
  export { defineResolver } from './defineResolver.ts'
14
14
  export { FileManager } from './FileManager.ts'
15
15
  export { FileProcessor } from './FileProcessor.ts'
16
- export { PluginDriver } from './PluginDriver.ts'
16
+ export { KubbDriver } from './KubbDriver.ts'
17
17
  export { fsStorage } from './storages/fsStorage.ts'
18
18
  export { memoryStorage } from './storages/memoryStorage.ts'
19
19
  export * from './types.ts'
20
- export { isInputPath } from './utils/isInputPath.ts'
20
+ export { isInputPath } from './createKubb.ts'