@kubb/core 5.0.0-beta.6 → 5.0.0-beta.60
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/LICENSE +17 -10
- package/README.md +25 -158
- package/dist/diagnostics-B-UZnFqP.d.ts +2906 -0
- package/dist/index.cjs +2497 -1071
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +80 -273
- package/dist/index.js +2487 -1067
- package/dist/index.js.map +1 -1
- package/dist/memoryStorage-CUj1hrxa.cjs +823 -0
- package/dist/memoryStorage-CUj1hrxa.cjs.map +1 -0
- package/dist/memoryStorage-CWFzAz4o.js +714 -0
- package/dist/memoryStorage-CWFzAz4o.js.map +1 -0
- package/dist/mocks.cjs +79 -19
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.ts +35 -9
- package/dist/mocks.js +80 -22
- package/dist/mocks.js.map +1 -1
- package/package.json +8 -28
- package/src/FileManager.ts +86 -64
- package/src/FileProcessor.ts +170 -44
- package/src/KubbDriver.ts +908 -0
- package/src/Transform.ts +75 -0
- package/src/constants.ts +111 -20
- package/src/createAdapter.ts +112 -17
- package/src/createKubb.ts +140 -517
- package/src/createRenderer.ts +43 -28
- package/src/createReporter.ts +134 -0
- package/src/createStorage.ts +36 -23
- package/src/defineGenerator.ts +147 -17
- package/src/defineParser.ts +30 -12
- package/src/definePlugin.ts +370 -21
- package/src/defineResolver.ts +402 -212
- package/src/diagnostics.ts +662 -0
- package/src/index.ts +8 -8
- package/src/mocks.ts +91 -20
- package/src/reporters/cliReporter.ts +89 -0
- package/src/reporters/fileReporter.ts +103 -0
- package/src/reporters/jsonReporter.ts +20 -0
- package/src/reporters/report.ts +85 -0
- package/src/storages/fsStorage.ts +23 -55
- package/src/types.ts +411 -887
- package/dist/PluginDriver-BkTRD2H2.js +0 -946
- package/dist/PluginDriver-BkTRD2H2.js.map +0 -1
- package/dist/PluginDriver-Cadu4ORh.cjs +0 -1037
- package/dist/PluginDriver-Cadu4ORh.cjs.map +0 -1
- package/dist/types-DVPKmzw_.d.ts +0 -2159
- package/src/Kubb.ts +0 -300
- package/src/PluginDriver.ts +0 -426
- package/src/defineLogger.ts +0 -19
- package/src/defineMiddleware.ts +0 -62
- package/src/devtools.ts +0 -59
- 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/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
package/src/defineResolver.ts
CHANGED
|
@@ -1,40 +1,255 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import { camelCase, pascalCase } from '@internals/utils'
|
|
3
|
-
import type { FileNode,
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
2
|
+
import { camelCase, pascalCase, toFilePath } from '@internals/utils'
|
|
3
|
+
import type { FileNode, InputMeta, Node, OperationNode, SchemaNode } from '@kubb/ast'
|
|
4
|
+
import { operationDef, schemaDef } from '@kubb/ast'
|
|
5
|
+
import * as factory from '@kubb/ast/factory'
|
|
6
|
+
import { Diagnostics } from './diagnostics.ts'
|
|
7
|
+
import type { PluginFactoryOptions } from './definePlugin.ts'
|
|
8
|
+
import type { Config, Group, Output } from './types.ts'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Type/string pattern filter for include/exclude/override matching.
|
|
12
|
+
*/
|
|
13
|
+
type PatternFilter = {
|
|
14
|
+
type: string
|
|
15
|
+
pattern: string | RegExp
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Pattern filter with partial option overrides applied when the pattern matches.
|
|
20
|
+
*/
|
|
21
|
+
type PatternOverride<TOptions> = PatternFilter & {
|
|
22
|
+
options: Omit<Partial<TOptions>, 'override'>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Context for resolving filtered options for a given operation or schema node.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export type ResolveOptionsContext<TOptions> = {
|
|
31
|
+
options: TOptions
|
|
32
|
+
exclude?: Array<PatternFilter>
|
|
33
|
+
include?: Array<PatternFilter>
|
|
34
|
+
override?: Array<PatternOverride<TOptions>>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Base constraint for all plugin resolver objects.
|
|
39
|
+
*
|
|
40
|
+
* `default`, `resolveOptions`, `resolvePath`, `resolveFile`, `resolveBanner`, and `resolveFooter`
|
|
41
|
+
* are injected automatically by `defineResolver`. Extend this type to add custom resolution methods.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* type MyResolver = Resolver & {
|
|
46
|
+
* resolveName(node: SchemaNode): string
|
|
47
|
+
* resolveTypedName(node: SchemaNode): string
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export type Resolver = {
|
|
52
|
+
name: string
|
|
53
|
+
pluginName: string
|
|
54
|
+
default(name: string, type?: 'file' | 'function' | 'type' | 'const'): string
|
|
55
|
+
resolveOptions<TOptions>(node: Node, context: ResolveOptionsContext<TOptions>): TOptions | null
|
|
56
|
+
resolvePath(params: ResolverPathParams, context: ResolverContext): string
|
|
57
|
+
resolveFile(params: ResolverFileParams, context: ResolverContext): FileNode
|
|
58
|
+
resolveBanner(meta: InputMeta | undefined, context: ResolveBannerContext): string | null
|
|
59
|
+
resolveFooter(meta: InputMeta | undefined, context: ResolveBannerContext): string | null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* File-specific parameters for `Resolver.resolvePath`.
|
|
64
|
+
*
|
|
65
|
+
* Pass alongside a `ResolverContext` to identify which file to resolve.
|
|
66
|
+
* Provide `tag` for tag-based grouping or `path` for path-based grouping.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* resolver.resolvePath(
|
|
71
|
+
* { baseName: 'petTypes.ts', tag: 'pets' },
|
|
72
|
+
* { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
|
|
73
|
+
* )
|
|
74
|
+
* // → '/src/types/pets/petTypes.ts'
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export type ResolverPathParams = {
|
|
78
|
+
baseName: FileNode['baseName']
|
|
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/pets/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 plugin) 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, since 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 plugin) 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.
|
|
19
237
|
*
|
|
20
238
|
* `default`, `resolveOptions`, `resolvePath`, `resolveFile`, `resolveBanner`, and `resolveFooter`
|
|
21
|
-
* are optional
|
|
239
|
+
* are optional, with built-in fallbacks 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
|
-
// String patterns are compiled lazily and cached
|
|
252
|
+
// String patterns are compiled lazily and cached, so the same filter is reused for every node.
|
|
38
253
|
const stringPatternCache = new Map<string, RegExp>()
|
|
39
254
|
|
|
40
255
|
function testPattern(value: string, pattern: string | RegExp): boolean {
|
|
@@ -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,39 +283,25 @@ 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
|
/**
|
|
88
291
|
* Default name resolver used by `defineResolver`.
|
|
89
292
|
*
|
|
90
|
-
* - `camelCase` for `
|
|
293
|
+
* - `camelCase` for `file`, with dotted names split into `/`-joined nested paths.
|
|
91
294
|
* - `PascalCase` for `type`.
|
|
92
|
-
* - `camelCase` for everything else.
|
|
295
|
+
* - `camelCase` for `function` and 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') return toFilePath(name)
|
|
299
|
+
if (type === 'type') return pascalCase(name)
|
|
300
|
+
return camelCase(name)
|
|
108
301
|
}
|
|
109
302
|
|
|
110
303
|
/**
|
|
111
|
-
* Default option resolver
|
|
304
|
+
* Default option resolver. Applies include/exclude filters and merges matching override options.
|
|
112
305
|
*
|
|
113
306
|
* Returns `null` when the node is filtered out by an `exclude` rule or not matched by any `include` rule.
|
|
114
307
|
*
|
|
@@ -130,38 +323,32 @@ 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
|
-
if (
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
return null
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (include && !include.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) {
|
|
144
|
-
return null
|
|
145
|
-
}
|
|
335
|
+
if (operationDef.is(node)) {
|
|
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
|
|
|
149
341
|
return { ...options, ...overrideOptions }
|
|
150
342
|
}
|
|
151
343
|
|
|
152
|
-
if (
|
|
153
|
-
if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true))
|
|
154
|
-
return null
|
|
155
|
-
}
|
|
156
|
-
|
|
344
|
+
if (schemaDef.is(node)) {
|
|
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
|
-
const applicable = results.filter((
|
|
160
|
-
if (applicable.length > 0 && !applicable.includes(true)) {
|
|
161
|
-
return null
|
|
162
|
-
}
|
|
163
|
-
}
|
|
348
|
+
const applicable = results.filter((result) => result !== null)
|
|
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,15 +357,32 @@ export function defaultResolveOptions<TOptions>(
|
|
|
170
357
|
return options
|
|
171
358
|
}
|
|
172
359
|
|
|
360
|
+
function defaultResolveOptions<TOptions>(node: Node, { options, exclude = [], include, override = [] }: ResolveOptionsContext<TOptions>): TOptions | null {
|
|
361
|
+
const optionsKey = options as object
|
|
362
|
+
let byOptions = resolveOptionsCache.get(optionsKey)
|
|
363
|
+
if (!byOptions) {
|
|
364
|
+
byOptions = new WeakMap()
|
|
365
|
+
resolveOptionsCache.set(optionsKey, byOptions)
|
|
366
|
+
}
|
|
367
|
+
const cached = byOptions.get(node)
|
|
368
|
+
if (cached !== undefined) return cached.value as TOptions | null
|
|
369
|
+
|
|
370
|
+
const result = computeOptions(node, options, exclude, include, override)
|
|
371
|
+
|
|
372
|
+
byOptions.set(node, { value: result })
|
|
373
|
+
|
|
374
|
+
return result
|
|
375
|
+
}
|
|
376
|
+
|
|
173
377
|
/**
|
|
174
378
|
* Default path resolver used by `defineResolver`.
|
|
175
379
|
*
|
|
176
|
-
* -
|
|
177
|
-
* -
|
|
178
|
-
*
|
|
380
|
+
* - `mode: 'file'` resolves directly to `output.path` (the full file path, extension included).
|
|
381
|
+
* - `mode: 'directory'` (default) resolves to `output.path/{baseName}`, or into a
|
|
382
|
+
* subdirectory when `group` and a `tag`/`path` value are provided.
|
|
179
383
|
*
|
|
180
384
|
* A custom `group.name` function overrides the default subdirectory naming.
|
|
181
|
-
* For `tag` groups the default is
|
|
385
|
+
* For `tag` groups the default is the camelCased tag.
|
|
182
386
|
* For `path` groups the default is the first path segment after `/`.
|
|
183
387
|
*
|
|
184
388
|
* @example Flat output
|
|
@@ -193,7 +397,7 @@ export function defaultResolveOptions<TOptions>(
|
|
|
193
397
|
* { baseName: 'petTypes.ts', tag: 'pets' },
|
|
194
398
|
* { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
|
|
195
399
|
* )
|
|
196
|
-
* // → '/src/types/
|
|
400
|
+
* // → '/src/types/pets/petTypes.ts'
|
|
197
401
|
* ```
|
|
198
402
|
*
|
|
199
403
|
* @example Path-based grouping
|
|
@@ -205,53 +409,57 @@ export function defaultResolveOptions<TOptions>(
|
|
|
205
409
|
* // → '/src/types/pets/petTypes.ts'
|
|
206
410
|
* ```
|
|
207
411
|
*
|
|
208
|
-
* @example Single
|
|
412
|
+
* @example Single file (`mode: 'file'`)
|
|
209
413
|
* ```ts
|
|
210
414
|
* defaultResolvePath(
|
|
211
|
-
* { baseName: 'petTypes.ts'
|
|
212
|
-
* { root: '/src', output: { path: 'types' } },
|
|
415
|
+
* { baseName: 'petTypes.ts' },
|
|
416
|
+
* { root: '/src', output: { path: 'types.ts', mode: 'file' } },
|
|
213
417
|
* )
|
|
214
|
-
* // → '/src/types'
|
|
418
|
+
* // → '/src/types.ts'
|
|
215
419
|
* ```
|
|
216
420
|
*/
|
|
217
|
-
export function defaultResolvePath({ baseName,
|
|
218
|
-
const mode =
|
|
421
|
+
export function defaultResolvePath({ baseName, tag, path: groupPath }: ResolverPathParams, { root, output, group }: ResolverContext): string {
|
|
422
|
+
const mode = output.mode ?? 'directory'
|
|
219
423
|
|
|
220
|
-
if (mode === '
|
|
424
|
+
if (mode === 'file') {
|
|
221
425
|
return path.resolve(root, output.path)
|
|
222
426
|
}
|
|
223
427
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
428
|
+
const result: string = (() => {
|
|
429
|
+
if (group && (groupPath || tag)) {
|
|
430
|
+
const groupValue = group.type === 'path' ? groupPath! : tag!
|
|
431
|
+
const defaultName =
|
|
432
|
+
group.type === 'tag'
|
|
433
|
+
? ({ group: groupName }: { group: string }) => camelCase(groupName)
|
|
434
|
+
: ({ group: groupName }: { group: string }) => {
|
|
435
|
+
// Strip traversal components (empty, '.', '..') before taking the first meaningful segment.
|
|
436
|
+
// When every segment is a traversal component (e.g. '../../') we fall back to '' so the
|
|
437
|
+
// file is placed directly in the output root, and the boundary check below ensures safety.
|
|
438
|
+
const segment = groupName.split('/').filter((part) => part !== '' && part !== '.' && part !== '..')[0]
|
|
439
|
+
return segment ? camelCase(segment) : ''
|
|
440
|
+
}
|
|
441
|
+
const resolveName = group.name ?? defaultName
|
|
442
|
+
const groupName = resolveName({ group: groupValue })
|
|
443
|
+
|
|
444
|
+
return path.resolve(root, output.path, groupName, baseName)
|
|
445
|
+
}
|
|
446
|
+
return path.resolve(root, output.path, baseName)
|
|
447
|
+
})()
|
|
243
448
|
|
|
244
449
|
// Ensure the resolved path stays within the configured output directory.
|
|
245
450
|
// This prevents path traversal from malicious OpenAPI specs or custom group.name functions.
|
|
246
|
-
// `result === outputDir` is intentionally permitted: it matches
|
|
247
|
-
//
|
|
451
|
+
// `result === outputDir` is intentionally permitted: it matches edge cases where baseName
|
|
452
|
+
// resolves to the output directory itself.
|
|
248
453
|
const outputDir = path.resolve(root, output.path)
|
|
249
454
|
const outputDirWithSep = outputDir.endsWith(path.sep) ? outputDir : `${outputDir}${path.sep}`
|
|
250
455
|
if (result !== outputDir && !result.startsWith(outputDirWithSep)) {
|
|
251
|
-
throw new Error(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
456
|
+
throw new Diagnostics.Error({
|
|
457
|
+
code: Diagnostics.code.pathTraversal,
|
|
458
|
+
severity: 'error',
|
|
459
|
+
message: `Resolved path "${result}" is outside the output directory "${outputDir}".`,
|
|
460
|
+
help: 'This can stem from a path traversal in the OpenAPI specification or a misconfigured `group.name` function. Keep generated paths within the output directory.',
|
|
461
|
+
location: { kind: 'config' },
|
|
462
|
+
})
|
|
255
463
|
}
|
|
256
464
|
|
|
257
465
|
return result
|
|
@@ -262,41 +470,41 @@ export function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }:
|
|
|
262
470
|
*
|
|
263
471
|
* Resolves a `FileNode` by combining name resolution (`resolver.default`) with
|
|
264
472
|
* path resolution (`resolver.resolvePath`). The resolved file always has empty
|
|
265
|
-
* `sources`, `imports`, and `exports` arrays
|
|
473
|
+
* `sources`, `imports`, and `exports` arrays, which consumers populate separately.
|
|
266
474
|
*
|
|
267
|
-
* In `
|
|
475
|
+
* In `mode: 'file'` the name is omitted and the file sits directly at the output path.
|
|
268
476
|
*
|
|
269
477
|
* @example Resolve a schema file
|
|
270
478
|
* ```ts
|
|
271
|
-
* const file = defaultResolveFile(
|
|
479
|
+
* const file = defaultResolveFile.call(
|
|
480
|
+
* resolver,
|
|
272
481
|
* { name: 'pet', extname: '.ts' },
|
|
273
482
|
* { root: '/src', output: { path: 'types' } },
|
|
274
|
-
* resolver,
|
|
275
483
|
* )
|
|
276
484
|
* // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... }
|
|
277
485
|
* ```
|
|
278
486
|
*
|
|
279
487
|
* @example Resolve an operation file with tag grouping
|
|
280
488
|
* ```ts
|
|
281
|
-
* const file = defaultResolveFile(
|
|
489
|
+
* const file = defaultResolveFile.call(
|
|
490
|
+
* resolver,
|
|
282
491
|
* { name: 'listPets', extname: '.ts', tag: 'pets' },
|
|
283
492
|
* { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
|
|
284
|
-
* resolver,
|
|
285
493
|
* )
|
|
286
|
-
* // → { baseName: 'listPets.ts', path: '/src/types/
|
|
494
|
+
* // → { baseName: 'listPets.ts', path: '/src/types/pets/listPets.ts', ... }
|
|
287
495
|
* ```
|
|
288
496
|
*/
|
|
289
|
-
export function defaultResolveFile({ name, extname, tag, path: groupPath }: ResolverFileParams, context: ResolverContext
|
|
290
|
-
const
|
|
291
|
-
const resolvedName =
|
|
497
|
+
export function defaultResolveFile(this: Resolver, { name, extname, tag, path: groupPath }: ResolverFileParams, context: ResolverContext): FileNode {
|
|
498
|
+
const mode = context.output.mode ?? 'directory'
|
|
499
|
+
const resolvedName = mode === 'file' ? '' : this.default(name, 'file')
|
|
292
500
|
const baseName = `${resolvedName}${extname}` as FileNode['baseName']
|
|
293
|
-
const filePath =
|
|
501
|
+
const filePath = this.resolvePath({ baseName, tag, path: groupPath }, context)
|
|
294
502
|
|
|
295
|
-
return createFile({
|
|
503
|
+
return factory.createFile({
|
|
296
504
|
path: filePath,
|
|
297
505
|
baseName: path.basename(filePath) as `${string}.${string}`,
|
|
298
506
|
meta: {
|
|
299
|
-
pluginName:
|
|
507
|
+
pluginName: this.pluginName,
|
|
300
508
|
},
|
|
301
509
|
sources: [],
|
|
302
510
|
imports: [],
|
|
@@ -307,29 +515,18 @@ export function defaultResolveFile({ name, extname, tag, path: groupPath }: Reso
|
|
|
307
515
|
/**
|
|
308
516
|
* Generates the default "Generated by Kubb" banner from config and optional node metadata.
|
|
309
517
|
*/
|
|
310
|
-
|
|
311
|
-
title,
|
|
312
|
-
description,
|
|
313
|
-
version,
|
|
314
|
-
config,
|
|
315
|
-
}: {
|
|
316
|
-
title?: string
|
|
317
|
-
description?: string
|
|
318
|
-
version?: string
|
|
319
|
-
config: Config
|
|
320
|
-
}): string {
|
|
518
|
+
function buildDefaultBanner({ title, description, version, config }: { title?: string; description?: string; version?: string; config: Config }): string {
|
|
321
519
|
try {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
520
|
+
const source = (() => {
|
|
521
|
+
if (Array.isArray(config.input)) {
|
|
522
|
+
const first = config.input[0]
|
|
523
|
+
if (first && 'path' in first) return path.basename(first.path)
|
|
524
|
+
return ''
|
|
327
525
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
526
|
+
if (config.input && 'path' in config.input) return path.basename(config.input.path)
|
|
527
|
+
if (config.input && 'data' in config.input) return 'text content'
|
|
528
|
+
return ''
|
|
529
|
+
})()
|
|
333
530
|
|
|
334
531
|
let banner = '/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n'
|
|
335
532
|
|
|
@@ -363,14 +560,13 @@ export function buildDefaultBanner({
|
|
|
363
560
|
}
|
|
364
561
|
|
|
365
562
|
/**
|
|
366
|
-
* Default banner resolver
|
|
563
|
+
* Default banner resolver. Returns the banner string for a generated file.
|
|
367
564
|
*
|
|
368
565
|
* A user-supplied `output.banner` overrides the default Kubb "Generated by Kubb" notice.
|
|
369
566
|
* When no `output.banner` is set, the Kubb notice is used (including `title` and `version`
|
|
370
|
-
* from the
|
|
567
|
+
* from the document metadata when `meta` is provided).
|
|
371
568
|
*
|
|
372
|
-
* - When `output.banner` is a function
|
|
373
|
-
* - When `output.banner` is a function and `node` is absent, falls back to the Kubb notice.
|
|
569
|
+
* - When `output.banner` is a function, calls it with the file's `BannerMeta` and returns the result.
|
|
374
570
|
* - When `output.banner` is a string, returns it directly.
|
|
375
571
|
* - When `config.output.defaultBanner` is `false`, returns `undefined`.
|
|
376
572
|
* - Otherwise returns the Kubb "Generated by Kubb" notice.
|
|
@@ -381,27 +577,33 @@ export function buildDefaultBanner({
|
|
|
381
577
|
* // → '// my banner'
|
|
382
578
|
* ```
|
|
383
579
|
*
|
|
384
|
-
* @example Function banner with
|
|
580
|
+
* @example Function banner with metadata
|
|
385
581
|
* ```ts
|
|
386
|
-
* defaultResolveBanner(
|
|
582
|
+
* defaultResolveBanner(meta, { output: { banner: (m) => `// v${m.version}` }, config })
|
|
387
583
|
* // → '// v3.0.0'
|
|
388
584
|
* ```
|
|
389
585
|
*
|
|
390
|
-
* @example
|
|
586
|
+
* @example Function banner skips re-export files
|
|
587
|
+
* ```ts
|
|
588
|
+
* defaultResolveBanner(meta, { output: { banner: (m) => (m.isBarrel ? '' : "'use server'") }, config, file: { path, baseName, isBarrel: true } })
|
|
589
|
+
* // → ''
|
|
590
|
+
* ```
|
|
591
|
+
*
|
|
592
|
+
* @example No user banner, Kubb notice with OAS metadata
|
|
391
593
|
* ```ts
|
|
392
|
-
* defaultResolveBanner(
|
|
594
|
+
* defaultResolveBanner(meta, { config })
|
|
393
595
|
* // → '/** Generated by Kubb ... Title: Pet Store ... *\/'
|
|
394
596
|
* ```
|
|
395
597
|
*
|
|
396
598
|
* @example Disabled default banner
|
|
397
599
|
* ```ts
|
|
398
600
|
* defaultResolveBanner(undefined, { config: { output: { defaultBanner: false }, ...config } })
|
|
399
|
-
* // →
|
|
601
|
+
* // → null
|
|
400
602
|
* ```
|
|
401
603
|
*/
|
|
402
|
-
export function defaultResolveBanner(
|
|
604
|
+
export function defaultResolveBanner(meta: InputMeta | undefined, { output, config, file }: ResolveBannerContext): string | null {
|
|
403
605
|
if (typeof output?.banner === 'function') {
|
|
404
|
-
return output.banner(
|
|
606
|
+
return output.banner(buildBannerMeta({ meta, file }))
|
|
405
607
|
}
|
|
406
608
|
|
|
407
609
|
if (typeof output?.banner === 'string') {
|
|
@@ -409,21 +611,20 @@ export function defaultResolveBanner(node: InputNode | undefined, { output, conf
|
|
|
409
611
|
}
|
|
410
612
|
|
|
411
613
|
if (config.output.defaultBanner === false) {
|
|
412
|
-
return
|
|
614
|
+
return null
|
|
413
615
|
}
|
|
414
616
|
|
|
415
617
|
return buildDefaultBanner({
|
|
416
|
-
title:
|
|
417
|
-
version:
|
|
618
|
+
title: meta?.title,
|
|
619
|
+
version: meta?.version,
|
|
418
620
|
config,
|
|
419
621
|
})
|
|
420
622
|
}
|
|
421
623
|
|
|
422
624
|
/**
|
|
423
|
-
* Default footer resolver
|
|
625
|
+
* Default footer resolver. Returns the footer string for a generated file.
|
|
424
626
|
*
|
|
425
|
-
* - When `output.footer` is a function
|
|
426
|
-
* - When `output.footer` is a function and `node` is absent, returns `undefined`.
|
|
627
|
+
* - When `output.footer` is a function, calls it with the file's `BannerMeta` and returns the result.
|
|
427
628
|
* - When `output.footer` is a string, returns it directly.
|
|
428
629
|
* - Otherwise returns `undefined`.
|
|
429
630
|
*
|
|
@@ -433,89 +634,78 @@ export function defaultResolveBanner(node: InputNode | undefined, { output, conf
|
|
|
433
634
|
* // → '// end of file'
|
|
434
635
|
* ```
|
|
435
636
|
*
|
|
436
|
-
* @example Function footer with
|
|
637
|
+
* @example Function footer with metadata
|
|
437
638
|
* ```ts
|
|
438
|
-
* defaultResolveFooter(
|
|
639
|
+
* defaultResolveFooter(meta, { output: { footer: (m) => `// ${m.title}` }, config })
|
|
439
640
|
* // → '// Pet Store'
|
|
440
641
|
* ```
|
|
441
642
|
*/
|
|
442
|
-
export function defaultResolveFooter(
|
|
643
|
+
export function defaultResolveFooter(meta: InputMeta | undefined, { output, file }: ResolveBannerContext): string | null {
|
|
443
644
|
if (typeof output?.footer === 'function') {
|
|
444
|
-
return
|
|
645
|
+
return output.footer(buildBannerMeta({ meta, file }))
|
|
445
646
|
}
|
|
446
647
|
if (typeof output?.footer === 'string') {
|
|
447
648
|
return output.footer
|
|
448
649
|
}
|
|
449
|
-
return
|
|
650
|
+
return null
|
|
450
651
|
}
|
|
451
652
|
|
|
452
653
|
/**
|
|
453
|
-
* Defines a resolver
|
|
454
|
-
*
|
|
654
|
+
* Defines a plugin resolver. The resolver is the object that decides what
|
|
655
|
+
* every generated symbol and file path is called. Built-in defaults handle
|
|
656
|
+
* name casing, include/exclude/override filtering, output path computation,
|
|
657
|
+
* and file construction. Supply your own to override any of them:
|
|
455
658
|
*
|
|
456
|
-
*
|
|
457
|
-
* - `
|
|
458
|
-
* - `
|
|
459
|
-
* - `
|
|
460
|
-
* - `
|
|
659
|
+
* - `default` sets the name casing strategy (camelCase or PascalCase).
|
|
660
|
+
* - `resolveOptions` does include/exclude/override filtering.
|
|
661
|
+
* - `resolvePath` computes the output path.
|
|
662
|
+
* - `resolveFile` builds the full `FileNode`.
|
|
663
|
+
* - `resolveBanner` and `resolveFooter` produce the top and bottom of file text.
|
|
461
664
|
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
665
|
+
* Methods in the returned object can call sibling resolver methods via `this`.
|
|
666
|
+
* A custom rule can delegate to a default, for example `this.default(name, 'type')`.
|
|
464
667
|
*
|
|
465
668
|
* @example Basic resolver with naming helpers
|
|
466
669
|
* ```ts
|
|
467
|
-
* export const
|
|
670
|
+
* export const resolverTs = defineResolver<PluginTs>(() => ({
|
|
468
671
|
* name: 'default',
|
|
469
|
-
* resolveName(
|
|
470
|
-
* return
|
|
672
|
+
* resolveName(name) {
|
|
673
|
+
* return this.default(name, 'function')
|
|
471
674
|
* },
|
|
472
|
-
*
|
|
473
|
-
* return
|
|
675
|
+
* resolveTypeName(name) {
|
|
676
|
+
* return this.default(name, 'type')
|
|
474
677
|
* },
|
|
475
678
|
* }))
|
|
476
679
|
* ```
|
|
477
680
|
*
|
|
478
|
-
* @example
|
|
681
|
+
* @example Custom output path
|
|
479
682
|
* ```ts
|
|
480
|
-
*
|
|
683
|
+
* import path from 'node:path'
|
|
684
|
+
*
|
|
685
|
+
* export const resolverTs = defineResolver<PluginTs>(() => ({
|
|
481
686
|
* name: 'custom',
|
|
482
687
|
* resolvePath({ baseName }, { root, output }) {
|
|
483
688
|
* return path.resolve(root, output.path, 'generated', baseName)
|
|
484
689
|
* },
|
|
485
690
|
* }))
|
|
486
691
|
* ```
|
|
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
692
|
*/
|
|
498
693
|
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']
|
|
694
|
+
// `resolver` is kept so the default `resolveFile` wrapper can reference the fully assembled
|
|
695
|
+
// object via `.call(resolver, ...)` at call-time, after the result is assigned below.
|
|
696
|
+
let resolver: T['resolver']
|
|
504
697
|
|
|
505
|
-
|
|
698
|
+
const result = {
|
|
506
699
|
default: defaultResolver,
|
|
507
700
|
resolveOptions: defaultResolveOptions,
|
|
508
701
|
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),
|
|
702
|
+
resolveFile: (params: ResolverFileParams, context: ResolverContext) => defaultResolveFile.call(resolver as Resolver, params, context),
|
|
513
703
|
resolveBanner: defaultResolveBanner,
|
|
514
704
|
resolveFooter: defaultResolveFooter,
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
705
|
+
...build(),
|
|
706
|
+
} as T['resolver']
|
|
707
|
+
|
|
708
|
+
resolver = result
|
|
519
709
|
|
|
520
710
|
return resolver
|
|
521
711
|
}
|