@kubb/core 5.0.0-alpha.43 → 5.0.0-alpha.45

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/src/constants.ts CHANGED
@@ -15,10 +15,18 @@ export const DEFAULT_CONCURRENCY = 15
15
15
  */
16
16
  export const PARALLEL_CONCURRENCY_LIMIT = 100
17
17
 
18
+ /**
19
+ * Basename (without extension) of generated barrel files.
20
+ *
21
+ * Used to detect whether a path already points at a barrel so the generator
22
+ * avoids re-creating one on top of it.
23
+ */
24
+ export const BARREL_BASENAME = 'index' as const
25
+
18
26
  /**
19
27
  * File name used for generated barrel (index) files.
20
28
  */
21
- export const BARREL_FILENAME = 'index.ts' as const
29
+ export const BARREL_FILENAME = `${BARREL_BASENAME}.ts` as const
22
30
 
23
31
  /**
24
32
  * Default banner style written at the top of every generated file.
package/src/createKubb.ts CHANGED
@@ -2,16 +2,17 @@ import { dirname, resolve } from 'node:path'
2
2
  import { AsyncEventEmitter, BuildError, exists, formatMs, getElapsedMs, getRelativePath, URLPath } from '@internals/utils'
3
3
  import type { ExportNode, FileNode, OperationNode } from '@kubb/ast'
4
4
  import { createExport, createFile, transform, walk } from '@kubb/ast'
5
- import { BARREL_FILENAME, DEFAULT_BANNER, DEFAULT_CONCURRENCY, DEFAULT_EXTENSION, DEFAULT_STUDIO_URL } from './constants.ts'
5
+ import { BARREL_FILENAME, DEFAULT_BANNER, DEFAULT_EXTENSION, DEFAULT_STUDIO_URL } from './constants.ts'
6
6
  import type { RendererFactory } from './createRenderer.ts'
7
7
  import type { Generator } from './defineGenerator.ts'
8
8
  import type { Parser } from './defineParser.ts'
9
+ import type { Plugin } from './definePlugin.ts'
9
10
  import { FileProcessor } from './FileProcessor.ts'
10
11
  import type { Kubb } from './Kubb.ts'
11
12
  import { PluginDriver } from './PluginDriver.ts'
12
13
  import { applyHookResult } from './renderNode.ts'
13
14
  import { fsStorage } from './storages/fsStorage.ts'
14
- import type { AdapterSource, Config, GeneratorContext, KubbHooks, Plugin, PluginContext, Storage, UserConfig } from './types.ts'
15
+ import type { AdapterSource, Config, GeneratorContext, KubbHooks, NormalizedPlugin, Storage, UserConfig } from './types.ts'
15
16
  import { getDiagnosticInfo } from './utils/diagnostics.ts'
16
17
  import type { FileMetaBase } from './utils/getBarrelFiles.ts'
17
18
  import { getBarrelFiles } from './utils/getBarrelFiles.ts'
@@ -138,7 +139,6 @@ async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promis
138
139
 
139
140
  const driver = new PluginDriver(config, {
140
141
  hooks,
141
- concurrency: DEFAULT_CONCURRENCY,
142
142
  })
143
143
 
144
144
  const adapter = config.adapter
@@ -177,7 +177,7 @@ async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promis
177
177
  * Walks the AST and dispatches nodes to a plugin's direct AST hooks
178
178
  * (`schema`, `operation`, `operations`).
179
179
  */
180
- async function runPluginAstHooks(plugin: Plugin, context: PluginContext): Promise<void> {
180
+ async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorContext): Promise<void> {
181
181
  const { adapter, inputNode, resolver, driver } = context
182
182
  const { exclude, include, override } = plugin.options
183
183
 
@@ -192,9 +192,8 @@ async function runPluginAstHooks(plugin: Plugin, context: PluginContext): Promis
192
192
  const generators = plugin.generators ?? []
193
193
  const collectedOperations: Array<OperationNode> = []
194
194
 
195
- const baseGeneratorContext = context as GeneratorContext
196
195
  const generatorContext = {
197
- ...baseGeneratorContext,
196
+ ...context,
198
197
  resolver: driver.getResolver(plugin.name),
199
198
  }
200
199
 
@@ -282,8 +281,6 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
282
281
  logs: ['Starting plugin...', ` • Plugin Name: ${plugin.name}`],
283
282
  })
284
283
 
285
- await plugin.buildStart.call(context)
286
-
287
284
  if (plugin.generators?.length || driver.hasRegisteredGenerators(plugin.name)) {
288
285
  await runPluginAstHooks(plugin, context)
289
286
  }
@@ -421,13 +418,6 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
421
418
  },
422
419
  })
423
420
 
424
- for (const plugin of driver.plugins.values()) {
425
- if (plugin.buildEnd) {
426
- const context = driver.getContext(plugin)
427
- await plugin.buildEnd.call(context)
428
- }
429
- }
430
-
431
421
  await hooks.emit('kubb:build:end', {
432
422
  files,
433
423
  config,
@@ -487,7 +477,7 @@ type BuildBarrelExportsParams = {
487
477
  }
488
478
 
489
479
  function buildBarrelExports({ barrelFiles, rootDir, existingExports, config, driver }: BuildBarrelExportsParams): ExportNode[] {
490
- const pluginNameMap = new Map<string, Plugin>()
480
+ const pluginNameMap = new Map<string, NormalizedPlugin>()
491
481
  for (const plugin of driver.plugins.values()) {
492
482
  pluginNameMap.set(plugin.name, plugin)
493
483
  }
@@ -2,39 +2,13 @@ import type { KubbHooks } from './Kubb.ts'
2
2
  import type { KubbPluginSetupContext, PluginFactoryOptions } from './types.ts'
3
3
 
4
4
  /**
5
- * Base hook handlers for all events except `kubb:plugin:setup`.
6
- * These handlers have identical signatures regardless of the plugin's
7
- * `PluginFactoryOptions` generic — they are split out so that the
8
- * interface below only needs to override the one event that depends on
9
- * the plugin type.
10
- */
11
- type PluginHooksBase = {
12
- [K in Exclude<keyof KubbHooks, 'kubb:plugin:setup'>]?: (...args: KubbHooks[K]) => void | Promise<void>
13
- }
14
-
15
- /**
16
- * Plugin hook handlers.
17
- *
18
- * `kubb:plugin:setup` is typed with the plugin's own `PluginFactoryOptions` so
19
- * `ctx.setResolver`, `ctx.setOptions`, `ctx.options` etc. use the correct types.
20
- *
21
- * Uses interface + method shorthand for `kubb:plugin:setup`
22
- * checking, allowing `PluginHooks<PluginTs>` to be assignable to `PluginHooks`.
23
- *
24
- * @template TFactory - The plugin's `PluginFactoryOptions` type.
25
- */
26
- export interface PluginHooks<TFactory extends PluginFactoryOptions = PluginFactoryOptions> extends PluginHooksBase {
27
- 'kubb:plugin:setup'?(ctx: KubbPluginSetupContext<TFactory>): void | Promise<void>
28
- }
29
-
30
- /**
31
- * A hook-style plugin object produced by `definePlugin`.
5
+ * A plugin object produced by `definePlugin`.
32
6
  * Instead of flat lifecycle methods, it groups all handlers under a `hooks:` property
33
7
  * (matching Astro's integration naming convention).
34
8
  *
35
9
  * @template TFactory - The plugin's `PluginFactoryOptions` type.
36
10
  */
37
- export type HookStylePlugin<TFactory extends PluginFactoryOptions = PluginFactoryOptions> = {
11
+ export type Plugin<TFactory extends PluginFactoryOptions = PluginFactoryOptions> = {
38
12
  /**
39
13
  * Unique name for the plugin, following the same naming convention as `createPlugin`.
40
14
  */
@@ -52,7 +26,11 @@ export type HookStylePlugin<TFactory extends PluginFactoryOptions = PluginFactor
52
26
  * Lifecycle event handlers for this plugin.
53
27
  * Any event from the global `KubbHooks` map can be subscribed to here.
54
28
  */
55
- hooks: PluginHooks<TFactory>
29
+ hooks: {
30
+ [K in Exclude<keyof KubbHooks, 'kubb:plugin:setup'>]?: (...args: KubbHooks[K]) => void | Promise<void>
31
+ } & {
32
+ 'kubb:plugin:setup'?(ctx: KubbPluginSetupContext<TFactory>): void | Promise<void>
33
+ }
56
34
  }
57
35
 
58
36
  /**
@@ -61,14 +39,14 @@ export type HookStylePlugin<TFactory extends PluginFactoryOptions = PluginFactor
61
39
  * Used by `PluginDriver` to distinguish hook-style plugins from legacy `createPlugin` plugins
62
40
  * so it can normalize them and register their handlers on the `AsyncEventEmitter`.
63
41
  */
64
- export function isHookStylePlugin(plugin: unknown): plugin is HookStylePlugin {
42
+ export function isPlugin(plugin: unknown): plugin is Plugin {
65
43
  return typeof plugin === 'object' && plugin !== null && 'hooks' in plugin
66
44
  }
67
45
 
68
46
  /**
69
- * Creates a plugin factory using the new hook-style (`hooks:`) API.
47
+ * Creates a plugin factory using the hook-style (`hooks:`) API.
70
48
  *
71
- * The returned factory is called with optional options and produces a `HookStylePlugin`
49
+ * The returned factory is called with optional options and produces a `Plugin`
72
50
  * that coexists with plugins created via the legacy `createPlugin` API in the same
73
51
  * `kubb.config.ts`.
74
52
  *
@@ -89,7 +67,7 @@ export function isHookStylePlugin(plugin: unknown): plugin is HookStylePlugin {
89
67
  * ```
90
68
  */
91
69
  export function definePlugin<TFactory extends PluginFactoryOptions = PluginFactoryOptions>(
92
- factory: (options: TFactory['options']) => HookStylePlugin<TFactory>,
93
- ): (options?: TFactory['options']) => HookStylePlugin<TFactory> {
70
+ factory: (options: TFactory['options']) => Plugin<TFactory>,
71
+ ): (options?: TFactory['options']) => Plugin<TFactory> {
94
72
  return (options) => factory(options ?? ({} as TFactory['options']))
95
73
  }
@@ -7,7 +7,6 @@ import type {
7
7
  Config,
8
8
  PluginFactoryOptions,
9
9
  ResolveBannerContext,
10
- ResolveNameParams,
11
10
  ResolveOptionsContext,
12
11
  Resolver,
13
12
  ResolverContext,
@@ -30,21 +29,37 @@ type ResolverBuilder<T extends PluginFactoryOptions> = () => Omit<
30
29
  pluginName: T['name']
31
30
  } & ThisType<T['resolver']>
32
31
 
32
+ // String patterns are compiled lazily and cached — the same filter is reused for every node.
33
+ const stringPatternCache = new Map<string, RegExp>()
34
+
35
+ function testPattern(value: string, pattern: string | RegExp): boolean {
36
+ if (typeof pattern === 'string') {
37
+ let regex = stringPatternCache.get(pattern)
38
+ if (!regex) {
39
+ regex = new RegExp(pattern)
40
+ stringPatternCache.set(pattern, regex)
41
+ }
42
+ return regex.test(value)
43
+ }
44
+ // Use .match() for user-supplied RegExp to preserve semantics regardless of `g`/`y` flags.
45
+ return value.match(pattern) !== null
46
+ }
47
+
33
48
  /**
34
49
  * Checks if an operation matches a pattern for a given filter type (`tag`, `operationId`, `path`, `method`).
35
50
  */
36
51
  function matchesOperationPattern(node: OperationNode, type: string, pattern: string | RegExp): boolean {
37
52
  switch (type) {
38
53
  case 'tag':
39
- return node.tags.some((tag) => !!tag.match(pattern))
54
+ return node.tags.some((tag) => testPattern(tag, pattern))
40
55
  case 'operationId':
41
- return !!node.operationId.match(pattern)
56
+ return testPattern(node.operationId, pattern)
42
57
  case 'path':
43
- return !!node.path.match(pattern)
58
+ return testPattern(node.path, pattern)
44
59
  case 'method':
45
- return !!(node.method.toLowerCase() as string).match(pattern)
60
+ return testPattern(node.method.toLowerCase(), pattern)
46
61
  case 'contentType':
47
- return !!node.requestBody?.contentType?.match(pattern)
62
+ return node.requestBody?.contentType ? testPattern(node.requestBody.contentType, pattern) : false
48
63
  default:
49
64
  return false
50
65
  }
@@ -58,7 +73,7 @@ function matchesOperationPattern(node: OperationNode, type: string, pattern: str
58
73
  function matchesSchemaPattern(node: SchemaNode, type: string, pattern: string | RegExp): boolean | null {
59
74
  switch (type) {
60
75
  case 'schemaName':
61
- return node.name ? !!node.name.match(pattern) : false
76
+ return node.name ? testPattern(node.name, pattern) : false
62
77
  default:
63
78
  return null
64
79
  }
@@ -71,7 +86,7 @@ function matchesSchemaPattern(node: SchemaNode, type: string, pattern: string |
71
86
  * - `PascalCase` for `type`.
72
87
  * - `camelCase` for everything else.
73
88
  */
74
- function defaultResolver(name: ResolveNameParams['name'], type: ResolveNameParams['type']): string {
89
+ function defaultResolver(name: string, type?: 'file' | 'function' | 'type' | 'const'): string {
75
90
  let resolvedName = camelCase(name)
76
91
 
77
92
  if (type === 'file' || type === 'function') {
package/src/mocks.ts CHANGED
@@ -4,95 +4,20 @@ import { transform } from '@kubb/ast'
4
4
  import { FileManager } from './FileManager.ts'
5
5
  import { PluginDriver } from './PluginDriver.ts'
6
6
  import { applyHookResult } from './renderNode.ts'
7
- import type {
8
- Adapter,
9
- AdapterFactoryOptions,
10
- Config,
11
- Generator,
12
- GeneratorContext,
13
- Plugin,
14
- PluginFactoryOptions,
15
- ResolveNameParams,
16
- ResolvePathParams,
17
- } from './types.ts'
18
-
19
- function toCamelOrPascal(text: string, pascal: boolean): string {
20
- const normalized = text
21
- .trim()
22
- .replace(/([a-z\d])([A-Z])/g, '$1 $2')
23
- .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
24
- .replace(/(\d)([a-z])/g, '$1 $2')
25
-
26
- const words = normalized.split(/[\s\-_./\\:]+/).filter(Boolean)
27
-
28
- return words
29
- .map((word, i) => {
30
- const allUpper = word.length > 1 && word === word.toUpperCase()
31
- if (allUpper) return word
32
- if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1)
33
- return word.charAt(0).toUpperCase() + word.slice(1)
34
- })
35
- .join('')
36
- .replace(/[^a-zA-Z0-9]/g, '')
37
- }
38
-
39
- function camelCase(text: string): string {
40
- return toCamelOrPascal(text, false)
41
- }
42
-
43
- function pascalCase(text: string): string {
44
- return toCamelOrPascal(text, true)
45
- }
7
+ import type { Adapter, AdapterFactoryOptions, Config, Generator, GeneratorContext, NormalizedPlugin, PluginFactoryOptions } from './types.ts'
46
8
 
47
9
  /**
48
10
  * Creates a minimal `PluginDriver` mock suitable for unit tests.
49
11
  */
50
- export function createMockedPluginDriver(options: { name?: string; plugin?: Plugin; config?: Config } = {}): PluginDriver {
12
+ export function createMockedPluginDriver(options: { name?: string; plugin?: NormalizedPlugin; config?: Config } = {}): PluginDriver {
51
13
  return {
52
- resolveName: (result: ResolveNameParams) => {
53
- if (result.type === 'file') {
54
- return camelCase(options?.name || result.name)
55
- }
56
-
57
- if (result.type === 'type') {
58
- return pascalCase(result.name)
59
- }
60
-
61
- if (result.type === 'function') {
62
- return camelCase(result.name)
63
- }
64
-
65
- return camelCase(result.name)
66
- },
67
14
  config: options?.config ?? {
68
15
  root: '.',
69
16
  output: {
70
17
  path: './path',
71
18
  },
72
19
  },
73
- resolvePath: ({ baseName }: ResolvePathParams) => baseName,
74
- getFile: ({
75
- name,
76
- extname,
77
- pluginName,
78
- options: fileOptions,
79
- }: {
80
- name: string
81
- extname: `.${string}`
82
- pluginName: string
83
- options?: { group?: { tag?: string; path?: string } }
84
- }) => {
85
- const baseName = `${name}${extname}`
86
- const groupDir = fileOptions?.group?.tag ?? fileOptions?.group?.path?.split('/').filter(Boolean)[0]
87
- const filePath = groupDir ? `${groupDir}/${baseName}` : baseName
88
-
89
- return {
90
- path: filePath,
91
- baseName,
92
- meta: { pluginName },
93
- }
94
- },
95
- getPlugin(_pluginName: Plugin['name']): Plugin | undefined {
20
+ getPlugin(_pluginName: string): NormalizedPlugin | undefined {
96
21
  return options?.plugin
97
22
  },
98
23
  fileManager: new FileManager(),
@@ -124,7 +49,7 @@ export function createMockedAdapter<TOptions extends AdapterFactoryOptions = Ada
124
49
  }
125
50
 
126
51
  /**
127
- * Creates a minimal `Plugin` mock suitable for unit tests.
52
+ * Creates a minimal plugin mock suitable for unit tests.
128
53
  *
129
54
  * @example
130
55
  * const plugin = createMockedPlugin<PluginTs>({ name: '@kubb/plugin-ts', options })
@@ -135,23 +60,22 @@ export function createMockedPlugin<TOptions extends PluginFactoryOptions = Plugi
135
60
  resolver?: TOptions['resolver']
136
61
  transformer?: Visitor
137
62
  dependencies?: Array<string>
138
- }): Plugin<TOptions> {
63
+ }): NormalizedPlugin<TOptions> {
139
64
  return {
140
65
  name: params.name,
141
66
  options: params.options,
142
67
  resolver: params.resolver,
143
68
  transformer: params.transformer,
144
69
  dependencies: params.dependencies,
145
- install: () => {},
146
- inject: () => undefined as TOptions['context'],
147
- } as unknown as Plugin<TOptions>
70
+ hooks: {},
71
+ } as unknown as NormalizedPlugin<TOptions>
148
72
  }
149
73
 
150
74
  type RenderGeneratorOptions<TOptions extends PluginFactoryOptions> = {
151
75
  config: Config
152
76
  adapter: Adapter
153
77
  driver: PluginDriver
154
- plugin: Plugin<TOptions>
78
+ plugin: NormalizedPlugin<TOptions>
155
79
  options: TOptions['resolvedOptions']
156
80
  resolver: TOptions['resolver']
157
81
  }
@@ -168,7 +92,9 @@ function createMockedPluginContext<TOptions extends PluginFactoryOptions>(opts:
168
92
  plugin: opts.plugin,
169
93
  driver: opts.driver,
170
94
  inputNode: { kind: 'Input', schemas: [], operations: [] },
95
+ addFile: async (...files: Array<FileNode>) => opts.driver.fileManager.add(...files),
171
96
  upsertFile: async (...files: Array<FileNode>) => opts.driver.fileManager.upsert(...files),
97
+ hooks: opts.driver.hooks ?? ({} as never),
172
98
  warn: (msg: string) => console.warn(msg),
173
99
  error: (msg: string) => console.error(msg),
174
100
  info: (msg: string) => console.info(msg),