@kubb/core 5.0.0-beta.100 → 5.0.0-beta.102

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 CHANGED
@@ -150,48 +150,8 @@ function randomCliColor(text) {
150
150
  return (0, node_util.styleText)(randomColors[index] ?? "white", text);
151
151
  }
152
152
  //#endregion
153
- //#region ../../internals/utils/src/promise.ts
154
- /**
155
- * Wraps `factory` with a keyed cache backed by the provided store.
156
- *
157
- * Pass a `WeakMap` for object keys (results are GC-eligible when the key is
158
- * collected) or a `Map` for primitive keys. For multi-argument functions,
159
- * nest two `memoize` calls — the outer keyed by the first argument, the
160
- * inner (created once per outer miss) keyed by the second.
161
- *
162
- * Because the cache is owned by the caller, it can be shared, inspected, or
163
- * cleared independently of the memoized function.
164
- *
165
- * @example Single WeakMap key
166
- * ```ts
167
- * const cache = new WeakMap<SchemaNode, Set<string>>()
168
- * const getRefs = memoize(cache, (node) => collectRefs(node))
169
- * ```
170
- *
171
- * @example Single Map key (primitive)
172
- * ```ts
173
- * const cache = new Map<string, Resolver>()
174
- * const getResolver = memoize(cache, (name) => buildResolver(name))
175
- * ```
176
- *
177
- * @example Two-level (object + primitive)
178
- * ```ts
179
- * const outer = new WeakMap<Params[], Map<string, Params[]>>()
180
- * const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
181
- * fn(params)('camelcase')
182
- * ```
183
- */
184
- function memoize(store, factory) {
185
- return (key) => {
186
- if (store.has(key)) return store.get(key);
187
- const value = factory(key);
188
- store.set(key, value);
189
- return value;
190
- };
191
- }
192
- //#endregion
193
153
  //#region package.json
194
- var version = "5.0.0-beta.100";
154
+ var version = "5.0.0-beta.102";
195
155
  //#endregion
196
156
  //#region src/constants.ts
197
157
  /**
@@ -1288,6 +1248,38 @@ const ENFORCE_ORDER = {
1288
1248
  post: 1
1289
1249
  };
1290
1250
  const enforceWeight = (plugin) => plugin.enforce ? ENFORCE_ORDER[plugin.enforce] : 0;
1251
+ /**
1252
+ * The options bag a `NormalizedPlugin` starts with before a plugin refines it: a directory output
1253
+ * at the plugin root and empty filter lists.
1254
+ */
1255
+ function defaultPluginOptions() {
1256
+ return {
1257
+ output: {
1258
+ path: ".",
1259
+ mode: "directory"
1260
+ },
1261
+ exclude: [],
1262
+ override: []
1263
+ };
1264
+ }
1265
+ /**
1266
+ * Fills in the `output`, `exclude`, and `override` a `NormalizedPlugin` needs from a plugin's raw
1267
+ * options, running `output` through `normalizeOutput`. Idempotent, so the driver can apply it after
1268
+ * `setOptions` has already run without disturbing an already-normalized bag.
1269
+ */
1270
+ function normalizePluginOptions(rawOptions, pluginName) {
1271
+ const options = {
1272
+ ...defaultPluginOptions(),
1273
+ ...rawOptions ?? {}
1274
+ };
1275
+ const group = "group" in options ? options.group : void 0;
1276
+ options.output = normalizeOutput({
1277
+ output: options.output,
1278
+ group,
1279
+ pluginName
1280
+ });
1281
+ return options;
1282
+ }
1291
1283
  var KubbDriver = class {
1292
1284
  config;
1293
1285
  options;
@@ -1309,13 +1301,6 @@ var KubbDriver = class {
1309
1301
  fileManager = new require_usingCtx.FileManager();
1310
1302
  plugins = /* @__PURE__ */ new Map();
1311
1303
  /**
1312
- * Tracks which plugins have generators registered via `addGenerator()` (hook-based path).
1313
- * Used by the build loop to decide whether to emit generator hooks for a given plugin.
1314
- */
1315
- #hookGeneratorPlugins = /* @__PURE__ */ new Set();
1316
- #resolvers = /* @__PURE__ */ new Map();
1317
- #defaultResolvers = /* @__PURE__ */ new Map();
1318
- /**
1319
1304
  * Removers for every listener the driver added (plugin, generator) so `dispose()` can detach
1320
1305
  * them in one pass. External `hooks.hook(...)` listeners are not tracked.
1321
1306
  */
@@ -1343,15 +1328,8 @@ var KubbDriver = class {
1343
1328
  dependencies: rawPlugin.dependencies,
1344
1329
  enforce: rawPlugin.enforce,
1345
1330
  hooks: rawPlugin.hooks,
1346
- options: rawPlugin.options ?? {
1347
- output: {
1348
- path: ".",
1349
- mode: "directory"
1350
- },
1351
- exclude: [],
1352
- override: []
1353
- },
1354
- resolver: this.#getDefaultResolver(rawPlugin.name)
1331
+ options: rawPlugin.options ?? defaultPluginOptions(),
1332
+ resolver: createResolver({ pluginName: rawPlugin.name })
1355
1333
  };
1356
1334
  }));
1357
1335
  for (const plugin of normalized) {
@@ -1460,46 +1438,30 @@ var KubbDriver = class {
1460
1438
  }
1461
1439
  }
1462
1440
  /**
1463
- * Registers a generator for the given plugin on the shared hook emitter.
1441
+ * Appends a generator to its owning plugin so the generate loop can call it directly.
1464
1442
  *
1465
- * The generator's `schema`, `operation`, and `operations` methods are registered as
1466
- * listeners on `kubb:generate:schema`, `kubb:generate:operation`, and `kubb:generate:operations`
1467
- * respectively. Each listener is scoped to the owning plugin via a `ctx.plugin.name` check
1468
- * so that generators from different plugins do not cross-fire.
1469
- *
1470
- * The renderer comes from `generator.renderer`. Set `generator.renderer = null` (or leave it
1471
- * unset) to opt out of rendering.
1443
+ * The generator's `schema`, `operation`, and `operations` methods run per node during the AST
1444
+ * walk in `#runGenerators`, and their result is routed through `dispatch`. Because a generator is
1445
+ * bound to a plugin, generators from different plugins never cross-fire without a name check. The
1446
+ * renderer comes from `generator.renderer`; set it to `null` (or leave it unset) to opt out of
1447
+ * rendering.
1472
1448
  *
1473
1449
  * Call this method inside `addGenerator()` (in `kubb:plugin:setup`) to wire up a generator.
1474
1450
  */
1475
1451
  registerGenerator(pluginName, generator) {
1476
- const wrap = (method) => {
1477
- if (!method) return void 0;
1478
- return async (node, ctx) => {
1479
- if (ctx.plugin.name !== pluginName) return;
1480
- const result = await method(node, ctx);
1481
- await this.dispatch({
1482
- result,
1483
- renderer: generator.renderer
1484
- });
1485
- };
1486
- };
1487
- this.#unhooks.push(this.hooks.addHooks({
1488
- "kubb:generate:schema": wrap(generator.schema),
1489
- "kubb:generate:operation": wrap(generator.operation),
1490
- "kubb:generate:operations": wrap(generator.operations)
1491
- }));
1492
- this.#hookGeneratorPlugins.add(pluginName);
1452
+ const plugin = this.plugins.get(pluginName);
1453
+ if (!plugin) return;
1454
+ plugin.generators = plugin.generators ? [...plugin.generators, generator] : [generator];
1493
1455
  }
1494
1456
  /**
1495
1457
  * Returns `true` when at least one generator was registered for the given plugin
1496
1458
  * via `addGenerator()` in `kubb:plugin:setup`.
1497
1459
  *
1498
- * Used by the build loop to decide whether to walk the AST and emit generator hooks
1460
+ * Used by the build loop to decide whether to walk the AST and run the generators
1499
1461
  * for a plugin.
1500
1462
  */
1501
1463
  hasHookGenerators(pluginName) {
1502
- return this.#hookGeneratorPlugins.has(pluginName);
1464
+ return (this.plugins.get(pluginName)?.generators?.length ?? 0) > 0;
1503
1465
  }
1504
1466
  /**
1505
1467
  * Runs the full plugin pipeline. Returns the diagnostics collected so far even
@@ -1534,12 +1496,16 @@ var KubbDriver = class {
1534
1496
  const outputRoot = (0, node_path.resolve)(config.root, config.output.path);
1535
1497
  await this.#parseInput();
1536
1498
  await this.setupHooks();
1537
- if (this.adapter && this.inputNode) await hooks.callHook("kubb:build:start", Object.assign({
1538
- config,
1539
- adapter: this.adapter,
1540
- meta: this.inputNode.meta,
1541
- getPlugin: this.getPlugin.bind(this)
1542
- }, this.#filesPayload()));
1499
+ for (const plugin of this.plugins.values()) plugin.options = normalizePluginOptions(plugin.options, plugin.name);
1500
+ if (this.adapter && this.inputNode) {
1501
+ const buildStartContext = this.#withFiles({
1502
+ config,
1503
+ adapter: this.adapter,
1504
+ meta: this.inputNode.meta,
1505
+ getPlugin: this.getPlugin.bind(this)
1506
+ });
1507
+ await hooks.callHook("kubb:build:start", buildStartContext);
1508
+ }
1543
1509
  const generatorPlugins = [];
1544
1510
  for (const plugin of this.plugins.values()) {
1545
1511
  const context = this.getContext(plugin);
@@ -1584,7 +1550,7 @@ var KubbDriver = class {
1584
1550
  });
1585
1551
  }
1586
1552
  diagnostics.push(...await this.#runGenerators(generatorPlugins));
1587
- await hooks.callHook("kubb:plugins:end", Object.assign({ config }, this.#filesPayload()));
1553
+ await hooks.callHook("kubb:plugins:end", this.#withFiles({ config }));
1588
1554
  await fileManager.write(fileManager.files, {
1589
1555
  storage: config.storage,
1590
1556
  parsers: parsersMap
@@ -1603,23 +1569,26 @@ var KubbDriver = class {
1603
1569
  }
1604
1570
  });
1605
1571
  }
1606
- #filesPayload() {
1607
- const driver = this;
1572
+ /**
1573
+ * Widens `extra` with the files present at emit time and a bound `upsertFile`, the shape every
1574
+ * file-carrying hook context shares. Building it here in one place keeps the `files` and
1575
+ * `upsertFile` keys from being dropped by a stray spread at the call site.
1576
+ */
1577
+ #withFiles(extra) {
1608
1578
  return {
1609
- get files() {
1610
- return driver.fileManager.files;
1611
- },
1612
- upsertFile: (...files) => driver.fileManager.upsert(...files)
1579
+ ...extra,
1580
+ files: this.fileManager.files,
1581
+ upsertFile: (...files) => this.fileManager.upsert(...files)
1613
1582
  };
1614
1583
  }
1615
1584
  #emitPluginEnd({ plugin, duration, success, error }) {
1616
- return this.hooks.callHook("kubb:plugin:end", Object.assign({
1585
+ return this.hooks.callHook("kubb:plugin:end", this.#withFiles({
1617
1586
  plugin,
1618
1587
  duration,
1619
1588
  success,
1620
1589
  ...error ? { error } : {},
1621
1590
  config: this.config
1622
- }, this.#filesPayload()));
1591
+ }));
1623
1592
  }
1624
1593
  /**
1625
1594
  * Runs schemas and operations through every plugin's generators. Each node is run
@@ -1656,9 +1625,6 @@ var KubbDriver = class {
1656
1625
  }
1657
1626
  const transforms = this.#transforms;
1658
1627
  const { schemas, operations } = this.inputNode;
1659
- const emitsSchemaHook = this.hooks.listenerCount("kubb:generate:schema") > 0;
1660
- const emitsOperationHook = this.hooks.listenerCount("kubb:generate:operation") > 0;
1661
- const emitsOperationsHook = this.hooks.listenerCount("kubb:generate:operations") > 0;
1662
1628
  const allowedSchemaNamesByPlugin = /* @__PURE__ */ new Map();
1663
1629
  for (const { plugin } of entries) {
1664
1630
  const { exclude, include, override } = plugin.options;
@@ -1680,6 +1646,10 @@ var KubbDriver = class {
1680
1646
  const { exclude, include, override } = plugin.options;
1681
1647
  const optionsAreStatic = !exclude?.length && !include?.length && !override?.length;
1682
1648
  const allowedSchemaNames = allowedSchemaNamesByPlugin.get(plugin.name) ?? null;
1649
+ const generators = plugin.generators ?? [];
1650
+ const schemaGenerators = generators.filter((generator) => generator.schema);
1651
+ const operationGenerators = generators.filter((generator) => generator.operation);
1652
+ const operationsGenerators = generators.filter((generator) => generator.operations);
1683
1653
  let error = null;
1684
1654
  const resolveForPlugin = (node) => {
1685
1655
  const transformedNode = transforms.applyTo(plugin.name, node);
@@ -1699,44 +1669,57 @@ var KubbDriver = class {
1699
1669
  options
1700
1670
  };
1701
1671
  };
1702
- if (emitsSchemaHook) for (const node of schemas) {
1672
+ if (schemaGenerators.length) for (const node of schemas) {
1703
1673
  if (error) break;
1704
1674
  try {
1705
1675
  const resolved = resolveForPlugin(node);
1706
1676
  if (!resolved) continue;
1707
1677
  const { transformedNode, options } = resolved;
1708
1678
  if (allowedSchemaNames !== null && transformedNode.name && !allowedSchemaNames.has(transformedNode.name)) continue;
1709
- await this.hooks.callHook("kubb:generate:schema", transformedNode, {
1679
+ const ctx = {
1710
1680
  ...generatorContext,
1711
1681
  options
1682
+ };
1683
+ for (const generator of schemaGenerators) await this.dispatch({
1684
+ result: await generator.schema(transformedNode, ctx),
1685
+ renderer: generator.renderer
1712
1686
  });
1687
+ await this.hooks.callHook("kubb:generate:schema", transformedNode, ctx);
1713
1688
  } catch (caughtError) {
1714
1689
  error = require_usingCtx.toError(caughtError);
1715
1690
  }
1716
1691
  }
1717
- if (emitsOperationHook) for (const node of operations) {
1692
+ const pluginOperations = [];
1693
+ if (operationGenerators.length || operationsGenerators.length) for (const node of operations) {
1718
1694
  if (error) break;
1719
1695
  try {
1720
1696
  const resolved = resolveForPlugin(node);
1721
1697
  if (!resolved) continue;
1722
- await this.hooks.callHook("kubb:generate:operation", resolved.transformedNode, {
1723
- ...generatorContext,
1724
- options: resolved.options
1725
- });
1698
+ pluginOperations.push(resolved.transformedNode);
1699
+ if (operationGenerators.length) {
1700
+ const ctx = {
1701
+ ...generatorContext,
1702
+ options: resolved.options
1703
+ };
1704
+ for (const generator of operationGenerators) await this.dispatch({
1705
+ result: await generator.operation(resolved.transformedNode, ctx),
1706
+ renderer: generator.renderer
1707
+ });
1708
+ await this.hooks.callHook("kubb:generate:operation", resolved.transformedNode, ctx);
1709
+ }
1726
1710
  } catch (caughtError) {
1727
1711
  error = require_usingCtx.toError(caughtError);
1728
1712
  }
1729
1713
  }
1730
- if (!error && emitsOperationsHook) try {
1714
+ if (!error && operationsGenerators.length) try {
1731
1715
  const ctx = {
1732
1716
  ...generatorContext,
1733
1717
  options: plugin.options
1734
1718
  };
1735
- const pluginOperations = operations.reduce((acc, node) => {
1736
- const resolved = resolveForPlugin(node);
1737
- if (resolved) acc.push(resolved.transformedNode);
1738
- return acc;
1739
- }, []);
1719
+ for (const generator of operationsGenerators) await this.dispatch({
1720
+ result: await generator.operations(pluginOperations, ctx),
1721
+ renderer: generator.renderer
1722
+ });
1740
1723
  await this.hooks.callHook("kubb:generate:operations", pluginOperations, ctx);
1741
1724
  } catch (caughtError) {
1742
1725
  error = require_usingCtx.toError(caughtError);
@@ -1798,10 +1781,7 @@ var KubbDriver = class {
1798
1781
  dispose() {
1799
1782
  for (const unhook of this.#unhooks) unhook();
1800
1783
  this.#unhooks.length = 0;
1801
- this.#hookGeneratorPlugins.clear();
1802
1784
  this.#transforms.dispose();
1803
- this.#resolvers.clear();
1804
- this.#defaultResolvers.clear();
1805
1785
  this.fileManager.dispose();
1806
1786
  this.inputNode = null;
1807
1787
  this.#adapterSource = null;
@@ -1809,21 +1789,17 @@ var KubbDriver = class {
1809
1789
  [Symbol.dispose]() {
1810
1790
  this.dispose();
1811
1791
  }
1812
- #getDefaultResolver = memoize(this.#defaultResolvers, (pluginName) => createResolver({ pluginName }));
1813
1792
  /**
1814
- * Merges `partial` with the plugin's default resolver and stores the result.
1815
- * Also mirrors it onto `plugin.resolver` so callers using `getPlugin(name).resolver`
1816
- * get the up-to-date resolver without going through `getResolver()`.
1793
+ * Merges `partial` onto a fresh default resolver and stores the result on `plugin.resolver`,
1794
+ * which is the single source `getResolver` and `getPlugin(name).resolver` both read.
1817
1795
  */
1818
1796
  setPluginResolver(pluginName, partial) {
1819
- const defaultResolver = this.#getDefaultResolver(pluginName);
1820
- const merged = Resolver.merge(defaultResolver, partial);
1821
- this.#resolvers.set(pluginName, merged);
1822
1797
  const plugin = this.plugins.get(pluginName);
1823
- if (plugin) plugin.resolver = merged;
1798
+ if (!plugin) return;
1799
+ plugin.resolver = Resolver.merge(createResolver({ pluginName }), partial);
1824
1800
  }
1825
1801
  getResolver(pluginName) {
1826
- return this.#resolvers.get(pluginName) ?? this.#getDefaultResolver(pluginName);
1802
+ return this.plugins.get(pluginName)?.resolver ?? createResolver({ pluginName });
1827
1803
  }
1828
1804
  getContext(plugin) {
1829
1805
  const driver = this;
@@ -2153,6 +2129,69 @@ var Kubb = class {
2153
2129
  _usingCtx$1.d();
2154
2130
  }
2155
2131
  }
2132
+ /**
2133
+ * Run one build and its output passes end to end, emitting the surrounding `kubb:generation:*`
2134
+ * hooks. Never throws on a build error: the outcome comes back in {@link GenerateResult} so the
2135
+ * host decides how failures surface. Telemetry and progress narration stay with the host, which
2136
+ * reads the result and subscribes to the `kubb:*` hooks.
2137
+ *
2138
+ * @example
2139
+ * ```ts
2140
+ * const result = await createKubb(config, { hooks }).generate()
2141
+ * if (!result.success) process.exitCode = 1
2142
+ * ```
2143
+ */
2144
+ async generate(options = {}) {
2145
+ const { hooks, config } = this;
2146
+ const hrStart = process.hrtime();
2147
+ await hooks.callHook("kubb:generation:start", { config });
2148
+ await hooks.callHook("kubb:setup:start");
2149
+ await this.setup();
2150
+ await hooks.callHook("kubb:setup:end");
2151
+ const { files, diagnostics, storage } = await this.safeBuild();
2152
+ for (const diagnostic of diagnostics) {
2153
+ if (!Diagnostics.isProblem(diagnostic)) continue;
2154
+ if (diagnostic.code === Diagnostics.code.unknown) {
2155
+ await hooks.callHook("kubb:error", { error: diagnostic.cause ?? new Error(diagnostic.message) });
2156
+ continue;
2157
+ }
2158
+ await Diagnostics.emit(hooks, diagnostic);
2159
+ }
2160
+ if (Diagnostics.hasError(diagnostics)) {
2161
+ await hooks.callHook("kubb:generation:end", {
2162
+ config,
2163
+ storage,
2164
+ diagnostics,
2165
+ filesCreated: files.length,
2166
+ status: "failed",
2167
+ hrStart
2168
+ });
2169
+ return {
2170
+ success: false,
2171
+ files,
2172
+ diagnostics
2173
+ };
2174
+ }
2175
+ const outputDiagnostics = options.processOutput ? await options.processOutput({
2176
+ config,
2177
+ outputPath: (0, node_path.resolve)(config.root, config.output.path)
2178
+ }) : [];
2179
+ const finalDiagnostics = [...diagnostics, ...outputDiagnostics];
2180
+ const failed = Diagnostics.hasError(outputDiagnostics);
2181
+ await hooks.callHook("kubb:generation:end", {
2182
+ config,
2183
+ storage,
2184
+ diagnostics: finalDiagnostics,
2185
+ filesCreated: files.length,
2186
+ status: failed ? "failed" : "success",
2187
+ hrStart
2188
+ });
2189
+ return {
2190
+ success: !failed,
2191
+ files,
2192
+ diagnostics: finalDiagnostics
2193
+ };
2194
+ }
2156
2195
  dispose() {
2157
2196
  this.#driver?.dispose();
2158
2197
  }