@kubb/core 5.11.6 → 5.12.1

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.
@@ -1,11 +1,16 @@
1
1
  import "node:module";
2
2
  import { satisfies } from "verkit";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
- import { glob, mkdir, readFile, rm, writeFile } from "node:fs/promises";
5
- import path, { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import process$1 from "node:process";
5
+ import { x } from "tinyexec";
6
+ import { glob, mkdir, mkdtemp, readFile, rm, rm as rm$1, writeFile } from "node:fs/promises";
7
+ import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
8
  import { existsSync as existsSync$1 } from "node:fs";
7
9
  import { stripVTControlCharacters, styleText } from "node:util";
8
10
  import { ast, collectImportedRefNames, collectUsedSchemaNames, extractStringsFromNodes } from "@kubb/ast";
11
+ import dns from "node:dns";
12
+ import net from "node:net";
13
+ import { tmpdir } from "node:os";
9
14
  //#region \0rolldown/runtime.js
10
15
  var __defProp = Object.defineProperty;
11
16
  var __name = (target, value) => __defProp(target, "name", {
@@ -14,7 +19,7 @@ var __name = (target, value) => __defProp(target, "name", {
14
19
  });
15
20
  //#endregion
16
21
  //#region package.json
17
- var version = "5.11.6";
22
+ var version = "5.12.1";
18
23
  //#endregion
19
24
  //#region ../../internals/utils/src/casing.ts
20
25
  /**
@@ -364,6 +369,12 @@ const diagnosticCatalog = {
364
369
  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.",
365
370
  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
371
  },
372
+ gitInputFailed: {
373
+ code: "KUBB_GIT_INPUT_FAILED",
374
+ title: "Git input failed",
375
+ cause: "A git `input` (`git+<repo>#<ref>:<path>`) is malformed, or `git` could not fetch it: git is not installed, the repository or ref does not exist, or the credentials were refused.",
376
+ fix: "Write the input as `git+<repo>#<ref>:<path>`. Run `git fetch --depth 1 <repo> <ref>` yourself to see the git error, and set up a credential helper or SSH key for a private repository."
377
+ },
367
378
  inputRequired: {
368
379
  code: "KUBB_INPUT_REQUIRED",
369
380
  title: "Input required",
@@ -1234,18 +1245,209 @@ function definePlugin(definition) {
1234
1245
  });
1235
1246
  }
1236
1247
  //#endregion
1248
+ //#region src/publicAddress.ts
1249
+ function createBlockList() {
1250
+ const list = new net.BlockList();
1251
+ for (const [address, prefix] of [
1252
+ ["0.0.0.0", 8],
1253
+ ["10.0.0.0", 8],
1254
+ ["100.64.0.0", 10],
1255
+ ["127.0.0.0", 8],
1256
+ ["169.254.0.0", 16],
1257
+ ["172.16.0.0", 12],
1258
+ ["192.0.0.0", 24],
1259
+ ["192.168.0.0", 16],
1260
+ ["198.18.0.0", 15],
1261
+ ["224.0.0.0", 3]
1262
+ ]) list.addSubnet(address, prefix, "ipv4");
1263
+ for (const [address, prefix] of [
1264
+ ["::", 128],
1265
+ ["::1", 128],
1266
+ ["64:ff9b::", 96],
1267
+ ["fc00::", 7],
1268
+ ["fe80::", 10],
1269
+ ["ff00::", 8]
1270
+ ]) list.addSubnet(address, prefix, "ipv6");
1271
+ return list;
1272
+ }
1273
+ const privateAddresses = createBlockList();
1274
+ /**
1275
+ * Whether `address` is a routable public IP. Loopback, private, link-local, cloud metadata, and
1276
+ * multicast ranges are not, and an IPv4-mapped IPv6 address counts as the IPv4 address it wraps.
1277
+ * Pass `blocked` to replace the ranges.
1278
+ */
1279
+ function isPublicAddress(address, blocked = privateAddresses) {
1280
+ const family = net.isIP(address);
1281
+ return family !== 0 && !blocked.check(address, family === 4 ? "ipv4" : "ipv6");
1282
+ }
1283
+ //#endregion
1284
+ //#region src/gitInput.ts
1285
+ /** Matches a git `input`: the `git+` prefix npm uses for git dependencies. */
1286
+ const gitInputRegExp = /^git\+/i;
1287
+ function gitError(message, cause) {
1288
+ return new Diagnostics.Error({
1289
+ code: Diagnostics.code.gitInputFailed,
1290
+ severity: "error",
1291
+ message,
1292
+ help: "Write a git `input` as `git+<repo>#<ref>:<path>`, for example `git+https://github.com/org/api.git#main:specs/openapi.yaml`. Check that `git` is installed and that `git fetch` can reach the repository with your credentials.",
1293
+ location: { kind: "config" },
1294
+ cause: cause === void 0 ? void 0 : toError(cause)
1295
+ });
1296
+ }
1297
+ /** The repository URL without a user or password, safe to print. */
1298
+ function redact(repo) {
1299
+ if (!URL.canParse(repo)) return repo;
1300
+ const url = new URL(repo);
1301
+ url.username = "";
1302
+ url.password = "";
1303
+ return url.href;
1304
+ }
1305
+ /**
1306
+ * Splits `git+<repo>#<ref>:<path>` into its parts. Git refs cannot contain `:`, so the first one
1307
+ * in the fragment ends the ref. Throws `KUBB_GIT_INPUT_FAILED` when the fragment or path is missing.
1308
+ */
1309
+ function parseGitInput(input) {
1310
+ const hashIndex = input.indexOf("#");
1311
+ const repo = input.slice(4, hashIndex === -1 ? void 0 : hashIndex);
1312
+ const fragment = hashIndex === -1 ? "" : input.slice(hashIndex + 1);
1313
+ const colonIndex = fragment.indexOf(":");
1314
+ const ref = colonIndex === -1 ? void 0 : fragment.slice(0, colonIndex);
1315
+ const path = colonIndex === -1 ? fragment : fragment.slice(colonIndex + 1);
1316
+ if (!repo || !path || ref === "") throw gitError(`The git \`input\` for ${redact(repo)} has no repository, ref, or spec path after \`#\`.`);
1317
+ return {
1318
+ repo,
1319
+ ref,
1320
+ path
1321
+ };
1322
+ }
1323
+ /**
1324
+ * The `git -c` settings that confine a fetch to https and to `addresses`: the connection goes to
1325
+ * the addresses already checked, whatever the host answers to a second lookup, and a redirect to
1326
+ * another host is an error. Needs git 2.37 for the pinning.
1327
+ *
1328
+ * @internal
1329
+ */
1330
+ function publicFetchConfig({ host, port, addresses }) {
1331
+ const pinned = addresses.map((address) => net.isIPv6(address) ? `[${address}]` : address).join(",");
1332
+ const config = [
1333
+ "http.followRedirects=false",
1334
+ "protocol.allow=never",
1335
+ "protocol.https.allow=always"
1336
+ ];
1337
+ if (!net.isIP(host)) config.push(`http.curloptResolve=${host}:${port}:${pinned}`);
1338
+ return config.flatMap((setting) => ["-c", setting]);
1339
+ }
1340
+ async function resolvePublicFetchConfig(repo, signal) {
1341
+ if (!URL.canParse(repo) || new URL(repo).protocol !== "https:") throw gitError("A public git `input` must be an https repository URL, not an ssh, git, or file address.");
1342
+ const url = new URL(repo);
1343
+ const host = url.hostname.replace(/^\[|\]$/g, "");
1344
+ const isLiteral = net.isIP(host) !== 0;
1345
+ const addresses = isLiteral ? [host] : (await dns.promises.lookup(host, { all: true })).map(({ address }) => address);
1346
+ if (!addresses.length || addresses.some((address) => !isPublicAddress(address))) throw gitError(`${host} is not a public address, so this git \`input\` is not fetched.`);
1347
+ if (!isLiteral) {
1348
+ const { stdout } = await x("git", ["--version"], {
1349
+ nodeOptions: { signal },
1350
+ throwOnError: true
1351
+ });
1352
+ const [, major = 0, minor = 0] = /(\d+)\.(\d+)/.exec(stdout)?.map(Number) ?? [];
1353
+ if (major < 2 || major === 2 && minor < 37) throw gitError(`Fetching a public git \`input\` needs git 2.37 or newer, found ${stdout.trim()}.`);
1354
+ }
1355
+ return publicFetchConfig({
1356
+ host,
1357
+ port: url.port || "443",
1358
+ addresses
1359
+ });
1360
+ }
1361
+ /**
1362
+ * Fetches one commit of a git repository into a temporary directory with a shallow `git fetch`,
1363
+ * so any host and the user's own git credentials work, and relative `$ref` files resolve.
1364
+ * Resolves to the spec path on disk and a `dispose` that deletes the checkout.
1365
+ *
1366
+ * With `publicOnly`, the repository must be an https URL on public addresses, the fetch connects to
1367
+ * the addresses checked, and it does not follow a redirect.
1368
+ */
1369
+ async function checkoutGitInput(input, signal, { publicOnly = false } = {}) {
1370
+ const { repo, ref, path } = parseGitInput(input);
1371
+ const fetchConfig = publicOnly ? await resolvePublicFetchConfig(repo, signal) : [];
1372
+ const dir = await mkdtemp(join(tmpdir(), "kubb-git-"));
1373
+ const dispose = () => rm(dir, {
1374
+ recursive: true,
1375
+ force: true
1376
+ });
1377
+ const specPath = resolve(dir, path);
1378
+ const relativePath = relative(dir, specPath);
1379
+ if (relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
1380
+ await dispose();
1381
+ throw gitError(`The spec path "${path}" in the git \`input\` points outside the repository.`);
1382
+ }
1383
+ const label = `${redact(repo)}${ref ? ` at ${ref}` : ""}`;
1384
+ const env = {
1385
+ ...process$1.env,
1386
+ GIT_TERMINAL_PROMPT: "0"
1387
+ };
1388
+ const steps = [
1389
+ ["init", "--quiet"],
1390
+ [
1391
+ "config",
1392
+ "core.symlinks",
1393
+ "false"
1394
+ ],
1395
+ [
1396
+ ...fetchConfig,
1397
+ "fetch",
1398
+ "--quiet",
1399
+ "--depth",
1400
+ "1",
1401
+ "--end-of-options",
1402
+ repo,
1403
+ ref ?? "HEAD"
1404
+ ],
1405
+ [
1406
+ "checkout",
1407
+ "--quiet",
1408
+ "FETCH_HEAD"
1409
+ ]
1410
+ ];
1411
+ try {
1412
+ for (const args of steps) {
1413
+ const result = await x("git", args, {
1414
+ nodeOptions: {
1415
+ cwd: dir,
1416
+ env,
1417
+ signal
1418
+ },
1419
+ throwOnError: false
1420
+ });
1421
+ if (result.exitCode !== 0) {
1422
+ const stderr = result.stderr.replaceAll(repo, redact(repo)).trim();
1423
+ throw gitError(`\`git ${args[0]}\` failed for ${label}: ${stderr || `exit code ${result.exitCode}`}`);
1424
+ }
1425
+ }
1426
+ } catch (error) {
1427
+ await dispose();
1428
+ if (Diagnostics.isError(error)) throw error;
1429
+ throw gitError(`Cannot run \`git\` for ${label}: ${getErrorMessage(error)}`, error);
1430
+ }
1431
+ return {
1432
+ path: specPath,
1433
+ dispose
1434
+ };
1435
+ }
1436
+ //#endregion
1237
1437
  //#region src/input.ts
1238
1438
  /**
1239
1439
  * Classifies an `input` value so callers branch on it once instead of repeating the checks.
1240
1440
  *
1241
1441
  * A non-string is a parsed spec (`object`). A string is `inline` when it holds OpenAPI content,
1242
1442
  * meaning it starts with `{` or `[`, spans multiple lines, or opens with a YAML `openapi:` or
1243
- * `swagger:` key. Otherwise a string is a `url` when it parses as one, or a `file` path.
1443
+ * `swagger:` key. Otherwise a string is `git` when it starts with `git+`, a `url` when it parses as
1444
+ * one, or a `file` path.
1244
1445
  */
1245
1446
  function getInputKind(input) {
1246
1447
  if (typeof input !== "string") return "object";
1247
1448
  const trimmed = input.trimStart();
1248
1449
  if (trimmed.startsWith("{") || trimmed.startsWith("[") || input.includes("\n") || /^(openapi|swagger)\s*:/i.test(trimmed)) return "inline";
1450
+ if (gitInputRegExp.test(input)) return "git";
1249
1451
  if (URL.canParse(input)) return "url";
1250
1452
  return "file";
1251
1453
  }
@@ -1269,8 +1471,9 @@ function isLegacyInput(input) {
1269
1471
  /**
1270
1472
  * Normalizes `config.input` into an `AdapterSource` the adapter can parse.
1271
1473
  *
1272
- * A parsed object and inline content become `{ type: 'data' }`; a URL is kept verbatim and a
1273
- * local path is resolved against `config.root`, both as `{ type: 'path' }`.
1474
+ * A parsed object and inline content become `{ type: 'data' }`; a URL or git input is kept
1475
+ * verbatim and a local path is resolved against `config.root`, all as `{ type: 'path' }`. The
1476
+ * driver checks out a git input before the adapter parses it.
1274
1477
  */
1275
1478
  function inputToAdapterSource(config) {
1276
1479
  const input = config.input;
@@ -1297,7 +1500,7 @@ function inputToAdapterSource(config) {
1297
1500
  type: "data",
1298
1501
  data: input
1299
1502
  };
1300
- if (kind === "url") return {
1503
+ if (kind === "url" || kind === "git") return {
1301
1504
  type: "path",
1302
1505
  path: input
1303
1506
  };
@@ -2231,13 +2434,20 @@ var KubbDriver = class {
2231
2434
  async #parseInput() {
2232
2435
  if (this.inputNode || !this.adapter || !this.#adapterSource) return;
2233
2436
  this.options.signal?.throwIfAborted();
2437
+ const source = this.#adapterSource;
2438
+ const checkout = source.type === "path" && gitInputRegExp.test(source.path) ? await checkoutGitInput(source.path, this.options.signal) : void 0;
2234
2439
  try {
2235
- this.inputNode = await this.adapter.parse(this.#adapterSource, { signal: this.options.signal });
2440
+ this.inputNode = await this.adapter.parse(checkout ? {
2441
+ type: "path",
2442
+ path: checkout.path
2443
+ } : source, { signal: this.options.signal });
2236
2444
  } catch (caughtError) {
2237
2445
  throw new Diagnostics.Error(Diagnostics.fromExtension(caughtError, {
2238
2446
  kind: "adapter",
2239
2447
  extension: this.adapter
2240
2448
  }));
2449
+ } finally {
2450
+ await checkout?.dispose();
2241
2451
  }
2242
2452
  }
2243
2453
  /**
@@ -2856,6 +3066,6 @@ var Hookable = class {
2856
3066
  }
2857
3067
  };
2858
3068
  //#endregion
2859
- export { inParallel as A, __name as B, useOptions as C, Diagnostics as D, resolveMeta as E, rm as F, write as I, writeRaw as L, isPathInside as M, listFiles as N, formatMs as O, read as P, toError as R, useKubb as S, defineExtension as T, setMacros as _, FileManager as a, tryUseKubb as b, Resolver as c, definePlugin as d, addGenerator as f, requirePlugin as g, kubbContext as h, createNodeCache as i, existsSync$1 as j, getElapsedMs as k, getInputKind as l, injectFile as m, KubbDriver as n, _usingCtx as o, addMacro as p, pluginNotFoundError as r, createResolver as s, Hookable as t, assertPluginOption as u, setOptions as v, checkCompatibility as w, useConfig as x, setResolver as y, camelCase as z };
3069
+ export { formatMs as A, toError as B, useConfig as C, defineExtension as D, checkCompatibility as E, listFiles as F, __name as H, read as I, rm$1 as L, inParallel as M, existsSync$1 as N, resolveMeta as O, isPathInside as P, write as R, tryUseKubb as S, useOptions as T, camelCase as V, kubbContext as _, FileManager as a, setOptions as b, Resolver as c, isPublicAddress as d, assertPluginOption as f, injectFile as g, addMacro as h, createNodeCache as i, getElapsedMs as j, Diagnostics as k, getInputKind as l, addGenerator as m, KubbDriver as n, _usingCtx as o, definePlugin as p, pluginNotFoundError as r, createResolver as s, Hookable as t, checkoutGitInput as u, requirePlugin as v, useKubb as w, setResolver as x, setMacros as y, writeRaw as z };
2860
3070
 
2861
- //# sourceMappingURL=Hookable-CvStaYYq.js.map
3071
+ //# sourceMappingURL=Hookable-D3jKB944.js.map