@kubb/core 5.0.0-beta.38 → 5.0.0-beta.39

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/dist/mocks.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { t as __name } from "./chunk-C0LytTxp.js";
2
- import { Gt as AdapterFactoryOptions, J as Generator, Wt as Adapter, Z as KubbDriver, at as NormalizedPlugin, lt as PluginFactoryOptions, x as Config } from "./diagnostics-CYfKPtbU.js";
3
- import { InputMeta, OperationNode, SchemaNode, Visitor } from "@kubb/ast";
2
+ import { It as Adapter, Lt as AdapterFactoryOptions, V as Generator, W as KubbDriver, Z as NormalizedPlugin, gt as Parser, h as Config, tt as PluginFactoryOptions } from "./diagnostics-DhfW8YpT.js";
3
+ import { FileNode, InputMeta, OperationNode, SchemaNode, Visitor } from "@kubb/ast";
4
4
 
5
5
  //#region src/mocks.d.ts
6
6
  /**
@@ -75,6 +75,33 @@ declare function renderGeneratorOperation<TOptions extends PluginFactoryOptions>
75
75
  * ```
76
76
  */
77
77
  declare function renderGeneratorOperations<TOptions extends PluginFactoryOptions>(generator: Generator<TOptions>, nodes: Array<OperationNode>, opts: RenderGeneratorOptions<TOptions>): Promise<void>;
78
+ type MatchFilesOptions = {
79
+ /**
80
+ * Parsers indexed by file extension, used to render each `FileNode` to source.
81
+ * Without a matching parser the file's raw content is used.
82
+ */
83
+ parsers?: Map<FileNode['extname'], Parser>;
84
+ /**
85
+ * Formatter applied to non-JSON output before snapshotting, e.g. prettier. When
86
+ * omitted the parsed source is snapshotted as-is.
87
+ */
88
+ format?: (source?: string) => string | Promise<string>;
89
+ /**
90
+ * Subfolder under `__snapshots__`, camelCased. Useful to keep variant snapshots apart.
91
+ */
92
+ pre?: string;
93
+ };
94
+ /**
95
+ * Renders the driver's collected `FileNode`s to source and asserts each against a file snapshot.
96
+ * Pair it with the `renderGenerator*` helpers to snapshot a generator's output.
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * await renderGeneratorSchema(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })
101
+ * await matchFiles(driver.fileManager.files, { parsers, format })
102
+ * ```
103
+ */
104
+ declare function matchFiles(files: Array<FileNode> | undefined, options?: MatchFilesOptions): Promise<Map<string, string> | undefined>;
78
105
  //#endregion
79
- export { createMockedAdapter, createMockedPlugin, createMockedPluginDriver, renderGeneratorOperation, renderGeneratorOperations, renderGeneratorSchema };
106
+ export { createMockedAdapter, createMockedPlugin, createMockedPluginDriver, matchFiles, renderGeneratorOperation, renderGeneratorOperations, renderGeneratorSchema };
80
107
  //# sourceMappingURL=mocks.d.ts.map
package/dist/mocks.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import "./chunk-C0LytTxp.js";
2
- import { i as FileManager, n as _usingCtx, t as KubbDriver } from "./KubbDriver-CyNF-NIb.js";
3
- import { resolve } from "node:path";
2
+ import { a as FileManager, i as FileProcessor, n as KubbDriver, p as camelCase, r as _usingCtx, t as memoryStorage } from "./memoryStorage-CNQTs-YG.js";
3
+ import path, { resolve } from "node:path";
4
4
  import { transform } from "@kubb/ast";
5
+ import { expect } from "vitest";
5
6
  //#region src/mocks.ts
6
7
  /**
7
8
 
@@ -96,8 +97,7 @@ function createMockedPluginContext(opts) {
96
97
  hooks: opts.driver.hooks ?? {},
97
98
  warn: (msg) => console.warn(msg),
98
99
  error: (msg) => console.error(msg),
99
- info: (msg) => console.info(msg),
100
- openInStudio: async () => {}
100
+ info: (msg) => console.info(msg)
101
101
  };
102
102
  }
103
103
  /**
@@ -166,7 +166,35 @@ async function renderGeneratorOperations(generator, nodes, opts) {
166
166
  renderer: generator.renderer
167
167
  });
168
168
  }
169
+ /**
170
+ * Renders the driver's collected `FileNode`s to source and asserts each against a file snapshot.
171
+ * Pair it with the `renderGenerator*` helpers to snapshot a generator's output.
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * await renderGeneratorSchema(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })
176
+ * await matchFiles(driver.fileManager.files, { parsers, format })
177
+ * ```
178
+ */
179
+ async function matchFiles(files, options = {}) {
180
+ if (!files?.length) return;
181
+ const { parsers = /* @__PURE__ */ new Map(), format, pre } = options;
182
+ const fileProcessor = new FileProcessor({
183
+ storage: memoryStorage(),
184
+ parsers
185
+ });
186
+ const processed = /* @__PURE__ */ new Map();
187
+ for (const file of files) {
188
+ if (!file?.path || processed.has(file.path)) continue;
189
+ const parsed = fileProcessor.parse(file);
190
+ const code = file.baseName.endsWith(".json") || !format ? parsed : await format(parsed);
191
+ processed.set(file.path, code);
192
+ const snapshotPath = path.join("__snapshots__", ...pre ? [camelCase(pre)] : [], file.baseName);
193
+ await expect(code).toMatchFileSnapshot(snapshotPath);
194
+ }
195
+ return processed;
196
+ }
169
197
  //#endregion
170
- export { createMockedAdapter, createMockedPlugin, createMockedPluginDriver, renderGeneratorOperation, renderGeneratorOperations, renderGeneratorSchema };
198
+ export { createMockedAdapter, createMockedPlugin, createMockedPluginDriver, matchFiles, renderGeneratorOperation, renderGeneratorOperations, renderGeneratorSchema };
171
199
 
172
200
  //# sourceMappingURL=mocks.js.map
package/dist/mocks.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"mocks.js","names":[],"sources":["../src/mocks.ts"],"sourcesContent":["import { resolve } from 'node:path'\nimport type { FileNode, InputMeta, OperationNode, SchemaNode, Visitor } from '@kubb/ast'\nimport { transform } from '@kubb/ast'\nimport { FileManager } from './FileManager.ts'\nimport { KubbDriver } from './KubbDriver.ts'\nimport type { Adapter, AdapterFactoryOptions, Config, Generator, GeneratorContext, NormalizedPlugin, PluginFactoryOptions, RendererFactory } from './types.ts'\n\n/**\n\n * Creates a minimal `PluginDriver` mock for unit tests.\n */\nexport function createMockedPluginDriver(options: { name?: string; plugin?: NormalizedPlugin; config?: Config } = {}): KubbDriver {\n const fileManager = new FileManager()\n\n return {\n config: options?.config ?? {\n root: '.',\n output: {\n path: './path',\n },\n },\n getPlugin(_pluginName: string): NormalizedPlugin | undefined {\n return options?.plugin\n },\n getResolver: (_pluginName: string) => options?.plugin?.resolver,\n fileManager,\n async dispatch({ result, renderer }: { result: unknown; renderer?: RendererFactory | null }): Promise<void> {\n if (!result) return\n\n if (Array.isArray(result)) {\n fileManager.upsert(...(result as Array<FileNode>))\n return\n }\n\n if (!renderer) return\n\n using instance = renderer()\n if (instance.stream) {\n for (const file of instance.stream(result)) fileManager.upsert(file)\n return\n }\n\n await instance.render(result)\n fileManager.upsert(...instance.files)\n },\n } as unknown as KubbDriver\n}\n\n/**\n * Creates a minimal `Adapter` mock for unit tests.\n * `parse` returns an empty `InputNode` by default. Override via `options.parse`.\n * `getImports` returns `[]` by default.\n */\nexport function createMockedAdapter<TOptions extends AdapterFactoryOptions = AdapterFactoryOptions>(\n options: {\n name?: TOptions['name']\n resolvedOptions?: TOptions['resolvedOptions']\n parse?: Adapter<TOptions>['parse']\n getImports?: Adapter<TOptions>['getImports']\n } = {},\n): Adapter<TOptions> {\n return {\n name: (options.name ?? 'oas') as TOptions['name'],\n options: (options.resolvedOptions ?? {}) as TOptions['resolvedOptions'],\n parse: options.parse ?? (async () => ({ kind: 'Input' as const, schemas: [], operations: [] })),\n getImports: options.getImports ?? ((_node: SchemaNode, _resolve: (schemaName: string) => { name: string; path: string }) => []),\n } as Adapter<TOptions>\n}\n\n/**\n * Creates a minimal plugin mock for unit tests.\n *\n * @example\n * `const plugin = createMockedPlugin<PluginTs>({ name: '@kubb/plugin-ts', options })`\n */\nexport function createMockedPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(params: {\n name: TOptions['name']\n options: TOptions['resolvedOptions']\n resolver?: TOptions['resolver']\n transformer?: Visitor\n dependencies?: Array<string>\n}): NormalizedPlugin<TOptions> {\n return {\n name: params.name,\n options: params.options,\n resolver: params.resolver,\n transformer: params.transformer,\n dependencies: params.dependencies,\n hooks: {},\n } as unknown as NormalizedPlugin<TOptions>\n}\n\ntype RenderGeneratorOptions<TOptions extends PluginFactoryOptions> = {\n config: Config\n adapter: Adapter\n meta?: InputMeta\n driver: KubbDriver\n plugin: NormalizedPlugin<TOptions>\n options: TOptions['resolvedOptions']\n resolver: TOptions['resolver']\n}\n\nfunction createMockedPluginContext<TOptions extends PluginFactoryOptions>(opts: RenderGeneratorOptions<TOptions>): Omit<GeneratorContext<TOptions>, 'options'> {\n const root = resolve(opts.config.root, opts.config.output.path)\n\n return {\n config: opts.config,\n root,\n getMode: (output: { path: string }) => KubbDriver.getMode(resolve(root, output.path)),\n adapter: opts.adapter,\n resolver: opts.resolver,\n plugin: opts.plugin,\n driver: opts.driver,\n getResolver: (name: string) => opts.driver.getResolver(name),\n meta: opts.meta ?? { circularNames: [], enumNames: [] },\n addFile: async (...files: Array<FileNode>) => opts.driver.fileManager.add(...files),\n upsertFile: async (...files: Array<FileNode>) => opts.driver.fileManager.upsert(...files),\n hooks: opts.driver.hooks ?? ({} as never),\n warn: (msg: string) => console.warn(msg),\n error: (msg: string) => console.error(msg),\n info: (msg: string) => console.info(msg),\n openInStudio: async () => {},\n } as unknown as Omit<GeneratorContext<TOptions>, 'options'>\n}\n\n/**\n * Renders a generator's `schema` method in a test context.\n *\n * @example\n * ```ts\n * await renderGeneratorSchema(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files)\n * ```\n */\nexport async function renderGeneratorSchema<TOptions extends PluginFactoryOptions>(\n generator: Generator<TOptions>,\n node: SchemaNode,\n opts: RenderGeneratorOptions<TOptions>,\n): Promise<void> {\n if (!generator.schema) return\n const context = createMockedPluginContext(opts)\n const transformedNode = opts.plugin.transformer ? transform(node, opts.plugin.transformer) : node\n const result = await generator.schema(transformedNode, {\n ...context,\n options: opts.options,\n })\n await opts.driver.dispatch({ result, renderer: generator.renderer })\n}\n\n/**\n * Renders a generator's `operation` method in a test context.\n *\n * @example\n * ```ts\n * await renderGeneratorOperation(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files)\n * ```\n */\nexport async function renderGeneratorOperation<TOptions extends PluginFactoryOptions>(\n generator: Generator<TOptions>,\n node: OperationNode,\n opts: RenderGeneratorOptions<TOptions>,\n): Promise<void> {\n if (!generator.operation) return\n const context = createMockedPluginContext(opts)\n const transformedNode = opts.plugin.transformer ? transform(node, opts.plugin.transformer) : node\n const result = await generator.operation(transformedNode, {\n ...context,\n options: opts.options,\n })\n await opts.driver.dispatch({ result, renderer: generator.renderer })\n}\n\n/**\n * Renders a generator's `operations` method in a test context.\n *\n * @example\n * ```ts\n * await renderGeneratorOperations(classClientGenerator, nodes, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files)\n * ```\n */\nexport async function renderGeneratorOperations<TOptions extends PluginFactoryOptions>(\n generator: Generator<TOptions>,\n nodes: Array<OperationNode>,\n opts: RenderGeneratorOptions<TOptions>,\n): Promise<void> {\n if (!generator.operations) return\n const context = createMockedPluginContext(opts)\n const transformedNodes = opts.plugin.transformer ? nodes.map((n) => transform(n, opts.plugin.transformer!)) : nodes\n const result = await generator.operations(transformedNodes, {\n ...context,\n options: opts.options,\n })\n await opts.driver.dispatch({ result, renderer: generator.renderer })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mocks.js","names":[],"sources":["../src/mocks.ts"],"sourcesContent":["import path, { resolve } from 'node:path'\nimport { camelCase } from '@internals/utils'\nimport type { FileNode, InputMeta, OperationNode, SchemaNode, Visitor } from '@kubb/ast'\nimport { transform } from '@kubb/ast'\nimport { expect } from 'vitest'\nimport type { Parser } from './defineParser.ts'\nimport { FileManager } from './FileManager.ts'\nimport { FileProcessor } from './FileProcessor.ts'\nimport { KubbDriver } from './KubbDriver.ts'\nimport { memoryStorage } from './storages/memoryStorage.ts'\nimport type { Adapter, AdapterFactoryOptions, Config, Generator, GeneratorContext, NormalizedPlugin, PluginFactoryOptions, RendererFactory } from './types.ts'\n\n/**\n\n * Creates a minimal `PluginDriver` mock for unit tests.\n */\nexport function createMockedPluginDriver(options: { name?: string; plugin?: NormalizedPlugin; config?: Config } = {}): KubbDriver {\n const fileManager = new FileManager()\n\n return {\n config: options?.config ?? {\n root: '.',\n output: {\n path: './path',\n },\n },\n getPlugin(_pluginName: string): NormalizedPlugin | undefined {\n return options?.plugin\n },\n getResolver: (_pluginName: string) => options?.plugin?.resolver,\n fileManager,\n async dispatch({ result, renderer }: { result: unknown; renderer?: RendererFactory | null }): Promise<void> {\n if (!result) return\n\n if (Array.isArray(result)) {\n fileManager.upsert(...(result as Array<FileNode>))\n return\n }\n\n if (!renderer) return\n\n using instance = renderer()\n if (instance.stream) {\n for (const file of instance.stream(result)) fileManager.upsert(file)\n return\n }\n\n await instance.render(result)\n fileManager.upsert(...instance.files)\n },\n } as unknown as KubbDriver\n}\n\n/**\n * Creates a minimal `Adapter` mock for unit tests.\n * `parse` returns an empty `InputNode` by default. Override via `options.parse`.\n * `getImports` returns `[]` by default.\n */\nexport function createMockedAdapter<TOptions extends AdapterFactoryOptions = AdapterFactoryOptions>(\n options: {\n name?: TOptions['name']\n resolvedOptions?: TOptions['resolvedOptions']\n parse?: Adapter<TOptions>['parse']\n getImports?: Adapter<TOptions>['getImports']\n } = {},\n): Adapter<TOptions> {\n return {\n name: (options.name ?? 'oas') as TOptions['name'],\n options: (options.resolvedOptions ?? {}) as TOptions['resolvedOptions'],\n parse: options.parse ?? (async () => ({ kind: 'Input' as const, schemas: [], operations: [] })),\n getImports: options.getImports ?? ((_node: SchemaNode, _resolve: (schemaName: string) => { name: string; path: string }) => []),\n } as Adapter<TOptions>\n}\n\n/**\n * Creates a minimal plugin mock for unit tests.\n *\n * @example\n * `const plugin = createMockedPlugin<PluginTs>({ name: '@kubb/plugin-ts', options })`\n */\nexport function createMockedPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(params: {\n name: TOptions['name']\n options: TOptions['resolvedOptions']\n resolver?: TOptions['resolver']\n transformer?: Visitor\n dependencies?: Array<string>\n}): NormalizedPlugin<TOptions> {\n return {\n name: params.name,\n options: params.options,\n resolver: params.resolver,\n transformer: params.transformer,\n dependencies: params.dependencies,\n hooks: {},\n } as unknown as NormalizedPlugin<TOptions>\n}\n\ntype RenderGeneratorOptions<TOptions extends PluginFactoryOptions> = {\n config: Config\n adapter: Adapter\n meta?: InputMeta\n driver: KubbDriver\n plugin: NormalizedPlugin<TOptions>\n options: TOptions['resolvedOptions']\n resolver: TOptions['resolver']\n}\n\nfunction createMockedPluginContext<TOptions extends PluginFactoryOptions>(opts: RenderGeneratorOptions<TOptions>): Omit<GeneratorContext<TOptions>, 'options'> {\n const root = resolve(opts.config.root, opts.config.output.path)\n\n return {\n config: opts.config,\n root,\n getMode: (output: { path: string }) => KubbDriver.getMode(resolve(root, output.path)),\n adapter: opts.adapter,\n resolver: opts.resolver,\n plugin: opts.plugin,\n driver: opts.driver,\n getResolver: (name: string) => opts.driver.getResolver(name),\n meta: opts.meta ?? { circularNames: [], enumNames: [] },\n addFile: async (...files: Array<FileNode>) => opts.driver.fileManager.add(...files),\n upsertFile: async (...files: Array<FileNode>) => opts.driver.fileManager.upsert(...files),\n hooks: opts.driver.hooks ?? ({} as never),\n warn: (msg: string) => console.warn(msg),\n error: (msg: string) => console.error(msg),\n info: (msg: string) => console.info(msg),\n } as unknown as Omit<GeneratorContext<TOptions>, 'options'>\n}\n\n/**\n * Renders a generator's `schema` method in a test context.\n *\n * @example\n * ```ts\n * await renderGeneratorSchema(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files)\n * ```\n */\nexport async function renderGeneratorSchema<TOptions extends PluginFactoryOptions>(\n generator: Generator<TOptions>,\n node: SchemaNode,\n opts: RenderGeneratorOptions<TOptions>,\n): Promise<void> {\n if (!generator.schema) return\n const context = createMockedPluginContext(opts)\n const transformedNode = opts.plugin.transformer ? transform(node, opts.plugin.transformer) : node\n const result = await generator.schema(transformedNode, {\n ...context,\n options: opts.options,\n })\n await opts.driver.dispatch({ result, renderer: generator.renderer })\n}\n\n/**\n * Renders a generator's `operation` method in a test context.\n *\n * @example\n * ```ts\n * await renderGeneratorOperation(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files)\n * ```\n */\nexport async function renderGeneratorOperation<TOptions extends PluginFactoryOptions>(\n generator: Generator<TOptions>,\n node: OperationNode,\n opts: RenderGeneratorOptions<TOptions>,\n): Promise<void> {\n if (!generator.operation) return\n const context = createMockedPluginContext(opts)\n const transformedNode = opts.plugin.transformer ? transform(node, opts.plugin.transformer) : node\n const result = await generator.operation(transformedNode, {\n ...context,\n options: opts.options,\n })\n await opts.driver.dispatch({ result, renderer: generator.renderer })\n}\n\n/**\n * Renders a generator's `operations` method in a test context.\n *\n * @example\n * ```ts\n * await renderGeneratorOperations(classClientGenerator, nodes, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files)\n * ```\n */\nexport async function renderGeneratorOperations<TOptions extends PluginFactoryOptions>(\n generator: Generator<TOptions>,\n nodes: Array<OperationNode>,\n opts: RenderGeneratorOptions<TOptions>,\n): Promise<void> {\n if (!generator.operations) return\n const context = createMockedPluginContext(opts)\n const transformedNodes = opts.plugin.transformer ? nodes.map((n) => transform(n, opts.plugin.transformer!)) : nodes\n const result = await generator.operations(transformedNodes, {\n ...context,\n options: opts.options,\n })\n await opts.driver.dispatch({ result, renderer: generator.renderer })\n}\n\ntype MatchFilesOptions = {\n /**\n * Parsers indexed by file extension, used to render each `FileNode` to source.\n * Without a matching parser the file's raw content is used.\n */\n parsers?: Map<FileNode['extname'], Parser>\n /**\n * Formatter applied to non-JSON output before snapshotting, e.g. prettier. When\n * omitted the parsed source is snapshotted as-is.\n */\n format?: (source?: string) => string | Promise<string>\n /**\n * Subfolder under `__snapshots__`, camelCased. Useful to keep variant snapshots apart.\n */\n pre?: string\n}\n\n/**\n * Renders the driver's collected `FileNode`s to source and asserts each against a file snapshot.\n * Pair it with the `renderGenerator*` helpers to snapshot a generator's output.\n *\n * @example\n * ```ts\n * await renderGeneratorSchema(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files, { parsers, format })\n * ```\n */\nexport async function matchFiles(files: Array<FileNode> | undefined, options: MatchFilesOptions = {}): Promise<Map<string, string> | undefined> {\n if (!files?.length) return\n\n const { parsers = new Map(), format, pre } = options\n const fileProcessor = new FileProcessor({ storage: memoryStorage(), parsers })\n const processed = new Map<string, string>()\n\n for (const file of files) {\n if (!file?.path || processed.has(file.path)) {\n continue\n }\n\n const parsed = fileProcessor.parse(file)\n const code = file.baseName.endsWith('.json') || !format ? parsed : await format(parsed)\n\n processed.set(file.path, code)\n\n const snapshotPath = path.join('__snapshots__', ...(pre ? [camelCase(pre)] : []), file.baseName)\n await expect(code).toMatchFileSnapshot(snapshotPath)\n }\n\n return processed\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/core",
3
- "version": "5.0.0-beta.38",
3
+ "version": "5.0.0-beta.39",
4
4
  "description": "Core engine for Kubb's plugin-based code generation system. Provides the plugin driver, file manager, defineConfig, and build orchestration used by every Kubb plugin.",
5
5
  "keywords": [
6
6
  "code-generator",
@@ -58,14 +58,14 @@
58
58
  "dependencies": {
59
59
  "fflate": "^0.8.3",
60
60
  "tinyexec": "~1.1.2",
61
- "@kubb/ast": "5.0.0-beta.38"
61
+ "@kubb/ast": "5.0.0-beta.39"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@internals/utils": "0.0.0",
65
- "@kubb/renderer-jsx": "5.0.0-beta.38"
65
+ "@kubb/renderer-jsx": "5.0.0-beta.39"
66
66
  },
67
67
  "peerDependencies": {
68
- "@kubb/renderer-jsx": "5.0.0-beta.38"
68
+ "@kubb/renderer-jsx": "5.0.0-beta.39"
69
69
  },
70
70
  "size-limit": [
71
71
  {
package/src/KubbDriver.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { resolve } from 'node:path'
2
2
  import { arrayToAsyncIterable, type AsyncEventEmitter, forBatches, getElapsedMs, isPromise, memoize, URLPath } from '@internals/utils'
3
3
  import { collectUsedSchemaNames, createFile, createStreamInput } from '@kubb/ast'
4
- import type { FileNode, InputMeta, InputNode, InputStreamNode, OperationNode, SchemaNode } from '@kubb/ast'
5
- import { DEFAULT_STUDIO_URL, diagnosticCode, OPERATION_FILTER_TYPES, SCHEMA_PARALLEL } from './constants.ts'
6
- import { type Diagnostic, DiagnosticError, Diagnostics, type ProblemDiagnostic } from './diagnostics.ts'
4
+ import type { FileNode, InputMeta, InputStreamNode, OperationNode, SchemaNode } from '@kubb/ast'
5
+ import { OPERATION_FILTER_TYPES, SCHEMA_PARALLEL } from './constants.ts'
6
+ import { type Diagnostic, Diagnostics, type ProblemDiagnostic } from './diagnostics.ts'
7
7
  import type { RendererFactory } from './createRenderer.ts'
8
8
  import type { Storage } from './createStorage.ts'
9
9
  import type { Generator } from './defineGenerator.ts'
@@ -11,7 +11,6 @@ import type { Parser } from './defineParser.ts'
11
11
  import type { Plugin } from './definePlugin.ts'
12
12
  import { getMode } from './definePlugin.ts'
13
13
  import { defineResolver } from './defineResolver.ts'
14
- import { openInStudio as openInStudioFn } from './devtools.ts'
15
14
  import { FileManager } from './FileManager.ts'
16
15
  import { FileProcessor } from './FileProcessor.ts'
17
16
  import { type HookListener, HookRegistry } from './HookRegistry.ts'
@@ -21,7 +20,6 @@ import type {
21
20
  Adapter,
22
21
  AdapterSource,
23
22
  Config,
24
- DevtoolsOptions,
25
23
  GeneratorContext,
26
24
  KubbHooks,
27
25
  KubbPluginSetupContext,
@@ -63,19 +61,10 @@ export class KubbDriver {
63
61
  inputNode: InputStreamNode | null = null
64
62
  adapter: Adapter | null = null
65
63
  /**
66
- * Studio session state, kept together so `dispose()` can reset it atomically.
67
- *
68
- * - `source` holds the raw adapter source so `adapter.parse()` can be called lazily.
69
- * Intentionally outlives the build, cleared by `dispose()`.
70
- * - `isOpen` prevents opening the studio more than once per build.
71
- * - `inputNode` caches the parse promise so `adapter.parse()` is called at most once
72
- * per studio session, even when `openInStudio()` is called multiple times.
64
+ * Raw adapter source so `adapter.parse()` / `adapter.stream()` can run lazily.
65
+ * Intentionally outlives the build, cleared by `dispose()`.
73
66
  */
74
- #studio: { source: AdapterSource | null; isOpen: boolean; inputNode: Promise<InputNode> | null } = {
75
- source: null,
76
- isOpen: false,
77
- inputNode: null,
78
- }
67
+ #adapterSource: AdapterSource | null = null
79
68
 
80
69
  /**
81
70
  * Central file store for all generated files.
@@ -140,7 +129,7 @@ export class KubbDriver {
140
129
  }
141
130
  }
142
131
  if (this.config.adapter) {
143
- this.#studio.source = inputToAdapterSource(this.config)
132
+ this.#adapterSource = inputToAdapterSource(this.config)
144
133
  }
145
134
  }
146
135
 
@@ -170,15 +159,15 @@ export class KubbDriver {
170
159
 
171
160
  /**
172
161
  * Parses the adapter source into `this.inputNode`. Idempotent, so repeated calls from
173
- * `run` or the studio path do not re-parse. Adapters with `stream()` are used directly.
162
+ * `run` do not re-parse. Adapters with `stream()` are used directly.
174
163
  * Adapters with only `parse()` are wrapped via `createStreamInput` so the dispatch loop
175
164
  * stays stream-only.
176
165
  */
177
166
  async #parseInput(): Promise<void> {
178
- if (this.inputNode || !this.adapter || !this.#studio.source) return
167
+ if (this.inputNode || !this.adapter || !this.#adapterSource) return
179
168
 
180
169
  const adapter = this.adapter
181
- const source = this.#studio.source
170
+ const source = this.#adapterSource
182
171
 
183
172
  if (adapter.stream) {
184
173
  this.inputNode = await adapter.stream(source)
@@ -755,12 +744,12 @@ export class KubbDriver {
755
744
  // so there is no value in retaining these maps after disposal.
756
745
  this.#resolvers.clear()
757
746
  this.#defaultResolvers.clear()
758
- // Release the FileNode cache, parsed adapter graph, and studio state so
759
- // memory is reclaimed between builds. The returned `BuildOutput.files`
760
- // array still references any FileNodes the caller needs to inspect.
747
+ // Release the FileNode cache and parsed adapter graph so memory is reclaimed
748
+ // between builds. The returned `BuildOutput.files` array still references any
749
+ // FileNodes the caller needs to inspect.
761
750
  this.fileManager.dispose()
762
751
  this.inputNode = null
763
- this.#studio = { source: null, isOpen: false, inputNode: null }
752
+ this.#adapterSource = null
764
753
  }
765
754
 
766
755
  [Symbol.dispose](): void {
@@ -844,47 +833,14 @@ export class KubbDriver {
844
833
  return driver.#transforms.get(plugin.name)
845
834
  },
846
835
  warn(message: string) {
847
- report({ code: diagnosticCode.pluginWarning, severity: 'warning', message })
836
+ report({ code: Diagnostics.code.pluginWarning, severity: 'warning', message })
848
837
  },
849
838
  error(error: string | Error) {
850
839
  const cause = typeof error === 'string' ? undefined : error
851
- report({ code: diagnosticCode.pluginFailed, severity: 'error', message: typeof error === 'string' ? error : error.message, cause })
840
+ report({ code: Diagnostics.code.pluginFailed, severity: 'error', message: typeof error === 'string' ? error : error.message, cause })
852
841
  },
853
842
  info(message: string) {
854
- report({ code: diagnosticCode.pluginInfo, severity: 'info', message })
855
- },
856
- async openInStudio(options?: DevtoolsOptions) {
857
- if (!driver.config.devtools || driver.#studio.isOpen) {
858
- return
859
- }
860
-
861
- if (typeof driver.config.devtools !== 'object') {
862
- throw new DiagnosticError({
863
- code: diagnosticCode.devtoolsInvalid,
864
- severity: 'error',
865
- message: 'The `devtools` config must be an object.',
866
- help: 'Set `devtools` to an options object, or remove it to disable Kubb Studio.',
867
- location: { kind: 'config' },
868
- })
869
- }
870
-
871
- if (!driver.adapter || !driver.#studio.source) {
872
- throw new DiagnosticError({
873
- code: diagnosticCode.adapterRequired,
874
- severity: 'error',
875
- message: 'An adapter is required to open Kubb Studio, but none is configured.',
876
- help: 'Set `adapter` in kubb.config.ts (for example `adapterOas()`).',
877
- location: { kind: 'config' },
878
- })
879
- }
880
-
881
- driver.#studio.isOpen = true
882
-
883
- const studioUrl = driver.config.devtools?.studioUrl ?? DEFAULT_STUDIO_URL
884
- driver.#studio.inputNode ??= Promise.resolve(driver.adapter.parse(driver.#studio.source))
885
- const inputNode = await driver.#studio.inputNode
886
-
887
- return openInStudioFn(inputNode, studioUrl, options)
843
+ report({ code: Diagnostics.code.pluginInfo, severity: 'info', message })
888
844
  },
889
845
  }
890
846
  }
@@ -903,8 +859,8 @@ export class KubbDriver {
903
859
  requirePlugin(pluginName: string): Plugin {
904
860
  const plugin = this.plugins.get(pluginName)
905
861
  if (!plugin) {
906
- throw new DiagnosticError({
907
- code: diagnosticCode.pluginNotFound,
862
+ throw new Diagnostics.Error({
863
+ code: Diagnostics.code.pluginNotFound,
908
864
  severity: 'error',
909
865
  message: `Plugin "${pluginName}" is required but not found. Make sure it is included in your Kubb config.`,
910
866
  help: `Add "${pluginName}" to the \`plugins\` array in kubb.config.ts, or remove the dependency on it.`,
@@ -918,8 +874,8 @@ export class KubbDriver {
918
874
  function inputToAdapterSource(config: Config): AdapterSource {
919
875
  const input = config.input
920
876
  if (!input) {
921
- throw new DiagnosticError({
922
- code: diagnosticCode.inputRequired,
877
+ throw new Diagnostics.Error({
878
+ code: Diagnostics.code.inputRequired,
923
879
  severity: 'error',
924
880
  message: 'An adapter is configured without an input.',
925
881
  help: 'Provide `input.path` (a file or URL) or `input.data` (an inline spec) in your Kubb config.',
@@ -0,0 +1,278 @@
1
+ import { randomBytes } from 'node:crypto'
2
+ import os from 'node:os'
3
+ import process from 'node:process'
4
+ import { executeIfOnline, isCIEnvironment } from '@internals/utils'
5
+ import { OTLP_ENDPOINT } from './constants.ts'
6
+
7
+ // OpenTelemetry OTLP JSON types
8
+ // https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/trace/v1/trace.proto
9
+ // https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/common/v1/common.proto
10
+
11
+ type OtlpStringValue = { stringValue: string }
12
+ type OtlpBoolValue = { boolValue: boolean }
13
+ type OtlpIntValue = { intValue: number }
14
+ type OtlpDoubleValue = { doubleValue: number }
15
+ type OtlpBytesValue = { bytesValue: string }
16
+ type OtlpArrayValue = { arrayValue: { values: Array<OtlpAnyValue> } }
17
+ type OtlpKvListValue = { kvlistValue: { values: Array<OtlpKeyValue> } }
18
+
19
+ type OtlpAnyValue = OtlpStringValue | OtlpBoolValue | OtlpIntValue | OtlpDoubleValue | OtlpBytesValue | OtlpArrayValue | OtlpKvListValue
20
+
21
+ type OtlpKeyValue = {
22
+ key: string
23
+ value: OtlpAnyValue
24
+ }
25
+
26
+ type OtlpResource = {
27
+ attributes: Array<OtlpKeyValue>
28
+ droppedAttributesCount?: number
29
+ }
30
+
31
+ type OtlpInstrumentationScope = {
32
+ name: string
33
+ version?: string
34
+ attributes?: Array<OtlpKeyValue>
35
+ droppedAttributesCount?: number
36
+ }
37
+
38
+ /** https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/trace/v1/trace.proto#L103 */
39
+ type OtlpSpanKind = 0 | 1 | 2 | 3 | 4 | 5
40
+
41
+ /** 0 = STATUS_CODE_UNSET, 1 = STATUS_CODE_OK, 2 = STATUS_CODE_ERROR */
42
+ type OtlpStatusCode = 0 | 1 | 2
43
+
44
+ type OtlpStatus = {
45
+ code: OtlpStatusCode
46
+ message?: string
47
+ }
48
+
49
+ type OtlpSpan = {
50
+ traceId: string
51
+ spanId: string
52
+ traceState?: string
53
+ parentSpanId?: string
54
+ name: string
55
+ kind: OtlpSpanKind
56
+ startTimeUnixNano: string
57
+ endTimeUnixNano: string
58
+ attributes?: Array<OtlpKeyValue>
59
+ droppedAttributesCount?: number
60
+ events?: Array<OtlpSpanEvent>
61
+ droppedEventsCount?: number
62
+ links?: Array<OtlpSpanLink>
63
+ droppedLinksCount?: number
64
+ status?: OtlpStatus
65
+ }
66
+
67
+ type OtlpSpanEvent = {
68
+ timeUnixNano: string
69
+ name: string
70
+ attributes?: Array<OtlpKeyValue>
71
+ droppedAttributesCount?: number
72
+ }
73
+
74
+ type OtlpSpanLink = {
75
+ traceId: string
76
+ spanId: string
77
+ traceState?: string
78
+ attributes?: Array<OtlpKeyValue>
79
+ droppedAttributesCount?: number
80
+ }
81
+
82
+ type OtlpScopeSpans = {
83
+ scope: OtlpInstrumentationScope
84
+ spans: Array<OtlpSpan>
85
+ schemaUrl?: string
86
+ }
87
+
88
+ type OtlpResourceSpans = {
89
+ resource: OtlpResource
90
+ scopeSpans: Array<OtlpScopeSpans>
91
+ schemaUrl?: string
92
+ }
93
+
94
+ /** Root payload sent to POST /v1/traces */
95
+ type OtlpExportTraceServiceRequest = {
96
+ resourceSpans: Array<OtlpResourceSpans>
97
+ }
98
+
99
+ /**
100
+ * Anonymous plugin name and options snapshot sent with each telemetry event.
101
+ */
102
+ export type TelemetryPlugin = {
103
+ /**
104
+ * Plugin name as registered in the Kubb config, e.g. `'@kubb/plugin-ts'`.
105
+ */
106
+ name: string
107
+ /**
108
+ * anonymized plugin options snapshot, values are included but cannot be traced back to the user.
109
+ */
110
+ options: Record<string, unknown>
111
+ }
112
+
113
+ export type TelemetryEvent = {
114
+ command: string
115
+ kubbVersion: string
116
+ nodeVersion: string
117
+ platform: string
118
+ ci: boolean
119
+ plugins: Array<TelemetryPlugin>
120
+ duration: number
121
+ filesCreated: number
122
+ status: 'success' | 'failed'
123
+ }
124
+
125
+ /**
126
+ * Anonymous OTLP usage telemetry for the Kubb run. All methods are static, so call them as
127
+ * `Telemetry.build(...)`, `Telemetry.send(...)`, and `Telemetry.isDisabled()`. No file paths,
128
+ * OpenAPI specs, or secrets are ever included, and sending fails silently to never break a run.
129
+ */
130
+ export class Telemetry {
131
+ /**
132
+ * Returns `true` when telemetry is disabled via `DO_NOT_TRACK` or `KUBB_DISABLE_TELEMETRY`.
133
+ */
134
+ static isDisabled(): boolean {
135
+ return (
136
+ process.env['DO_NOT_TRACK'] === '1' ||
137
+ process.env['DO_NOT_TRACK'] === 'true' ||
138
+ process.env['KUBB_DISABLE_TELEMETRY'] === '1' ||
139
+ process.env['KUBB_DISABLE_TELEMETRY'] === 'true'
140
+ )
141
+ }
142
+
143
+ /**
144
+ * Build an anonymous telemetry payload from a completed generation run.
145
+ */
146
+ static build(options: {
147
+ command: 'generate' | 'mcp' | 'validate' | 'agent'
148
+ kubbVersion: string
149
+ plugins?: Array<TelemetryPlugin>
150
+ hrStart: [number, number]
151
+ filesCreated?: number
152
+ status: 'success' | 'failed'
153
+ }): TelemetryEvent {
154
+ const [seconds, nanoseconds] = process.hrtime(options.hrStart)
155
+ const duration = Math.round(seconds * 1000 + nanoseconds / 1e6)
156
+
157
+ return {
158
+ command: options.command,
159
+ kubbVersion: options.kubbVersion,
160
+ nodeVersion: process.versions.node.split('.')[0] as string,
161
+ platform: os.platform(),
162
+ ci: isCIEnvironment(),
163
+ plugins: options.plugins ?? [],
164
+ duration,
165
+ filesCreated: options.filesCreated ?? 0,
166
+ status: options.status,
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Convert a {@link TelemetryEvent} into an OTLP-compatible JSON trace payload.
172
+ * See https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/
173
+ */
174
+ static buildOtlpPayload(event: TelemetryEvent): OtlpExportTraceServiceRequest {
175
+ const traceId = randomBytes(16).toString('hex')
176
+ const spanId = randomBytes(8).toString('hex')
177
+ const endTimeNs = BigInt(Date.now()) * 1_000_000n
178
+ const startTimeNs = endTimeNs - BigInt(event.duration) * 1_000_000n
179
+
180
+ const attributes: Array<OtlpKeyValue> = [
181
+ { key: 'kubb.command', value: { stringValue: event.command } },
182
+ { key: 'kubb.version', value: { stringValue: event.kubbVersion } },
183
+ { key: 'kubb.node_version', value: { stringValue: event.nodeVersion } },
184
+ { key: 'kubb.platform', value: { stringValue: event.platform } },
185
+ { key: 'kubb.ci', value: { boolValue: event.ci } },
186
+ { key: 'kubb.files_created', value: { intValue: event.filesCreated } },
187
+ { key: 'kubb.status', value: { stringValue: event.status } },
188
+ {
189
+ key: 'kubb.plugins',
190
+ value: {
191
+ arrayValue: {
192
+ values: event.plugins.map(
193
+ (p): OtlpKvListValue => ({
194
+ kvlistValue: {
195
+ values: [
196
+ { key: 'name', value: { stringValue: p.name } },
197
+ {
198
+ key: 'options',
199
+ value: {
200
+ stringValue: JSON.stringify({
201
+ ...p.options,
202
+ usedEnumNames: undefined,
203
+ }),
204
+ },
205
+ },
206
+ ],
207
+ },
208
+ }),
209
+ ),
210
+ },
211
+ },
212
+ },
213
+ ]
214
+
215
+ return {
216
+ resourceSpans: [
217
+ {
218
+ resource: {
219
+ attributes: [
220
+ { key: 'service.name', value: { stringValue: 'kubb-core' } },
221
+ {
222
+ key: 'service.version',
223
+ value: { stringValue: event.kubbVersion },
224
+ },
225
+ { key: 'telemetry.sdk.language', value: { stringValue: 'nodejs' } },
226
+ ],
227
+ },
228
+ scopeSpans: [
229
+ {
230
+ scope: { name: 'kubb-core', version: event.kubbVersion },
231
+ spans: [
232
+ {
233
+ traceId,
234
+ spanId,
235
+ name: event.command,
236
+ kind: 1 satisfies OtlpSpanKind,
237
+ startTimeUnixNano: String(startTimeNs),
238
+ endTimeUnixNano: String(endTimeNs),
239
+ attributes,
240
+ status: {
241
+ code: (event.status === 'success' ? 1 : 2) satisfies OtlpStatusCode,
242
+ },
243
+ },
244
+ ],
245
+ },
246
+ ],
247
+ },
248
+ ],
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Send an anonymous telemetry event to the Kubb OTLP endpoint. Respects `DO_NOT_TRACK` and
254
+ * `KUBB_DISABLE_TELEMETRY`, and fails silently so telemetry never interrupts a run.
255
+ */
256
+ static async send(event: TelemetryEvent): Promise<void> {
257
+ if (Telemetry.isDisabled()) {
258
+ return
259
+ }
260
+
261
+ await executeIfOnline(async () => {
262
+ try {
263
+ await fetch(`${OTLP_ENDPOINT}/v1/traces`, {
264
+ method: 'POST',
265
+ headers: {
266
+ 'Content-Type': 'application/json',
267
+ 'Kubb-Telemetry-Version': '1',
268
+ 'Kubb-Telemetry-Source': 'kubb-core',
269
+ },
270
+ body: JSON.stringify(Telemetry.buildOtlpPayload(event)),
271
+ signal: AbortSignal.timeout(5_000),
272
+ })
273
+ } catch (_e) {
274
+ // Fail silently, telemetry must never break the run
275
+ }
276
+ })
277
+ }
278
+ }