@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.
- package/README.md +8 -38
- package/dist/KubbDriver-CFx2DdhF.js +2131 -0
- package/dist/KubbDriver-CFx2DdhF.js.map +1 -0
- package/dist/KubbDriver-vyD7F0Ip.cjs +2252 -0
- package/dist/KubbDriver-vyD7F0Ip.cjs.map +1 -0
- package/dist/{types-CC09VtBt.d.ts → createKubb-6zii1jo-.d.ts} +1610 -1257
- package/dist/index.cjs +351 -1125
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -186
- package/dist/index.js +341 -1119
- package/dist/index.js.map +1 -1
- package/dist/mocks.cjs +30 -21
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.ts +5 -5
- package/dist/mocks.js +29 -20
- package/dist/mocks.js.map +1 -1
- package/package.json +6 -18
- package/src/FileManager.ts +78 -61
- package/src/FileProcessor.ts +48 -38
- package/src/KubbDriver.ts +930 -0
- package/src/constants.ts +11 -6
- package/src/createAdapter.ts +113 -17
- package/src/createKubb.ts +1039 -478
- package/src/createRenderer.ts +58 -27
- package/src/createStorage.ts +36 -23
- package/src/defineGenerator.ts +127 -15
- package/src/defineLogger.ts +66 -7
- package/src/defineMiddleware.ts +19 -17
- package/src/defineParser.ts +30 -13
- package/src/definePlugin.ts +329 -14
- package/src/defineResolver.ts +365 -167
- package/src/devtools.ts +8 -1
- package/src/index.ts +2 -2
- package/src/mocks.ts +11 -14
- package/src/storages/fsStorage.ts +13 -37
- package/src/types.ts +48 -1292
- package/dist/PluginDriver-BXibeQk-.cjs +0 -1036
- package/dist/PluginDriver-BXibeQk-.cjs.map +0 -1
- package/dist/PluginDriver-DV3p2Hky.js +0 -945
- package/dist/PluginDriver-DV3p2Hky.js.map +0 -1
- package/src/Kubb.ts +0 -300
- package/src/PluginDriver.ts +0 -424
- package/src/renderNode.ts +0 -35
- package/src/utils/diagnostics.ts +0 -18
- package/src/utils/isInputPath.ts +0 -10
- package/src/utils/packageJSON.ts +0 -99
package/src/defineResolver.ts
CHANGED
|
@@ -1,18 +1,236 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import { camelCase, pascalCase } from '@internals/utils'
|
|
3
|
-
import type { FileNode,
|
|
3
|
+
import type { FileNode, InputMeta, Node, OperationNode, SchemaNode } from '@kubb/ast'
|
|
4
4
|
import { createFile, isOperationNode, isSchemaNode } from '@kubb/ast'
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
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
|
-
*
|
|
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> = (
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
326
|
+
const resolveOptionsCache = new WeakMap<object, WeakMap<Node, { value: unknown }>>()
|
|
327
|
+
|
|
328
|
+
function computeOptions<TOptions>(
|
|
134
329
|
node: Node,
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
if (
|
|
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 ??
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
290
|
-
const pathMode =
|
|
291
|
-
const resolvedName = pathMode === 'single' ? '' :
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
|
575
|
+
* from the document metadata when `meta` is provided).
|
|
371
576
|
*
|
|
372
|
-
* - When `output.banner` is a function
|
|
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
|
|
588
|
+
* @example Function banner with metadata
|
|
385
589
|
* ```ts
|
|
386
|
-
* defaultResolveBanner(
|
|
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(
|
|
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
|
-
* // →
|
|
609
|
+
* // → null
|
|
400
610
|
* ```
|
|
401
611
|
*/
|
|
402
|
-
export function defaultResolveBanner(
|
|
612
|
+
export function defaultResolveBanner(meta: InputMeta | undefined, { output, config, file }: ResolveBannerContext): string | null {
|
|
403
613
|
if (typeof output?.banner === 'function') {
|
|
404
|
-
return output.banner(
|
|
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
|
|
622
|
+
return null
|
|
413
623
|
}
|
|
414
624
|
|
|
415
625
|
return buildDefaultBanner({
|
|
416
|
-
title:
|
|
417
|
-
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
|
|
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
|
|
645
|
+
* @example Function footer with metadata
|
|
437
646
|
* ```ts
|
|
438
|
-
* defaultResolveFooter(
|
|
647
|
+
* defaultResolveFooter(meta, { output: { footer: (m) => `// ${m.title}` }, config })
|
|
439
648
|
* // → '// Pet Store'
|
|
440
649
|
* ```
|
|
441
650
|
*/
|
|
442
|
-
export function defaultResolveFooter(
|
|
651
|
+
export function defaultResolveFooter(meta: InputMeta | undefined, { output, file }: ResolveBannerContext): string | null {
|
|
443
652
|
if (typeof output?.footer === 'function') {
|
|
444
|
-
return
|
|
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
|
|
658
|
+
return null
|
|
450
659
|
}
|
|
451
660
|
|
|
452
661
|
/**
|
|
453
|
-
* Defines a resolver
|
|
454
|
-
*
|
|
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
|
-
*
|
|
457
|
-
* - `
|
|
458
|
-
* - `
|
|
459
|
-
* - `
|
|
460
|
-
* - `
|
|
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
|
-
*
|
|
463
|
-
*
|
|
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
|
|
678
|
+
* export const resolverTs = defineResolver<PluginTs>(() => ({
|
|
468
679
|
* name: 'default',
|
|
469
|
-
* resolveName(
|
|
470
|
-
* return
|
|
680
|
+
* resolveName(name) {
|
|
681
|
+
* return this.default(name, 'function')
|
|
471
682
|
* },
|
|
472
|
-
*
|
|
473
|
-
* return
|
|
683
|
+
* resolveTypeName(name) {
|
|
684
|
+
* return this.default(name, 'type')
|
|
474
685
|
* },
|
|
475
686
|
* }))
|
|
476
687
|
* ```
|
|
477
688
|
*
|
|
478
|
-
* @example
|
|
689
|
+
* @example Custom output path
|
|
479
690
|
* ```ts
|
|
480
|
-
*
|
|
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
|
-
//
|
|
500
|
-
//
|
|
501
|
-
|
|
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
|
-
|
|
706
|
+
const result = {
|
|
506
707
|
default: defaultResolver,
|
|
507
708
|
resolveOptions: defaultResolveOptions,
|
|
508
709
|
resolvePath: defaultResolvePath,
|
|
509
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
713
|
+
...build(),
|
|
714
|
+
} as T['resolver']
|
|
715
|
+
|
|
716
|
+
resolver = result
|
|
519
717
|
|
|
520
718
|
return resolver
|
|
521
719
|
}
|