@kubb/core 5.0.0-beta.75 → 5.0.0-beta.9

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.
@@ -2,17 +2,159 @@ import path from 'node:path'
2
2
  import { camelCase, pascalCase } from '@internals/utils'
3
3
  import type { FileNode, InputNode, Node, OperationNode, SchemaNode } from '@kubb/ast'
4
4
  import { createFile, isOperationNode, isSchemaNode } from '@kubb/ast'
5
- import { PluginDriver } from './PluginDriver.ts'
6
- import type {
7
- Config,
8
- PluginFactoryOptions,
9
- ResolveBannerContext,
10
- ResolveOptionsContext,
11
- Resolver,
12
- ResolverContext,
13
- ResolverFileParams,
14
- ResolverPathParams,
15
- } from './types.ts'
5
+ import type { PluginFactoryOptions } from './definePlugin.ts'
6
+ import { getMode } from './definePlugin.ts'
7
+ import type { Config, Group, Output } from './types.ts'
8
+
9
+ /**
10
+ * Type/string pattern filter for include/exclude/override matching.
11
+ */
12
+ type PatternFilter = {
13
+ type: string
14
+ pattern: string | RegExp
15
+ }
16
+
17
+ /**
18
+ * Pattern filter with partial option overrides applied when the pattern matches.
19
+ */
20
+ type PatternOverride<TOptions> = PatternFilter & {
21
+ options: Omit<Partial<TOptions>, 'override'>
22
+ }
23
+
24
+ /**
25
+ * Context for resolving filtered options for a given operation or schema node.
26
+ *
27
+ * @internal
28
+ */
29
+ export type ResolveOptionsContext<TOptions> = {
30
+ options: TOptions
31
+ exclude?: Array<PatternFilter>
32
+ include?: Array<PatternFilter>
33
+ override?: Array<PatternOverride<TOptions>>
34
+ }
35
+
36
+ /**
37
+ * Base constraint for all plugin resolver objects.
38
+ *
39
+ * `default`, `resolveOptions`, `resolvePath`, `resolveFile`, `resolveBanner`, and `resolveFooter`
40
+ * are injected automatically by `defineResolver` — extend this type to add custom resolution methods.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * type MyResolver = Resolver & {
45
+ * resolveName(node: SchemaNode): string
46
+ * resolveTypedName(node: SchemaNode): string
47
+ * }
48
+ * ```
49
+ */
50
+ export type Resolver = {
51
+ name: string
52
+ pluginName: string
53
+ default(name: string, type?: 'file' | 'function' | 'type' | 'const'): string
54
+ resolveOptions<TOptions>(node: Node, context: ResolveOptionsContext<TOptions>): TOptions | null
55
+ resolvePath(params: ResolverPathParams, context: ResolverContext): string
56
+ resolveFile(params: ResolverFileParams, context: ResolverContext): FileNode
57
+ resolveBanner(node: InputNode | null, context: ResolveBannerContext): string | undefined
58
+ resolveFooter(node: InputNode | null, context: ResolveBannerContext): string | undefined
59
+ }
60
+
61
+ /**
62
+ * File-specific parameters for `Resolver.resolvePath`.
63
+ *
64
+ * Pass alongside a `ResolverContext` to identify which file to resolve.
65
+ * Provide `tag` for tag-based grouping or `path` for path-based grouping.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * resolver.resolvePath(
70
+ * { baseName: 'petTypes.ts', tag: 'pets' },
71
+ * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
72
+ * )
73
+ * // → '/src/types/petsController/petTypes.ts'
74
+ * ```
75
+ */
76
+ export type ResolverPathParams = {
77
+ baseName: FileNode['baseName']
78
+ pathMode?: 'single' | 'split'
79
+ /**
80
+ * Tag value used when `group.type === 'tag'`.
81
+ */
82
+ tag?: string
83
+ /**
84
+ * Path value used when `group.type === 'path'`.
85
+ */
86
+ path?: string
87
+ }
88
+
89
+ /**
90
+ * Shared context passed as the second argument to `Resolver.resolvePath` and `Resolver.resolveFile`.
91
+ *
92
+ * Describes where on disk output is rooted, which output config is active, and the optional
93
+ * grouping strategy that controls subdirectory layout.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const context: ResolverContext = {
98
+ * root: config.root,
99
+ * output,
100
+ * group,
101
+ * }
102
+ * ```
103
+ */
104
+ export type ResolverContext = {
105
+ root: string
106
+ output: Output
107
+ group?: Group
108
+ /**
109
+ * Plugin name used to populate `meta.pluginName` on the resolved file.
110
+ */
111
+ pluginName?: string
112
+ }
113
+
114
+ /**
115
+ * File-specific parameters for `Resolver.resolveFile`.
116
+ *
117
+ * Pass alongside a `ResolverContext` to fully describe the file to resolve.
118
+ * `tag` and `path` are used only when a matching `group` is present in the context.
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * resolver.resolveFile(
123
+ * { name: 'listPets', extname: '.ts', tag: 'pets' },
124
+ * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
125
+ * )
126
+ * // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... }
127
+ * ```
128
+ */
129
+ export type ResolverFileParams = {
130
+ name: string
131
+ extname: FileNode['extname']
132
+ /**
133
+ * Tag value used when `group.type === 'tag'`.
134
+ */
135
+ tag?: string
136
+ /**
137
+ * Path value used when `group.type === 'path'`.
138
+ */
139
+ path?: string
140
+ }
141
+
142
+ /**
143
+ * Context passed to `Resolver.resolveBanner` and `Resolver.resolveFooter`.
144
+ *
145
+ * `output` is optional — not every plugin configures a banner/footer.
146
+ * `config` carries the global Kubb config, used to derive the default Kubb banner.
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * resolver.resolveBanner(inputNode, { output: { banner: '// generated' }, config })
151
+ * // → '// generated'
152
+ * ```
153
+ */
154
+ export type ResolveBannerContext = {
155
+ output?: Pick<Output, 'banner' | 'footer'>
156
+ config: Config
157
+ }
16
158
 
17
159
  /**
18
160
  * Builder type for the plugin-specific resolver fields.
@@ -20,19 +162,16 @@ import type {
20
162
  * `default`, `resolveOptions`, `resolvePath`, `resolveFile`, `resolveBanner`, and `resolveFooter`
21
163
  * are optional — built-in fallbacks are injected when omitted.
22
164
  *
23
- * The builder receives `ctx` a reference to the fully assembled resolver so methods can
24
- * call sibling resolver methods without using `this`. Because `ctx` is captured by the closure
25
- * and the resolver is populated after the builder runs, `ctx` correctly reflects any overrides
26
- * that were applied by the builder itself.
165
+ * Methods in the returned object can call sibling resolver methods via `this`.
27
166
  */
28
- type ResolverBuilder<T extends PluginFactoryOptions> = (ctx: T['resolver']) => Omit<
167
+ type ResolverBuilder<T extends PluginFactoryOptions> = () => Omit<
29
168
  T['resolver'],
30
169
  'default' | 'resolveOptions' | 'resolvePath' | 'resolveFile' | 'resolveBanner' | 'resolveFooter' | 'name' | 'pluginName'
31
170
  > &
32
171
  Partial<Pick<T['resolver'], 'default' | 'resolveOptions' | 'resolvePath' | 'resolveFile' | 'resolveBanner' | 'resolveFooter'>> & {
33
172
  name: string
34
173
  pluginName: T['name']
35
- }
174
+ } & ThisType<T['resolver']>
36
175
 
37
176
  // String patterns are compiled lazily and cached — the same filter is reused for every node.
38
177
  const stringPatternCache = new Map<string, RegExp>()
@@ -215,7 +354,7 @@ export function defaultResolveOptions<TOptions>(
215
354
  * ```
216
355
  */
217
356
  export function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }: ResolverPathParams, { root, output, group }: ResolverContext): string {
218
- const mode = pathMode ?? PluginDriver.getMode(path.resolve(root, output.path))
357
+ const mode = pathMode ?? getMode(path.resolve(root, output.path))
219
358
 
220
359
  if (mode === 'single') {
221
360
  return path.resolve(root, output.path)
@@ -268,35 +407,35 @@ export function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }:
268
407
  *
269
408
  * @example Resolve a schema file
270
409
  * ```ts
271
- * const file = defaultResolveFile(
410
+ * const file = defaultResolveFile.call(
411
+ * resolver,
272
412
  * { name: 'pet', extname: '.ts' },
273
413
  * { root: '/src', output: { path: 'types' } },
274
- * resolver,
275
414
  * )
276
415
  * // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... }
277
416
  * ```
278
417
  *
279
418
  * @example Resolve an operation file with tag grouping
280
419
  * ```ts
281
- * const file = defaultResolveFile(
420
+ * const file = defaultResolveFile.call(
421
+ * resolver,
282
422
  * { name: 'listPets', extname: '.ts', tag: 'pets' },
283
423
  * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
284
- * resolver,
285
424
  * )
286
425
  * // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... }
287
426
  * ```
288
427
  */
289
- export function defaultResolveFile({ name, extname, tag, path: groupPath }: ResolverFileParams, context: ResolverContext, ctx: Resolver): FileNode {
290
- const pathMode = PluginDriver.getMode(path.resolve(context.root, context.output.path))
291
- const resolvedName = pathMode === 'single' ? '' : ctx.default(name, 'file')
428
+ export function defaultResolveFile(this: Resolver, { name, extname, tag, path: groupPath }: ResolverFileParams, context: ResolverContext): FileNode {
429
+ const pathMode = getMode(path.resolve(context.root, context.output.path))
430
+ const resolvedName = pathMode === 'single' ? '' : this.default(name, 'file')
292
431
  const baseName = `${resolvedName}${extname}` as FileNode['baseName']
293
- const filePath = ctx.resolvePath({ baseName, pathMode, tag, path: groupPath }, context)
432
+ const filePath = this.resolvePath({ baseName, pathMode, tag, path: groupPath }, context)
294
433
 
295
434
  return createFile({
296
435
  path: filePath,
297
436
  baseName: path.basename(filePath) as `${string}.${string}`,
298
437
  meta: {
299
- pluginName: ctx.pluginName,
438
+ pluginName: this.pluginName,
300
439
  },
301
440
  sources: [],
302
441
  imports: [],
@@ -325,9 +464,9 @@ export function buildDefaultBanner({
325
464
  if (first && 'path' in first) {
326
465
  source = path.basename(first.path)
327
466
  }
328
- } else if ('path' in config.input) {
467
+ } else if (config.input && 'path' in config.input) {
329
468
  source = path.basename(config.input.path)
330
- } else if ('data' in config.input) {
469
+ } else if (config.input && 'data' in config.input) {
331
470
  source = 'text content'
332
471
  }
333
472
 
@@ -459,25 +598,24 @@ export function defaultResolveFooter(node: InputNode | undefined, { output }: Re
459
598
  * - `resolvePath` — output path computation
460
599
  * - `resolveFile` — full `FileNode` construction
461
600
  *
462
- * The builder receives `ctx` a reference to the assembled resolver so methods can
463
- * call sibling resolver methods using `ctx` instead of `this`.
601
+ * Methods in the returned object can call sibling resolver methods via `this`.
464
602
  *
465
603
  * @example Basic resolver with naming helpers
466
604
  * ```ts
467
- * export const resolver = defineResolver<PluginTs>((ctx) => ({
605
+ * export const resolver = defineResolver<PluginTs>(() => ({
468
606
  * name: 'default',
469
607
  * resolveName(node) {
470
- * return ctx.default(node.name, 'function')
608
+ * return this.default(node.name, 'function')
471
609
  * },
472
610
  * resolveTypedName(node) {
473
- * return ctx.default(node.name, 'type')
611
+ * return this.default(node.name, 'type')
474
612
  * },
475
613
  * }))
476
614
  * ```
477
615
  *
478
616
  * @example Override resolvePath for a custom output structure
479
617
  * ```ts
480
- * export const resolver = defineResolver<PluginTs>((_ctx) => ({
618
+ * export const resolver = defineResolver<PluginTs>(() => ({
481
619
  * name: 'custom',
482
620
  * resolvePath({ baseName }, { root, output }) {
483
621
  * return path.resolve(root, output.path, 'generated', baseName)
@@ -485,37 +623,32 @@ export function defaultResolveFooter(node: InputNode | undefined, { output }: Re
485
623
  * }))
486
624
  * ```
487
625
  *
488
- * @example Use ctx.default inside a helper
626
+ * @example Use this.default inside a helper
489
627
  * ```ts
490
- * export const resolver = defineResolver<PluginTs>((ctx) => ({
628
+ * export const resolver = defineResolver<PluginTs>(() => ({
491
629
  * name: 'default',
492
630
  * resolveParamName(node, param) {
493
- * return ctx.default(`${node.operationId} ${param.in} ${param.name}`, 'type')
631
+ * return this.default(`${node.operationId} ${param.in} ${param.name}`, 'type')
494
632
  * },
495
633
  * }))
496
634
  * ```
497
635
  */
498
636
  export function defineResolver<T extends PluginFactoryOptions>(build: ResolverBuilder<T>): T['resolver'] {
499
- // Create the resolver shell first. When `build(resolver)` executes below, `resolver` is
500
- // still empty, but methods returned by the builder capture it by reference. By the time
501
- // those methods are actually called, `Object.assign` will have already populated all
502
- // properties (including any overrides from the builder itself).
503
- const resolver = {} as T['resolver']
637
+ // `resolver` is kept so the default `resolveFile` wrapper can reference the fully assembled
638
+ // object via `.call(resolver, ...)` at call-time, after the result is assigned below.
639
+ let resolver: T['resolver']
504
640
 
505
- Object.assign(resolver, {
641
+ const result = {
506
642
  default: defaultResolver,
507
643
  resolveOptions: defaultResolveOptions,
508
644
  resolvePath: defaultResolvePath,
509
- // Wire the default resolveFile implementation with a wrapper that passes resolver as ctx.
510
- // Unlike other defaults which can be assigned directly, defaultResolveFile requires the
511
- // resolver as its third parameter.
512
- resolveFile: (params: ResolverFileParams, context: ResolverContext) => defaultResolveFile(params, context, resolver as Resolver),
645
+ resolveFile: (params: ResolverFileParams, context: ResolverContext) => defaultResolveFile.call(resolver as Resolver, params, context),
513
646
  resolveBanner: defaultResolveBanner,
514
647
  resolveFooter: defaultResolveFooter,
515
- // Builder overrides are applied last. Any method in the builder can call
516
- // ctx.xxx() and will see the fully merged resolver (including its own overrides).
517
- ...build(resolver),
518
- })
648
+ ...build(),
649
+ } as T['resolver']
650
+
651
+ resolver = result
519
652
 
520
653
  return resolver
521
654
  }
package/src/devtools.ts CHANGED
@@ -1,7 +1,14 @@
1
1
  import type { InputNode } from '@kubb/ast'
2
2
  import { deflateSync, inflateSync } from 'fflate'
3
3
  import { x } from 'tinyexec'
4
- import type { DevtoolsOptions } from './types.ts'
4
+
5
+ export type DevtoolsOptions = {
6
+ /**
7
+ * Open the AST inspector in Kubb Studio (`/ast`). Defaults to the main Studio page.
8
+ * @default false
9
+ */
10
+ ast?: boolean
11
+ }
5
12
 
6
13
  /**
7
14
  * Encodes an `InputNode` as a compressed, URL-safe string.
package/src/index.ts CHANGED
@@ -17,4 +17,4 @@ export { PluginDriver } from './PluginDriver.ts'
17
17
  export { fsStorage } from './storages/fsStorage.ts'
18
18
  export { memoryStorage } from './storages/memoryStorage.ts'
19
19
  export * from './types.ts'
20
- export { isInputPath } from './utils/isInputPath.ts'
20
+ export { isInputPath } from './createKubb.ts'
package/src/mocks.ts CHANGED
@@ -2,8 +2,7 @@ import { resolve } from 'node:path'
2
2
  import type { FileNode, OperationNode, SchemaNode, Visitor } from '@kubb/ast'
3
3
  import { transform } from '@kubb/ast'
4
4
  import { FileManager } from './FileManager.ts'
5
- import { PluginDriver } from './PluginDriver.ts'
6
- import { applyHookResult } from './renderNode.ts'
5
+ import { applyHookResult, PluginDriver } from './PluginDriver.ts'
7
6
  import type { Adapter, AdapterFactoryOptions, Config, Generator, GeneratorContext, NormalizedPlugin, PluginFactoryOptions } from './types.ts'
8
7
 
9
8
  /**
@@ -4,12 +4,6 @@ import { join, resolve } from 'node:path'
4
4
  import { clean, write } from '@internals/utils'
5
5
  import { createStorage } from '../createStorage.ts'
6
6
 
7
- /**
8
- * Detects the filesystem error used to indicate that a path does not exist.
9
- */
10
- function isMissingPathError(error: unknown): error is NodeJS.ErrnoException {
11
- return typeof error === 'object' && error !== null && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT'
12
- }
13
7
 
14
8
  /**
15
9
  * Built-in filesystem storage driver.
@@ -42,27 +36,15 @@ export const fsStorage = createStorage(() => ({
42
36
  try {
43
37
  await access(resolve(key))
44
38
  return true
45
- } catch (error) {
46
- if (isMissingPathError(error)) {
47
- return false
48
- }
49
-
50
- throw new Error(`Failed to access storage item "${key}"`, {
51
- cause: error as Error,
52
- })
39
+ } catch (_error) {
40
+ return false
53
41
  }
54
42
  },
55
43
  async getItem(key: string) {
56
44
  try {
57
45
  return await readFile(resolve(key), 'utf8')
58
- } catch (error) {
59
- if (isMissingPathError(error)) {
60
- return null
61
- }
62
-
63
- throw new Error(`Failed to read storage item "${key}"`, {
64
- cause: error as Error,
65
- })
46
+ } catch (_error) {
47
+ return null
66
48
  }
67
49
  },
68
50
  async setItem(key: string, value: string) {
@@ -81,14 +63,8 @@ export const fsStorage = createStorage(() => ({
81
63
  entries = (await readdir(dir, {
82
64
  withFileTypes: true,
83
65
  })) as Array<Dirent>
84
- } catch (error) {
85
- if (isMissingPathError(error)) {
86
- return
87
- }
88
-
89
- throw new Error(`Failed to list storage keys under "${resolvedBase}"`, {
90
- cause: error as Error,
91
- })
66
+ } catch (_error) {
67
+ return
92
68
  }
93
69
  for (const entry of entries) {
94
70
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name