@emeryld/rrroutes-export 1.0.3 → 1.0.5

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
@@ -31,20 +31,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  DEFAULT_VIEWER_TEMPLATE: () => DEFAULT_VIEWER_TEMPLATE,
34
+ __private: () => __private,
34
35
  clearSchemaIntrospectionHandlers: () => clearSchemaIntrospectionHandlers,
35
36
  createSchemaIntrospector: () => createSchemaIntrospector,
36
37
  exportFinalizedLeaves: () => exportFinalizedLeaves,
38
+ exportFinalizedLeavesChangelog: () => exportFinalizedLeavesChangelog,
37
39
  extractLeafSourceByAst: () => extractLeafSourceByAst,
38
40
  flattenLeafSchemas: () => flattenLeafSchemas,
39
41
  flattenSerializableSchema: () => flattenSerializableSchema,
40
42
  introspectSchema: () => introspectSchema,
41
43
  loadFinalizedLeavesInput: () => loadFinalizedLeavesInput,
44
+ parseFinalizedLeavesChangelogCliArgs: () => parseFinalizedLeavesChangelogCliArgs,
42
45
  parseFinalizedLeavesCliArgs: () => parseFinalizedLeavesCliArgs,
43
46
  registerSchemaIntrospectionHandler: () => registerSchemaIntrospectionHandler,
47
+ runExportFinalizedLeavesChangelogCli: () => runExportFinalizedLeavesChangelogCli,
44
48
  runExportFinalizedLeavesCli: () => runExportFinalizedLeavesCli,
45
49
  serializableSchemaKinds: () => serializableSchemaKinds,
46
50
  serializeLeafContract: () => serializeLeafContract,
47
51
  serializeLeavesContract: () => serializeLeavesContract,
52
+ writeFinalizedLeavesChangelogExport: () => writeFinalizedLeavesChangelogExport,
48
53
  writeFinalizedLeavesExport: () => writeFinalizedLeavesExport
49
54
  });
50
55
  module.exports = __toCommonJS(index_exports);
@@ -345,46 +350,46 @@ function normalizeType(schema) {
345
350
  return "unknown";
346
351
  }
347
352
  }
348
- function isNonEmptyPath(path4) {
349
- return typeof path4 === "string" && path4.length > 0;
353
+ function isNonEmptyPath(path6) {
354
+ return typeof path6 === "string" && path6.length > 0;
350
355
  }
351
- function setNode(out, path4, schema, inherited) {
352
- if (!isNonEmptyPath(path4)) return;
353
- out[path4] = {
356
+ function setNode(out, path6, schema, inherited) {
357
+ if (!isNonEmptyPath(path6)) return;
358
+ out[path6] = {
354
359
  type: normalizeType(schema),
355
360
  nullable: inherited.nullable || Boolean(schema.nullable),
356
361
  optional: inherited.optional || Boolean(schema.optional),
357
362
  literal: schema.kind === "literal" ? schema.literal : void 0
358
363
  };
359
364
  }
360
- function flattenInto(out, schema, path4, inherited) {
361
- if (!schema || !isNonEmptyPath(path4)) return;
365
+ function flattenInto(out, schema, path6, inherited) {
366
+ if (!schema || !isNonEmptyPath(path6)) return;
362
367
  const nextInherited = {
363
368
  optional: inherited.optional || Boolean(schema.optional),
364
369
  nullable: inherited.nullable || Boolean(schema.nullable)
365
370
  };
366
371
  if (schema.kind === "union" && Array.isArray(schema.union) && schema.union.length > 0) {
367
372
  schema.union.forEach((option, index) => {
368
- const optionPath = `${path4}-${index + 1}`;
373
+ const optionPath = `${path6}-${index + 1}`;
369
374
  flattenInto(out, option, optionPath, inherited);
370
375
  });
371
376
  return;
372
377
  }
373
- setNode(out, path4, schema, inherited);
378
+ setNode(out, path6, schema, inherited);
374
379
  if (schema.kind === "object" && schema.properties) {
375
380
  for (const [key, child] of Object.entries(schema.properties)) {
376
- const childPath = `${path4}.${key}`;
381
+ const childPath = `${path6}.${key}`;
377
382
  flattenInto(out, child, childPath, nextInherited);
378
383
  }
379
384
  return;
380
385
  }
381
386
  if (schema.kind === "array" && schema.element) {
382
- flattenInto(out, schema.element, `${path4}[]`, nextInherited);
387
+ flattenInto(out, schema.element, `${path6}[]`, nextInherited);
383
388
  }
384
389
  }
385
- function flattenSerializableSchema(schema, path4) {
390
+ function flattenSerializableSchema(schema, path6) {
386
391
  const out = {};
387
- flattenInto(out, schema, path4, { optional: false, nullable: false });
392
+ flattenInto(out, schema, path6, { optional: false, nullable: false });
388
393
  return out;
389
394
  }
390
395
  function isSerializedLeaf(value) {
@@ -457,7 +462,27 @@ var DEFAULT_VIEWER_TEMPLATE = `<!doctype html>
457
462
  const resultsEl = document.getElementById('results')
458
463
  const payload = window.__FINALIZED_LEAVES_PAYLOAD
459
464
 
460
- if (!payload || !Array.isArray(payload.leaves)) {
465
+ if (payload && payload.kind === 'finalized-leaves-changelog') {
466
+ statusEl.textContent =
467
+ 'Loaded changelog payload with ' + (payload.commits?.length || 0) + ' commits.'
468
+
469
+ const wrapSection = (title, value) => {
470
+ const card = document.createElement('details')
471
+ const summary = document.createElement('summary')
472
+ summary.textContent = title
473
+ const pre = document.createElement('pre')
474
+ pre.textContent = JSON.stringify(value, null, 2)
475
+ card.appendChild(summary)
476
+ card.appendChild(pre)
477
+ return card
478
+ }
479
+
480
+ resultsEl.appendChild(wrapSection('Commits', payload.commits || []))
481
+ resultsEl.appendChild(wrapSection('By Route', payload.byRoute || {}))
482
+ resultsEl.appendChild(
483
+ wrapSection('By Source Object', payload.bySourceObject || {}),
484
+ )
485
+ } else if (!payload || !Array.isArray(payload.leaves)) {
461
486
  statusEl.textContent = 'No baked payload found in this HTML file.'
462
487
  } else {
463
488
  statusEl.textContent = 'Loaded baked payload with ' + payload.leaves.length + ' routes.'
@@ -1364,23 +1389,866 @@ async function runExportFinalizedLeavesCli(argv) {
1364
1389
  outFile: import_node_path3.default.resolve(args.outFile)
1365
1390
  };
1366
1391
  }
1392
+
1393
+ // src/exportFinalizedLeaves.changelog.ts
1394
+ var import_promises2 = __toESM(require("fs/promises"), 1);
1395
+ var import_node_os = __toESM(require("os"), 1);
1396
+ var import_node_path4 = __toESM(require("path"), 1);
1397
+ var import_node_child_process2 = require("child_process");
1398
+ var import_node_util = require("util");
1399
+ var import_node_url2 = require("url");
1400
+ var execFile = (0, import_node_util.promisify)(import_node_child_process2.execFile);
1401
+ var CHANGESET_CFG_FIELDS = [
1402
+ "summary",
1403
+ "description",
1404
+ "tags",
1405
+ "deprecated",
1406
+ "stability",
1407
+ "docsGroup",
1408
+ "docsHidden",
1409
+ "feed"
1410
+ ];
1411
+ var SCHEMA_SECTIONS = ["params", "query", "body", "output"];
1412
+ var SOURCE_KEY_BY_SECTION = {
1413
+ params: "paramsSchema",
1414
+ query: "querySchema",
1415
+ body: "bodySchema",
1416
+ output: "outputSchema"
1417
+ };
1418
+ function normalizePath(p) {
1419
+ return p.replace(/\\/g, "/");
1420
+ }
1421
+ function stableStringify(value) {
1422
+ return JSON.stringify(value, (_key, v) => {
1423
+ if (v && typeof v === "object" && !Array.isArray(v)) {
1424
+ const sorted = Object.entries(v).sort(([a], [b]) => a.localeCompare(b)).reduce((acc, [k, inner]) => {
1425
+ acc[k] = inner;
1426
+ return acc;
1427
+ }, {});
1428
+ return sorted;
1429
+ }
1430
+ return v;
1431
+ });
1432
+ }
1433
+ function sanitizeCfgValue(value) {
1434
+ if (Array.isArray(value)) {
1435
+ return value.slice().sort((a, b) => String(a).localeCompare(String(b)));
1436
+ }
1437
+ return value;
1438
+ }
1439
+ function toCfgSubset(cfg) {
1440
+ const out = {};
1441
+ for (const field of CHANGESET_CFG_FIELDS) {
1442
+ const value = sanitizeCfgValue(cfg[field]);
1443
+ if (value !== void 0) {
1444
+ out[field] = value;
1445
+ }
1446
+ }
1447
+ return out;
1448
+ }
1449
+ function entrySignature(entry) {
1450
+ return stableStringify({
1451
+ type: entry.type,
1452
+ nullable: Boolean(entry.nullable),
1453
+ optional: Boolean(entry.optional),
1454
+ literal: entry.literal
1455
+ });
1456
+ }
1457
+ function semverToTuple(tag) {
1458
+ const match = tag.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
1459
+ if (!match) return null;
1460
+ return {
1461
+ major: Number(match[1]),
1462
+ minor: Number(match[2]),
1463
+ patch: Number(match[3]),
1464
+ prerelease: match[4] ?? ""
1465
+ };
1466
+ }
1467
+ function compareSemverTags(a, b) {
1468
+ const av = semverToTuple(a);
1469
+ const bv = semverToTuple(b);
1470
+ if (!av && !bv) return 0;
1471
+ if (!av) return -1;
1472
+ if (!bv) return 1;
1473
+ if (av.major !== bv.major) return av.major - bv.major;
1474
+ if (av.minor !== bv.minor) return av.minor - bv.minor;
1475
+ if (av.patch !== bv.patch) return av.patch - bv.patch;
1476
+ if (!av.prerelease && bv.prerelease) return 1;
1477
+ if (av.prerelease && !bv.prerelease) return -1;
1478
+ return av.prerelease.localeCompare(bv.prerelease);
1479
+ }
1480
+ async function runGit(cwd, args) {
1481
+ const { stdout } = await execFile("git", args, { cwd });
1482
+ return stdout.trim();
1483
+ }
1484
+ async function resolveCommit(cwd, rev) {
1485
+ return runGit(cwd, ["rev-parse", "--verify", `${rev}^{commit}`]);
1486
+ }
1487
+ async function findLastReachableSemverTag(cwd, to) {
1488
+ const tagsRaw = await runGit(cwd, ["tag", "--merged", to]);
1489
+ const tags = tagsRaw.split("\n").map((item) => item.trim()).filter(Boolean).filter((tag) => semverToTuple(tag) !== null);
1490
+ if (tags.length === 0) {
1491
+ return void 0;
1492
+ }
1493
+ tags.sort(compareSemverTags);
1494
+ return tags[tags.length - 1];
1495
+ }
1496
+ async function findDefaultFromCommit(cwd, toCommit) {
1497
+ const tag = await findLastReachableSemverTag(cwd, toCommit);
1498
+ if (tag) {
1499
+ return resolveCommit(cwd, tag);
1500
+ }
1501
+ const root = await runGit(cwd, ["rev-list", "--max-parents=0", toCommit]);
1502
+ const rootCommit = root.split("\n").map((item) => item.trim()).filter(Boolean)[0];
1503
+ if (!rootCommit) {
1504
+ throw new Error("Failed to resolve a baseline commit for changelog generation.");
1505
+ }
1506
+ return rootCommit;
1507
+ }
1508
+ async function resolveCommitWindow(cwd, from, to) {
1509
+ const toCommit = await resolveCommit(cwd, to ?? "HEAD");
1510
+ const fromCommit = from ? await resolveCommit(cwd, from) : await findDefaultFromCommit(cwd, toCommit);
1511
+ return { from: fromCommit, to: toCommit };
1512
+ }
1513
+ async function resolveCommitPath(cwd, fromCommit, toCommit) {
1514
+ if (fromCommit === toCommit) {
1515
+ return [toCommit];
1516
+ }
1517
+ const pathRaw = await runGit(cwd, [
1518
+ "rev-list",
1519
+ "--reverse",
1520
+ "--ancestry-path",
1521
+ `${fromCommit}..${toCommit}`
1522
+ ]);
1523
+ const commits = pathRaw.split("\n").map((item) => item.trim()).filter(Boolean);
1524
+ return [fromCommit, ...commits];
1525
+ }
1526
+ function parentDirectories(relativePath) {
1527
+ const clean = normalizePath(relativePath).replace(/^\/+/, "");
1528
+ const dir = import_node_path4.default.posix.dirname(clean);
1529
+ if (!dir || dir === ".") return [];
1530
+ const parts = dir.split("/");
1531
+ const out = [];
1532
+ for (let i = parts.length; i > 0; i -= 1) {
1533
+ out.push(parts.slice(0, i).join("/"));
1534
+ }
1535
+ return out;
1536
+ }
1537
+ function buildRelevantConfigFiles(moduleRel, tsconfigRel) {
1538
+ const dirs = new Set(parentDirectories(moduleRel));
1539
+ if (tsconfigRel) {
1540
+ parentDirectories(tsconfigRel).forEach((item) => dirs.add(item));
1541
+ dirs.add(import_node_path4.default.posix.dirname(normalizePath(tsconfigRel)));
1542
+ }
1543
+ const files = /* @__PURE__ */ new Set(["pnpm-workspace.yaml"]);
1544
+ for (const dir of dirs) {
1545
+ if (!dir || dir === ".") continue;
1546
+ files.add(`${dir}/package.json`);
1547
+ files.add(`${dir}/tsconfig.json`);
1548
+ }
1549
+ return files;
1550
+ }
1551
+ async function commitTouchedRelevantPaths(cwd, commit, moduleRel, tsconfigRel) {
1552
+ const changed = await runGit(cwd, [
1553
+ "show",
1554
+ "--pretty=format:",
1555
+ "--name-only",
1556
+ "--diff-filter=ACMRTUXB",
1557
+ commit
1558
+ ]);
1559
+ const changedFiles = changed.split("\n").map((item) => normalizePath(item.trim()).replace(/^\/+/, "")).filter(Boolean);
1560
+ const moduleFile = normalizePath(moduleRel).replace(/^\/+/, "");
1561
+ const moduleDir = normalizePath(import_node_path4.default.posix.dirname(moduleFile)).replace(/^\/+/, "");
1562
+ const tsconfigFile = tsconfigRel ? normalizePath(tsconfigRel).replace(/^\/+/, "") : void 0;
1563
+ const configFiles = buildRelevantConfigFiles(moduleFile, tsconfigFile);
1564
+ return changedFiles.some((file) => {
1565
+ if (file === moduleFile) return true;
1566
+ if (moduleDir && moduleDir !== "." && (file === moduleDir || file.startsWith(`${moduleDir}/`))) {
1567
+ return true;
1568
+ }
1569
+ if (tsconfigFile && file === tsconfigFile) return true;
1570
+ return configFiles.has(file);
1571
+ });
1572
+ }
1573
+ async function filterCommits(cwd, commits, moduleRel, tsconfigRel) {
1574
+ if (commits.length <= 1) {
1575
+ return commits;
1576
+ }
1577
+ const keep = [commits[0]];
1578
+ for (let i = 1; i < commits.length; i += 1) {
1579
+ const commit = commits[i];
1580
+ if (await commitTouchedRelevantPaths(cwd, commit, moduleRel, tsconfigRel)) {
1581
+ keep.push(commit);
1582
+ }
1583
+ }
1584
+ const tip = commits[commits.length - 1];
1585
+ if (!keep.includes(tip)) {
1586
+ keep.push(tip);
1587
+ }
1588
+ return keep;
1589
+ }
1590
+ async function getCommitMetadata(cwd, commit) {
1591
+ const out = await runGit(cwd, ["show", "-s", "--format=%H%x1f%aI%x1f%s", commit]);
1592
+ const [sha, authorDate, subject] = out.split("");
1593
+ if (!sha || !authorDate) {
1594
+ throw new Error(`Unable to read commit metadata for ${commit}`);
1595
+ }
1596
+ return {
1597
+ sha,
1598
+ authorDate,
1599
+ subject: subject ?? ""
1600
+ };
1601
+ }
1602
+ function buildSourceIdentity(section, value) {
1603
+ const sourceName = typeof value.sourceName === "string" ? value.sourceName : void 0;
1604
+ const tag = typeof value.tag === "string" ? value.tag : void 0;
1605
+ const file = typeof value.file === "string" ? value.file : void 0;
1606
+ const line = typeof value.line === "number" ? value.line : void 0;
1607
+ const column = typeof value.column === "number" ? value.column : void 0;
1608
+ const fallbackName = sourceName ?? tag ?? section;
1609
+ const suffix = file ? `${file}:${line ?? 1}:${column ?? 1}` : `${section}:unknown`;
1610
+ return {
1611
+ id: sourceName ?? `${fallbackName}@${suffix}`,
1612
+ displayName: sourceName ?? `${fallbackName} (${suffix})`,
1613
+ sourceName,
1614
+ tag,
1615
+ file,
1616
+ line,
1617
+ column
1618
+ };
1619
+ }
1620
+ function asRecord(value) {
1621
+ return value && typeof value === "object" ? value : {};
1622
+ }
1623
+ function toRouteSnapshots(payload) {
1624
+ const routes = {};
1625
+ for (const leaf of payload.leaves) {
1626
+ const flatSchema = payload.schemaFlatByLeaf[leaf.key] ?? {};
1627
+ const cfg = toCfgSubset(asRecord(leaf.cfg));
1628
+ const sourceLeaf = payload.sourceByLeaf?.[leaf.key];
1629
+ const sourceSchemas = sourceLeaf?.schemas;
1630
+ const sourceBySection = {};
1631
+ for (const section of SCHEMA_SECTIONS) {
1632
+ const schemaKey = SOURCE_KEY_BY_SECTION[section];
1633
+ const sectionSource = sourceSchemas?.[schemaKey];
1634
+ if (!sectionSource || typeof sectionSource !== "object") continue;
1635
+ sourceBySection[section] = buildSourceIdentity(
1636
+ section,
1637
+ sectionSource
1638
+ );
1639
+ }
1640
+ routes[leaf.key] = {
1641
+ key: leaf.key,
1642
+ method: String(leaf.method).toUpperCase(),
1643
+ path: leaf.path,
1644
+ cfg,
1645
+ flatSchema,
1646
+ sourceBySection
1647
+ };
1648
+ }
1649
+ return routes;
1650
+ }
1651
+ function sectionPathEntries(flatSchema, section) {
1652
+ return Object.entries(flatSchema).filter(
1653
+ ([schemaPath]) => schemaPath === section || schemaPath.startsWith(`${section}.`) || schemaPath.startsWith(`${section}[]`)
1654
+ );
1655
+ }
1656
+ function toSourceObjectSnapshots(routesByKey) {
1657
+ const working = /* @__PURE__ */ new Map();
1658
+ for (const route of Object.values(routesByKey)) {
1659
+ for (const section of SCHEMA_SECTIONS) {
1660
+ const sourceIdentity = route.sourceBySection[section];
1661
+ if (!sourceIdentity) continue;
1662
+ const entries = sectionPathEntries(route.flatSchema, section);
1663
+ if (entries.length === 0) continue;
1664
+ const existing = working.get(sourceIdentity.id) ?? {
1665
+ id: sourceIdentity.id,
1666
+ displayName: sourceIdentity.displayName,
1667
+ routeKeys: /* @__PURE__ */ new Set(),
1668
+ sections: {
1669
+ params: /* @__PURE__ */ new Map(),
1670
+ query: /* @__PURE__ */ new Map(),
1671
+ body: /* @__PURE__ */ new Map(),
1672
+ output: /* @__PURE__ */ new Map()
1673
+ }
1674
+ };
1675
+ existing.routeKeys.add(route.key);
1676
+ for (const [schemaPath, schemaEntry] of entries) {
1677
+ const sectionMap = existing.sections[section];
1678
+ const signatures = sectionMap.get(schemaPath) ?? /* @__PURE__ */ new Set();
1679
+ signatures.add(entrySignature(schemaEntry));
1680
+ sectionMap.set(schemaPath, signatures);
1681
+ }
1682
+ working.set(sourceIdentity.id, existing);
1683
+ }
1684
+ }
1685
+ const out = {};
1686
+ for (const [id, item] of working.entries()) {
1687
+ const sections = {
1688
+ params: {},
1689
+ query: {},
1690
+ body: {},
1691
+ output: {}
1692
+ };
1693
+ for (const section of SCHEMA_SECTIONS) {
1694
+ for (const [schemaPath, signatures] of item.sections[section].entries()) {
1695
+ sections[section][schemaPath] = Array.from(signatures).sort();
1696
+ }
1697
+ }
1698
+ out[id] = {
1699
+ id,
1700
+ displayName: item.displayName,
1701
+ routeKeys: Array.from(item.routeKeys).sort(),
1702
+ sections
1703
+ };
1704
+ }
1705
+ return out;
1706
+ }
1707
+ function diffRouteSchemas(before, after) {
1708
+ const paths = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
1709
+ const out = [];
1710
+ for (const schemaPath of Array.from(paths).sort()) {
1711
+ const prev = before[schemaPath];
1712
+ const next = after[schemaPath];
1713
+ if (!prev && next) {
1714
+ out.push({ path: schemaPath, op: "added", after: next });
1715
+ continue;
1716
+ }
1717
+ if (prev && !next) {
1718
+ out.push({ path: schemaPath, op: "removed", before: prev });
1719
+ continue;
1720
+ }
1721
+ if (!prev || !next) continue;
1722
+ if (entrySignature(prev) !== entrySignature(next)) {
1723
+ out.push({ path: schemaPath, op: "changed", before: prev, after: next });
1724
+ }
1725
+ }
1726
+ return out;
1727
+ }
1728
+ function diffRouteCfg(before, after) {
1729
+ const out = [];
1730
+ for (const field of CHANGESET_CFG_FIELDS) {
1731
+ const prev = before[field];
1732
+ const next = after[field];
1733
+ if (stableStringify(prev) !== stableStringify(next)) {
1734
+ out.push({
1735
+ field,
1736
+ before: prev,
1737
+ after: next
1738
+ });
1739
+ }
1740
+ }
1741
+ return out;
1742
+ }
1743
+ function flattenSourceSections(snapshot) {
1744
+ const map = /* @__PURE__ */ new Map();
1745
+ for (const section of SCHEMA_SECTIONS) {
1746
+ const entries = snapshot.sections[section];
1747
+ for (const [schemaPath, signatures] of Object.entries(entries)) {
1748
+ map.set(`${section}:${schemaPath}`, signatures);
1749
+ }
1750
+ }
1751
+ return map;
1752
+ }
1753
+ function diffSourceSchemas(before, after) {
1754
+ const prev = flattenSourceSections(before);
1755
+ const next = flattenSourceSections(after);
1756
+ const keys = /* @__PURE__ */ new Set([...prev.keys(), ...next.keys()]);
1757
+ const out = [];
1758
+ for (const key of Array.from(keys).sort()) {
1759
+ const prevSignatures = prev.get(key);
1760
+ const nextSignatures = next.get(key);
1761
+ const [section, ...rest] = key.split(":");
1762
+ const schemaPath = rest.join(":");
1763
+ if (!prevSignatures && nextSignatures) {
1764
+ out.push({
1765
+ section,
1766
+ path: schemaPath,
1767
+ op: "added",
1768
+ after: nextSignatures
1769
+ });
1770
+ continue;
1771
+ }
1772
+ if (prevSignatures && !nextSignatures) {
1773
+ out.push({
1774
+ section,
1775
+ path: schemaPath,
1776
+ op: "removed",
1777
+ before: prevSignatures
1778
+ });
1779
+ continue;
1780
+ }
1781
+ if (!prevSignatures || !nextSignatures) continue;
1782
+ if (stableStringify(prevSignatures) !== stableStringify(nextSignatures)) {
1783
+ out.push({
1784
+ section,
1785
+ path: schemaPath,
1786
+ op: "changed",
1787
+ before: prevSignatures,
1788
+ after: nextSignatures
1789
+ });
1790
+ }
1791
+ }
1792
+ return out;
1793
+ }
1794
+ function routeRecordByKey(events) {
1795
+ var _a;
1796
+ const out = {};
1797
+ for (const event of events) {
1798
+ out[_a = event.routeKey] ?? (out[_a] = []);
1799
+ out[event.routeKey].push(event);
1800
+ }
1801
+ return out;
1802
+ }
1803
+ function sourceRecordById(events) {
1804
+ var _a;
1805
+ const out = {};
1806
+ for (const event of events) {
1807
+ out[_a = event.sourceObjectId] ?? (out[_a] = []);
1808
+ out[event.sourceObjectId].push(event);
1809
+ }
1810
+ return out;
1811
+ }
1812
+ function buildRollups(routeEvents, sourceEvents, commits) {
1813
+ var _a, _b;
1814
+ const route = {
1815
+ route_added: 0,
1816
+ route_removed: 0,
1817
+ schema_changed: 0,
1818
+ cfg_changed: 0
1819
+ };
1820
+ const sourceObject = {
1821
+ source_object_added: 0,
1822
+ source_object_removed: 0,
1823
+ source_schema_changed: 0
1824
+ };
1825
+ for (const event of routeEvents) {
1826
+ route[event.type] += 1;
1827
+ }
1828
+ for (const event of sourceEvents) {
1829
+ sourceObject[event.type] += 1;
1830
+ }
1831
+ const byCommit = {};
1832
+ for (const commit of commits) {
1833
+ byCommit[commit.sha] = { routeEvents: 0, sourceEvents: 0 };
1834
+ }
1835
+ for (const event of routeEvents) {
1836
+ byCommit[_a = event.commitSha] ?? (byCommit[_a] = { routeEvents: 0, sourceEvents: 0 });
1837
+ byCommit[event.commitSha].routeEvents += 1;
1838
+ }
1839
+ for (const event of sourceEvents) {
1840
+ byCommit[_b = event.commitSha] ?? (byCommit[_b] = { routeEvents: 0, sourceEvents: 0 });
1841
+ byCommit[event.commitSha].sourceEvents += 1;
1842
+ }
1843
+ return {
1844
+ route,
1845
+ sourceObject,
1846
+ byCommit
1847
+ };
1848
+ }
1849
+ async function loadChangelogInput(modulePath, exportName) {
1850
+ const mod = await import(`${(0, import_node_url2.pathToFileURL)(modulePath).href}?v=${Date.now()}`);
1851
+ const value = mod[exportName] ?? (mod.default && mod.default[exportName]);
1852
+ if (!value) {
1853
+ throw new Error(`Export "${exportName}" not found in module: ${modulePath}`);
1854
+ }
1855
+ return value;
1856
+ }
1857
+ async function createSnapshot(repoRoot, moduleRel, exportName, commit, tempRoot, tsconfigRel, exportOptions) {
1858
+ const worktreeDir = import_node_path4.default.join(tempRoot, `wt-${commit.sha.slice(0, 12)}`);
1859
+ await runGit(repoRoot, ["worktree", "add", "--detach", worktreeDir, commit.sha]);
1860
+ try {
1861
+ const modulePath = import_node_path4.default.resolve(worktreeDir, moduleRel);
1862
+ const tsconfigPath = tsconfigRel ? import_node_path4.default.resolve(worktreeDir, tsconfigRel) : void 0;
1863
+ const input = await loadChangelogInput(modulePath, exportName);
1864
+ const payload = await exportFinalizedLeaves(input, {
1865
+ includeSource: true,
1866
+ sourceModulePath: modulePath,
1867
+ sourceExportName: exportName,
1868
+ tsconfigPath,
1869
+ ...exportOptions
1870
+ });
1871
+ const routesByKey = toRouteSnapshots(payload);
1872
+ const sourceObjectsById = toSourceObjectSnapshots(routesByKey);
1873
+ return {
1874
+ commit,
1875
+ routesByKey,
1876
+ sourceObjectsById
1877
+ };
1878
+ } finally {
1879
+ await runGit(repoRoot, ["worktree", "remove", "--force", worktreeDir]).catch(() => void 0);
1880
+ await import_promises2.default.rm(worktreeDir, { recursive: true, force: true }).catch(() => void 0);
1881
+ }
1882
+ }
1883
+ function diffSnapshots(previous, current) {
1884
+ const routeEvents = [];
1885
+ const sourceEvents = [];
1886
+ const routeKeys = /* @__PURE__ */ new Set([
1887
+ ...Object.keys(previous.routesByKey),
1888
+ ...Object.keys(current.routesByKey)
1889
+ ]);
1890
+ for (const routeKey of Array.from(routeKeys).sort()) {
1891
+ const prevRoute = previous.routesByKey[routeKey];
1892
+ const nextRoute = current.routesByKey[routeKey];
1893
+ if (!prevRoute && nextRoute) {
1894
+ routeEvents.push({
1895
+ type: "route_added",
1896
+ commitSha: current.commit.sha,
1897
+ routeKey,
1898
+ method: nextRoute.method,
1899
+ path: nextRoute.path
1900
+ });
1901
+ continue;
1902
+ }
1903
+ if (prevRoute && !nextRoute) {
1904
+ routeEvents.push({
1905
+ type: "route_removed",
1906
+ commitSha: current.commit.sha,
1907
+ routeKey,
1908
+ method: prevRoute.method,
1909
+ path: prevRoute.path
1910
+ });
1911
+ continue;
1912
+ }
1913
+ if (!prevRoute || !nextRoute) continue;
1914
+ const schemaDiff = diffRouteSchemas(prevRoute.flatSchema, nextRoute.flatSchema);
1915
+ if (schemaDiff.length > 0) {
1916
+ routeEvents.push({
1917
+ type: "schema_changed",
1918
+ commitSha: current.commit.sha,
1919
+ routeKey,
1920
+ method: nextRoute.method,
1921
+ path: nextRoute.path,
1922
+ schemaDiff
1923
+ });
1924
+ }
1925
+ const cfgDiff = diffRouteCfg(prevRoute.cfg, nextRoute.cfg);
1926
+ if (cfgDiff.length > 0) {
1927
+ routeEvents.push({
1928
+ type: "cfg_changed",
1929
+ commitSha: current.commit.sha,
1930
+ routeKey,
1931
+ method: nextRoute.method,
1932
+ path: nextRoute.path,
1933
+ cfgDiff
1934
+ });
1935
+ }
1936
+ }
1937
+ const sourceKeys = /* @__PURE__ */ new Set([
1938
+ ...Object.keys(previous.sourceObjectsById),
1939
+ ...Object.keys(current.sourceObjectsById)
1940
+ ]);
1941
+ for (const sourceObjectId of Array.from(sourceKeys).sort()) {
1942
+ const prevSource = previous.sourceObjectsById[sourceObjectId];
1943
+ const nextSource = current.sourceObjectsById[sourceObjectId];
1944
+ if (!prevSource && nextSource) {
1945
+ sourceEvents.push({
1946
+ type: "source_object_added",
1947
+ commitSha: current.commit.sha,
1948
+ sourceObjectId,
1949
+ displayName: nextSource.displayName,
1950
+ impactedRoutes: nextSource.routeKeys
1951
+ });
1952
+ continue;
1953
+ }
1954
+ if (prevSource && !nextSource) {
1955
+ sourceEvents.push({
1956
+ type: "source_object_removed",
1957
+ commitSha: current.commit.sha,
1958
+ sourceObjectId,
1959
+ displayName: prevSource.displayName,
1960
+ impactedRoutes: prevSource.routeKeys
1961
+ });
1962
+ continue;
1963
+ }
1964
+ if (!prevSource || !nextSource) continue;
1965
+ const schemaDiff = diffSourceSchemas(prevSource, nextSource);
1966
+ const prevRoutes = new Set(prevSource.routeKeys);
1967
+ const nextRoutes = new Set(nextSource.routeKeys);
1968
+ const routeAdded = Array.from(nextRoutes).filter((routeKey) => !prevRoutes.has(routeKey));
1969
+ const routeRemoved = Array.from(prevRoutes).filter((routeKey) => !nextRoutes.has(routeKey));
1970
+ if (schemaDiff.length > 0 || routeAdded.length > 0 || routeRemoved.length > 0) {
1971
+ sourceEvents.push({
1972
+ type: "source_schema_changed",
1973
+ commitSha: current.commit.sha,
1974
+ sourceObjectId,
1975
+ displayName: nextSource.displayName,
1976
+ impactedRoutes: nextSource.routeKeys,
1977
+ schemaDiff: schemaDiff.length > 0 ? schemaDiff : void 0,
1978
+ routeAdded: routeAdded.length > 0 ? routeAdded : void 0,
1979
+ routeRemoved: routeRemoved.length > 0 ? routeRemoved : void 0
1980
+ });
1981
+ }
1982
+ }
1983
+ return {
1984
+ routeEvents,
1985
+ sourceEvents
1986
+ };
1987
+ }
1988
+ var BAKED_PAYLOAD_MARKER2 = "<!--__FINALIZED_LEAVES_BAKED_PAYLOAD__-->";
1989
+ function escapePayloadForInlineScript2(payload) {
1990
+ return JSON.stringify(payload).replace(/<\//g, "<\\/").replace(/<!--/g, "<\\!--");
1991
+ }
1992
+ function injectPayloadIntoViewerHtml2(htmlTemplate, payload) {
1993
+ const payloadScript = `${BAKED_PAYLOAD_MARKER2}
1994
+ <script id="finalized-leaves-baked-payload">window.__FINALIZED_LEAVES_PAYLOAD = ${escapePayloadForInlineScript2(
1995
+ payload
1996
+ )};</script>`;
1997
+ if (htmlTemplate.includes(BAKED_PAYLOAD_MARKER2)) {
1998
+ return htmlTemplate.replace(BAKED_PAYLOAD_MARKER2, payloadScript);
1999
+ }
2000
+ if (htmlTemplate.includes("</body>")) {
2001
+ return htmlTemplate.replace("</body>", `${payloadScript}
2002
+ </body>`);
2003
+ }
2004
+ return `${payloadScript}
2005
+ ${htmlTemplate}`;
2006
+ }
2007
+ async function resolveViewerTemplatePath2(viewerTemplateFile) {
2008
+ if (viewerTemplateFile) {
2009
+ const resolved = import_node_path4.default.resolve(viewerTemplateFile);
2010
+ await import_promises2.default.access(resolved);
2011
+ return resolved;
2012
+ }
2013
+ const candidates = [
2014
+ import_node_path4.default.resolve(
2015
+ process.cwd(),
2016
+ "node_modules/@emeryld/rrroutes-export/tools/finalized-leaves-viewer.html"
2017
+ ),
2018
+ import_node_path4.default.resolve(process.cwd(), "tools/finalized-leaves-viewer.html"),
2019
+ import_node_path4.default.resolve(process.cwd(), "packages/export/tools/finalized-leaves-viewer.html")
2020
+ ];
2021
+ for (const candidate of candidates) {
2022
+ try {
2023
+ await import_promises2.default.access(candidate);
2024
+ return candidate;
2025
+ } catch {
2026
+ }
2027
+ }
2028
+ return void 0;
2029
+ }
2030
+ async function writeJsonExport2(payload, outFile) {
2031
+ const resolved = import_node_path4.default.resolve(outFile);
2032
+ await import_promises2.default.mkdir(import_node_path4.default.dirname(resolved), { recursive: true });
2033
+ await import_promises2.default.writeFile(resolved, `${JSON.stringify(payload, null, 2)}
2034
+ `, "utf8");
2035
+ return resolved;
2036
+ }
2037
+ async function writeBakedHtmlExport2(payload, htmlFile, viewerTemplateFile) {
2038
+ const templatePath = await resolveViewerTemplatePath2(viewerTemplateFile);
2039
+ const template = templatePath ? await import_promises2.default.readFile(templatePath, "utf8") : DEFAULT_VIEWER_TEMPLATE;
2040
+ const baked = injectPayloadIntoViewerHtml2(template, payload);
2041
+ const resolved = import_node_path4.default.resolve(htmlFile);
2042
+ await import_promises2.default.mkdir(import_node_path4.default.dirname(resolved), { recursive: true });
2043
+ await import_promises2.default.writeFile(resolved, baked, "utf8");
2044
+ return resolved;
2045
+ }
2046
+ async function openHtmlInBrowser2(filePath) {
2047
+ const resolved = import_node_path4.default.resolve(filePath);
2048
+ const platform = process.platform;
2049
+ if (platform === "darwin") {
2050
+ (0, import_node_child_process2.spawn)("open", [resolved], { detached: true, stdio: "ignore" }).unref();
2051
+ return;
2052
+ }
2053
+ if (platform === "win32") {
2054
+ (0, import_node_child_process2.spawn)("cmd", ["/c", "start", "", resolved], {
2055
+ detached: true,
2056
+ stdio: "ignore"
2057
+ }).unref();
2058
+ return;
2059
+ }
2060
+ (0, import_node_child_process2.spawn)("xdg-open", [resolved], { detached: true, stdio: "ignore" }).unref();
2061
+ }
2062
+ async function writeFinalizedLeavesChangelogExport(payload, outFileOrOptions) {
2063
+ const options = typeof outFileOrOptions === "string" ? { outFile: outFileOrOptions } : outFileOrOptions;
2064
+ const written = {};
2065
+ if (options.outFile) {
2066
+ written.outFile = await writeJsonExport2(payload, options.outFile);
2067
+ }
2068
+ if (options.htmlFile) {
2069
+ written.htmlFile = await writeBakedHtmlExport2(
2070
+ payload,
2071
+ options.htmlFile,
2072
+ options.viewerTemplateFile
2073
+ );
2074
+ }
2075
+ if (options.openOnFinish) {
2076
+ if (!written.htmlFile) {
2077
+ throw new Error(
2078
+ "openOnFinish requires htmlFile. Provide htmlFile to open the baked viewer."
2079
+ );
2080
+ }
2081
+ await openHtmlInBrowser2(written.htmlFile);
2082
+ }
2083
+ return written;
2084
+ }
2085
+ async function exportFinalizedLeavesChangelog(options) {
2086
+ const cwd = process.cwd();
2087
+ const repoRoot = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
2088
+ const modulePathAbsolute = import_node_path4.default.resolve(cwd, options.modulePath);
2089
+ const modulePathRelative = normalizePath(import_node_path4.default.relative(repoRoot, modulePathAbsolute));
2090
+ if (modulePathRelative.startsWith("..")) {
2091
+ throw new Error("modulePath must resolve inside the git repository root.");
2092
+ }
2093
+ const tsconfigAbsolute = options.tsconfigPath ? import_node_path4.default.resolve(cwd, options.tsconfigPath) : void 0;
2094
+ const tsconfigRelative = tsconfigAbsolute ? normalizePath(import_node_path4.default.relative(repoRoot, tsconfigAbsolute)) : void 0;
2095
+ if (tsconfigRelative && tsconfigRelative.startsWith("..")) {
2096
+ throw new Error("tsconfigPath must resolve inside the git repository root.");
2097
+ }
2098
+ const exportName = options.exportName ?? "leaves";
2099
+ const window = await resolveCommitWindow(repoRoot, options.from, options.to);
2100
+ const allCommits = await resolveCommitPath(repoRoot, window.from, window.to);
2101
+ const selectedCommitShas = await filterCommits(
2102
+ repoRoot,
2103
+ allCommits,
2104
+ modulePathRelative,
2105
+ tsconfigRelative
2106
+ );
2107
+ if (selectedCommitShas.length === 0) {
2108
+ selectedCommitShas.push(window.to);
2109
+ }
2110
+ const commits = [];
2111
+ for (const commit of selectedCommitShas) {
2112
+ commits.push(await getCommitMetadata(repoRoot, commit));
2113
+ }
2114
+ const tempRoot = await import_promises2.default.mkdtemp(import_node_path4.default.join(import_node_os.default.tmpdir(), "rrroutes-export-changelog-"));
2115
+ try {
2116
+ const snapshots = [];
2117
+ for (const commit of commits) {
2118
+ snapshots.push(
2119
+ await createSnapshot(
2120
+ repoRoot,
2121
+ modulePathRelative,
2122
+ exportName,
2123
+ commit,
2124
+ tempRoot,
2125
+ tsconfigRelative,
2126
+ options
2127
+ )
2128
+ );
2129
+ }
2130
+ const routeEvents = [];
2131
+ const sourceEvents = [];
2132
+ for (let i = 1; i < snapshots.length; i += 1) {
2133
+ const { routeEvents: nextRouteEvents, sourceEvents: nextSourceEvents } = diffSnapshots(
2134
+ snapshots[i - 1],
2135
+ snapshots[i]
2136
+ );
2137
+ routeEvents.push(...nextRouteEvents);
2138
+ sourceEvents.push(...nextSourceEvents);
2139
+ }
2140
+ const payload = {
2141
+ kind: "finalized-leaves-changelog",
2142
+ _meta: {
2143
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2144
+ description: "Git-history changelog for RRRoutes finalized leaves with route-level and source-object schema diffs.",
2145
+ modulePath: options.modulePath,
2146
+ exportName,
2147
+ from: window.from,
2148
+ to: window.to,
2149
+ selectedCommitCount: commits.length
2150
+ },
2151
+ commits,
2152
+ byRoute: routeRecordByKey(routeEvents),
2153
+ bySourceObject: sourceRecordById(sourceEvents),
2154
+ rollups: buildRollups(routeEvents, sourceEvents, commits)
2155
+ };
2156
+ if (options.outFile || options.htmlFile) {
2157
+ await writeFinalizedLeavesChangelogExport(payload, {
2158
+ outFile: options.outFile,
2159
+ htmlFile: options.htmlFile,
2160
+ viewerTemplateFile: options.viewerTemplateFile,
2161
+ openOnFinish: options.openOnFinish
2162
+ });
2163
+ }
2164
+ return payload;
2165
+ } finally {
2166
+ await import_promises2.default.rm(tempRoot, { recursive: true, force: true }).catch(() => void 0);
2167
+ }
2168
+ }
2169
+ var __private = {
2170
+ resolveCommitWindow,
2171
+ resolveCommitPath,
2172
+ filterCommits,
2173
+ toRouteSnapshots,
2174
+ toSourceObjectSnapshots,
2175
+ diffSnapshots,
2176
+ buildSourceIdentity
2177
+ };
2178
+
2179
+ // src/exportFinalizedLeaves.changelog.cli.ts
2180
+ var import_node_path5 = __toESM(require("path"), 1);
2181
+ function parseFinalizedLeavesChangelogCliArgs(argv) {
2182
+ const args = /* @__PURE__ */ new Map();
2183
+ for (let i = 0; i < argv.length; i += 1) {
2184
+ const key = argv[i];
2185
+ if (key === "--") continue;
2186
+ if (!key.startsWith("--")) continue;
2187
+ if (key === "--with-source") {
2188
+ continue;
2189
+ }
2190
+ const value = argv[i + 1];
2191
+ if (!value || value.startsWith("--")) {
2192
+ throw new Error(`Missing value for ${key}`);
2193
+ }
2194
+ args.set(key, value);
2195
+ i += 1;
2196
+ }
2197
+ const modulePath = args.get("--module");
2198
+ if (!modulePath) {
2199
+ throw new Error("Missing required --module argument");
2200
+ }
2201
+ return {
2202
+ modulePath,
2203
+ exportName: args.get("--export") ?? "leaves",
2204
+ outFile: args.get("--out") ?? "finalized-leaves.changelog.json",
2205
+ htmlFile: args.get("--html") ?? void 0,
2206
+ tsconfigPath: args.get("--tsconfig") ?? void 0,
2207
+ from: args.get("--from") ?? void 0,
2208
+ to: args.get("--to") ?? void 0
2209
+ };
2210
+ }
2211
+ async function runExportFinalizedLeavesChangelogCli(argv) {
2212
+ const args = parseFinalizedLeavesChangelogCliArgs(argv);
2213
+ const options = {
2214
+ modulePath: args.modulePath,
2215
+ exportName: args.exportName,
2216
+ outFile: args.outFile,
2217
+ htmlFile: args.htmlFile,
2218
+ tsconfigPath: args.tsconfigPath,
2219
+ from: args.from,
2220
+ to: args.to,
2221
+ includeSource: true
2222
+ };
2223
+ const payload = await exportFinalizedLeavesChangelog(options);
2224
+ return {
2225
+ payload,
2226
+ outFile: import_node_path5.default.resolve(args.outFile),
2227
+ htmlFile: args.htmlFile ? import_node_path5.default.resolve(args.htmlFile) : void 0
2228
+ };
2229
+ }
1367
2230
  // Annotate the CommonJS export names for ESM import in node:
1368
2231
  0 && (module.exports = {
1369
2232
  DEFAULT_VIEWER_TEMPLATE,
2233
+ __private,
1370
2234
  clearSchemaIntrospectionHandlers,
1371
2235
  createSchemaIntrospector,
1372
2236
  exportFinalizedLeaves,
2237
+ exportFinalizedLeavesChangelog,
1373
2238
  extractLeafSourceByAst,
1374
2239
  flattenLeafSchemas,
1375
2240
  flattenSerializableSchema,
1376
2241
  introspectSchema,
1377
2242
  loadFinalizedLeavesInput,
2243
+ parseFinalizedLeavesChangelogCliArgs,
1378
2244
  parseFinalizedLeavesCliArgs,
1379
2245
  registerSchemaIntrospectionHandler,
2246
+ runExportFinalizedLeavesChangelogCli,
1380
2247
  runExportFinalizedLeavesCli,
1381
2248
  serializableSchemaKinds,
1382
2249
  serializeLeafContract,
1383
2250
  serializeLeavesContract,
2251
+ writeFinalizedLeavesChangelogExport,
1384
2252
  writeFinalizedLeavesExport
1385
2253
  });
1386
2254
  //# sourceMappingURL=index.cjs.map