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