@kubb/core 5.0.0-beta.4 → 5.0.0-beta.40
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 +15 -148
- package/dist/diagnostics-DhfW8YpT.d.ts +2963 -0
- package/dist/index.cjs +775 -1193
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +165 -171
- package/dist/index.js +760 -1188
- package/dist/index.js.map +1 -1
- package/dist/memoryStorage-DTv1Kub1.js +2852 -0
- package/dist/memoryStorage-DTv1Kub1.js.map +1 -0
- package/dist/memoryStorage-Dkxnid2K.cjs +2985 -0
- package/dist/memoryStorage-Dkxnid2K.cjs.map +1 -0
- package/dist/mocks.cjs +80 -18
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.ts +35 -8
- package/dist/mocks.js +81 -21
- package/dist/mocks.js.map +1 -1
- package/package.json +8 -19
- package/src/FileManager.ts +85 -63
- package/src/FileProcessor.ts +171 -43
- package/src/HookRegistry.ts +45 -0
- package/src/KubbDriver.ts +897 -0
- package/src/Telemetry.ts +278 -0
- package/src/Transform.ts +58 -0
- package/src/constants.ts +111 -19
- package/src/createAdapter.ts +113 -17
- package/src/createKubb.ts +921 -493
- package/src/createRenderer.ts +58 -27
- package/src/createReporter.ts +147 -0
- package/src/createStorage.ts +36 -23
- package/src/defineGenerator.ts +129 -17
- package/src/defineLogger.ts +78 -7
- package/src/defineMiddleware.ts +19 -17
- package/src/defineParser.ts +30 -13
- package/src/definePlugin.ts +320 -17
- package/src/defineResolver.ts +381 -179
- package/src/diagnostics.ts +660 -0
- package/src/index.ts +8 -6
- package/src/mocks.ts +92 -19
- package/src/reporters/cliReporter.ts +90 -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 +13 -37
- package/src/types.ts +59 -1297
- package/dist/PluginDriver-Ds-Us-e4.cjs +0 -1036
- package/dist/PluginDriver-Ds-Us-e4.cjs.map +0 -1
- package/dist/PluginDriver-Wi34Pegx.js +0 -945
- package/dist/PluginDriver-Wi34Pegx.js.map +0 -1
- package/dist/types-Cd0jhNmx.d.ts +0 -2153
- package/src/Kubb.ts +0 -300
- package/src/PluginDriver.ts +0 -424
- 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,256 @@
|
|
|
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 type {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
import { Diagnostics } from './diagnostics.ts'
|
|
6
|
+
import type { PluginFactoryOptions } from './definePlugin.ts'
|
|
7
|
+
import { getMode } 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/petsController/petTypes.ts'
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export type ResolverPathParams = {
|
|
78
|
+
baseName: FileNode['baseName']
|
|
79
|
+
pathMode?: 'single' | 'split'
|
|
80
|
+
/**
|
|
81
|
+
* Tag value used when `group.type === 'tag'`.
|
|
82
|
+
*/
|
|
83
|
+
tag?: string
|
|
84
|
+
/**
|
|
85
|
+
* Path value used when `group.type === 'path'`.
|
|
86
|
+
*/
|
|
87
|
+
path?: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Shared context passed as the second argument to `Resolver.resolvePath` and `Resolver.resolveFile`.
|
|
92
|
+
*
|
|
93
|
+
* Describes where on disk output is rooted, which output config is active, and the optional
|
|
94
|
+
* grouping strategy that controls subdirectory layout.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* const context: ResolverContext = {
|
|
99
|
+
* root: config.root,
|
|
100
|
+
* output,
|
|
101
|
+
* group,
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export type ResolverContext = {
|
|
106
|
+
root: string
|
|
107
|
+
output: Output
|
|
108
|
+
group?: Group
|
|
109
|
+
/**
|
|
110
|
+
* Plugin name used to populate `meta.pluginName` on the resolved file.
|
|
111
|
+
*/
|
|
112
|
+
pluginName?: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* File-specific parameters for `Resolver.resolveFile`.
|
|
117
|
+
*
|
|
118
|
+
* Pass alongside a `ResolverContext` to fully describe the file to resolve.
|
|
119
|
+
* `tag` and `path` are used only when a matching `group` is present in the context.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* resolver.resolveFile(
|
|
124
|
+
* { name: 'listPets', extname: '.ts', tag: 'pets' },
|
|
125
|
+
* { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
|
|
126
|
+
* )
|
|
127
|
+
* // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... }
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export type ResolverFileParams = {
|
|
131
|
+
name: string
|
|
132
|
+
extname: FileNode['extname']
|
|
133
|
+
/**
|
|
134
|
+
* Tag value used when `group.type === 'tag'`.
|
|
135
|
+
*/
|
|
136
|
+
tag?: string
|
|
137
|
+
/**
|
|
138
|
+
* Path value used when `group.type === 'path'`.
|
|
139
|
+
*/
|
|
140
|
+
path?: string
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Per-file context describing the file a banner/footer is being resolved for.
|
|
145
|
+
*
|
|
146
|
+
* Supplied by the generator (or the barrel middleware) at resolve-time and merged
|
|
147
|
+
* into `BannerMeta` so a `banner`/`footer` function can branch on the file kind,
|
|
148
|
+
* e.g. omit a `'use server'` directive on re-export files.
|
|
149
|
+
*/
|
|
150
|
+
export type ResolveBannerFile = {
|
|
151
|
+
/**
|
|
152
|
+
* Full output path of the file being generated.
|
|
153
|
+
*/
|
|
154
|
+
path: string
|
|
155
|
+
/**
|
|
156
|
+
* File name only, e.g. `'stocks.ts'`.
|
|
157
|
+
*/
|
|
158
|
+
baseName: string
|
|
159
|
+
/**
|
|
160
|
+
* `true` for `index.ts` re-export barrels.
|
|
161
|
+
*/
|
|
162
|
+
isBarrel?: boolean
|
|
163
|
+
/**
|
|
164
|
+
* `true` for group `[dir]/[dir].ts` aggregation files.
|
|
165
|
+
*/
|
|
166
|
+
isAggregation?: boolean
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Document metadata extended with per-file context, passed to a `banner`/`footer` function.
|
|
171
|
+
*
|
|
172
|
+
* Carries everything in {@link InputMeta} plus the file the banner is rendered into, so a
|
|
173
|
+
* single function can decide per file (e.g. skip a directive on barrel/aggregation files).
|
|
174
|
+
*
|
|
175
|
+
* @example Skip a directive on re-export files
|
|
176
|
+
* `banner: (meta) => (meta.isBarrel || meta.isAggregation) ? '' : "'use server'"`
|
|
177
|
+
*/
|
|
178
|
+
export type BannerMeta = InputMeta & {
|
|
179
|
+
/**
|
|
180
|
+
* Full output path of the file being generated.
|
|
181
|
+
*/
|
|
182
|
+
filePath: string
|
|
183
|
+
/**
|
|
184
|
+
* File name only, e.g. `'stocks.ts'`.
|
|
185
|
+
*/
|
|
186
|
+
baseName: string
|
|
187
|
+
/**
|
|
188
|
+
* `true` for `index.ts` re-export barrels.
|
|
189
|
+
*/
|
|
190
|
+
isBarrel: boolean
|
|
191
|
+
/**
|
|
192
|
+
* `true` for group `[dir]/[dir].ts` aggregation files.
|
|
193
|
+
*/
|
|
194
|
+
isAggregation: boolean
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Context passed to `Resolver.resolveBanner` and `Resolver.resolveFooter`.
|
|
199
|
+
*
|
|
200
|
+
* `output` is optional, since not every plugin configures a banner/footer.
|
|
201
|
+
* `config` carries the global Kubb config, used to derive the default Kubb banner.
|
|
202
|
+
* `file` carries per-file context forwarded to a `banner`/`footer` function.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```ts
|
|
206
|
+
* resolver.resolveBanner(meta, { output: { banner: '// generated' }, config })
|
|
207
|
+
* // → '// generated'
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
export type ResolveBannerContext = {
|
|
211
|
+
output?: Pick<Output, 'banner' | 'footer'>
|
|
212
|
+
config: Config
|
|
213
|
+
file?: ResolveBannerFile
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Merges document `meta` with per-file `file` context into the `BannerMeta` passed to a
|
|
218
|
+
* `banner`/`footer` function. Missing fields default to empty/`false` so the object shape
|
|
219
|
+
* is stable even when a caller (e.g. the barrel middleware) has no document metadata.
|
|
220
|
+
*/
|
|
221
|
+
function buildBannerMeta({ meta, file }: { meta: InputMeta | undefined; file: ResolveBannerFile | undefined }): BannerMeta {
|
|
222
|
+
return {
|
|
223
|
+
title: meta?.title,
|
|
224
|
+
description: meta?.description,
|
|
225
|
+
version: meta?.version,
|
|
226
|
+
baseURL: meta?.baseURL,
|
|
227
|
+
circularNames: meta?.circularNames ?? [],
|
|
228
|
+
enumNames: meta?.enumNames ?? [],
|
|
229
|
+
filePath: file?.path ?? '',
|
|
230
|
+
baseName: file?.baseName ?? '',
|
|
231
|
+
isBarrel: file?.isBarrel ?? false,
|
|
232
|
+
isAggregation: file?.isAggregation ?? false,
|
|
233
|
+
}
|
|
234
|
+
}
|
|
16
235
|
|
|
17
236
|
/**
|
|
18
237
|
* Builder type for the plugin-specific resolver fields.
|
|
19
238
|
*
|
|
20
239
|
* `default`, `resolveOptions`, `resolvePath`, `resolveFile`, `resolveBanner`, and `resolveFooter`
|
|
21
|
-
* are optional
|
|
240
|
+
* are optional, with built-in fallbacks injected when omitted.
|
|
22
241
|
*
|
|
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.
|
|
242
|
+
* Methods in the returned object can call sibling resolver methods via `this`.
|
|
27
243
|
*/
|
|
28
|
-
type ResolverBuilder<T extends PluginFactoryOptions> = (
|
|
244
|
+
type ResolverBuilder<T extends PluginFactoryOptions> = () => Omit<
|
|
29
245
|
T['resolver'],
|
|
30
246
|
'default' | 'resolveOptions' | 'resolvePath' | 'resolveFile' | 'resolveBanner' | 'resolveFooter' | 'name' | 'pluginName'
|
|
31
247
|
> &
|
|
32
248
|
Partial<Pick<T['resolver'], 'default' | 'resolveOptions' | 'resolvePath' | 'resolveFile' | 'resolveBanner' | 'resolveFooter'>> & {
|
|
33
249
|
name: string
|
|
34
250
|
pluginName: T['name']
|
|
35
|
-
}
|
|
251
|
+
} & ThisType<T['resolver']>
|
|
36
252
|
|
|
37
|
-
// String patterns are compiled lazily and cached
|
|
253
|
+
// String patterns are compiled lazily and cached, so the same filter is reused for every node.
|
|
38
254
|
const stringPatternCache = new Map<string, RegExp>()
|
|
39
255
|
|
|
40
256
|
function testPattern(value: string, pattern: string | RegExp): boolean {
|
|
@@ -54,20 +270,12 @@ function testPattern(value: string, pattern: string | RegExp): boolean {
|
|
|
54
270
|
* Checks if an operation matches a pattern for a given filter type (`tag`, `operationId`, `path`, `method`).
|
|
55
271
|
*/
|
|
56
272
|
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
|
-
}
|
|
273
|
+
if (type === 'tag') return node.tags.some((tag) => testPattern(tag, pattern))
|
|
274
|
+
if (type === 'operationId') return testPattern(node.operationId, pattern)
|
|
275
|
+
if (type === 'path') return node.path !== undefined && testPattern(node.path, pattern)
|
|
276
|
+
if (type === 'method') return node.method !== undefined && testPattern(node.method.toLowerCase(), pattern)
|
|
277
|
+
if (type === 'contentType') return node.requestBody?.content?.some((c) => testPattern(c.contentType, pattern)) ?? false
|
|
278
|
+
return false
|
|
71
279
|
}
|
|
72
280
|
|
|
73
281
|
/**
|
|
@@ -76,12 +284,8 @@ function matchesOperationPattern(node: OperationNode, type: string, pattern: str
|
|
|
76
284
|
* Returns `null` when the filter type doesn't apply to schemas.
|
|
77
285
|
*/
|
|
78
286
|
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
|
-
}
|
|
287
|
+
if (type === 'schemaName') return node.name ? testPattern(node.name, pattern) : false
|
|
288
|
+
return null
|
|
85
289
|
}
|
|
86
290
|
|
|
87
291
|
/**
|
|
@@ -92,23 +296,13 @@ function matchesSchemaPattern(node: SchemaNode, type: string, pattern: string |
|
|
|
92
296
|
* - `camelCase` for everything else.
|
|
93
297
|
*/
|
|
94
298
|
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
|
|
299
|
+
if (type === 'file' || type === 'function') return camelCase(name, { isFile: type === 'file' })
|
|
300
|
+
if (type === 'type') return pascalCase(name)
|
|
301
|
+
return camelCase(name)
|
|
108
302
|
}
|
|
109
303
|
|
|
110
304
|
/**
|
|
111
|
-
* Default option resolver
|
|
305
|
+
* Default option resolver. Applies include/exclude filters and merges matching override options.
|
|
112
306
|
*
|
|
113
307
|
* Returns `null` when the node is filtered out by an `exclude` rule or not matched by any `include` rule.
|
|
114
308
|
*
|
|
@@ -130,19 +324,18 @@ function defaultResolver(name: string, type?: 'file' | 'function' | 'type' | 'co
|
|
|
130
324
|
* // → { enumType: 'enum' } when operationId matches
|
|
131
325
|
* ```
|
|
132
326
|
*/
|
|
133
|
-
|
|
327
|
+
const resolveOptionsCache = new WeakMap<object, WeakMap<Node, { value: unknown }>>()
|
|
328
|
+
|
|
329
|
+
function computeOptions<TOptions>(
|
|
134
330
|
node: Node,
|
|
135
|
-
|
|
331
|
+
options: TOptions,
|
|
332
|
+
exclude: Array<PatternFilter>,
|
|
333
|
+
include: Array<PatternFilter> | undefined,
|
|
334
|
+
override: Array<PatternOverride<TOptions>>,
|
|
136
335
|
): TOptions | null {
|
|
137
336
|
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
|
-
}
|
|
337
|
+
if (exclude.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null
|
|
338
|
+
if (include && !include.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null
|
|
146
339
|
|
|
147
340
|
const overrideOptions = override.find(({ type, pattern }) => matchesOperationPattern(node, type, pattern))?.options
|
|
148
341
|
|
|
@@ -150,18 +343,13 @@ export function defaultResolveOptions<TOptions>(
|
|
|
150
343
|
}
|
|
151
344
|
|
|
152
345
|
if (isSchemaNode(node)) {
|
|
153
|
-
if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true))
|
|
154
|
-
return null
|
|
155
|
-
}
|
|
156
|
-
|
|
346
|
+
if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)) return null
|
|
157
347
|
if (include) {
|
|
158
348
|
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
|
-
}
|
|
349
|
+
const applicable = results.filter((result) => result !== null)
|
|
164
350
|
|
|
351
|
+
if (applicable.length > 0 && !applicable.includes(true)) return null
|
|
352
|
+
}
|
|
165
353
|
const overrideOptions = override.find(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)?.options
|
|
166
354
|
|
|
167
355
|
return { ...options, ...overrideOptions }
|
|
@@ -170,6 +358,26 @@ export function defaultResolveOptions<TOptions>(
|
|
|
170
358
|
return options
|
|
171
359
|
}
|
|
172
360
|
|
|
361
|
+
export function defaultResolveOptions<TOptions>(
|
|
362
|
+
node: Node,
|
|
363
|
+
{ options, exclude = [], include, override = [] }: ResolveOptionsContext<TOptions>,
|
|
364
|
+
): TOptions | null {
|
|
365
|
+
const optionsKey = options as object
|
|
366
|
+
let byOptions = resolveOptionsCache.get(optionsKey)
|
|
367
|
+
if (!byOptions) {
|
|
368
|
+
byOptions = new WeakMap()
|
|
369
|
+
resolveOptionsCache.set(optionsKey, byOptions)
|
|
370
|
+
}
|
|
371
|
+
const cached = byOptions.get(node)
|
|
372
|
+
if (cached !== undefined) return cached.value as TOptions | null
|
|
373
|
+
|
|
374
|
+
const result = computeOptions(node, options, exclude, include, override)
|
|
375
|
+
|
|
376
|
+
byOptions.set(node, { value: result })
|
|
377
|
+
|
|
378
|
+
return result
|
|
379
|
+
}
|
|
380
|
+
|
|
173
381
|
/**
|
|
174
382
|
* Default path resolver used by `defineResolver`.
|
|
175
383
|
*
|
|
@@ -215,31 +423,30 @@ export function defaultResolveOptions<TOptions>(
|
|
|
215
423
|
* ```
|
|
216
424
|
*/
|
|
217
425
|
export function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }: ResolverPathParams, { root, output, group }: ResolverContext): string {
|
|
218
|
-
const mode = pathMode ??
|
|
426
|
+
const mode = pathMode ?? getMode(path.resolve(root, output.path))
|
|
219
427
|
|
|
220
428
|
if (mode === 'single') {
|
|
221
429
|
return path.resolve(root, output.path)
|
|
222
430
|
}
|
|
223
431
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
432
|
+
const result: string = (() => {
|
|
433
|
+
if (group && (groupPath || tag)) {
|
|
434
|
+
const groupValue = group.type === 'path' ? groupPath! : tag!
|
|
435
|
+
const defaultName =
|
|
436
|
+
group.type === 'tag'
|
|
437
|
+
? ({ group: groupName }: { group: string }) => `${camelCase(groupName)}Controller`
|
|
438
|
+
: ({ group: groupName }: { group: string }) => {
|
|
439
|
+
// Strip traversal components (empty, '.', '..') before taking the first meaningful segment.
|
|
440
|
+
// When every segment is a traversal component (e.g. '../../') we fall back to '' so the
|
|
441
|
+
// file is placed directly in the output root, and the boundary check below ensures safety.
|
|
442
|
+
const segment = groupName.split('/').filter((part) => part !== '' && part !== '.' && part !== '..')[0]
|
|
443
|
+
return segment ? camelCase(segment) : ''
|
|
444
|
+
}
|
|
445
|
+
const resolveName = group.name ?? defaultName
|
|
446
|
+
return path.resolve(root, output.path, resolveName({ group: groupValue }), baseName)
|
|
447
|
+
}
|
|
448
|
+
return path.resolve(root, output.path, baseName)
|
|
449
|
+
})()
|
|
243
450
|
|
|
244
451
|
// Ensure the resolved path stays within the configured output directory.
|
|
245
452
|
// This prevents path traversal from malicious OpenAPI specs or custom group.name functions.
|
|
@@ -248,10 +455,13 @@ export function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }:
|
|
|
248
455
|
const outputDir = path.resolve(root, output.path)
|
|
249
456
|
const outputDirWithSep = outputDir.endsWith(path.sep) ? outputDir : `${outputDir}${path.sep}`
|
|
250
457
|
if (result !== outputDir && !result.startsWith(outputDirWithSep)) {
|
|
251
|
-
throw new Error(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
458
|
+
throw new Diagnostics.Error({
|
|
459
|
+
code: Diagnostics.code.pathTraversal,
|
|
460
|
+
severity: 'error',
|
|
461
|
+
message: `Resolved path "${result}" is outside the output directory "${outputDir}".`,
|
|
462
|
+
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.',
|
|
463
|
+
location: { kind: 'config' },
|
|
464
|
+
})
|
|
255
465
|
}
|
|
256
466
|
|
|
257
467
|
return result
|
|
@@ -262,41 +472,41 @@ export function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }:
|
|
|
262
472
|
*
|
|
263
473
|
* Resolves a `FileNode` by combining name resolution (`resolver.default`) with
|
|
264
474
|
* path resolution (`resolver.resolvePath`). The resolved file always has empty
|
|
265
|
-
* `sources`, `imports`, and `exports` arrays
|
|
475
|
+
* `sources`, `imports`, and `exports` arrays, which consumers populate separately.
|
|
266
476
|
*
|
|
267
477
|
* In `single` mode the name is omitted and the file sits directly in the output directory.
|
|
268
478
|
*
|
|
269
479
|
* @example Resolve a schema file
|
|
270
480
|
* ```ts
|
|
271
|
-
* const file = defaultResolveFile(
|
|
481
|
+
* const file = defaultResolveFile.call(
|
|
482
|
+
* resolver,
|
|
272
483
|
* { name: 'pet', extname: '.ts' },
|
|
273
484
|
* { root: '/src', output: { path: 'types' } },
|
|
274
|
-
* resolver,
|
|
275
485
|
* )
|
|
276
486
|
* // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... }
|
|
277
487
|
* ```
|
|
278
488
|
*
|
|
279
489
|
* @example Resolve an operation file with tag grouping
|
|
280
490
|
* ```ts
|
|
281
|
-
* const file = defaultResolveFile(
|
|
491
|
+
* const file = defaultResolveFile.call(
|
|
492
|
+
* resolver,
|
|
282
493
|
* { name: 'listPets', extname: '.ts', tag: 'pets' },
|
|
283
494
|
* { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
|
|
284
|
-
* resolver,
|
|
285
495
|
* )
|
|
286
496
|
* // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... }
|
|
287
497
|
* ```
|
|
288
498
|
*/
|
|
289
|
-
export function defaultResolveFile({ name, extname, tag, path: groupPath }: ResolverFileParams, context: ResolverContext
|
|
290
|
-
const pathMode =
|
|
291
|
-
const resolvedName = pathMode === 'single' ? '' :
|
|
499
|
+
export function defaultResolveFile(this: Resolver, { name, extname, tag, path: groupPath }: ResolverFileParams, context: ResolverContext): FileNode {
|
|
500
|
+
const pathMode = getMode(path.resolve(context.root, context.output.path))
|
|
501
|
+
const resolvedName = pathMode === 'single' ? '' : this.default(name, 'file')
|
|
292
502
|
const baseName = `${resolvedName}${extname}` as FileNode['baseName']
|
|
293
|
-
const filePath =
|
|
503
|
+
const filePath = this.resolvePath({ baseName, pathMode, tag, path: groupPath }, context)
|
|
294
504
|
|
|
295
505
|
return createFile({
|
|
296
506
|
path: filePath,
|
|
297
507
|
baseName: path.basename(filePath) as `${string}.${string}`,
|
|
298
508
|
meta: {
|
|
299
|
-
pluginName:
|
|
509
|
+
pluginName: this.pluginName,
|
|
300
510
|
},
|
|
301
511
|
sources: [],
|
|
302
512
|
imports: [],
|
|
@@ -319,17 +529,16 @@ export function buildDefaultBanner({
|
|
|
319
529
|
config: Config
|
|
320
530
|
}): string {
|
|
321
531
|
try {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
532
|
+
const source = (() => {
|
|
533
|
+
if (Array.isArray(config.input)) {
|
|
534
|
+
const first = config.input[0]
|
|
535
|
+
if (first && 'path' in first) return path.basename(first.path)
|
|
536
|
+
return ''
|
|
327
537
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
538
|
+
if (config.input && 'path' in config.input) return path.basename(config.input.path)
|
|
539
|
+
if (config.input && 'data' in config.input) return 'text content'
|
|
540
|
+
return ''
|
|
541
|
+
})()
|
|
333
542
|
|
|
334
543
|
let banner = '/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n'
|
|
335
544
|
|
|
@@ -363,14 +572,13 @@ export function buildDefaultBanner({
|
|
|
363
572
|
}
|
|
364
573
|
|
|
365
574
|
/**
|
|
366
|
-
* Default banner resolver
|
|
575
|
+
* Default banner resolver. Returns the banner string for a generated file.
|
|
367
576
|
*
|
|
368
577
|
* A user-supplied `output.banner` overrides the default Kubb "Generated by Kubb" notice.
|
|
369
578
|
* When no `output.banner` is set, the Kubb notice is used (including `title` and `version`
|
|
370
|
-
* from the
|
|
579
|
+
* from the document metadata when `meta` is provided).
|
|
371
580
|
*
|
|
372
|
-
* - When `output.banner` is a function
|
|
373
|
-
* - When `output.banner` is a function and `node` is absent, falls back to the Kubb notice.
|
|
581
|
+
* - When `output.banner` is a function, calls it with the file's `BannerMeta` and returns the result.
|
|
374
582
|
* - When `output.banner` is a string, returns it directly.
|
|
375
583
|
* - When `config.output.defaultBanner` is `false`, returns `undefined`.
|
|
376
584
|
* - Otherwise returns the Kubb "Generated by Kubb" notice.
|
|
@@ -381,27 +589,33 @@ export function buildDefaultBanner({
|
|
|
381
589
|
* // → '// my banner'
|
|
382
590
|
* ```
|
|
383
591
|
*
|
|
384
|
-
* @example Function banner with
|
|
592
|
+
* @example Function banner with metadata
|
|
385
593
|
* ```ts
|
|
386
|
-
* defaultResolveBanner(
|
|
594
|
+
* defaultResolveBanner(meta, { output: { banner: (m) => `// v${m.version}` }, config })
|
|
387
595
|
* // → '// v3.0.0'
|
|
388
596
|
* ```
|
|
389
597
|
*
|
|
390
|
-
* @example
|
|
598
|
+
* @example Function banner skips re-export files
|
|
391
599
|
* ```ts
|
|
392
|
-
* defaultResolveBanner(
|
|
600
|
+
* defaultResolveBanner(meta, { output: { banner: (m) => (m.isBarrel ? '' : "'use server'") }, config, file: { path, baseName, isBarrel: true } })
|
|
601
|
+
* // → ''
|
|
602
|
+
* ```
|
|
603
|
+
*
|
|
604
|
+
* @example No user banner, Kubb notice with OAS metadata
|
|
605
|
+
* ```ts
|
|
606
|
+
* defaultResolveBanner(meta, { config })
|
|
393
607
|
* // → '/** Generated by Kubb ... Title: Pet Store ... *\/'
|
|
394
608
|
* ```
|
|
395
609
|
*
|
|
396
610
|
* @example Disabled default banner
|
|
397
611
|
* ```ts
|
|
398
612
|
* defaultResolveBanner(undefined, { config: { output: { defaultBanner: false }, ...config } })
|
|
399
|
-
* // →
|
|
613
|
+
* // → null
|
|
400
614
|
* ```
|
|
401
615
|
*/
|
|
402
|
-
export function defaultResolveBanner(
|
|
616
|
+
export function defaultResolveBanner(meta: InputMeta | undefined, { output, config, file }: ResolveBannerContext): string | null {
|
|
403
617
|
if (typeof output?.banner === 'function') {
|
|
404
|
-
return output.banner(
|
|
618
|
+
return output.banner(buildBannerMeta({ meta, file }))
|
|
405
619
|
}
|
|
406
620
|
|
|
407
621
|
if (typeof output?.banner === 'string') {
|
|
@@ -409,21 +623,20 @@ export function defaultResolveBanner(node: InputNode | undefined, { output, conf
|
|
|
409
623
|
}
|
|
410
624
|
|
|
411
625
|
if (config.output.defaultBanner === false) {
|
|
412
|
-
return
|
|
626
|
+
return null
|
|
413
627
|
}
|
|
414
628
|
|
|
415
629
|
return buildDefaultBanner({
|
|
416
|
-
title:
|
|
417
|
-
version:
|
|
630
|
+
title: meta?.title,
|
|
631
|
+
version: meta?.version,
|
|
418
632
|
config,
|
|
419
633
|
})
|
|
420
634
|
}
|
|
421
635
|
|
|
422
636
|
/**
|
|
423
|
-
* Default footer resolver
|
|
637
|
+
* Default footer resolver. Returns the footer string for a generated file.
|
|
424
638
|
*
|
|
425
|
-
* - When `output.footer` is a function
|
|
426
|
-
* - When `output.footer` is a function and `node` is absent, returns `undefined`.
|
|
639
|
+
* - When `output.footer` is a function, calls it with the file's `BannerMeta` and returns the result.
|
|
427
640
|
* - When `output.footer` is a string, returns it directly.
|
|
428
641
|
* - Otherwise returns `undefined`.
|
|
429
642
|
*
|
|
@@ -433,89 +646,78 @@ export function defaultResolveBanner(node: InputNode | undefined, { output, conf
|
|
|
433
646
|
* // → '// end of file'
|
|
434
647
|
* ```
|
|
435
648
|
*
|
|
436
|
-
* @example Function footer with
|
|
649
|
+
* @example Function footer with metadata
|
|
437
650
|
* ```ts
|
|
438
|
-
* defaultResolveFooter(
|
|
651
|
+
* defaultResolveFooter(meta, { output: { footer: (m) => `// ${m.title}` }, config })
|
|
439
652
|
* // → '// Pet Store'
|
|
440
653
|
* ```
|
|
441
654
|
*/
|
|
442
|
-
export function defaultResolveFooter(
|
|
655
|
+
export function defaultResolveFooter(meta: InputMeta | undefined, { output, file }: ResolveBannerContext): string | null {
|
|
443
656
|
if (typeof output?.footer === 'function') {
|
|
444
|
-
return
|
|
657
|
+
return output.footer(buildBannerMeta({ meta, file }))
|
|
445
658
|
}
|
|
446
659
|
if (typeof output?.footer === 'string') {
|
|
447
660
|
return output.footer
|
|
448
661
|
}
|
|
449
|
-
return
|
|
662
|
+
return null
|
|
450
663
|
}
|
|
451
664
|
|
|
452
665
|
/**
|
|
453
|
-
* Defines a resolver
|
|
454
|
-
*
|
|
666
|
+
* Defines a plugin resolver. The resolver is the object that decides what
|
|
667
|
+
* every generated symbol and file path is called. Built-in defaults handle
|
|
668
|
+
* name casing, include/exclude/override filtering, output path computation,
|
|
669
|
+
* and file construction. Supply your own to override any of them:
|
|
455
670
|
*
|
|
456
|
-
*
|
|
457
|
-
* - `
|
|
458
|
-
* - `
|
|
459
|
-
* - `
|
|
460
|
-
* - `
|
|
671
|
+
* - `default` sets the name casing strategy (camelCase or PascalCase).
|
|
672
|
+
* - `resolveOptions` does include/exclude/override filtering.
|
|
673
|
+
* - `resolvePath` computes the output path.
|
|
674
|
+
* - `resolveFile` builds the full `FileNode`.
|
|
675
|
+
* - `resolveBanner` and `resolveFooter` produce the top and bottom of file text.
|
|
461
676
|
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
677
|
+
* Methods in the returned object can call sibling resolver methods via `this`,
|
|
678
|
+
* which keeps custom rules small (`this.default(name, 'type')` to delegate).
|
|
464
679
|
*
|
|
465
680
|
* @example Basic resolver with naming helpers
|
|
466
681
|
* ```ts
|
|
467
|
-
* export const
|
|
682
|
+
* export const resolverTs = defineResolver<PluginTs>(() => ({
|
|
468
683
|
* name: 'default',
|
|
469
|
-
* resolveName(
|
|
470
|
-
* return
|
|
684
|
+
* resolveName(name) {
|
|
685
|
+
* return this.default(name, 'function')
|
|
471
686
|
* },
|
|
472
|
-
*
|
|
473
|
-
* return
|
|
687
|
+
* resolveTypeName(name) {
|
|
688
|
+
* return this.default(name, 'type')
|
|
474
689
|
* },
|
|
475
690
|
* }))
|
|
476
691
|
* ```
|
|
477
692
|
*
|
|
478
|
-
* @example
|
|
693
|
+
* @example Custom output path
|
|
479
694
|
* ```ts
|
|
480
|
-
*
|
|
695
|
+
* import path from 'node:path'
|
|
696
|
+
*
|
|
697
|
+
* export const resolverTs = defineResolver<PluginTs>(() => ({
|
|
481
698
|
* name: 'custom',
|
|
482
699
|
* resolvePath({ baseName }, { root, output }) {
|
|
483
700
|
* return path.resolve(root, output.path, 'generated', baseName)
|
|
484
701
|
* },
|
|
485
702
|
* }))
|
|
486
703
|
* ```
|
|
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
704
|
*/
|
|
498
705
|
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']
|
|
706
|
+
// `resolver` is kept so the default `resolveFile` wrapper can reference the fully assembled
|
|
707
|
+
// object via `.call(resolver, ...)` at call-time, after the result is assigned below.
|
|
708
|
+
let resolver: T['resolver']
|
|
504
709
|
|
|
505
|
-
|
|
710
|
+
const result = {
|
|
506
711
|
default: defaultResolver,
|
|
507
712
|
resolveOptions: defaultResolveOptions,
|
|
508
713
|
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),
|
|
714
|
+
resolveFile: (params: ResolverFileParams, context: ResolverContext) => defaultResolveFile.call(resolver as Resolver, params, context),
|
|
513
715
|
resolveBanner: defaultResolveBanner,
|
|
514
716
|
resolveFooter: defaultResolveFooter,
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
717
|
+
...build(),
|
|
718
|
+
} as T['resolver']
|
|
719
|
+
|
|
720
|
+
resolver = result
|
|
519
721
|
|
|
520
722
|
return resolver
|
|
521
723
|
}
|