@kubb/core 5.0.0-beta.3 → 5.0.0-beta.31

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 (46) hide show
  1. package/README.md +8 -38
  2. package/dist/KubbDriver-CFx2DdhF.js +2131 -0
  3. package/dist/KubbDriver-CFx2DdhF.js.map +1 -0
  4. package/dist/KubbDriver-vyD7F0Ip.cjs +2252 -0
  5. package/dist/KubbDriver-vyD7F0Ip.cjs.map +1 -0
  6. package/dist/{types-CC09VtBt.d.ts → createKubb-6zii1jo-.d.ts} +1610 -1257
  7. package/dist/index.cjs +351 -1125
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +3 -186
  10. package/dist/index.js +341 -1119
  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 +78 -61
  19. package/src/FileProcessor.ts +48 -38
  20. package/src/KubbDriver.ts +930 -0
  21. package/src/constants.ts +11 -6
  22. package/src/createAdapter.ts +113 -17
  23. package/src/createKubb.ts +1039 -478
  24. package/src/createRenderer.ts +58 -27
  25. package/src/createStorage.ts +36 -23
  26. package/src/defineGenerator.ts +127 -15
  27. package/src/defineLogger.ts +66 -7
  28. package/src/defineMiddleware.ts +19 -17
  29. package/src/defineParser.ts +30 -13
  30. package/src/definePlugin.ts +329 -14
  31. package/src/defineResolver.ts +365 -167
  32. package/src/devtools.ts +8 -1
  33. package/src/index.ts +2 -2
  34. package/src/mocks.ts +11 -14
  35. package/src/storages/fsStorage.ts +13 -37
  36. package/src/types.ts +48 -1292
  37. package/dist/PluginDriver-BXibeQk-.cjs +0 -1036
  38. package/dist/PluginDriver-BXibeQk-.cjs.map +0 -1
  39. package/dist/PluginDriver-DV3p2Hky.js +0 -945
  40. package/dist/PluginDriver-DV3p2Hky.js.map +0 -1
  41. package/src/Kubb.ts +0 -300
  42. package/src/PluginDriver.ts +0 -424
  43. package/src/renderNode.ts +0 -35
  44. package/src/utils/diagnostics.ts +0 -18
  45. package/src/utils/isInputPath.ts +0 -10
  46. package/src/utils/packageJSON.ts +0 -99
@@ -1,18 +1,236 @@
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
+ * Per-file context describing the file a banner/footer is being resolved for.
144
+ *
145
+ * Supplied by the generator (or the barrel middleware) at resolve-time and merged
146
+ * into `BannerMeta` so a `banner`/`footer` function can branch on the file kind —
147
+ * e.g. omit a `'use server'` directive on re-export files.
148
+ */
149
+ export type ResolveBannerFile = {
150
+ /**
151
+ * Full output path of the file being generated.
152
+ */
153
+ path: string
154
+ /**
155
+ * File name only, e.g. `'stocks.ts'`.
156
+ */
157
+ baseName: string
158
+ /**
159
+ * `true` for `index.ts` re-export barrels.
160
+ */
161
+ isBarrel?: boolean
162
+ /**
163
+ * `true` for group `[dir]/[dir].ts` aggregation files.
164
+ */
165
+ isAggregation?: boolean
166
+ }
167
+
168
+ /**
169
+ * Document metadata extended with per-file context, passed to a `banner`/`footer` function.
170
+ *
171
+ * Carries everything in {@link InputMeta} plus the file the banner is rendered into, so a
172
+ * single function can decide per file (e.g. skip a directive on barrel/aggregation files).
173
+ *
174
+ * @example Skip a directive on re-export files
175
+ * `banner: (meta) => (meta.isBarrel || meta.isAggregation) ? '' : "'use server'"`
176
+ */
177
+ export type BannerMeta = InputMeta & {
178
+ /**
179
+ * Full output path of the file being generated.
180
+ */
181
+ filePath: string
182
+ /**
183
+ * File name only, e.g. `'stocks.ts'`.
184
+ */
185
+ baseName: string
186
+ /**
187
+ * `true` for `index.ts` re-export barrels.
188
+ */
189
+ isBarrel: boolean
190
+ /**
191
+ * `true` for group `[dir]/[dir].ts` aggregation files.
192
+ */
193
+ isAggregation: boolean
194
+ }
195
+
196
+ /**
197
+ * Context passed to `Resolver.resolveBanner` and `Resolver.resolveFooter`.
198
+ *
199
+ * `output` is optional — not every plugin configures a banner/footer.
200
+ * `config` carries the global Kubb config, used to derive the default Kubb banner.
201
+ * `file` carries per-file context forwarded to a `banner`/`footer` function.
202
+ *
203
+ * @example
204
+ * ```ts
205
+ * resolver.resolveBanner(meta, { output: { banner: '// generated' }, config })
206
+ * // → '// generated'
207
+ * ```
208
+ */
209
+ export type ResolveBannerContext = {
210
+ output?: Pick<Output, 'banner' | 'footer'>
211
+ config: Config
212
+ file?: ResolveBannerFile
213
+ }
214
+
215
+ /**
216
+ * Merges document `meta` with per-file `file` context into the `BannerMeta` passed to a
217
+ * `banner`/`footer` function. Missing fields default to empty/`false` so the object shape
218
+ * is stable even when a caller (e.g. the barrel middleware) has no document metadata.
219
+ */
220
+ function buildBannerMeta({ meta, file }: { meta: InputMeta | undefined; file: ResolveBannerFile | undefined }): BannerMeta {
221
+ return {
222
+ title: meta?.title,
223
+ description: meta?.description,
224
+ version: meta?.version,
225
+ baseURL: meta?.baseURL,
226
+ circularNames: meta?.circularNames ?? [],
227
+ enumNames: meta?.enumNames ?? [],
228
+ filePath: file?.path ?? '',
229
+ baseName: file?.baseName ?? '',
230
+ isBarrel: file?.isBarrel ?? false,
231
+ isAggregation: file?.isAggregation ?? false,
232
+ }
233
+ }
16
234
 
17
235
  /**
18
236
  * Builder type for the plugin-specific resolver fields.
@@ -20,19 +238,16 @@ import type {
20
238
  * `default`, `resolveOptions`, `resolvePath`, `resolveFile`, `resolveBanner`, and `resolveFooter`
21
239
  * are optional — built-in fallbacks are injected when omitted.
22
240
  *
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.
241
+ * Methods in the returned object can call sibling resolver methods via `this`.
27
242
  */
28
- type ResolverBuilder<T extends PluginFactoryOptions> = (ctx: T['resolver']) => Omit<
243
+ type ResolverBuilder<T extends PluginFactoryOptions> = () => Omit<
29
244
  T['resolver'],
30
245
  'default' | 'resolveOptions' | 'resolvePath' | 'resolveFile' | 'resolveBanner' | 'resolveFooter' | 'name' | 'pluginName'
31
246
  > &
32
247
  Partial<Pick<T['resolver'], 'default' | 'resolveOptions' | 'resolvePath' | 'resolveFile' | 'resolveBanner' | 'resolveFooter'>> & {
33
248
  name: string
34
249
  pluginName: T['name']
35
- }
250
+ } & ThisType<T['resolver']>
36
251
 
37
252
  // String patterns are compiled lazily and cached — the same filter is reused for every node.
38
253
  const stringPatternCache = new Map<string, RegExp>()
@@ -54,20 +269,12 @@ function testPattern(value: string, pattern: string | RegExp): boolean {
54
269
  * Checks if an operation matches a pattern for a given filter type (`tag`, `operationId`, `path`, `method`).
55
270
  */
56
271
  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
- }
272
+ if (type === 'tag') return node.tags.some((tag) => testPattern(tag, pattern))
273
+ if (type === 'operationId') return testPattern(node.operationId, pattern)
274
+ if (type === 'path') return node.path !== undefined && testPattern(node.path, pattern)
275
+ if (type === 'method') return node.method !== undefined && testPattern(node.method.toLowerCase(), pattern)
276
+ if (type === 'contentType') return node.requestBody?.content?.some((c) => testPattern(c.contentType, pattern)) ?? false
277
+ return false
71
278
  }
72
279
 
73
280
  /**
@@ -76,12 +283,8 @@ function matchesOperationPattern(node: OperationNode, type: string, pattern: str
76
283
  * Returns `null` when the filter type doesn't apply to schemas.
77
284
  */
78
285
  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
- }
286
+ if (type === 'schemaName') return node.name ? testPattern(node.name, pattern) : false
287
+ return null
85
288
  }
86
289
 
87
290
  /**
@@ -92,19 +295,9 @@ function matchesSchemaPattern(node: SchemaNode, type: string, pattern: string |
92
295
  * - `camelCase` for everything else.
93
296
  */
94
297
  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
298
+ if (type === 'file' || type === 'function') return camelCase(name, { isFile: type === 'file' })
299
+ if (type === 'type') return pascalCase(name)
300
+ return camelCase(name)
108
301
  }
109
302
 
110
303
  /**
@@ -130,19 +323,18 @@ function defaultResolver(name: string, type?: 'file' | 'function' | 'type' | 'co
130
323
  * // → { enumType: 'enum' } when operationId matches
131
324
  * ```
132
325
  */
133
- export function defaultResolveOptions<TOptions>(
326
+ const resolveOptionsCache = new WeakMap<object, WeakMap<Node, { value: unknown }>>()
327
+
328
+ function computeOptions<TOptions>(
134
329
  node: Node,
135
- { options, exclude = [], include, override = [] }: ResolveOptionsContext<TOptions>,
330
+ options: TOptions,
331
+ exclude: Array<PatternFilter>,
332
+ include: Array<PatternFilter> | undefined,
333
+ override: Array<PatternOverride<TOptions>>,
136
334
  ): TOptions | null {
137
335
  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
- }
336
+ if (exclude.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null
337
+ if (include && !include.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null
146
338
 
147
339
  const overrideOptions = override.find(({ type, pattern }) => matchesOperationPattern(node, type, pattern))?.options
148
340
 
@@ -150,18 +342,13 @@ export function defaultResolveOptions<TOptions>(
150
342
  }
151
343
 
152
344
  if (isSchemaNode(node)) {
153
- if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)) {
154
- return null
155
- }
156
-
345
+ if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)) return null
157
346
  if (include) {
158
347
  const results = include.map(({ type, pattern }) => matchesSchemaPattern(node, type, pattern))
159
348
  const applicable = results.filter((r) => r !== null)
160
- if (applicable.length > 0 && !applicable.includes(true)) {
161
- return null
162
- }
163
- }
164
349
 
350
+ if (applicable.length > 0 && !applicable.includes(true)) return null
351
+ }
165
352
  const overrideOptions = override.find(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)?.options
166
353
 
167
354
  return { ...options, ...overrideOptions }
@@ -170,6 +357,26 @@ export function defaultResolveOptions<TOptions>(
170
357
  return options
171
358
  }
172
359
 
360
+ export function defaultResolveOptions<TOptions>(
361
+ node: Node,
362
+ { options, exclude = [], include, override = [] }: ResolveOptionsContext<TOptions>,
363
+ ): TOptions | null {
364
+ const optionsKey = options as object
365
+ let byOptions = resolveOptionsCache.get(optionsKey)
366
+ if (!byOptions) {
367
+ byOptions = new WeakMap()
368
+ resolveOptionsCache.set(optionsKey, byOptions)
369
+ }
370
+ const cached = byOptions.get(node)
371
+ if (cached !== undefined) return cached.value as TOptions | null
372
+
373
+ const result = computeOptions(node, options, exclude, include, override)
374
+
375
+ byOptions.set(node, { value: result })
376
+
377
+ return result
378
+ }
379
+
173
380
  /**
174
381
  * Default path resolver used by `defineResolver`.
175
382
  *
@@ -215,31 +422,30 @@ export function defaultResolveOptions<TOptions>(
215
422
  * ```
216
423
  */
217
424
  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))
425
+ const mode = pathMode ?? getMode(path.resolve(root, output.path))
219
426
 
220
427
  if (mode === 'single') {
221
428
  return path.resolve(root, output.path)
222
429
  }
223
430
 
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
- }
431
+ const result: string = (() => {
432
+ if (group && (groupPath || tag)) {
433
+ const groupValue = group.type === 'path' ? groupPath! : tag!
434
+ const defaultName =
435
+ group.type === 'tag'
436
+ ? ({ group: g }: { group: string }) => `${camelCase(g)}Controller`
437
+ : ({ group: g }: { group: string }) => {
438
+ // Strip traversal components (empty, '.', '..') before taking the first meaningful segment.
439
+ // When every segment is a traversal component (e.g. '../../') we fall back to '' so the
440
+ // file is placed directly in the output root the boundary check below ensures safety.
441
+ const segment = g.split('/').filter((s) => s !== '' && s !== '.' && s !== '..')[0]
442
+ return segment ? camelCase(segment) : ''
443
+ }
444
+ const resolveName = group.name ?? defaultName
445
+ return path.resolve(root, output.path, resolveName({ group: groupValue }), baseName)
446
+ }
447
+ return path.resolve(root, output.path, baseName)
448
+ })()
243
449
 
244
450
  // Ensure the resolved path stays within the configured output directory.
245
451
  // This prevents path traversal from malicious OpenAPI specs or custom group.name functions.
@@ -268,35 +474,35 @@ export function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }:
268
474
  *
269
475
  * @example Resolve a schema file
270
476
  * ```ts
271
- * const file = defaultResolveFile(
477
+ * const file = defaultResolveFile.call(
478
+ * resolver,
272
479
  * { name: 'pet', extname: '.ts' },
273
480
  * { root: '/src', output: { path: 'types' } },
274
- * resolver,
275
481
  * )
276
482
  * // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... }
277
483
  * ```
278
484
  *
279
485
  * @example Resolve an operation file with tag grouping
280
486
  * ```ts
281
- * const file = defaultResolveFile(
487
+ * const file = defaultResolveFile.call(
488
+ * resolver,
282
489
  * { name: 'listPets', extname: '.ts', tag: 'pets' },
283
490
  * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
284
- * resolver,
285
491
  * )
286
492
  * // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... }
287
493
  * ```
288
494
  */
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')
495
+ export function defaultResolveFile(this: Resolver, { name, extname, tag, path: groupPath }: ResolverFileParams, context: ResolverContext): FileNode {
496
+ const pathMode = getMode(path.resolve(context.root, context.output.path))
497
+ const resolvedName = pathMode === 'single' ? '' : this.default(name, 'file')
292
498
  const baseName = `${resolvedName}${extname}` as FileNode['baseName']
293
- const filePath = ctx.resolvePath({ baseName, pathMode, tag, path: groupPath }, context)
499
+ const filePath = this.resolvePath({ baseName, pathMode, tag, path: groupPath }, context)
294
500
 
295
501
  return createFile({
296
502
  path: filePath,
297
503
  baseName: path.basename(filePath) as `${string}.${string}`,
298
504
  meta: {
299
- pluginName: ctx.pluginName,
505
+ pluginName: this.pluginName,
300
506
  },
301
507
  sources: [],
302
508
  imports: [],
@@ -319,17 +525,16 @@ export function buildDefaultBanner({
319
525
  config: Config
320
526
  }): string {
321
527
  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)
528
+ const source = (() => {
529
+ if (Array.isArray(config.input)) {
530
+ const first = config.input[0]
531
+ if (first && 'path' in first) return path.basename(first.path)
532
+ return ''
327
533
  }
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
- }
534
+ if (config.input && 'path' in config.input) return path.basename(config.input.path)
535
+ if (config.input && 'data' in config.input) return 'text content'
536
+ return ''
537
+ })()
333
538
 
334
539
  let banner = '/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n'
335
540
 
@@ -367,10 +572,9 @@ export function buildDefaultBanner({
367
572
  *
368
573
  * A user-supplied `output.banner` overrides the default Kubb "Generated by Kubb" notice.
369
574
  * 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).
575
+ * from the document metadata when `meta` is provided).
371
576
  *
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.
577
+ * - When `output.banner` is a function, calls it with the file's `BannerMeta` and returns the result.
374
578
  * - When `output.banner` is a string, returns it directly.
375
579
  * - When `config.output.defaultBanner` is `false`, returns `undefined`.
376
580
  * - Otherwise returns the Kubb "Generated by Kubb" notice.
@@ -381,27 +585,33 @@ export function buildDefaultBanner({
381
585
  * // → '// my banner'
382
586
  * ```
383
587
  *
384
- * @example Function banner with node
588
+ * @example Function banner with metadata
385
589
  * ```ts
386
- * defaultResolveBanner(inputNode, { output: { banner: (node) => `// v${node.version}` }, config })
590
+ * defaultResolveBanner(meta, { output: { banner: (m) => `// v${m.version}` }, config })
387
591
  * // → '// v3.0.0'
388
592
  * ```
389
593
  *
594
+ * @example Function banner skips re-export files
595
+ * ```ts
596
+ * defaultResolveBanner(meta, { output: { banner: (m) => (m.isBarrel ? '' : "'use server'") }, config, file: { path, baseName, isBarrel: true } })
597
+ * // → ''
598
+ * ```
599
+ *
390
600
  * @example No user banner — Kubb notice with OAS metadata
391
601
  * ```ts
392
- * defaultResolveBanner(inputNode, { config })
602
+ * defaultResolveBanner(meta, { config })
393
603
  * // → '/** Generated by Kubb ... Title: Pet Store ... *\/'
394
604
  * ```
395
605
  *
396
606
  * @example Disabled default banner
397
607
  * ```ts
398
608
  * defaultResolveBanner(undefined, { config: { output: { defaultBanner: false }, ...config } })
399
- * // → undefined
609
+ * // → null
400
610
  * ```
401
611
  */
402
- export function defaultResolveBanner(node: InputNode | undefined, { output, config }: ResolveBannerContext): string | undefined {
612
+ export function defaultResolveBanner(meta: InputMeta | undefined, { output, config, file }: ResolveBannerContext): string | null {
403
613
  if (typeof output?.banner === 'function') {
404
- return output.banner(node)
614
+ return output.banner(buildBannerMeta({ meta, file }))
405
615
  }
406
616
 
407
617
  if (typeof output?.banner === 'string') {
@@ -409,12 +619,12 @@ export function defaultResolveBanner(node: InputNode | undefined, { output, conf
409
619
  }
410
620
 
411
621
  if (config.output.defaultBanner === false) {
412
- return undefined
622
+ return null
413
623
  }
414
624
 
415
625
  return buildDefaultBanner({
416
- title: node?.meta?.title,
417
- version: node?.meta?.version,
626
+ title: meta?.title,
627
+ version: meta?.version,
418
628
  config,
419
629
  })
420
630
  }
@@ -422,8 +632,7 @@ export function defaultResolveBanner(node: InputNode | undefined, { output, conf
422
632
  /**
423
633
  * Default footer resolver — returns the footer string for a generated file.
424
634
  *
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`.
635
+ * - When `output.footer` is a function, calls it with the file's `BannerMeta` and returns the result.
427
636
  * - When `output.footer` is a string, returns it directly.
428
637
  * - Otherwise returns `undefined`.
429
638
  *
@@ -433,89 +642,78 @@ export function defaultResolveBanner(node: InputNode | undefined, { output, conf
433
642
  * // → '// end of file'
434
643
  * ```
435
644
  *
436
- * @example Function footer with node
645
+ * @example Function footer with metadata
437
646
  * ```ts
438
- * defaultResolveFooter(inputNode, { output: { footer: (node) => `// ${node.title}` }, config })
647
+ * defaultResolveFooter(meta, { output: { footer: (m) => `// ${m.title}` }, config })
439
648
  * // → '// Pet Store'
440
649
  * ```
441
650
  */
442
- export function defaultResolveFooter(node: InputNode | undefined, { output }: ResolveBannerContext): string | undefined {
651
+ export function defaultResolveFooter(meta: InputMeta | undefined, { output, file }: ResolveBannerContext): string | null {
443
652
  if (typeof output?.footer === 'function') {
444
- return node ? output.footer(node) : undefined
653
+ return output.footer(buildBannerMeta({ meta, file }))
445
654
  }
446
655
  if (typeof output?.footer === 'string') {
447
656
  return output.footer
448
657
  }
449
- return undefined
658
+ return null
450
659
  }
451
660
 
452
661
  /**
453
- * Defines a resolver for a plugin, injecting built-in defaults for name casing,
454
- * include/exclude/override filtering, path resolution, and file construction.
662
+ * Defines a plugin resolver. The resolver is the object that decides what
663
+ * every generated symbol and file path is called. Built-in defaults handle
664
+ * name casing, include/exclude/override filtering, output path computation,
665
+ * and file construction. Supply your own to override any of them:
455
666
  *
456
- * All four defaults can be overridden by providing them in the builder function:
457
- * - `default` — name casing strategy (camelCase / PascalCase)
458
- * - `resolveOptions` — include/exclude/override filtering
459
- * - `resolvePath` — output path computation
460
- * - `resolveFile` full `FileNode` construction
667
+ * - `default` name casing strategy (camelCase / PascalCase).
668
+ * - `resolveOptions` — include/exclude/override filtering.
669
+ * - `resolvePath` — output path computation.
670
+ * - `resolveFile` — full `FileNode` construction.
671
+ * - `resolveBanner` / `resolveFooter` — top/bottom-of-file text.
461
672
  *
462
- * The builder receives `ctx` a reference to the assembled resolver so methods can
463
- * call sibling resolver methods using `ctx` instead of `this`.
673
+ * Methods in the returned object can call sibling resolver methods via `this`,
674
+ * which keeps custom rules small (`this.default(name, 'type')` to delegate).
464
675
  *
465
676
  * @example Basic resolver with naming helpers
466
677
  * ```ts
467
- * export const resolver = defineResolver<PluginTs>((ctx) => ({
678
+ * export const resolverTs = defineResolver<PluginTs>(() => ({
468
679
  * name: 'default',
469
- * resolveName(node) {
470
- * return ctx.default(node.name, 'function')
680
+ * resolveName(name) {
681
+ * return this.default(name, 'function')
471
682
  * },
472
- * resolveTypedName(node) {
473
- * return ctx.default(node.name, 'type')
683
+ * resolveTypeName(name) {
684
+ * return this.default(name, 'type')
474
685
  * },
475
686
  * }))
476
687
  * ```
477
688
  *
478
- * @example Override resolvePath for a custom output structure
689
+ * @example Custom output path
479
690
  * ```ts
480
- * export const resolver = defineResolver<PluginTs>((_ctx) => ({
691
+ * import path from 'node:path'
692
+ *
693
+ * export const resolverTs = defineResolver<PluginTs>(() => ({
481
694
  * name: 'custom',
482
695
  * resolvePath({ baseName }, { root, output }) {
483
696
  * return path.resolve(root, output.path, 'generated', baseName)
484
697
  * },
485
698
  * }))
486
699
  * ```
487
- *
488
- * @example Use ctx.default inside a helper
489
- * ```ts
490
- * export const resolver = defineResolver<PluginTs>((ctx) => ({
491
- * name: 'default',
492
- * resolveParamName(node, param) {
493
- * return ctx.default(`${node.operationId} ${param.in} ${param.name}`, 'type')
494
- * },
495
- * }))
496
- * ```
497
700
  */
498
701
  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']
702
+ // `resolver` is kept so the default `resolveFile` wrapper can reference the fully assembled
703
+ // object via `.call(resolver, ...)` at call-time, after the result is assigned below.
704
+ let resolver: T['resolver']
504
705
 
505
- Object.assign(resolver, {
706
+ const result = {
506
707
  default: defaultResolver,
507
708
  resolveOptions: defaultResolveOptions,
508
709
  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),
710
+ resolveFile: (params: ResolverFileParams, context: ResolverContext) => defaultResolveFile.call(resolver as Resolver, params, context),
513
711
  resolveBanner: defaultResolveBanner,
514
712
  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
- })
713
+ ...build(),
714
+ } as T['resolver']
715
+
716
+ resolver = result
519
717
 
520
718
  return resolver
521
719
  }