@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/{diagnostics-CYfKPtbU.d.ts → diagnostics-DhfW8YpT.d.ts} +289 -347
- package/dist/index.cjs +553 -174
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +164 -119
- package/dist/index.js +532 -149
- package/dist/index.js.map +1 -1
- package/dist/{KubbDriver-CyNF-NIb.js → memoryStorage-CNQTs-YG.js} +240 -157
- package/dist/memoryStorage-CNQTs-YG.js.map +1 -0
- package/dist/{KubbDriver-BYBUfOZ8.cjs → memoryStorage-DHi1d0To.cjs} +255 -196
- package/dist/memoryStorage-DHi1d0To.cjs.map +1 -0
- package/dist/mocks.cjs +36 -6
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.ts +30 -3
- package/dist/mocks.js +33 -5
- package/dist/mocks.js.map +1 -1
- package/package.json +4 -4
- package/src/KubbDriver.ts +21 -65
- package/src/Telemetry.ts +278 -0
- package/src/constants.ts +14 -21
- package/src/createKubb.ts +22 -50
- package/src/createReporter.ts +43 -13
- package/src/defineGenerator.ts +2 -6
- package/src/defineLogger.ts +13 -1
- package/src/defineResolver.ts +3 -4
- package/src/diagnostics.ts +130 -61
- package/src/index.ts +8 -16
- package/src/mocks.ts +57 -2
- 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/types.ts +0 -1
- package/dist/KubbDriver-BYBUfOZ8.cjs.map +0 -1
- package/dist/KubbDriver-CyNF-NIb.js.map +0 -1
- package/src/devtools.ts +0 -53
package/dist/mocks.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as __name } from "./chunk-C0LytTxp.js";
|
|
2
|
-
import {
|
|
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 {
|
|
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":";;;;;;;;;;AAgBA,SAAgB,yBAAyB,UAAyE,CAAC,GAAe;CAChI,MAAM,cAAc,IAAI,YAAY;CAEpC,OAAO;EACL,QAAQ,SAAS,UAAU;GACzB,MAAM;GACN,QAAQ,EACN,MAAM,SACR;EACF;EACA,UAAU,aAAmD;GAC3D,OAAO,SAAS;EAClB;EACA,cAAc,gBAAwB,SAAS,QAAQ;EACvD;EACA,MAAM,SAAS,EAAE,QAAQ,YAAmF;;;IAC1G,IAAI,CAAC,QAAQ;IAEb,IAAI,MAAM,QAAQ,MAAM,GAAG;KACzB,YAAY,OAAO,GAAI,MAA0B;KACjD;IACF;IAEA,IAAI,CAAC,UAAU;IAEf,MAAM,WAAA,YAAA,EAAW,SAAS,CAAA;IAC1B,IAAI,SAAS,QAAQ;KACnB,KAAK,MAAM,QAAQ,SAAS,OAAO,MAAM,GAAG,YAAY,OAAO,IAAI;KACnE;IACF;IAEA,MAAM,SAAS,OAAO,MAAM;IAC5B,YAAY,OAAO,GAAG,SAAS,KAAK;;;;;;EACtC;CACF;AACF;;;;;;AAOA,SAAgB,oBACd,UAKI,CAAC,GACc;CACnB,OAAO;EACL,MAAO,QAAQ,QAAQ;EACvB,SAAU,QAAQ,mBAAmB,CAAC;EACtC,OAAO,QAAQ,UAAU,aAAa;GAAE,MAAM;GAAkB,SAAS,CAAC;GAAG,YAAY,CAAC;EAAE;EAC5F,YAAY,QAAQ,gBAAgB,OAAmB,aAAqE,CAAC;CAC/H;AACF;;;;;;;AAQA,SAAgB,mBAAiF,QAMlE;CAC7B,OAAO;EACL,MAAM,OAAO;EACb,SAAS,OAAO;EAChB,UAAU,OAAO;EACjB,aAAa,OAAO;EACpB,cAAc,OAAO;EACrB,OAAO,CAAC;CACV;AACF;AAYA,SAAS,0BAAiE,MAAqF;CAC7J,MAAM,OAAO,QAAQ,KAAK,OAAO,MAAM,KAAK,OAAO,OAAO,IAAI;CAE9D,OAAO;EACL,QAAQ,KAAK;EACb;EACA,UAAU,WAA6B,WAAW,QAAQ,QAAQ,MAAM,OAAO,IAAI,CAAC;EACpF,SAAS,KAAK;EACd,UAAU,KAAK;EACf,QAAQ,KAAK;EACb,QAAQ,KAAK;EACb,cAAc,SAAiB,KAAK,OAAO,YAAY,IAAI;EAC3D,MAAM,KAAK,QAAQ;GAAE,eAAe,CAAC;GAAG,WAAW,CAAC;EAAE;EACtD,SAAS,OAAO,GAAG,UAA2B,KAAK,OAAO,YAAY,IAAI,GAAG,KAAK;EAClF,YAAY,OAAO,GAAG,UAA2B,KAAK,OAAO,YAAY,OAAO,GAAG,KAAK;EACxF,OAAO,KAAK,OAAO,SAAU,CAAC;EAC9B,OAAO,QAAgB,QAAQ,KAAK,GAAG;EACvC,QAAQ,QAAgB,QAAQ,MAAM,GAAG;EACzC,OAAO,QAAgB,QAAQ,KAAK,GAAG;CACzC;AACF;;;;;;;;;;AAWA,eAAsB,sBACpB,WACA,MACA,MACe;CACf,IAAI,CAAC,UAAU,QAAQ;CACvB,MAAM,UAAU,0BAA0B,IAAI;CAC9C,MAAM,kBAAkB,KAAK,OAAO,cAAc,UAAU,MAAM,KAAK,OAAO,WAAW,IAAI;CAC7F,MAAM,SAAS,MAAM,UAAU,OAAO,iBAAiB;EACrD,GAAG;EACH,SAAS,KAAK;CAChB,CAAC;CACD,MAAM,KAAK,OAAO,SAAS;EAAE;EAAQ,UAAU,UAAU;CAAS,CAAC;AACrE;;;;;;;;;;AAWA,eAAsB,yBACpB,WACA,MACA,MACe;CACf,IAAI,CAAC,UAAU,WAAW;CAC1B,MAAM,UAAU,0BAA0B,IAAI;CAC9C,MAAM,kBAAkB,KAAK,OAAO,cAAc,UAAU,MAAM,KAAK,OAAO,WAAW,IAAI;CAC7F,MAAM,SAAS,MAAM,UAAU,UAAU,iBAAiB;EACxD,GAAG;EACH,SAAS,KAAK;CAChB,CAAC;CACD,MAAM,KAAK,OAAO,SAAS;EAAE;EAAQ,UAAU,UAAU;CAAS,CAAC;AACrE;;;;;;;;;;AAWA,eAAsB,0BACpB,WACA,OACA,MACe;CACf,IAAI,CAAC,UAAU,YAAY;CAC3B,MAAM,UAAU,0BAA0B,IAAI;CAC9C,MAAM,mBAAmB,KAAK,OAAO,cAAc,MAAM,KAAK,MAAM,UAAU,GAAG,KAAK,OAAO,WAAY,CAAC,IAAI;CAC9G,MAAM,SAAS,MAAM,UAAU,WAAW,kBAAkB;EAC1D,GAAG;EACH,SAAS,KAAK;CAChB,CAAC;CACD,MAAM,KAAK,OAAO,SAAS;EAAE;EAAQ,UAAU,UAAU;CAAS,CAAC;AACrE;;;;;;;;;;;AA6BA,eAAsB,WAAW,OAAoC,UAA6B,CAAC,GAA6C;CAC9I,IAAI,CAAC,OAAO,QAAQ;CAEpB,MAAM,EAAE,0BAAU,IAAI,IAAI,GAAG,QAAQ,QAAQ;CAC7C,MAAM,gBAAgB,IAAI,cAAc;EAAE,SAAS,cAAc;EAAG;CAAQ,CAAC;CAC7E,MAAM,4BAAY,IAAI,IAAoB;CAE1C,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,CAAC,MAAM,QAAQ,UAAU,IAAI,KAAK,IAAI,GACxC;EAGF,MAAM,SAAS,cAAc,MAAM,IAAI;EACvC,MAAM,OAAO,KAAK,SAAS,SAAS,OAAO,KAAK,CAAC,SAAS,SAAS,MAAM,OAAO,MAAM;EAEtF,UAAU,IAAI,KAAK,MAAM,IAAI;EAE7B,MAAM,eAAe,KAAK,KAAK,iBAAiB,GAAI,MAAM,CAAC,UAAU,GAAG,CAAC,IAAI,CAAC,GAAI,KAAK,QAAQ;EAC/F,MAAM,OAAO,IAAI,EAAE,oBAAoB,YAAY;CACrD;CAEA,OAAO;AACT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/core",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
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.
|
|
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.
|
|
65
|
+
"@kubb/renderer-jsx": "5.0.0-beta.39"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
|
-
"@kubb/renderer-jsx": "5.0.0-beta.
|
|
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,
|
|
5
|
-
import {
|
|
6
|
-
import { type Diagnostic,
|
|
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
|
-
*
|
|
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
|
-
#
|
|
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.#
|
|
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`
|
|
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.#
|
|
167
|
+
if (this.inputNode || !this.adapter || !this.#adapterSource) return
|
|
179
168
|
|
|
180
169
|
const adapter = this.adapter
|
|
181
|
-
const source = this.#
|
|
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
|
|
759
|
-
//
|
|
760
|
-
//
|
|
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.#
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
907
|
-
code:
|
|
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
|
|
922
|
-
code:
|
|
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.',
|
package/src/Telemetry.ts
ADDED
|
@@ -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
|
+
}
|