@kubb/core 5.0.0-beta.99 → 5.0.0
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/index.cjs +524 -370
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +500 -345
- package/dist/index.js.map +1 -1
- package/dist/mocks.cjs +10 -5
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.ts +1 -1
- package/dist/mocks.js +10 -5
- package/dist/mocks.js.map +1 -1
- package/dist/{types-DM7-MGjZ.d.ts → types-Ba5Mo-G8.d.ts} +228 -50
- package/dist/{usingCtx-CZyLSqds.cjs → usingCtx-BdYw7ICK.cjs} +279 -30
- package/dist/usingCtx-BdYw7ICK.cjs.map +1 -0
- package/dist/{usingCtx-BNggxUEL.js → usingCtx-njZUKKsY.js} +255 -30
- package/dist/usingCtx-njZUKKsY.js.map +1 -0
- package/package.json +3 -3
- package/dist/usingCtx-BNggxUEL.js.map +0 -1
- package/dist/usingCtx-CZyLSqds.cjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import "./rolldown-runtime-C0LytTxp.js";
|
|
2
|
-
import { a as
|
|
1
|
+
import { t as __name } from "./rolldown-runtime-C0LytTxp.js";
|
|
2
|
+
import { a as OPERATION_FILTER_TYPES, c as clean, d as toPosixPath, f as write, g as camelCase, h as toError, i as Hookable, l as isPathInside, m as getErrorMessage, n as createNodeCache, o as diagnosticCode, p as BuildError, r as FileManager, s as inParallel, t as _usingCtx, u as toFilePath } from "./usingCtx-njZUKKsY.js";
|
|
3
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
4
|
import { stripVTControlCharacters, styleText } from "node:util";
|
|
5
|
-
import { hash } from "node:crypto";
|
|
5
|
+
import { createHash, hash } from "node:crypto";
|
|
6
6
|
import { access, glob, readFile, rm } from "node:fs/promises";
|
|
7
7
|
import path, { join, relative, resolve } from "node:path";
|
|
8
|
-
import { ast,
|
|
8
|
+
import { ast, collectImportedRefNames, collectUsedSchemaNames, composeMacros, operationDef, schemaDef, transform } from "@kubb/ast";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
9
11
|
import process$1 from "node:process";
|
|
10
12
|
//#region src/createAdapter.ts
|
|
11
13
|
/**
|
|
@@ -145,163 +147,15 @@ const randomColors = [
|
|
|
145
147
|
function randomCliColor(text) {
|
|
146
148
|
if (!text) return "";
|
|
147
149
|
const index = hash("sha256", text, "buffer").readUInt32BE(0) % randomColors.length;
|
|
148
|
-
|
|
150
|
+
const color = randomColors[index] ?? "white";
|
|
151
|
+
return styleText(color, text);
|
|
149
152
|
}
|
|
150
153
|
//#endregion
|
|
151
|
-
//#region ../../internals/utils/src/promise.ts
|
|
152
|
-
/**
|
|
153
|
-
* Wraps `factory` with a keyed cache backed by the provided store.
|
|
154
|
-
*
|
|
155
|
-
* Pass a `WeakMap` for object keys (results are GC-eligible when the key is
|
|
156
|
-
* collected) or a `Map` for primitive keys. For multi-argument functions,
|
|
157
|
-
* nest two `memoize` calls — the outer keyed by the first argument, the
|
|
158
|
-
* inner (created once per outer miss) keyed by the second.
|
|
159
|
-
*
|
|
160
|
-
* Because the cache is owned by the caller, it can be shared, inspected, or
|
|
161
|
-
* cleared independently of the memoized function.
|
|
162
|
-
*
|
|
163
|
-
* @example Single WeakMap key
|
|
164
|
-
* ```ts
|
|
165
|
-
* const cache = new WeakMap<SchemaNode, Set<string>>()
|
|
166
|
-
* const getRefs = memoize(cache, (node) => collectRefs(node))
|
|
167
|
-
* ```
|
|
168
|
-
*
|
|
169
|
-
* @example Single Map key (primitive)
|
|
170
|
-
* ```ts
|
|
171
|
-
* const cache = new Map<string, Resolver>()
|
|
172
|
-
* const getResolver = memoize(cache, (name) => buildResolver(name))
|
|
173
|
-
* ```
|
|
174
|
-
*
|
|
175
|
-
* @example Two-level (object + primitive)
|
|
176
|
-
* ```ts
|
|
177
|
-
* const outer = new WeakMap<Params[], Map<string, Params[]>>()
|
|
178
|
-
* const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
|
|
179
|
-
* fn(params)('camelcase')
|
|
180
|
-
* ```
|
|
181
|
-
*/
|
|
182
|
-
function memoize(store, factory) {
|
|
183
|
-
return (key) => {
|
|
184
|
-
if (store.has(key)) return store.get(key);
|
|
185
|
-
const value = factory(key);
|
|
186
|
-
store.set(key, value);
|
|
187
|
-
return value;
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
//#endregion
|
|
191
|
-
//#region package.json
|
|
192
|
-
var version = "5.0.0-beta.99";
|
|
193
|
-
//#endregion
|
|
194
|
-
//#region src/constants.ts
|
|
195
|
-
/**
|
|
196
|
-
* Plugin `include` filter types that select operations directly. When one of these is set
|
|
197
|
-
* without a `schemaName` include, the generate phase pre-scans operations to compute the set
|
|
198
|
-
* of schemas they reach, so unreachable schemas can be pruned for that plugin.
|
|
199
|
-
*/
|
|
200
|
-
const OPERATION_FILTER_TYPES = /* @__PURE__ */ new Set([
|
|
201
|
-
"tag",
|
|
202
|
-
"operationId",
|
|
203
|
-
"path",
|
|
204
|
-
"method",
|
|
205
|
-
"contentType"
|
|
206
|
-
]);
|
|
207
|
-
/**
|
|
208
|
-
* Stable codes Kubb attaches to a `Diagnostic`. Each maps to a known failure mode
|
|
209
|
-
* and stays stable so it can be referenced in tooling and (later) docs. Reference
|
|
210
|
-
* these instead of inlining the string at a throw site.
|
|
211
|
-
*/
|
|
212
|
-
const diagnosticCode = {
|
|
213
|
-
/**
|
|
214
|
-
* Fallback for an unstructured error with no specific code.
|
|
215
|
-
*/
|
|
216
|
-
unknown: "KUBB_UNKNOWN",
|
|
217
|
-
/**
|
|
218
|
-
* The file or URL set as `input` could not be read.
|
|
219
|
-
*/
|
|
220
|
-
inputNotFound: "KUBB_INPUT_NOT_FOUND",
|
|
221
|
-
/**
|
|
222
|
-
* An adapter was configured without an `input`.
|
|
223
|
-
*/
|
|
224
|
-
inputRequired: "KUBB_INPUT_REQUIRED",
|
|
225
|
-
/**
|
|
226
|
-
* A `$ref` (or equivalent reference) could not be resolved in the source document.
|
|
227
|
-
*/
|
|
228
|
-
refNotFound: "KUBB_REF_NOT_FOUND",
|
|
229
|
-
/**
|
|
230
|
-
* A server variable value is not allowed by its `enum`.
|
|
231
|
-
*/
|
|
232
|
-
invalidServerVariable: "KUBB_INVALID_SERVER_VARIABLE",
|
|
233
|
-
/**
|
|
234
|
-
* A required plugin is missing from the config.
|
|
235
|
-
*/
|
|
236
|
-
pluginNotFound: "KUBB_PLUGIN_NOT_FOUND",
|
|
237
|
-
/**
|
|
238
|
-
* A plugin threw while generating.
|
|
239
|
-
*/
|
|
240
|
-
pluginFailed: "KUBB_PLUGIN_FAILED",
|
|
241
|
-
/**
|
|
242
|
-
* A plugin reported a non-fatal warning through `ctx.warn`.
|
|
243
|
-
*/
|
|
244
|
-
pluginWarning: "KUBB_PLUGIN_WARNING",
|
|
245
|
-
/**
|
|
246
|
-
* A plugin reported an informational message through `ctx.info`.
|
|
247
|
-
*/
|
|
248
|
-
pluginInfo: "KUBB_PLUGIN_INFO",
|
|
249
|
-
/**
|
|
250
|
-
* A schema uses a `format` Kubb does not map to a specific type. Reserved for
|
|
251
|
-
* adapters to emit as a `warning`.
|
|
252
|
-
*/
|
|
253
|
-
unsupportedFormat: "KUBB_UNSUPPORTED_FORMAT",
|
|
254
|
-
/**
|
|
255
|
-
* A referenced schema or operation is marked `deprecated`. Reserved for adapters
|
|
256
|
-
* to emit as an `info`.
|
|
257
|
-
*/
|
|
258
|
-
deprecated: "KUBB_DEPRECATED",
|
|
259
|
-
/**
|
|
260
|
-
* An adapter is required but the config has none. The build cannot read the input
|
|
261
|
-
* without one.
|
|
262
|
-
*/
|
|
263
|
-
adapterRequired: "KUBB_ADAPTER_REQUIRED",
|
|
264
|
-
/**
|
|
265
|
-
* A resolved output path escapes the output directory, which can stem from a path
|
|
266
|
-
* traversal in the spec or a misconfigured `group.name`.
|
|
267
|
-
*/
|
|
268
|
-
pathTraversal: "KUBB_PATH_TRAVERSAL",
|
|
269
|
-
/**
|
|
270
|
-
* `output.clean` is enabled but `output.path` resolves to the project root or a parent of it,
|
|
271
|
-
* so cleaning would delete kubb.config and every source file.
|
|
272
|
-
*/
|
|
273
|
-
cleanRoot: "KUBB_CLEAN_ROOT",
|
|
274
|
-
/**
|
|
275
|
-
* A plugin's options are invalid, for example `output.mode: 'file'` paired with a `group` option.
|
|
276
|
-
*/
|
|
277
|
-
invalidPluginOptions: "KUBB_INVALID_PLUGIN_OPTIONS",
|
|
278
|
-
/**
|
|
279
|
-
* A post-generate command (`output.postGenerate`) exited with a failure.
|
|
280
|
-
*/
|
|
281
|
-
postGenerateFailed: "KUBB_POST_GENERATE_FAILED",
|
|
282
|
-
/**
|
|
283
|
-
* The formatter pass over the generated files failed.
|
|
284
|
-
*/
|
|
285
|
-
formatFailed: "KUBB_FORMAT_FAILED",
|
|
286
|
-
/**
|
|
287
|
-
* The linter pass over the generated files failed.
|
|
288
|
-
*/
|
|
289
|
-
lintFailed: "KUBB_LINT_FAILED",
|
|
290
|
-
/**
|
|
291
|
-
* Not a failure. Carries a plugin's elapsed time, summed into the run total.
|
|
292
|
-
*/
|
|
293
|
-
performance: "KUBB_PERFORMANCE",
|
|
294
|
-
/**
|
|
295
|
-
* Not a failure. A newer Kubb version is available on npm.
|
|
296
|
-
*/
|
|
297
|
-
updateAvailable: "KUBB_UPDATE_AVAILABLE"
|
|
298
|
-
};
|
|
299
|
-
//#endregion
|
|
300
154
|
//#region src/Diagnostics.ts
|
|
301
155
|
/**
|
|
302
156
|
* Docs major version, derived from the package version so the link tracks the published major.
|
|
303
157
|
*/
|
|
304
|
-
const docsMajor =
|
|
158
|
+
const docsMajor = "5.0.0".split(".")[0] ?? "5";
|
|
305
159
|
/**
|
|
306
160
|
* Builds a type guard that narrows a {@link Diagnostic} to the variant for `kind`. A diagnostic
|
|
307
161
|
* with no `kind` is treated as a `problem`.
|
|
@@ -361,14 +215,34 @@ const diagnosticCatalog = {
|
|
|
361
215
|
},
|
|
362
216
|
[diagnosticCode.inputNotFound]: {
|
|
363
217
|
title: "Input not found",
|
|
364
|
-
cause: "The file
|
|
365
|
-
fix: "Check that the path
|
|
218
|
+
cause: "The file set as `input` (or passed as `kubb generate PATH`) could not be read. A URL reports `KUBB_INPUT_REQUEST_FAILED` or `KUBB_INPUT_UNREACHABLE` instead.",
|
|
219
|
+
fix: "Check that the path exists and is readable, then set it as `input` or pass it on the CLI."
|
|
220
|
+
},
|
|
221
|
+
[diagnosticCode.inputRequestFailed]: {
|
|
222
|
+
title: "Input request failed",
|
|
223
|
+
cause: "A URL set as `input` (or reached through a `$ref`) answered with a 4xx or 5xx status instead of the document.",
|
|
224
|
+
fix: "Open the URL to see what the server returns. A 401 or 403 needs credentials Kubb does not send, so download the document and point `input` at the local file. A 404 means the path is wrong, and a 5xx means the server itself failed."
|
|
225
|
+
},
|
|
226
|
+
[diagnosticCode.inputUnreachable]: {
|
|
227
|
+
title: "Input unreachable",
|
|
228
|
+
cause: "A URL set as `input` (or reached through a `$ref`) never answered, so the request failed before a status came back. A refused connection, an unknown host, an expired certificate, and a timeout all land here.",
|
|
229
|
+
fix: "Check that the host is running and reachable from this machine. For a local server, start it and confirm the port matches the one in `input`."
|
|
366
230
|
},
|
|
367
231
|
[diagnosticCode.inputRequired]: {
|
|
368
232
|
title: "Input required",
|
|
369
233
|
cause: "An adapter is configured but no `input` was provided.",
|
|
370
234
|
fix: "Set `input` to a file path, a URL, an inline spec (JSON/YAML string), or a parsed object in your Kubb config."
|
|
371
235
|
},
|
|
236
|
+
[diagnosticCode.legacyInput]: {
|
|
237
|
+
title: "Legacy input shape",
|
|
238
|
+
cause: "`input` is a `{ path }` or `{ data }` wrapper, which v4 used to point at a document and v5 reads as the document itself.",
|
|
239
|
+
fix: "Unwrap it: `input: { path: \"./petStore.yaml\" }` becomes `input: \"./petStore.yaml\"`, and `input: { data: spec }` becomes `input: spec`."
|
|
240
|
+
},
|
|
241
|
+
[diagnosticCode.invalidDocument]: {
|
|
242
|
+
title: "Invalid document",
|
|
243
|
+
cause: "The parsed `input` has no `openapi` or `swagger` version field, so it is not an OpenAPI or Swagger document.",
|
|
244
|
+
fix: "Point `input` at a document that declares `openapi` or `swagger`, and check that a passed object is the spec itself rather than a wrapper around it."
|
|
245
|
+
},
|
|
372
246
|
[diagnosticCode.refNotFound]: {
|
|
373
247
|
title: "Reference not found",
|
|
374
248
|
cause: "A `$ref` could not be resolved in the source document.",
|
|
@@ -709,14 +583,18 @@ var Diagnostics = class Diagnostics {
|
|
|
709
583
|
* Merges the `output.mode` default into the output config and validates the combination.
|
|
710
584
|
* Throws `KUBB_INVALID_PLUGIN_OPTIONS` when `mode: 'file'` is paired with a `group` option,
|
|
711
585
|
* since a single-file output has nothing to group.
|
|
586
|
+
*
|
|
587
|
+
* An omitted `mode` follows the shape of `path`: an extension means a single file, anything
|
|
588
|
+
* else is a directory. Plugin defaults such as `path: 'types'` name a directory, so they
|
|
589
|
+
* generate without the caller spelling out `mode`.
|
|
712
590
|
*/
|
|
713
591
|
function normalizeOutput({ output, group, pluginName }) {
|
|
714
|
-
const mode = output.mode ?? "file";
|
|
592
|
+
const mode = output.mode ?? (path.extname(output.path) ? "file" : "directory");
|
|
715
593
|
if (mode === "file" && group) throw new Diagnostics.Error({
|
|
716
594
|
code: diagnosticCode.invalidPluginOptions,
|
|
717
595
|
severity: "error",
|
|
718
|
-
message: `Plugin "${pluginName}"
|
|
719
|
-
help: "A single-file output has nothing to group. Remove the `group` option, or
|
|
596
|
+
message: `Plugin "${pluginName}" resolves \`output.mode\` to 'file' but also configures a \`group\` option.`,
|
|
597
|
+
help: "A single-file output has nothing to group. Remove the `group` option, give `output.path` an extensionless directory name, or set `output.mode: 'directory'` explicitly.",
|
|
720
598
|
location: { kind: "config" },
|
|
721
599
|
plugin: pluginName
|
|
722
600
|
});
|
|
@@ -767,6 +645,23 @@ function getInputKind(input) {
|
|
|
767
645
|
return "file";
|
|
768
646
|
}
|
|
769
647
|
/**
|
|
648
|
+
* The v4 `input` wrapper keys. v4 typed `input` as `{ path }` or `{ data }`; v5 takes the value
|
|
649
|
+
* directly, so the wrapper now matches the "already-parsed document" branch and silently yields
|
|
650
|
+
* an empty build.
|
|
651
|
+
*/
|
|
652
|
+
const legacyInputKeys = ["path", "data"];
|
|
653
|
+
/**
|
|
654
|
+
* Detects the v4 `{ path }` / `{ data }` wrapper so it fails loudly instead of being read as a
|
|
655
|
+
* document. A real spec always carries more than these keys, so an object whose keys are drawn
|
|
656
|
+
* only from them is the old shape rather than a document that happens to have a `path` property.
|
|
657
|
+
*/
|
|
658
|
+
function isLegacyInput(input) {
|
|
659
|
+
if (Array.isArray(input)) return input.some(isLegacyInput);
|
|
660
|
+
if (typeof input !== "object" || input === null) return false;
|
|
661
|
+
const keys = Object.keys(input);
|
|
662
|
+
return keys.length > 0 && keys.every((key) => legacyInputKeys.includes(key));
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
770
665
|
* Normalizes `config.input` into an `AdapterSource` the adapter can parse.
|
|
771
666
|
*
|
|
772
667
|
* A parsed object and inline content become `{ type: 'data' }`; a URL is kept verbatim and a
|
|
@@ -774,6 +669,13 @@ function getInputKind(input) {
|
|
|
774
669
|
*/
|
|
775
670
|
function inputToAdapterSource(config) {
|
|
776
671
|
const input = config.input;
|
|
672
|
+
if (input && isLegacyInput(input)) throw new Diagnostics.Error({
|
|
673
|
+
code: Diagnostics.code.legacyInput,
|
|
674
|
+
severity: "error",
|
|
675
|
+
message: "The `input` option uses the v4 `{ path }` / `{ data }` wrapper.",
|
|
676
|
+
help: "Unwrap it: `input: { path: \"./petStore.yaml\" }` becomes `input: \"./petStore.yaml\"`, and `input: { data: spec }` becomes `input: spec`.",
|
|
677
|
+
location: { kind: "config" }
|
|
678
|
+
});
|
|
777
679
|
if (!input) throw new Diagnostics.Error({
|
|
778
680
|
code: Diagnostics.code.inputRequired,
|
|
779
681
|
severity: "error",
|
|
@@ -885,28 +787,24 @@ var Resolver = class Resolver {
|
|
|
885
787
|
* schemas (`targetName`) import the emitted name. Names and paths go through the top-level
|
|
886
788
|
* `name` and `file`, so import entries follow the plugin's conventions, and a per-call
|
|
887
789
|
* `name` override wins over both.
|
|
790
|
+
*
|
|
791
|
+
* The subtree scan runs through `collectImportedRefNames`, which memoizes by node identity, so a
|
|
792
|
+
* schema shared across the ts, zod, and faker plugins is walked once and every plugin's resolver
|
|
793
|
+
* reads the same ref set instead of re-scanning it per plugin.
|
|
888
794
|
*/
|
|
889
795
|
imports(options) {
|
|
890
796
|
const { node, root, output, group, extname = ".ts", name } = options;
|
|
891
797
|
const resolveName = name ?? ((schemaName) => this.name(schemaName));
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
name: schemaName,
|
|
903
|
-
extname,
|
|
904
|
-
root,
|
|
905
|
-
output,
|
|
906
|
-
group
|
|
907
|
-
}).path
|
|
908
|
-
});
|
|
909
|
-
} });
|
|
798
|
+
return collectImportedRefNames(node).map((schemaName) => ast.factory.createImport({
|
|
799
|
+
name: [resolveName(schemaName)],
|
|
800
|
+
path: this.file({
|
|
801
|
+
name: schemaName,
|
|
802
|
+
extname,
|
|
803
|
+
root,
|
|
804
|
+
output,
|
|
805
|
+
group
|
|
806
|
+
}).path
|
|
807
|
+
}));
|
|
910
808
|
}
|
|
911
809
|
/**
|
|
912
810
|
* Folds each `override` over `base`, left to right, and returns a new resolver with helpers
|
|
@@ -1286,6 +1184,38 @@ const ENFORCE_ORDER = {
|
|
|
1286
1184
|
post: 1
|
|
1287
1185
|
};
|
|
1288
1186
|
const enforceWeight = (plugin) => plugin.enforce ? ENFORCE_ORDER[plugin.enforce] : 0;
|
|
1187
|
+
/**
|
|
1188
|
+
* The options bag a `NormalizedPlugin` starts with before a plugin refines it: a directory output
|
|
1189
|
+
* at the plugin root and empty filter lists.
|
|
1190
|
+
*/
|
|
1191
|
+
function defaultPluginOptions() {
|
|
1192
|
+
return {
|
|
1193
|
+
output: {
|
|
1194
|
+
path: ".",
|
|
1195
|
+
mode: "directory"
|
|
1196
|
+
},
|
|
1197
|
+
exclude: [],
|
|
1198
|
+
override: []
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Fills in the `output`, `exclude`, and `override` a `NormalizedPlugin` needs from a plugin's raw
|
|
1203
|
+
* options, running `output` through `normalizeOutput`. Idempotent, so the driver can apply it after
|
|
1204
|
+
* `setOptions` has already run without disturbing an already-normalized bag.
|
|
1205
|
+
*/
|
|
1206
|
+
function normalizePluginOptions(rawOptions, pluginName) {
|
|
1207
|
+
const options = {
|
|
1208
|
+
...defaultPluginOptions(),
|
|
1209
|
+
...rawOptions ?? {}
|
|
1210
|
+
};
|
|
1211
|
+
const group = "group" in options ? options.group : void 0;
|
|
1212
|
+
options.output = normalizeOutput({
|
|
1213
|
+
output: options.output,
|
|
1214
|
+
group,
|
|
1215
|
+
pluginName
|
|
1216
|
+
});
|
|
1217
|
+
return options;
|
|
1218
|
+
}
|
|
1289
1219
|
var KubbDriver = class {
|
|
1290
1220
|
config;
|
|
1291
1221
|
options;
|
|
@@ -1307,13 +1237,6 @@ var KubbDriver = class {
|
|
|
1307
1237
|
fileManager = new FileManager();
|
|
1308
1238
|
plugins = /* @__PURE__ */ new Map();
|
|
1309
1239
|
/**
|
|
1310
|
-
* Tracks which plugins have generators registered via `addGenerator()` (hook-based path).
|
|
1311
|
-
* Used by the build loop to decide whether to emit generator hooks for a given plugin.
|
|
1312
|
-
*/
|
|
1313
|
-
#hookGeneratorPlugins = /* @__PURE__ */ new Set();
|
|
1314
|
-
#resolvers = /* @__PURE__ */ new Map();
|
|
1315
|
-
#defaultResolvers = /* @__PURE__ */ new Map();
|
|
1316
|
-
/**
|
|
1317
1240
|
* Removers for every listener the driver added (plugin, generator) so `dispose()` can detach
|
|
1318
1241
|
* them in one pass. External `hooks.hook(...)` listeners are not tracked.
|
|
1319
1242
|
*/
|
|
@@ -1341,15 +1264,8 @@ var KubbDriver = class {
|
|
|
1341
1264
|
dependencies: rawPlugin.dependencies,
|
|
1342
1265
|
enforce: rawPlugin.enforce,
|
|
1343
1266
|
hooks: rawPlugin.hooks,
|
|
1344
|
-
options: rawPlugin.options ??
|
|
1345
|
-
|
|
1346
|
-
path: ".",
|
|
1347
|
-
mode: "directory"
|
|
1348
|
-
},
|
|
1349
|
-
exclude: [],
|
|
1350
|
-
override: []
|
|
1351
|
-
},
|
|
1352
|
-
resolver: this.#getDefaultResolver(rawPlugin.name)
|
|
1267
|
+
options: rawPlugin.options ?? defaultPluginOptions(),
|
|
1268
|
+
resolver: createResolver({ pluginName: rawPlugin.name })
|
|
1353
1269
|
};
|
|
1354
1270
|
}));
|
|
1355
1271
|
for (const plugin of normalized) {
|
|
@@ -1458,46 +1374,30 @@ var KubbDriver = class {
|
|
|
1458
1374
|
}
|
|
1459
1375
|
}
|
|
1460
1376
|
/**
|
|
1461
|
-
*
|
|
1462
|
-
*
|
|
1463
|
-
* The generator's `schema`, `operation`, and `operations` methods are registered as
|
|
1464
|
-
* listeners on `kubb:generate:schema`, `kubb:generate:operation`, and `kubb:generate:operations`
|
|
1465
|
-
* respectively. Each listener is scoped to the owning plugin via a `ctx.plugin.name` check
|
|
1466
|
-
* so that generators from different plugins do not cross-fire.
|
|
1377
|
+
* Appends a generator to its owning plugin so the generate loop can call it directly.
|
|
1467
1378
|
*
|
|
1468
|
-
* The
|
|
1469
|
-
*
|
|
1379
|
+
* The generator's `schema`, `operation`, and `operations` methods run per node during the AST
|
|
1380
|
+
* walk in `#runGenerators`, and their result is routed through `dispatch`. Because a generator is
|
|
1381
|
+
* bound to a plugin, generators from different plugins never cross-fire without a name check. The
|
|
1382
|
+
* renderer comes from `generator.renderer`; set it to `null` (or leave it unset) to opt out of
|
|
1383
|
+
* rendering.
|
|
1470
1384
|
*
|
|
1471
1385
|
* Call this method inside `addGenerator()` (in `kubb:plugin:setup`) to wire up a generator.
|
|
1472
1386
|
*/
|
|
1473
1387
|
registerGenerator(pluginName, generator) {
|
|
1474
|
-
const
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
if (ctx.plugin.name !== pluginName) return;
|
|
1478
|
-
const result = await method(node, ctx);
|
|
1479
|
-
await this.dispatch({
|
|
1480
|
-
result,
|
|
1481
|
-
renderer: generator.renderer
|
|
1482
|
-
});
|
|
1483
|
-
};
|
|
1484
|
-
};
|
|
1485
|
-
this.#unhooks.push(this.hooks.addHooks({
|
|
1486
|
-
"kubb:generate:schema": wrap(generator.schema),
|
|
1487
|
-
"kubb:generate:operation": wrap(generator.operation),
|
|
1488
|
-
"kubb:generate:operations": wrap(generator.operations)
|
|
1489
|
-
}));
|
|
1490
|
-
this.#hookGeneratorPlugins.add(pluginName);
|
|
1388
|
+
const plugin = this.plugins.get(pluginName);
|
|
1389
|
+
if (!plugin) return;
|
|
1390
|
+
plugin.generators = plugin.generators ? [...plugin.generators, generator] : [generator];
|
|
1491
1391
|
}
|
|
1492
1392
|
/**
|
|
1493
1393
|
* Returns `true` when at least one generator was registered for the given plugin
|
|
1494
1394
|
* via `addGenerator()` in `kubb:plugin:setup`.
|
|
1495
1395
|
*
|
|
1496
|
-
* Used by the build loop to decide whether to walk the AST and
|
|
1396
|
+
* Used by the build loop to decide whether to walk the AST and run the generators
|
|
1497
1397
|
* for a plugin.
|
|
1498
1398
|
*/
|
|
1499
1399
|
hasHookGenerators(pluginName) {
|
|
1500
|
-
return this
|
|
1400
|
+
return (this.plugins.get(pluginName)?.generators?.length ?? 0) > 0;
|
|
1501
1401
|
}
|
|
1502
1402
|
/**
|
|
1503
1403
|
* Runs the full plugin pipeline. Returns the diagnostics collected so far even
|
|
@@ -1508,21 +1408,25 @@ var KubbDriver = class {
|
|
|
1508
1408
|
async run() {
|
|
1509
1409
|
const { hooks, config, fileManager } = this;
|
|
1510
1410
|
const diagnostics = [];
|
|
1511
|
-
const updateBuffer = [];
|
|
1512
1411
|
const parsersMap = /* @__PURE__ */ new Map();
|
|
1513
1412
|
for (const parser of config.parsers) if (parser.extNames) for (const ext of parser.extNames) parsersMap.set(ext, parser);
|
|
1413
|
+
const updateBuffer = [];
|
|
1514
1414
|
const unhookWrites = fileManager.hooks.addHooks({
|
|
1515
1415
|
start: async (files) => {
|
|
1516
1416
|
await hooks.callHook("kubb:files:processing:start", { files });
|
|
1517
1417
|
},
|
|
1518
|
-
update: (
|
|
1519
|
-
updateBuffer.push(
|
|
1418
|
+
update: ({ file, processed, total, percentage }) => {
|
|
1419
|
+
updateBuffer.push({
|
|
1420
|
+
file,
|
|
1421
|
+
processed,
|
|
1422
|
+
total,
|
|
1423
|
+
percentage,
|
|
1424
|
+
config
|
|
1425
|
+
});
|
|
1520
1426
|
},
|
|
1521
1427
|
end: async (files) => {
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
config
|
|
1525
|
-
})) });
|
|
1428
|
+
updateBuffer.sort((a, b) => a.processed - b.processed);
|
|
1429
|
+
await hooks.callHook("kubb:files:processing:update", { files: updateBuffer });
|
|
1526
1430
|
updateBuffer.length = 0;
|
|
1527
1431
|
await hooks.callHook("kubb:files:processing:end", { files });
|
|
1528
1432
|
}
|
|
@@ -1532,12 +1436,16 @@ var KubbDriver = class {
|
|
|
1532
1436
|
const outputRoot = resolve(config.root, config.output.path);
|
|
1533
1437
|
await this.#parseInput();
|
|
1534
1438
|
await this.setupHooks();
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1439
|
+
for (const plugin of this.plugins.values()) plugin.options = normalizePluginOptions(plugin.options, plugin.name);
|
|
1440
|
+
if (this.adapter && this.inputNode) {
|
|
1441
|
+
const buildStartContext = this.#withFiles({
|
|
1442
|
+
config,
|
|
1443
|
+
adapter: this.adapter,
|
|
1444
|
+
meta: this.inputNode.meta,
|
|
1445
|
+
getPlugin: this.getPlugin.bind(this)
|
|
1446
|
+
});
|
|
1447
|
+
await hooks.callHook("kubb:build:start", buildStartContext);
|
|
1448
|
+
}
|
|
1541
1449
|
const generatorPlugins = [];
|
|
1542
1450
|
for (const plugin of this.plugins.values()) {
|
|
1543
1451
|
const context = this.getContext(plugin);
|
|
@@ -1582,10 +1490,11 @@ var KubbDriver = class {
|
|
|
1582
1490
|
});
|
|
1583
1491
|
}
|
|
1584
1492
|
diagnostics.push(...await this.#runGenerators(generatorPlugins));
|
|
1585
|
-
await hooks.callHook("kubb:plugins:end",
|
|
1493
|
+
await hooks.callHook("kubb:plugins:end", this.#withFiles({ config }));
|
|
1586
1494
|
await fileManager.write(fileManager.files, {
|
|
1587
1495
|
storage: config.storage,
|
|
1588
|
-
parsers: parsersMap
|
|
1496
|
+
parsers: parsersMap,
|
|
1497
|
+
manifest: this.options.manifest
|
|
1589
1498
|
});
|
|
1590
1499
|
await hooks.callHook("kubb:build:end", {
|
|
1591
1500
|
files: this.fileManager.files,
|
|
@@ -1601,35 +1510,42 @@ var KubbDriver = class {
|
|
|
1601
1510
|
}
|
|
1602
1511
|
});
|
|
1603
1512
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1513
|
+
/**
|
|
1514
|
+
* Widens `extra` with the files present at emit time and a bound `upsertFile`, the shape every
|
|
1515
|
+
* file-carrying hook context shares. Building it here in one place keeps the `files` and
|
|
1516
|
+
* `upsertFile` keys from being dropped by a stray spread at the call site.
|
|
1517
|
+
*/
|
|
1518
|
+
#withFiles(extra) {
|
|
1606
1519
|
return {
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
upsertFile: (...files) => driver.fileManager.upsert(...files)
|
|
1520
|
+
...extra,
|
|
1521
|
+
files: this.fileManager.files,
|
|
1522
|
+
upsertFile: (...files) => this.fileManager.upsert(...files)
|
|
1611
1523
|
};
|
|
1612
1524
|
}
|
|
1613
1525
|
#emitPluginEnd({ plugin, duration, success, error }) {
|
|
1614
|
-
return this.hooks.callHook("kubb:plugin:end",
|
|
1526
|
+
return this.hooks.callHook("kubb:plugin:end", this.#withFiles({
|
|
1615
1527
|
plugin,
|
|
1616
1528
|
duration,
|
|
1617
1529
|
success,
|
|
1618
1530
|
...error ? { error } : {},
|
|
1619
1531
|
config: this.config
|
|
1620
|
-
}
|
|
1532
|
+
}));
|
|
1621
1533
|
}
|
|
1622
1534
|
/**
|
|
1623
|
-
* Runs schemas and operations through every plugin's generators.
|
|
1624
|
-
*
|
|
1625
|
-
*
|
|
1626
|
-
* so
|
|
1627
|
-
*
|
|
1628
|
-
*
|
|
1535
|
+
* Runs schemas and operations through every plugin's generators. The walk is node-outer: each
|
|
1536
|
+
* schema is visited once and each operation once, and the node fans out to the matching
|
|
1537
|
+
* generators of every plugin in dependency order. A per-node cache (`createNodeCache`) is created
|
|
1538
|
+
* once per node and shared by all of that node's plugins, so node-derived work is computed once
|
|
1539
|
+
* and reused instead of recomputed per plugin. Each node still runs through the plugin's macros
|
|
1540
|
+
* (from `this.#transforms`) and its exclude/include/override filters before that plugin's
|
|
1541
|
+
* generator sees it, so plugins stay isolated. Schemas run before operations so file output
|
|
1542
|
+
* stays deterministic across runs. A generator with a `match` predicate that resolves `false`
|
|
1543
|
+
* for a node is skipped for that node, without calling `schema`/`operation`.
|
|
1629
1544
|
*
|
|
1630
|
-
*
|
|
1631
|
-
*
|
|
1632
|
-
* `Plugins N/M` counter.
|
|
1545
|
+
* A failing plugin is dropped from the remaining walk, contributes an error diagnostic, and no
|
|
1546
|
+
* longer aborts the other plugins. `kubb:plugin:end` and each plugin's `timing` diagnostic fire
|
|
1547
|
+
* in dependency order once the walk finishes, driving the CLI's `Plugins N/M` counter. The
|
|
1548
|
+
* `operations` batch fires once per plugin after the single operation walk.
|
|
1633
1549
|
*
|
|
1634
1550
|
* When `this.inputNode` is `null`, every entry still gets a `kubb:plugin:end` so
|
|
1635
1551
|
* post-plugin listeners (the barrel writer and friends) complete.
|
|
@@ -1654,9 +1570,6 @@ var KubbDriver = class {
|
|
|
1654
1570
|
}
|
|
1655
1571
|
const transforms = this.#transforms;
|
|
1656
1572
|
const { schemas, operations } = this.inputNode;
|
|
1657
|
-
const emitsSchemaHook = this.hooks.listenerCount("kubb:generate:schema") > 0;
|
|
1658
|
-
const emitsOperationHook = this.hooks.listenerCount("kubb:generate:operation") > 0;
|
|
1659
|
-
const emitsOperationsHook = this.hooks.listenerCount("kubb:generate:operations") > 0;
|
|
1660
1573
|
const allowedSchemaNamesByPlugin = /* @__PURE__ */ new Map();
|
|
1661
1574
|
for (const { plugin } of entries) {
|
|
1662
1575
|
const { exclude, include, override } = plugin.options;
|
|
@@ -1670,88 +1583,133 @@ var KubbDriver = class {
|
|
|
1670
1583
|
}) !== null);
|
|
1671
1584
|
allowedSchemaNamesByPlugin.set(plugin.name, collectUsedSchemaNames(includedOps, schemas));
|
|
1672
1585
|
}
|
|
1673
|
-
|
|
1586
|
+
const states = entries.map(({ plugin, context, hrStart }) => {
|
|
1674
1587
|
const generatorContext = {
|
|
1675
1588
|
...context,
|
|
1676
1589
|
resolver: this.getResolver(plugin.name)
|
|
1677
1590
|
};
|
|
1678
1591
|
const { exclude, include, override } = plugin.options;
|
|
1679
|
-
const
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
if (options === null) return null;
|
|
1695
|
-
return {
|
|
1696
|
-
transformedNode,
|
|
1697
|
-
options
|
|
1698
|
-
};
|
|
1592
|
+
const generators = plugin.generators ?? [];
|
|
1593
|
+
return {
|
|
1594
|
+
plugin,
|
|
1595
|
+
hrStart,
|
|
1596
|
+
generatorContext,
|
|
1597
|
+
exclude,
|
|
1598
|
+
include,
|
|
1599
|
+
override,
|
|
1600
|
+
optionsAreStatic: !exclude?.length && !include?.length && !override?.length,
|
|
1601
|
+
allowedSchemaNames: allowedSchemaNamesByPlugin.get(plugin.name) ?? null,
|
|
1602
|
+
schemaGenerators: generators.filter((generator) => generator.schema),
|
|
1603
|
+
operationGenerators: generators.filter((generator) => generator.operation),
|
|
1604
|
+
operationsGenerators: generators.filter((generator) => generator.operations),
|
|
1605
|
+
pluginOperations: [],
|
|
1606
|
+
error: null
|
|
1699
1607
|
};
|
|
1700
|
-
|
|
1701
|
-
|
|
1608
|
+
});
|
|
1609
|
+
const resolveForPlugin = (state, node) => {
|
|
1610
|
+
const transformedNode = transforms.applyTo(state.plugin.name, node);
|
|
1611
|
+
if (state.optionsAreStatic) return {
|
|
1612
|
+
transformedNode,
|
|
1613
|
+
options: state.plugin.options
|
|
1614
|
+
};
|
|
1615
|
+
const options = state.generatorContext.resolver.default.options(transformedNode, {
|
|
1616
|
+
options: state.plugin.options,
|
|
1617
|
+
exclude: state.exclude,
|
|
1618
|
+
include: state.include,
|
|
1619
|
+
override: state.override
|
|
1620
|
+
});
|
|
1621
|
+
if (options === null) return null;
|
|
1622
|
+
return {
|
|
1623
|
+
transformedNode,
|
|
1624
|
+
options
|
|
1625
|
+
};
|
|
1626
|
+
};
|
|
1627
|
+
for (const node of schemas) {
|
|
1628
|
+
const cache = createNodeCache();
|
|
1629
|
+
for (const state of states) {
|
|
1630
|
+
if (state.error || !state.schemaGenerators.length) continue;
|
|
1702
1631
|
try {
|
|
1703
|
-
const resolved = resolveForPlugin(node);
|
|
1632
|
+
const resolved = resolveForPlugin(state, node);
|
|
1704
1633
|
if (!resolved) continue;
|
|
1705
1634
|
const { transformedNode, options } = resolved;
|
|
1706
|
-
if (allowedSchemaNames !== null && transformedNode.name && !allowedSchemaNames.has(transformedNode.name)) continue;
|
|
1707
|
-
|
|
1708
|
-
...generatorContext,
|
|
1709
|
-
options
|
|
1710
|
-
|
|
1635
|
+
if (state.allowedSchemaNames !== null && transformedNode.name && !state.allowedSchemaNames.has(transformedNode.name)) continue;
|
|
1636
|
+
const ctx = {
|
|
1637
|
+
...state.generatorContext,
|
|
1638
|
+
options,
|
|
1639
|
+
cache
|
|
1640
|
+
};
|
|
1641
|
+
for (const generator of state.schemaGenerators) {
|
|
1642
|
+
if (!(generator.match ? await generator.match(transformedNode, ctx) : true)) continue;
|
|
1643
|
+
await this.dispatch({
|
|
1644
|
+
result: await generator.schema(transformedNode, ctx),
|
|
1645
|
+
renderer: generator.renderer
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
await this.hooks.callHook("kubb:generate:schema", transformedNode, ctx);
|
|
1711
1649
|
} catch (caughtError) {
|
|
1712
|
-
error = toError(caughtError);
|
|
1650
|
+
state.error = toError(caughtError);
|
|
1713
1651
|
}
|
|
1714
1652
|
}
|
|
1715
|
-
|
|
1716
|
-
|
|
1653
|
+
}
|
|
1654
|
+
for (const node of operations) {
|
|
1655
|
+
const cache = createNodeCache();
|
|
1656
|
+
for (const state of states) {
|
|
1657
|
+
if (state.error || !state.operationGenerators.length && !state.operationsGenerators.length) continue;
|
|
1717
1658
|
try {
|
|
1718
|
-
const resolved = resolveForPlugin(node);
|
|
1659
|
+
const resolved = resolveForPlugin(state, node);
|
|
1719
1660
|
if (!resolved) continue;
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1661
|
+
state.pluginOperations.push(resolved.transformedNode);
|
|
1662
|
+
if (state.operationGenerators.length) {
|
|
1663
|
+
const ctx = {
|
|
1664
|
+
...state.generatorContext,
|
|
1665
|
+
options: resolved.options,
|
|
1666
|
+
cache
|
|
1667
|
+
};
|
|
1668
|
+
for (const generator of state.operationGenerators) {
|
|
1669
|
+
if (!(generator.match ? await generator.match(resolved.transformedNode, ctx) : true)) continue;
|
|
1670
|
+
await this.dispatch({
|
|
1671
|
+
result: await generator.operation(resolved.transformedNode, ctx),
|
|
1672
|
+
renderer: generator.renderer
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
await this.hooks.callHook("kubb:generate:operation", resolved.transformedNode, ctx);
|
|
1676
|
+
}
|
|
1724
1677
|
} catch (caughtError) {
|
|
1725
|
-
error = toError(caughtError);
|
|
1678
|
+
state.error = toError(caughtError);
|
|
1726
1679
|
}
|
|
1727
1680
|
}
|
|
1728
|
-
|
|
1681
|
+
}
|
|
1682
|
+
for (const state of states) {
|
|
1683
|
+
if (state.error || !state.operationsGenerators.length) continue;
|
|
1684
|
+
try {
|
|
1729
1685
|
const ctx = {
|
|
1730
|
-
...generatorContext,
|
|
1731
|
-
options: plugin.options
|
|
1686
|
+
...state.generatorContext,
|
|
1687
|
+
options: state.plugin.options,
|
|
1688
|
+
cache: createNodeCache()
|
|
1732
1689
|
};
|
|
1733
|
-
const
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
await this.hooks.callHook("kubb:generate:operations", pluginOperations, ctx);
|
|
1690
|
+
for (const generator of state.operationsGenerators) await this.dispatch({
|
|
1691
|
+
result: await generator.operations(state.pluginOperations, ctx),
|
|
1692
|
+
renderer: generator.renderer
|
|
1693
|
+
});
|
|
1694
|
+
await this.hooks.callHook("kubb:generate:operations", state.pluginOperations, ctx);
|
|
1739
1695
|
} catch (caughtError) {
|
|
1740
|
-
error = toError(caughtError);
|
|
1696
|
+
state.error = toError(caughtError);
|
|
1741
1697
|
}
|
|
1742
|
-
|
|
1698
|
+
}
|
|
1699
|
+
for (const state of states) {
|
|
1700
|
+
const duration = getElapsedMs(state.hrStart);
|
|
1743
1701
|
await this.#emitPluginEnd({
|
|
1744
|
-
plugin,
|
|
1702
|
+
plugin: state.plugin,
|
|
1745
1703
|
duration,
|
|
1746
|
-
success: !error,
|
|
1747
|
-
error: error ?? void 0
|
|
1704
|
+
success: !state.error,
|
|
1705
|
+
error: state.error ?? void 0
|
|
1748
1706
|
});
|
|
1749
|
-
if (error) diagnostics.push({
|
|
1750
|
-
...Diagnostics.from(error),
|
|
1751
|
-
plugin: plugin.name
|
|
1707
|
+
if (state.error) diagnostics.push({
|
|
1708
|
+
...Diagnostics.from(state.error),
|
|
1709
|
+
plugin: state.plugin.name
|
|
1752
1710
|
});
|
|
1753
1711
|
diagnostics.push(Diagnostics.performance({
|
|
1754
|
-
plugin: plugin.name,
|
|
1712
|
+
plugin: state.plugin.name,
|
|
1755
1713
|
duration
|
|
1756
1714
|
}));
|
|
1757
1715
|
}
|
|
@@ -1796,10 +1754,7 @@ var KubbDriver = class {
|
|
|
1796
1754
|
dispose() {
|
|
1797
1755
|
for (const unhook of this.#unhooks) unhook();
|
|
1798
1756
|
this.#unhooks.length = 0;
|
|
1799
|
-
this.#hookGeneratorPlugins.clear();
|
|
1800
1757
|
this.#transforms.dispose();
|
|
1801
|
-
this.#resolvers.clear();
|
|
1802
|
-
this.#defaultResolvers.clear();
|
|
1803
1758
|
this.fileManager.dispose();
|
|
1804
1759
|
this.inputNode = null;
|
|
1805
1760
|
this.#adapterSource = null;
|
|
@@ -1807,21 +1762,17 @@ var KubbDriver = class {
|
|
|
1807
1762
|
[Symbol.dispose]() {
|
|
1808
1763
|
this.dispose();
|
|
1809
1764
|
}
|
|
1810
|
-
#getDefaultResolver = memoize(this.#defaultResolvers, (pluginName) => createResolver({ pluginName }));
|
|
1811
1765
|
/**
|
|
1812
|
-
* Merges `partial`
|
|
1813
|
-
*
|
|
1814
|
-
* get the up-to-date resolver without going through `getResolver()`.
|
|
1766
|
+
* Merges `partial` onto a fresh default resolver and stores the result on `plugin.resolver`,
|
|
1767
|
+
* which is the single source `getResolver` and `getPlugin(name).resolver` both read.
|
|
1815
1768
|
*/
|
|
1816
1769
|
setPluginResolver(pluginName, partial) {
|
|
1817
|
-
const defaultResolver = this.#getDefaultResolver(pluginName);
|
|
1818
|
-
const merged = Resolver.merge(defaultResolver, partial);
|
|
1819
|
-
this.#resolvers.set(pluginName, merged);
|
|
1820
1770
|
const plugin = this.plugins.get(pluginName);
|
|
1821
|
-
if (plugin)
|
|
1771
|
+
if (!plugin) return;
|
|
1772
|
+
plugin.resolver = Resolver.merge(createResolver({ pluginName }), partial);
|
|
1822
1773
|
}
|
|
1823
1774
|
getResolver(pluginName) {
|
|
1824
|
-
return this
|
|
1775
|
+
return this.plugins.get(pluginName)?.resolver ?? createResolver({ pluginName });
|
|
1825
1776
|
}
|
|
1826
1777
|
getContext(plugin) {
|
|
1827
1778
|
const driver = this;
|
|
@@ -1904,6 +1855,73 @@ var KubbDriver = class {
|
|
|
1904
1855
|
}
|
|
1905
1856
|
};
|
|
1906
1857
|
//#endregion
|
|
1858
|
+
//#region src/outputManifest.ts
|
|
1859
|
+
/**
|
|
1860
|
+
* Bumped when the stored shape changes, so an older cache is discarded instead of misread.
|
|
1861
|
+
*/
|
|
1862
|
+
const VERSION = 1;
|
|
1863
|
+
function hash$1(value) {
|
|
1864
|
+
return createHash("sha256").update(value).digest("hex");
|
|
1865
|
+
}
|
|
1866
|
+
__name(hash$1, "hash");
|
|
1867
|
+
const MANIFEST_KEY = "output-manifest.json";
|
|
1868
|
+
async function loadEntries({ cache }) {
|
|
1869
|
+
try {
|
|
1870
|
+
const stored = await cache.readItem(MANIFEST_KEY);
|
|
1871
|
+
if (stored === null) return {};
|
|
1872
|
+
const data = JSON.parse(stored);
|
|
1873
|
+
if (data.version !== VERSION) return {};
|
|
1874
|
+
if (typeof data.entries !== "object" || data.entries === null || Array.isArray(data.entries)) return {};
|
|
1875
|
+
return data.entries;
|
|
1876
|
+
} catch {
|
|
1877
|
+
return {};
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Loads the stored manifest, starting empty when it is missing, unreadable, or from an older
|
|
1882
|
+
* version. `storage` holds the generated files, `cache` holds the manifest itself.
|
|
1883
|
+
*
|
|
1884
|
+
* @example
|
|
1885
|
+
* ```ts
|
|
1886
|
+
* const manifest = await createOutputManifest({ storage: config.storage, cache: cacheStorage({ root: config.root }) })
|
|
1887
|
+
* ```
|
|
1888
|
+
*/
|
|
1889
|
+
async function createOutputManifest({ storage, cache }) {
|
|
1890
|
+
const entries = await loadEntries({ cache });
|
|
1891
|
+
const tracked = /* @__PURE__ */ new Map();
|
|
1892
|
+
return {
|
|
1893
|
+
isUpToDate({ key, source, disk }) {
|
|
1894
|
+
const entry = entries[key];
|
|
1895
|
+
if (!entry) return false;
|
|
1896
|
+
return entry.source === hash$1(source) && entry.output === hash$1(disk);
|
|
1897
|
+
},
|
|
1898
|
+
track({ key, source }) {
|
|
1899
|
+
tracked.set(key, hash$1(source));
|
|
1900
|
+
},
|
|
1901
|
+
async commit() {
|
|
1902
|
+
try {
|
|
1903
|
+
const next = { ...entries };
|
|
1904
|
+
await inParallel({
|
|
1905
|
+
items: [...tracked],
|
|
1906
|
+
limit: 50,
|
|
1907
|
+
run: async ([key, source]) => {
|
|
1908
|
+
const stored = await storage.readItem(key);
|
|
1909
|
+
if (stored === null) return;
|
|
1910
|
+
next[key] = {
|
|
1911
|
+
source,
|
|
1912
|
+
output: hash$1(stored)
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
await cache.writeItem(MANIFEST_KEY, JSON.stringify({
|
|
1917
|
+
version: VERSION,
|
|
1918
|
+
entries: next
|
|
1919
|
+
}));
|
|
1920
|
+
} catch {}
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
//#endregion
|
|
1907
1925
|
//#region src/createStorage.ts
|
|
1908
1926
|
/**
|
|
1909
1927
|
* Defines a custom storage backend. The builder receives user options and
|
|
@@ -1920,23 +1938,23 @@ var KubbDriver = class {
|
|
|
1920
1938
|
*
|
|
1921
1939
|
* return {
|
|
1922
1940
|
* name: 'memory',
|
|
1923
|
-
* async
|
|
1941
|
+
* async existsItem(key) {
|
|
1924
1942
|
* return store.has(key)
|
|
1925
1943
|
* },
|
|
1926
|
-
* async
|
|
1944
|
+
* async readItem(key) {
|
|
1927
1945
|
* return store.get(key) ?? null
|
|
1928
1946
|
* },
|
|
1929
|
-
* async
|
|
1947
|
+
* async writeItem(key, value) {
|
|
1930
1948
|
* store.set(key, value)
|
|
1931
1949
|
* },
|
|
1932
1950
|
* async removeItem(key) {
|
|
1933
1951
|
* store.delete(key)
|
|
1934
1952
|
* },
|
|
1935
|
-
* async
|
|
1953
|
+
* async readKeys(base) {
|
|
1936
1954
|
* const keys = [...store.keys()]
|
|
1937
1955
|
* return base ? keys.filter((k) => k.startsWith(base)) : keys
|
|
1938
1956
|
* },
|
|
1939
|
-
* async
|
|
1957
|
+
* async empty(base) {
|
|
1940
1958
|
* if (!base) store.clear()
|
|
1941
1959
|
* },
|
|
1942
1960
|
* }
|
|
@@ -1980,10 +1998,11 @@ function createLimiter(concurrency) {
|
|
|
1980
1998
|
*
|
|
1981
1999
|
* Writes are deduplicated and directory-safe:
|
|
1982
2000
|
* - leading and trailing whitespace is trimmed before writing
|
|
1983
|
-
* - the write is skipped when the file content
|
|
2001
|
+
* - the write is skipped when the file already holds that content, ignoring any trailing newline
|
|
2002
|
+
* a formatter left behind
|
|
1984
2003
|
* - missing parent directories are created automatically
|
|
1985
2004
|
* - Bun's native file API is used when running under Bun
|
|
1986
|
-
* - concurrent `
|
|
2005
|
+
* - concurrent `writeItem` calls are capped at {@link WRITE_CONCURRENCY} in flight, so a caller
|
|
1987
2006
|
* can fire every file's write without pacing itself
|
|
1988
2007
|
*
|
|
1989
2008
|
* @example
|
|
@@ -2002,7 +2021,7 @@ const fsStorage = createStorage(() => {
|
|
|
2002
2021
|
const limit = createLimiter(WRITE_CONCURRENCY);
|
|
2003
2022
|
return {
|
|
2004
2023
|
name: "fs",
|
|
2005
|
-
async
|
|
2024
|
+
async existsItem(key) {
|
|
2006
2025
|
try {
|
|
2007
2026
|
await access(resolve(key));
|
|
2008
2027
|
return true;
|
|
@@ -2010,20 +2029,20 @@ const fsStorage = createStorage(() => {
|
|
|
2010
2029
|
return false;
|
|
2011
2030
|
}
|
|
2012
2031
|
},
|
|
2013
|
-
async
|
|
2032
|
+
async readItem(key) {
|
|
2014
2033
|
try {
|
|
2015
2034
|
return await readFile(resolve(key), "utf8");
|
|
2016
2035
|
} catch (_error) {
|
|
2017
2036
|
return null;
|
|
2018
2037
|
}
|
|
2019
2038
|
},
|
|
2020
|
-
async
|
|
2039
|
+
async writeItem(key, value) {
|
|
2021
2040
|
await limit(() => write(resolve(key), value, { sanity: false }));
|
|
2022
2041
|
},
|
|
2023
2042
|
async removeItem(key) {
|
|
2024
2043
|
await rm(resolve(key), { force: true });
|
|
2025
2044
|
},
|
|
2026
|
-
async
|
|
2045
|
+
async readKeys(base) {
|
|
2027
2046
|
const resolvedBase = resolve(base ?? process.cwd());
|
|
2028
2047
|
const keys = [];
|
|
2029
2048
|
try {
|
|
@@ -2034,13 +2053,67 @@ const fsStorage = createStorage(() => {
|
|
|
2034
2053
|
} catch (_error) {}
|
|
2035
2054
|
return keys;
|
|
2036
2055
|
},
|
|
2037
|
-
async
|
|
2056
|
+
async empty(base) {
|
|
2038
2057
|
if (!base) return;
|
|
2039
2058
|
await clean(resolve(base));
|
|
2040
2059
|
}
|
|
2041
2060
|
};
|
|
2042
2061
|
});
|
|
2043
2062
|
//#endregion
|
|
2063
|
+
//#region src/storages/cacheStorage.ts
|
|
2064
|
+
/**
|
|
2065
|
+
* Directory Kubb keeps build caches in. A project with a `node_modules` gets
|
|
2066
|
+
* `node_modules/.cache/kubb`, the convention babel and eslint already use, so the cache stays out
|
|
2067
|
+
* of version control. Without one it falls back to the OS temp directory, keyed by root so two
|
|
2068
|
+
* projects sharing that directory keep their own cache.
|
|
2069
|
+
*
|
|
2070
|
+
* @example Inside a project
|
|
2071
|
+
* `resolveCacheDir('/project') // '/project/node_modules/.cache/kubb'`
|
|
2072
|
+
*/
|
|
2073
|
+
function resolveCacheDir(root) {
|
|
2074
|
+
const nodeModules = join(root, "node_modules");
|
|
2075
|
+
if (existsSync(nodeModules)) return join(nodeModules, ".cache", "kubb");
|
|
2076
|
+
return join(tmpdir(), "kubb", createHash("sha256").update(root).digest("hex").slice(0, 16));
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Filesystem storage for build caches rather than generated code. Keys are plain names resolved
|
|
2080
|
+
* inside {@link resolveCacheDir}, so a caller stores `'x.json'` without knowing where the cache
|
|
2081
|
+
* lives. Kept apart from the configured output storage, which may not be a local disk at all.
|
|
2082
|
+
*
|
|
2083
|
+
* @example
|
|
2084
|
+
* ```ts
|
|
2085
|
+
* const cache = cacheStorage({ root: config.root })
|
|
2086
|
+
* await cache.writeItem('output-manifest.json', JSON.stringify(entries))
|
|
2087
|
+
* ```
|
|
2088
|
+
*/
|
|
2089
|
+
const cacheStorage = createStorage(({ root = process.cwd() }) => {
|
|
2090
|
+
const dir = resolveCacheDir(root);
|
|
2091
|
+
const storage = fsStorage();
|
|
2092
|
+
const toPath = (key) => join(dir, key);
|
|
2093
|
+
return {
|
|
2094
|
+
name: "cache",
|
|
2095
|
+
async existsItem(key) {
|
|
2096
|
+
return storage.existsItem(toPath(key));
|
|
2097
|
+
},
|
|
2098
|
+
async readItem(key) {
|
|
2099
|
+
return storage.readItem(toPath(key));
|
|
2100
|
+
},
|
|
2101
|
+
async writeItem(key, value) {
|
|
2102
|
+
return storage.writeItem(toPath(key), value);
|
|
2103
|
+
},
|
|
2104
|
+
async removeItem(key) {
|
|
2105
|
+
return storage.removeItem(toPath(key));
|
|
2106
|
+
},
|
|
2107
|
+
async readKeys(base) {
|
|
2108
|
+
return storage.readKeys(base ? toPath(base) : dir);
|
|
2109
|
+
},
|
|
2110
|
+
async empty(base) {
|
|
2111
|
+
if (!base) return;
|
|
2112
|
+
return storage.empty(toPath(base));
|
|
2113
|
+
}
|
|
2114
|
+
};
|
|
2115
|
+
});
|
|
2116
|
+
//#endregion
|
|
2044
2117
|
//#region src/createKubb.ts
|
|
2045
2118
|
function resolveConfig(userConfig) {
|
|
2046
2119
|
return {
|
|
@@ -2059,6 +2132,13 @@ function resolveConfig(userConfig) {
|
|
|
2059
2132
|
};
|
|
2060
2133
|
}
|
|
2061
2134
|
/**
|
|
2135
|
+
* Whether anything runs over the output directory after the files are written. Only then can the
|
|
2136
|
+
* bytes on disk stop matching what Kubb wrote, which is what the manifest exists to track.
|
|
2137
|
+
*/
|
|
2138
|
+
function hasOutputPasses(output) {
|
|
2139
|
+
return Boolean(output.format || output.lint || output.postGenerate?.length);
|
|
2140
|
+
}
|
|
2141
|
+
/**
|
|
2062
2142
|
* Kubb code-generation instance bound to a single config entry. Resolves the user
|
|
2063
2143
|
* config in the constructor, so `config` is available right away, and shares `hooks`,
|
|
2064
2144
|
* `storage`, and `driver` across the `setup → build` lifecycle.
|
|
@@ -2080,6 +2160,7 @@ var Kubb = class {
|
|
|
2080
2160
|
config;
|
|
2081
2161
|
#driver = null;
|
|
2082
2162
|
#storage = null;
|
|
2163
|
+
#manifest = null;
|
|
2083
2164
|
constructor(userConfig, options = {}) {
|
|
2084
2165
|
this.config = resolveConfig(userConfig);
|
|
2085
2166
|
this.hooks = options.hooks ?? new Hookable();
|
|
@@ -2097,7 +2178,14 @@ var Kubb = class {
|
|
|
2097
2178
|
*/
|
|
2098
2179
|
async setup() {
|
|
2099
2180
|
const config = this.config;
|
|
2100
|
-
const
|
|
2181
|
+
const manifest = hasOutputPasses(config.output) ? await createOutputManifest({
|
|
2182
|
+
storage: config.storage,
|
|
2183
|
+
cache: cacheStorage({ root: config.root })
|
|
2184
|
+
}) : void 0;
|
|
2185
|
+
const driver = new KubbDriver(config, {
|
|
2186
|
+
hooks: this.hooks,
|
|
2187
|
+
manifest
|
|
2188
|
+
});
|
|
2101
2189
|
this.hooks.setMaxListeners(Math.max(10, config.plugins.length * 4));
|
|
2102
2190
|
if (config.output.clean) {
|
|
2103
2191
|
const cleanPath = resolve(config.root, config.output.path);
|
|
@@ -2108,11 +2196,12 @@ var Kubb = class {
|
|
|
2108
2196
|
help: "Point `output.path` at a subdirectory such as `./src/gen` so clean only removes generated code.",
|
|
2109
2197
|
location: { kind: "config" }
|
|
2110
2198
|
});
|
|
2111
|
-
await config.storage.
|
|
2199
|
+
await config.storage.empty(cleanPath);
|
|
2112
2200
|
}
|
|
2113
2201
|
await driver.setup();
|
|
2114
2202
|
this.#driver = driver;
|
|
2115
2203
|
this.#storage = config.storage;
|
|
2204
|
+
this.#manifest = manifest ?? null;
|
|
2116
2205
|
}
|
|
2117
2206
|
/**
|
|
2118
2207
|
* Runs the full pipeline and throws on any plugin error.
|
|
@@ -2151,6 +2240,70 @@ var Kubb = class {
|
|
|
2151
2240
|
_usingCtx$1.d();
|
|
2152
2241
|
}
|
|
2153
2242
|
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Run one build and its output passes end to end, emitting the surrounding `kubb:generation:*`
|
|
2245
|
+
* hooks. Never throws on a build error: the outcome comes back in {@link GenerateResult} so the
|
|
2246
|
+
* host decides how failures surface. Telemetry and progress narration stay with the host, which
|
|
2247
|
+
* reads the result and subscribes to the `kubb:*` hooks.
|
|
2248
|
+
*
|
|
2249
|
+
* @example
|
|
2250
|
+
* ```ts
|
|
2251
|
+
* const result = await createKubb(config, { hooks }).generate()
|
|
2252
|
+
* if (!result.success) process.exitCode = 1
|
|
2253
|
+
* ```
|
|
2254
|
+
*/
|
|
2255
|
+
async generate(options = {}) {
|
|
2256
|
+
const { hooks, config } = this;
|
|
2257
|
+
const hrStart = process.hrtime();
|
|
2258
|
+
await hooks.callHook("kubb:generation:start", { config });
|
|
2259
|
+
await hooks.callHook("kubb:setup:start");
|
|
2260
|
+
await this.setup();
|
|
2261
|
+
await hooks.callHook("kubb:setup:end");
|
|
2262
|
+
const { files, diagnostics, storage } = await this.safeBuild();
|
|
2263
|
+
for (const diagnostic of diagnostics) {
|
|
2264
|
+
if (!Diagnostics.isProblem(diagnostic)) continue;
|
|
2265
|
+
if (diagnostic.code === Diagnostics.code.unknown) {
|
|
2266
|
+
await hooks.callHook("kubb:error", { error: diagnostic.cause ?? new Error(diagnostic.message) });
|
|
2267
|
+
continue;
|
|
2268
|
+
}
|
|
2269
|
+
await Diagnostics.emit(hooks, diagnostic);
|
|
2270
|
+
}
|
|
2271
|
+
if (Diagnostics.hasError(diagnostics)) {
|
|
2272
|
+
await hooks.callHook("kubb:generation:end", {
|
|
2273
|
+
config,
|
|
2274
|
+
storage,
|
|
2275
|
+
diagnostics,
|
|
2276
|
+
filesCreated: files.length,
|
|
2277
|
+
status: "failed",
|
|
2278
|
+
hrStart
|
|
2279
|
+
});
|
|
2280
|
+
return {
|
|
2281
|
+
success: false,
|
|
2282
|
+
files,
|
|
2283
|
+
diagnostics
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
const outputDiagnostics = options.processOutput ? await options.processOutput({
|
|
2287
|
+
config,
|
|
2288
|
+
outputPath: resolve(config.root, config.output.path)
|
|
2289
|
+
}) : [];
|
|
2290
|
+
const finalDiagnostics = [...diagnostics, ...outputDiagnostics];
|
|
2291
|
+
const failed = Diagnostics.hasError(outputDiagnostics);
|
|
2292
|
+
if (!failed) await this.#manifest?.commit();
|
|
2293
|
+
await hooks.callHook("kubb:generation:end", {
|
|
2294
|
+
config,
|
|
2295
|
+
storage,
|
|
2296
|
+
diagnostics: finalDiagnostics,
|
|
2297
|
+
filesCreated: files.length,
|
|
2298
|
+
status: failed ? "failed" : "success",
|
|
2299
|
+
hrStart
|
|
2300
|
+
});
|
|
2301
|
+
return {
|
|
2302
|
+
success: !failed,
|
|
2303
|
+
files,
|
|
2304
|
+
diagnostics: finalDiagnostics
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2154
2307
|
dispose() {
|
|
2155
2308
|
this.#driver?.dispose();
|
|
2156
2309
|
}
|
|
@@ -2391,11 +2544,13 @@ const fileReporter = createReporter({
|
|
|
2391
2544
|
const { diagnostics, config } = result;
|
|
2392
2545
|
if (diagnostics.length === 0) return;
|
|
2393
2546
|
const report = buildReport(result);
|
|
2394
|
-
const
|
|
2547
|
+
const header = config.name ? `# ${config.name} — ${(/* @__PURE__ */ new Date()).toISOString()}` : `# ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
2548
|
+
const sections = [
|
|
2395
2549
|
buildSummarySection(report),
|
|
2396
2550
|
buildProblemSection(diagnostics),
|
|
2397
2551
|
buildTimingSection(report)
|
|
2398
|
-
].filter((section) => section.length > 0)
|
|
2552
|
+
].filter((section) => section.length > 0);
|
|
2553
|
+
const content = stripVTControlCharacters([header, ...sections.map((section) => section.join("\n"))].join("\n\n"));
|
|
2399
2554
|
const baseName = `${[
|
|
2400
2555
|
"kubb",
|
|
2401
2556
|
config.name,
|
|
@@ -2548,23 +2703,23 @@ const memoryStorage = createStorage(() => {
|
|
|
2548
2703
|
const store = /* @__PURE__ */ new Map();
|
|
2549
2704
|
return {
|
|
2550
2705
|
name: "memory",
|
|
2551
|
-
async
|
|
2706
|
+
async existsItem(key) {
|
|
2552
2707
|
return store.has(key);
|
|
2553
2708
|
},
|
|
2554
|
-
async
|
|
2709
|
+
async readItem(key) {
|
|
2555
2710
|
return store.get(key) ?? null;
|
|
2556
2711
|
},
|
|
2557
|
-
async
|
|
2712
|
+
async writeItem(key, value) {
|
|
2558
2713
|
store.set(key, value);
|
|
2559
2714
|
},
|
|
2560
2715
|
async removeItem(key) {
|
|
2561
2716
|
store.delete(key);
|
|
2562
2717
|
},
|
|
2563
|
-
async
|
|
2718
|
+
async readKeys(base) {
|
|
2564
2719
|
const keys = [...store.keys()];
|
|
2565
2720
|
return base ? keys.filter((k) => k.startsWith(base)) : keys;
|
|
2566
2721
|
},
|
|
2567
|
-
async
|
|
2722
|
+
async empty(base) {
|
|
2568
2723
|
if (!base) {
|
|
2569
2724
|
store.clear();
|
|
2570
2725
|
return;
|