@alimaandev/ripple 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -46
- package/dist/bin.js +352 -54
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-LDCFNORX.js → chunk-CFH4UW2E.js} +8 -4
- package/dist/chunk-CFH4UW2E.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1 -1
- package/package.json +13 -6
- package/ripple.schema.json +123 -0
- package/dist/chunk-LDCFNORX.js.map +0 -1
package/dist/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { rippleConfigSchema, DEFAULT_CONFIG, formatSchemaIssues } from './chunk-CFH4UW2E.js';
|
|
3
3
|
import { CommanderError, Command } from 'commander';
|
|
4
4
|
import { createJiti } from 'jiti';
|
|
5
5
|
import path8 from 'path';
|
|
@@ -9,9 +9,9 @@ import { Project, ts, SyntaxKind } from 'ts-morph';
|
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import figures from 'figures';
|
|
11
11
|
import treeify from 'treeify';
|
|
12
|
-
import boxen from 'boxen';
|
|
13
12
|
import fg from 'fast-glob';
|
|
14
13
|
import ora from 'ora';
|
|
14
|
+
import picomatch from 'picomatch';
|
|
15
15
|
import { execFileSync } from 'child_process';
|
|
16
16
|
|
|
17
17
|
// src/output/writer.ts
|
|
@@ -507,7 +507,7 @@ function reverseTraverse(graph, targetKey, options) {
|
|
|
507
507
|
const { maxDepth, categorize } = options;
|
|
508
508
|
const affected = /* @__PURE__ */ new Map();
|
|
509
509
|
const visited = /* @__PURE__ */ new Set([targetKey]);
|
|
510
|
-
const
|
|
510
|
+
const cycleMembers2 = cycleMemberPaths(graph);
|
|
511
511
|
if (!graph.reverse.has(targetKey)) {
|
|
512
512
|
return { affected, maxDepth: 0 };
|
|
513
513
|
}
|
|
@@ -529,7 +529,7 @@ function reverseTraverse(graph, targetKey, options) {
|
|
|
529
529
|
depth: current.depth,
|
|
530
530
|
direct: current.depth === 1,
|
|
531
531
|
categories: categorize(filePath),
|
|
532
|
-
inCycle:
|
|
532
|
+
inCycle: cycleMembers2.has(current.key)
|
|
533
533
|
});
|
|
534
534
|
if (maxDepth === void 0 || current.depth < maxDepth) {
|
|
535
535
|
for (const importer of graph.reverse.get(current.key) ?? []) {
|
|
@@ -786,24 +786,17 @@ function renderTree(nodes) {
|
|
|
786
786
|
}
|
|
787
787
|
return treeify.asTree(root, false, false).trimEnd();
|
|
788
788
|
}
|
|
789
|
-
var DIVIDER_WIDTH = 38;
|
|
790
789
|
function brandHeader(options) {
|
|
791
790
|
const { label, version, style } = options;
|
|
792
791
|
if (!style.color) {
|
|
793
792
|
return `ripple \u2014 ${label}${version ? ` \xB7 v${version}` : ""}`;
|
|
794
793
|
}
|
|
795
|
-
const
|
|
796
|
-
return
|
|
797
|
-
padding: { left: 1, right: 1 },
|
|
798
|
-
title: "ripple",
|
|
799
|
-
borderStyle: "round",
|
|
800
|
-
borderColor: "cyan"
|
|
801
|
-
});
|
|
794
|
+
const title = chalk.bold.cyan(`ripple \xB7 ${label}`);
|
|
795
|
+
return `${title}${version ? ` ${chalk.dim(`\xB7 v${version}`)}` : ""}`;
|
|
802
796
|
}
|
|
803
797
|
function sectionHeader(title, style) {
|
|
804
798
|
if (!style.color) return title;
|
|
805
|
-
return `${chalk.dim("\
|
|
806
|
-
${chalk.bold(title)}`;
|
|
799
|
+
return `${chalk.dim("\u203A")} ${chalk.bold(title)}`;
|
|
807
800
|
}
|
|
808
801
|
|
|
809
802
|
// src/ui/gauge.ts
|
|
@@ -1392,7 +1385,7 @@ function buildGraph(project, filePaths, context) {
|
|
|
1392
1385
|
const reverse = /* @__PURE__ */ new Map();
|
|
1393
1386
|
const external = /* @__PURE__ */ new Map();
|
|
1394
1387
|
let parsedCount = 0;
|
|
1395
|
-
let
|
|
1388
|
+
let internalEdges2 = 0;
|
|
1396
1389
|
let unresolvedEdges = 0;
|
|
1397
1390
|
let externalEdges = 0;
|
|
1398
1391
|
for (const parsed of parsedFiles) {
|
|
@@ -1412,7 +1405,7 @@ function buildGraph(project, filePaths, context) {
|
|
|
1412
1405
|
parsed.imports.map((decl) => decl.raw),
|
|
1413
1406
|
context
|
|
1414
1407
|
);
|
|
1415
|
-
|
|
1408
|
+
internalEdges2 += internal.length;
|
|
1416
1409
|
unresolvedEdges += unresolved.length;
|
|
1417
1410
|
externalEdges += externalSpecifiers.length;
|
|
1418
1411
|
for (const target of internal) {
|
|
@@ -1437,7 +1430,7 @@ function buildGraph(project, filePaths, context) {
|
|
|
1437
1430
|
stats: {
|
|
1438
1431
|
files: parsedFiles.length,
|
|
1439
1432
|
parsed: parsedCount,
|
|
1440
|
-
internalEdges,
|
|
1433
|
+
internalEdges: internalEdges2,
|
|
1441
1434
|
unresolvedEdges,
|
|
1442
1435
|
externalEdges,
|
|
1443
1436
|
cycles: 0
|
|
@@ -1518,6 +1511,150 @@ async function analyzeCommand(fileArg, options, ctx2) {
|
|
|
1518
1511
|
return ExitCode.Success;
|
|
1519
1512
|
}
|
|
1520
1513
|
|
|
1514
|
+
// src/formatter/graph-export.ts
|
|
1515
|
+
var escapeDot = (value) => value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1516
|
+
var escapeMermaid = (value) => value.replace(/#/g, "#35;").replace(/"/g, "#quot;");
|
|
1517
|
+
var escapeHtml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1518
|
+
function sortedKeys(input) {
|
|
1519
|
+
return [...input.nodes.keys()].sort((a, b) => {
|
|
1520
|
+
const left = input.nodes.get(a) ?? a;
|
|
1521
|
+
const right = input.nodes.get(b) ?? b;
|
|
1522
|
+
return left.localeCompare(right);
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
var nodeLabel = (input, key) => displayPath(input.nodes.get(key) ?? key, input.cwd);
|
|
1526
|
+
function cycleMembers(input) {
|
|
1527
|
+
const present = /* @__PURE__ */ new Set();
|
|
1528
|
+
for (const cycle of input.cycles) {
|
|
1529
|
+
for (const member of cycle.members) {
|
|
1530
|
+
if (input.nodes.has(member)) present.add(member);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return [...present].sort((a, b) => nodeLabel(input, a).localeCompare(nodeLabel(input, b)));
|
|
1534
|
+
}
|
|
1535
|
+
function internalEdges(input) {
|
|
1536
|
+
const pairs = [];
|
|
1537
|
+
for (const [source, targets] of input.edges) {
|
|
1538
|
+
if (!input.nodes.has(source)) continue;
|
|
1539
|
+
for (const target of targets) {
|
|
1540
|
+
if (!input.nodes.has(target)) continue;
|
|
1541
|
+
pairs.push([source, target]);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
return pairs.sort((a, b) => {
|
|
1545
|
+
const left = nodeLabel(input, a[0]).localeCompare(nodeLabel(input, b[0]));
|
|
1546
|
+
return left !== 0 ? left : nodeLabel(input, a[1]).localeCompare(nodeLabel(input, b[1]));
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
function renderMermaidGraph(input) {
|
|
1550
|
+
const keys = sortedKeys(input);
|
|
1551
|
+
const idByKey = new Map(keys.map((key, index) => [key, `n${index}`]));
|
|
1552
|
+
const lines = ["flowchart LR"];
|
|
1553
|
+
lines.push(" classDef cycle fill:#4a2525,stroke:#f85149,color:#f0b3b3;");
|
|
1554
|
+
for (const key of keys) {
|
|
1555
|
+
lines.push(` ${idByKey.get(key)}["${escapeMermaid(nodeLabel(input, key))}"]`);
|
|
1556
|
+
}
|
|
1557
|
+
for (const [source, target] of internalEdges(input)) {
|
|
1558
|
+
lines.push(` ${idByKey.get(source)} --> ${idByKey.get(target)}`);
|
|
1559
|
+
}
|
|
1560
|
+
const members = cycleMembers(input).map((key) => idByKey.get(key)).filter((id) => id !== void 0);
|
|
1561
|
+
if (members.length > 0) {
|
|
1562
|
+
lines.push(` class ${members.join(",")} cycle;`);
|
|
1563
|
+
}
|
|
1564
|
+
return `${lines.join("\n")}
|
|
1565
|
+
`;
|
|
1566
|
+
}
|
|
1567
|
+
function renderDotGraph(input) {
|
|
1568
|
+
const lines = [
|
|
1569
|
+
"digraph ripple {",
|
|
1570
|
+
" rankdir=LR;",
|
|
1571
|
+
' node [shape=box, style="rounded,filled", fillcolor="#161b22", color="#30363d", fontcolor="#e6edf3"];',
|
|
1572
|
+
' edge [color="#8b949e"];'
|
|
1573
|
+
];
|
|
1574
|
+
for (const key of sortedKeys(input)) {
|
|
1575
|
+
const label = nodeLabel(input, key);
|
|
1576
|
+
lines.push(` "${escapeDot(label)}" [label="${escapeDot(label)}"];`);
|
|
1577
|
+
}
|
|
1578
|
+
for (const [source, target] of internalEdges(input)) {
|
|
1579
|
+
lines.push(
|
|
1580
|
+
` "${escapeDot(nodeLabel(input, source))}" -> "${escapeDot(nodeLabel(input, target))}";`
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
for (const key of cycleMembers(input)) {
|
|
1584
|
+
const label = nodeLabel(input, key);
|
|
1585
|
+
lines.push(` "${escapeDot(label)}" [color="#f85149", penwidth=1.5];`);
|
|
1586
|
+
}
|
|
1587
|
+
lines.push("}");
|
|
1588
|
+
return `${lines.join("\n")}
|
|
1589
|
+
`;
|
|
1590
|
+
}
|
|
1591
|
+
function renderHtmlGraph(input) {
|
|
1592
|
+
const keys = sortedKeys(input);
|
|
1593
|
+
const edges = internalEdges(input);
|
|
1594
|
+
const members = cycleMembers(input);
|
|
1595
|
+
const cycleLines = input.cycles.map(
|
|
1596
|
+
(cycle) => cycle.path.filter((key) => input.nodes.has(key)).map((key) => escapeHtml(nodeLabel(input, key))).join(" → ")
|
|
1597
|
+
).filter((line) => line.length > 0);
|
|
1598
|
+
const edgeRows = edges.map(
|
|
1599
|
+
([source, target]) => ` <tr><td>${escapeHtml(nodeLabel(input, source))}</td><td>→</td><td>${escapeHtml(nodeLabel(input, target))}</td></tr>`
|
|
1600
|
+
).join("\n");
|
|
1601
|
+
const cycleRows = cycleLines.length > 0 ? cycleLines.map((line) => ` <li><code>${line}</code></li>`).join("\n") : " <li><em>none</em></li>";
|
|
1602
|
+
return `<!DOCTYPE html>
|
|
1603
|
+
<html lang="en">
|
|
1604
|
+
<head>
|
|
1605
|
+
<meta charset="utf-8">
|
|
1606
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1607
|
+
<title>Ripple \u2014 dependency graph</title>
|
|
1608
|
+
<style>
|
|
1609
|
+
:root { color-scheme: dark; }
|
|
1610
|
+
body { margin: 0; padding: 2rem; background: #0d1117; color: #e6edf3;
|
|
1611
|
+
font: 15px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
1612
|
+
h1 { font-size: 1.4rem; margin: 0 0 .25rem; }
|
|
1613
|
+
h2 { font-size: 1.05rem; margin: 2rem 0 .5rem; color: #79c0ff; }
|
|
1614
|
+
code, pre { font-family: SFMono-Regular, Consolas, monospace; }
|
|
1615
|
+
pre { background: #161b22; border: 1px solid #30363d; border-radius: 8px;
|
|
1616
|
+
padding: 1rem; overflow-x: auto; font-size: .85rem; }
|
|
1617
|
+
table { border-collapse: collapse; width: 100%; font-size: .9rem; }
|
|
1618
|
+
th, td { text-align: left; padding: .35rem .6rem; border-bottom: 1px solid #21262d; }
|
|
1619
|
+
th { color: #8b949e; font-weight: 600; }
|
|
1620
|
+
.stats { display: flex; gap: 1.5rem; margin-top: 1rem; }
|
|
1621
|
+
.stat b { display: block; font-size: 1.6rem; color: #79c0ff; }
|
|
1622
|
+
.stat span { color: #8b949e; font-size: .8rem; }
|
|
1623
|
+
.muted { color: #8b949e; }
|
|
1624
|
+
a { color: #79c0ff; }
|
|
1625
|
+
</style>
|
|
1626
|
+
</head>
|
|
1627
|
+
<body>
|
|
1628
|
+
<h1>Ripple dependency graph</h1>
|
|
1629
|
+
<p class="muted">Exported with <code>ripple graph --format html</code> \u2014 deterministic and offline.</p>
|
|
1630
|
+
<div class="stats">
|
|
1631
|
+
<div class="stat"><b>${keys.length}</b><span>files</span></div>
|
|
1632
|
+
<div class="stat"><b>${edges.length}</b><span>edges</span></div>
|
|
1633
|
+
<div class="stat"><b>${input.cycles.length}</b><span>cycle groups</span></div>
|
|
1634
|
+
</div>
|
|
1635
|
+
|
|
1636
|
+
<h2>Circular dependencies</h2>
|
|
1637
|
+
<ul>${cycleRows}</ul>
|
|
1638
|
+
<p class="muted">Cycle members: ${members.map(escapeHtml).join(", ")}</p>
|
|
1639
|
+
|
|
1640
|
+
<h2>Graph</h2>
|
|
1641
|
+
<pre><code class="language-mermaid">${escapeHtml(renderMermaidGraph(input)).trimEnd()}</code></pre>
|
|
1642
|
+
<p class="muted">Render this diagram with the
|
|
1643
|
+
<a href="https://mermaid.live">Mermaid live editor</a> or a tool that
|
|
1644
|
+
understands Mermaid (GitHub markdown, docs sites, IDEs).</p>
|
|
1645
|
+
|
|
1646
|
+
<h2>Edges (${edges.length})</h2>
|
|
1647
|
+
<table>
|
|
1648
|
+
<thead><tr><th>Source</th><th></th><th>Target</th></tr></thead>
|
|
1649
|
+
<tbody>
|
|
1650
|
+
${edgeRows}
|
|
1651
|
+
</tbody>
|
|
1652
|
+
</table>
|
|
1653
|
+
</body>
|
|
1654
|
+
</html>
|
|
1655
|
+
`;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1521
1658
|
// src/commands/graph.ts
|
|
1522
1659
|
function walk(graph, startKey, edges, maxDepth, cwd) {
|
|
1523
1660
|
const seen = /* @__PURE__ */ new Set([startKey]);
|
|
@@ -1539,8 +1676,36 @@ function walk(graph, startKey, edges, maxDepth, cwd) {
|
|
|
1539
1676
|
}
|
|
1540
1677
|
return entries.sort((a, b) => a.depth - b.depth || a.path.localeCompare(b.path)).map((entry) => entry.path);
|
|
1541
1678
|
}
|
|
1679
|
+
function reachableSubgraph(graph, startKey, reverse, maxDepth, cwd) {
|
|
1680
|
+
const edges = reverse ? graph.reverse : graph.forward;
|
|
1681
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
1682
|
+
const visited = /* @__PURE__ */ new Set([startKey]);
|
|
1683
|
+
const queue = [{ key: startKey, depth: 0 }];
|
|
1684
|
+
const collectedEdges = /* @__PURE__ */ new Map();
|
|
1685
|
+
while (queue.length > 0) {
|
|
1686
|
+
const current = queue.shift();
|
|
1687
|
+
if (!current) continue;
|
|
1688
|
+
nodes.set(current.key, graph.nodes.get(current.key)?.path ?? current.key);
|
|
1689
|
+
if (maxDepth !== void 0 && current.depth >= maxDepth) continue;
|
|
1690
|
+
const targets = /* @__PURE__ */ new Set();
|
|
1691
|
+
for (const candidate of edges.get(current.key) ?? []) {
|
|
1692
|
+
targets.add(candidate);
|
|
1693
|
+
if (!visited.has(candidate)) {
|
|
1694
|
+
visited.add(candidate);
|
|
1695
|
+
queue.push({ key: candidate, depth: current.depth + 1 });
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
if (targets.size > 0) collectedEdges.set(current.key, targets);
|
|
1699
|
+
}
|
|
1700
|
+
return { nodes, edges: collectedEdges, cycles: graph.cycles, cwd };
|
|
1701
|
+
}
|
|
1702
|
+
function writeExport(format, input, writer) {
|
|
1703
|
+
const rendered = format === "mermaid" ? renderMermaidGraph(input) : format === "dot" ? renderDotGraph(input) : renderHtmlGraph(input);
|
|
1704
|
+
writer.write(rendered);
|
|
1705
|
+
}
|
|
1542
1706
|
async function graphCommand(fileArg, options, ctx2) {
|
|
1543
|
-
const
|
|
1707
|
+
const format = options.format ?? (options.json ? "json" : "terminal");
|
|
1708
|
+
const tracker = createStageTracker(format === "terminal");
|
|
1544
1709
|
tracker.next("Loading config");
|
|
1545
1710
|
const cwd = ctx2.cwd;
|
|
1546
1711
|
try {
|
|
@@ -1553,9 +1718,24 @@ async function graphCommand(fileArg, options, ctx2) {
|
|
|
1553
1718
|
nodes: graph.nodes,
|
|
1554
1719
|
cwd
|
|
1555
1720
|
};
|
|
1721
|
+
if (format === "mermaid" || format === "dot" || format === "html") {
|
|
1722
|
+
let exportInput;
|
|
1723
|
+
if (fileArg) {
|
|
1724
|
+
const targetPath2 = resolveTargetFile(fileArg, cwd);
|
|
1725
|
+
const { key: key2 } = requireNode(graph, targetPath2, context.rootDir);
|
|
1726
|
+
exportInput = reachableSubgraph(graph, key2, options.reverse, options.depth, cwd);
|
|
1727
|
+
} else {
|
|
1728
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
1729
|
+
for (const [key2, node] of graph.nodes) nodes.set(key2, node.path);
|
|
1730
|
+
exportInput = { nodes, edges: graph.forward, cycles: graph.cycles, cwd };
|
|
1731
|
+
}
|
|
1732
|
+
tracker.done();
|
|
1733
|
+
writeExport(format, exportInput, ctx2.writer);
|
|
1734
|
+
return ExitCode.Success;
|
|
1735
|
+
}
|
|
1556
1736
|
if (!fileArg) {
|
|
1557
1737
|
tracker.done();
|
|
1558
|
-
if (
|
|
1738
|
+
if (format === "json") {
|
|
1559
1739
|
const report = buildGraphJsonReport(input, ctx2.version);
|
|
1560
1740
|
ctx2.writer.write(serializeJson(report));
|
|
1561
1741
|
} else {
|
|
@@ -1577,7 +1757,7 @@ async function graphCommand(fileArg, options, ctx2) {
|
|
|
1577
1757
|
const dependants = walk(graph, key, graph.forward, options.depth, cwd);
|
|
1578
1758
|
const dependents = walk(graph, key, graph.reverse, options.depth, cwd);
|
|
1579
1759
|
tracker.done();
|
|
1580
|
-
if (
|
|
1760
|
+
if (format === "json") {
|
|
1581
1761
|
const report = buildGraphJsonReport(
|
|
1582
1762
|
{
|
|
1583
1763
|
...input,
|
|
@@ -1720,29 +1900,72 @@ function renderResults(results, ctx2, color) {
|
|
|
1720
1900
|
}
|
|
1721
1901
|
ctx2.writer.writeLine();
|
|
1722
1902
|
}
|
|
1723
|
-
var
|
|
1903
|
+
var SCHEMA_REF = "./node_modules/@alimaandev/ripple/ripple.schema.json";
|
|
1904
|
+
function jsonPayload(full) {
|
|
1905
|
+
const payload = full ? { ...DEFAULT_CONFIG } : { include: DEFAULT_CONFIG.include };
|
|
1906
|
+
return { ...payload, $schema: SCHEMA_REF };
|
|
1907
|
+
}
|
|
1908
|
+
function tsValue(value, indent) {
|
|
1909
|
+
if (value === void 0) return "undefined";
|
|
1910
|
+
if (Array.isArray(value)) {
|
|
1911
|
+
if (value.length === 0) return "[]";
|
|
1912
|
+
const items = value.map((item) => tsValue(item, `${indent} `)).join(`,
|
|
1913
|
+
${indent} `);
|
|
1914
|
+
return `[
|
|
1915
|
+
${indent} ${items}
|
|
1916
|
+
${indent}]`;
|
|
1917
|
+
}
|
|
1918
|
+
if (typeof value === "object" && value !== null) {
|
|
1919
|
+
const entries = Object.entries(value).filter(
|
|
1920
|
+
([, entry]) => entry !== void 0
|
|
1921
|
+
);
|
|
1922
|
+
if (entries.length === 0) return "{}";
|
|
1923
|
+
const items = entries.map(([key, entry]) => `${key}: ${tsValue(entry, `${indent} `)}`).join(`,
|
|
1924
|
+
${indent} `);
|
|
1925
|
+
return `{
|
|
1926
|
+
${indent} ${items}
|
|
1927
|
+
${indent}}`;
|
|
1928
|
+
}
|
|
1929
|
+
return JSON.stringify(value);
|
|
1930
|
+
}
|
|
1931
|
+
function tsPayload(full) {
|
|
1932
|
+
if (!full) {
|
|
1933
|
+
return `import type { RippleConfig } from "@alimaandev/ripple";
|
|
1934
|
+
|
|
1935
|
+
const config: RippleConfig = {
|
|
1936
|
+
include: ${JSON.stringify(DEFAULT_CONFIG.include)},
|
|
1937
|
+
};
|
|
1938
|
+
|
|
1939
|
+
export default config;
|
|
1940
|
+
`;
|
|
1941
|
+
}
|
|
1942
|
+
const body = tsValue({ ...DEFAULT_CONFIG }, " ");
|
|
1943
|
+
return `import type { RippleConfig } from "@alimaandev/ripple";
|
|
1944
|
+
|
|
1945
|
+
const config: RippleConfig = ${body};
|
|
1946
|
+
|
|
1947
|
+
export default config;
|
|
1948
|
+
`;
|
|
1949
|
+
}
|
|
1724
1950
|
async function initCommand(options, ctx2) {
|
|
1725
1951
|
const style = { color: resolveColor(options.color) };
|
|
1726
|
-
const
|
|
1952
|
+
const fileName = options.ts ? "ripple.config.ts" : "ripple.config.json";
|
|
1953
|
+
const target = path8.join(ctx2.cwd, fileName);
|
|
1727
1954
|
try {
|
|
1728
1955
|
await fs.access(target);
|
|
1729
1956
|
if (!options.force) {
|
|
1730
1957
|
const mark = style.color ? chalk.red(icon("cross")) : icon("cross");
|
|
1731
|
-
ctx2.writer.writeLine(
|
|
1732
|
-
`${mark} ${INIT_FILENAME} already exists. Pass --force to overwrite it.`
|
|
1733
|
-
);
|
|
1958
|
+
ctx2.writer.writeLine(`${mark} ${fileName} already exists. Pass --force to overwrite it.`);
|
|
1734
1959
|
return ExitCode.Failure;
|
|
1735
1960
|
}
|
|
1736
1961
|
} catch {
|
|
1737
1962
|
}
|
|
1738
|
-
const
|
|
1739
|
-
|
|
1740
|
-
};
|
|
1963
|
+
const content = options.ts ? tsPayload(Boolean(options.full)) : `${JSON.stringify(jsonPayload(Boolean(options.full)), null, 2)}
|
|
1964
|
+
`;
|
|
1741
1965
|
try {
|
|
1742
|
-
await fs.writeFile(target,
|
|
1743
|
-
`, "utf8");
|
|
1966
|
+
await fs.writeFile(target, content, "utf8");
|
|
1744
1967
|
const mark = style.color ? chalk.green(icon("tick")) : icon("tick");
|
|
1745
|
-
ctx2.writer.writeLine(`${mark} Wrote ${
|
|
1968
|
+
ctx2.writer.writeLine(`${mark} Wrote ${fileName}`);
|
|
1746
1969
|
ctx2.writer.writeLine(
|
|
1747
1970
|
` ${dim("Edit it to tune include globs, aliases and risk weights.", style)}`
|
|
1748
1971
|
);
|
|
@@ -1750,7 +1973,7 @@ async function initCommand(options, ctx2) {
|
|
|
1750
1973
|
} catch (error) {
|
|
1751
1974
|
const mark = style.color ? chalk.red(icon("cross")) : icon("cross");
|
|
1752
1975
|
ctx2.writer.writeLine(
|
|
1753
|
-
`${mark} Could not write ${
|
|
1976
|
+
`${mark} Could not write ${fileName}: ${error instanceof Error ? error.message : String(error)}`
|
|
1754
1977
|
);
|
|
1755
1978
|
return ExitCode.Failure;
|
|
1756
1979
|
}
|
|
@@ -1860,6 +2083,12 @@ function countLevels(results) {
|
|
|
1860
2083
|
}
|
|
1861
2084
|
return counts;
|
|
1862
2085
|
}
|
|
2086
|
+
function isAllowedByDiffAllowlist(relPath, patterns) {
|
|
2087
|
+
if (patterns.length === 0) return false;
|
|
2088
|
+
const rel = toPosix(relPath);
|
|
2089
|
+
const matcher = picomatch(patterns.map(toPosix), { dot: true });
|
|
2090
|
+
return matcher(rel);
|
|
2091
|
+
}
|
|
1863
2092
|
function riskySummary(counts, gate) {
|
|
1864
2093
|
const gateIndex = GATE_INDEX2[gate];
|
|
1865
2094
|
const parts = [];
|
|
@@ -1905,9 +2134,16 @@ async function diffCommand(options, ctx2) {
|
|
|
1905
2134
|
);
|
|
1906
2135
|
}
|
|
1907
2136
|
tracker.done();
|
|
1908
|
-
const
|
|
1909
|
-
const
|
|
1910
|
-
const
|
|
2137
|
+
const allowPatterns = [...options.allow ?? [], ...context.config.diff.allow ?? []];
|
|
2138
|
+
const allowedByRel = /* @__PURE__ */ new Map();
|
|
2139
|
+
for (const rel of byRel.keys()) {
|
|
2140
|
+
allowedByRel.set(rel, isAllowedByDiffAllowlist(rel, allowPatterns));
|
|
2141
|
+
}
|
|
2142
|
+
const entries = [...byRel.entries()].map(([rel, result]) => ({ rel, result, allowed: allowedByRel.get(rel) ?? false })).sort((a, b) => b.result.risk.score - a.result.risk.score);
|
|
2143
|
+
const gateResults = entries.filter((entry) => !entry.allowed).map((entry) => entry.result);
|
|
2144
|
+
const counts = countLevels(gateResults);
|
|
2145
|
+
const allowedCount = entries.filter((entry) => entry.allowed).length;
|
|
2146
|
+
const blocked = gateResults.some((result) => LEVEL_INDEX2[result.risk.level] >= GATE_INDEX2[gate]);
|
|
1911
2147
|
const durationMs = Date.now() - started;
|
|
1912
2148
|
if (format === "json") {
|
|
1913
2149
|
ctx2.writer.write(
|
|
@@ -1916,9 +2152,11 @@ async function diffCommand(options, ctx2) {
|
|
|
1916
2152
|
changed.baseLabel,
|
|
1917
2153
|
changed.files,
|
|
1918
2154
|
byRel,
|
|
2155
|
+
allowedByRel,
|
|
1919
2156
|
gate,
|
|
1920
2157
|
blocked,
|
|
1921
2158
|
counts,
|
|
2159
|
+
allowedCount,
|
|
1922
2160
|
durationMs,
|
|
1923
2161
|
ctx2.version
|
|
1924
2162
|
)
|
|
@@ -1929,7 +2167,7 @@ async function diffCommand(options, ctx2) {
|
|
|
1929
2167
|
if (format === "github") {
|
|
1930
2168
|
const annotations = [
|
|
1931
2169
|
...buildFileAnnotations(
|
|
1932
|
-
[...byRel.entries()].map(([file, result]) => ({ file, result })),
|
|
2170
|
+
[...byRel.entries()].filter(([rel]) => !(allowedByRel.get(rel) ?? false)).map(([file, result]) => ({ file, result })),
|
|
1933
2171
|
gate
|
|
1934
2172
|
),
|
|
1935
2173
|
buildGateAnnotation(blocked, gate, counts)
|
|
@@ -1943,9 +2181,10 @@ async function diffCommand(options, ctx2) {
|
|
|
1943
2181
|
cwd: ctx2.cwd,
|
|
1944
2182
|
baseLabel: changed.baseLabel,
|
|
1945
2183
|
files: changed.files,
|
|
1946
|
-
|
|
2184
|
+
entries,
|
|
1947
2185
|
skipped,
|
|
1948
2186
|
counts,
|
|
2187
|
+
allowedCount,
|
|
1949
2188
|
gate,
|
|
1950
2189
|
blocked,
|
|
1951
2190
|
durationMs,
|
|
@@ -1956,12 +2195,25 @@ async function diffCommand(options, ctx2) {
|
|
|
1956
2195
|
return blocked ? ExitCode.Failure : ExitCode.Success;
|
|
1957
2196
|
}
|
|
1958
2197
|
function renderDiffReport(input, writer) {
|
|
1959
|
-
const {
|
|
2198
|
+
const {
|
|
2199
|
+
cwd,
|
|
2200
|
+
baseLabel,
|
|
2201
|
+
files,
|
|
2202
|
+
entries,
|
|
2203
|
+
skipped,
|
|
2204
|
+
counts,
|
|
2205
|
+
allowedCount,
|
|
2206
|
+
gate,
|
|
2207
|
+
blocked,
|
|
2208
|
+
durationMs,
|
|
2209
|
+
style
|
|
2210
|
+
} = input;
|
|
1960
2211
|
writer.writeLine(brandHeader({ label: `diff vs ${baseLabel}`, style }));
|
|
1961
2212
|
writer.writeLine();
|
|
1962
2213
|
const rows = [
|
|
1963
2214
|
["Changed", pluralize(files.length, "file")],
|
|
1964
|
-
["Source", pluralize(
|
|
2215
|
+
["Source", pluralize(entries.length, "file")],
|
|
2216
|
+
["Allowed", pluralize(allowedCount, "file")],
|
|
1965
2217
|
["Duration", dim(formatDuration(durationMs), style)]
|
|
1966
2218
|
];
|
|
1967
2219
|
const width = Math.max(...rows.map(([key]) => key.length));
|
|
@@ -1978,16 +2230,17 @@ function renderDiffReport(input, writer) {
|
|
|
1978
2230
|
`${markText} ${verdictText} \u2014 ${riskySummary(counts, gate)} ${dim(`(${VERDICT_NOTE[gate]})`, style)}`
|
|
1979
2231
|
);
|
|
1980
2232
|
writer.writeLine();
|
|
1981
|
-
writer.writeLine(sectionHeader(`Risk analysis (${
|
|
1982
|
-
for (const result of
|
|
2233
|
+
writer.writeLine(sectionHeader(`Risk analysis (${entries.length} files)`, style));
|
|
2234
|
+
for (const { result, allowed } of entries) {
|
|
1983
2235
|
const risky = LEVEL_INDEX2[result.risk.level] >= GATE_INDEX2[gate];
|
|
1984
2236
|
const fileIcon = risky ? icon("cross") : icon("tick");
|
|
1985
2237
|
const fileIconText = style.color ? risky ? chalk.red(fileIcon) : chalk.green(fileIcon) : fileIcon;
|
|
1986
2238
|
const score = result.risk.score.toFixed(1);
|
|
1987
2239
|
const gauge = riskGauge(result.risk.score, result.risk.level, style);
|
|
1988
2240
|
const file = displayPath(result.targetPath, cwd);
|
|
2241
|
+
const allowedTag = allowed ? ` ${dim("(allowed)", style)}` : "";
|
|
1989
2242
|
writer.writeLine(
|
|
1990
|
-
`${fileIconText} ${file} ${riskBadge(result.risk.level, style)} \xB7 ${score}/100${gauge ? ` ${gauge}` : ""}`
|
|
2243
|
+
`${fileIconText} ${file} ${riskBadge(result.risk.level, style)} \xB7 ${score}/100${gauge ? ` ${gauge}` : ""}${allowedTag}`
|
|
1991
2244
|
);
|
|
1992
2245
|
}
|
|
1993
2246
|
for (const rel of skipped) {
|
|
@@ -1997,7 +2250,7 @@ function renderDiffReport(input, writer) {
|
|
|
1997
2250
|
}
|
|
1998
2251
|
writer.writeLine();
|
|
1999
2252
|
}
|
|
2000
|
-
function buildDiffJsonReport(baseLabel, changedFileNames, byRel, gate, blocked, counts, durationMs, version) {
|
|
2253
|
+
function buildDiffJsonReport(baseLabel, changedFileNames, byRel, allowedByRel, gate, blocked, counts, allowedCount, durationMs, version) {
|
|
2001
2254
|
const files = [];
|
|
2002
2255
|
for (const rel of changedFileNames) {
|
|
2003
2256
|
const result = byRel.get(rel);
|
|
@@ -2007,7 +2260,8 @@ function buildDiffJsonReport(baseLabel, changedFileNames, byRel, gate, blocked,
|
|
|
2007
2260
|
analyzed: true,
|
|
2008
2261
|
risk: result.risk,
|
|
2009
2262
|
affectedFiles: result.summary.affectedFiles,
|
|
2010
|
-
targetInCycle: result.targetInCycle
|
|
2263
|
+
targetInCycle: result.targetInCycle,
|
|
2264
|
+
allowed: allowedByRel.get(rel) ?? false
|
|
2011
2265
|
});
|
|
2012
2266
|
} else {
|
|
2013
2267
|
files.push({ file: rel, analyzed: false });
|
|
@@ -2021,16 +2275,29 @@ function buildDiffJsonReport(baseLabel, changedFileNames, byRel, gate, blocked,
|
|
|
2021
2275
|
base: baseLabel,
|
|
2022
2276
|
changedFiles: changedFileNames.length,
|
|
2023
2277
|
files,
|
|
2024
|
-
gate: { level: gate, blocked, counts },
|
|
2278
|
+
gate: { level: gate, blocked, counts, allowed: allowedCount },
|
|
2025
2279
|
durationMs
|
|
2026
2280
|
};
|
|
2027
2281
|
}
|
|
2028
2282
|
|
|
2029
2283
|
// src/cli/program.ts
|
|
2030
2284
|
var PROGRAM_NAME = "ripple";
|
|
2285
|
+
function helpExamples(examples) {
|
|
2286
|
+
const lines = examples.map((example) => ` ${example}`);
|
|
2287
|
+
return `
|
|
2288
|
+
Examples:
|
|
2289
|
+
${lines.join("\n")}
|
|
2290
|
+
`;
|
|
2291
|
+
}
|
|
2031
2292
|
function createProgram(ctx2) {
|
|
2032
2293
|
const program = new Command().name(PROGRAM_NAME).description("Dependency impact analysis for TypeScript and JavaScript projects.").version(ctx2.version, "-V, --version", "print the Ripple version").showHelpAfterError();
|
|
2033
|
-
program.command("analyze <file>").description("Analyze the impact of changing a file").
|
|
2294
|
+
program.command("analyze <file>").description("Analyze the impact of changing a file").addHelpText(
|
|
2295
|
+
"after",
|
|
2296
|
+
helpExamples([
|
|
2297
|
+
"ripple analyze src/authentication/login.ts",
|
|
2298
|
+
"ripple analyze src/index.ts --depth 3"
|
|
2299
|
+
])
|
|
2300
|
+
).option("-j, --json", "emit a machine-readable JSON report").option("-v, --verbose", "include the risk factor breakdown").option("-d, --depth <number>", "cap the reverse traversal depth", parsePositiveInt).option("-c, --config <path>", "path to a ripple config file").option("--no-color", "disable ANSI colors").action(async (file, options) => {
|
|
2034
2301
|
const exitCode2 = await analyzeCommand(
|
|
2035
2302
|
file,
|
|
2036
2303
|
{
|
|
@@ -2046,11 +2313,23 @@ function createProgram(ctx2) {
|
|
|
2046
2313
|
throw new ExitError(exitCode2);
|
|
2047
2314
|
}
|
|
2048
2315
|
});
|
|
2049
|
-
program.command("graph [file]").description("Show project graph stats, or the dependency tree of one file").
|
|
2316
|
+
program.command("graph [file]").description("Show project graph stats, or the dependency tree of one file").addHelpText(
|
|
2317
|
+
"after",
|
|
2318
|
+
helpExamples([
|
|
2319
|
+
"ripple graph",
|
|
2320
|
+
"ripple graph src/index.ts --reverse --depth 3",
|
|
2321
|
+
"ripple graph --format mermaid"
|
|
2322
|
+
])
|
|
2323
|
+
).option("-j, --json", "emit a machine-readable JSON report").option(
|
|
2324
|
+
"-f, --format <format>",
|
|
2325
|
+
"output format: terminal | json | mermaid | dot | html",
|
|
2326
|
+
parseGraphFormat
|
|
2327
|
+
).option("-v, --verbose", "include extra sections").option("-r, --reverse", "show dependents (what imports the file) instead of dependants").option("-d, --depth <number>", "cap the tree depth", parsePositiveInt).option("-c, --config <path>", "path to a ripple config file").option("--no-color", "disable ANSI colors").action(async (file, options) => {
|
|
2050
2328
|
const exitCode2 = await graphCommand(
|
|
2051
2329
|
file,
|
|
2052
2330
|
{
|
|
2053
2331
|
json: Boolean(options.json),
|
|
2332
|
+
format: options.format ?? (options.json ? "json" : "terminal"),
|
|
2054
2333
|
verbose: Boolean(options.verbose),
|
|
2055
2334
|
reverse: Boolean(options.reverse),
|
|
2056
2335
|
...options.depth !== void 0 ? { depth: options.depth } : {},
|
|
@@ -2063,11 +2342,14 @@ function createProgram(ctx2) {
|
|
|
2063
2342
|
throw new ExitError(exitCode2);
|
|
2064
2343
|
}
|
|
2065
2344
|
});
|
|
2066
|
-
program.command("diff").description("Analyze changed files since a base ref and gate risky changes").
|
|
2345
|
+
program.command("diff").description("Analyze changed files since a base ref and gate risky changes").addHelpText(
|
|
2346
|
+
"after",
|
|
2347
|
+
helpExamples(["ripple diff --base main --gate critical", "ripple diff --format github"])
|
|
2348
|
+
).option("-j, --json", "emit a machine-readable JSON report").option("-f, --format <format>", "output format: terminal | json | github", parseDiffFormat).option("-v, --verbose", "include extra sections").option("-b, --base <ref>", "git ref to diff against (default: origin/main, main, HEAD~1)").option(
|
|
2067
2349
|
"-g, --gate <level>",
|
|
2068
2350
|
"risk level that blocks the gate (medium | high | critical)",
|
|
2069
2351
|
parseGateLevel
|
|
2070
|
-
).option("-d, --depth <number>", "cap the reverse traversal depth", parsePositiveInt).option("-c, --config <path>", "path to a ripple config file").option("--no-color", "disable ANSI colors").action(async (options) => {
|
|
2352
|
+
).option("-d, --depth <number>", "cap the reverse traversal depth", parsePositiveInt).option("-a, --allow <glob...>", "changed files matching these globs never block the gate").option("-c, --config <path>", "path to a ripple config file").option("--no-color", "disable ANSI colors").action(async (options) => {
|
|
2071
2353
|
const exitCode2 = await diffCommand(
|
|
2072
2354
|
{
|
|
2073
2355
|
json: Boolean(options.json),
|
|
@@ -2076,6 +2358,7 @@ function createProgram(ctx2) {
|
|
|
2076
2358
|
...options.base !== void 0 ? { base: options.base } : {},
|
|
2077
2359
|
...options.gate !== void 0 ? { gate: options.gate } : {},
|
|
2078
2360
|
...options.depth !== void 0 ? { depth: options.depth } : {},
|
|
2361
|
+
...options.allow !== void 0 ? { allow: options.allow } : {},
|
|
2079
2362
|
...options.config !== void 0 ? { config: options.config } : {},
|
|
2080
2363
|
color: options.color
|
|
2081
2364
|
},
|
|
@@ -2085,7 +2368,7 @@ function createProgram(ctx2) {
|
|
|
2085
2368
|
throw new ExitError(exitCode2);
|
|
2086
2369
|
}
|
|
2087
2370
|
});
|
|
2088
|
-
program.command("doctor").description("Check the project and environment for Ripple readiness").option("-v, --verbose", "print detailed diagnostics").option("-c, --config <path>", "path to a ripple config file").option("--no-color", "disable ANSI colors").action(async (options) => {
|
|
2371
|
+
program.command("doctor").description("Check the project and environment for Ripple readiness").addHelpText("after", helpExamples(["ripple doctor", "ripple doctor -v"])).option("-v, --verbose", "print detailed diagnostics").option("-c, --config <path>", "path to a ripple config file").option("--no-color", "disable ANSI colors").action(async (options) => {
|
|
2089
2372
|
const exitCode2 = await doctorCommand(
|
|
2090
2373
|
{
|
|
2091
2374
|
verbose: Boolean(options.verbose),
|
|
@@ -2098,16 +2381,21 @@ function createProgram(ctx2) {
|
|
|
2098
2381
|
throw new ExitError(exitCode2);
|
|
2099
2382
|
}
|
|
2100
2383
|
});
|
|
2101
|
-
program.command("init").description("Create a ripple.config
|
|
2384
|
+
program.command("init").description("Create a ripple.config with the default settings").addHelpText("after", helpExamples(["ripple init --force", "ripple init --ts --full"])).option("-f, --force", "overwrite an existing config file").option("--full", "write every built-in default instead of a minimal config").option("--ts", "write a typed ripple.config.ts instead of JSON").option("--no-color", "disable ANSI colors").action(async (options) => {
|
|
2102
2385
|
const exitCode2 = await initCommand(
|
|
2103
|
-
{
|
|
2386
|
+
{
|
|
2387
|
+
force: Boolean(options.force),
|
|
2388
|
+
full: Boolean(options.full),
|
|
2389
|
+
ts: Boolean(options.ts),
|
|
2390
|
+
color: options.color
|
|
2391
|
+
},
|
|
2104
2392
|
ctx2
|
|
2105
2393
|
);
|
|
2106
2394
|
if (exitCode2 !== ExitCode.Success) {
|
|
2107
2395
|
throw new ExitError(exitCode2);
|
|
2108
2396
|
}
|
|
2109
2397
|
});
|
|
2110
|
-
program.command("version").description("Print the Ripple version").action(() => {
|
|
2398
|
+
program.command("version").description("Print the Ripple version").addHelpText("after", helpExamples(["ripple version"])).action(() => {
|
|
2111
2399
|
ctx2.writer.writeLine(ctx2.version);
|
|
2112
2400
|
});
|
|
2113
2401
|
program.addHelpText(
|
|
@@ -2156,6 +2444,16 @@ function parseDiffFormat(value) {
|
|
|
2156
2444
|
`expected one of terminal | json | github, got "${value}"`
|
|
2157
2445
|
);
|
|
2158
2446
|
}
|
|
2447
|
+
function parseGraphFormat(value) {
|
|
2448
|
+
if (value === "terminal" || value === "json" || value === "mermaid" || value === "dot" || value === "html") {
|
|
2449
|
+
return value;
|
|
2450
|
+
}
|
|
2451
|
+
throw new CommanderError(
|
|
2452
|
+
1,
|
|
2453
|
+
"commander.invalidArgument",
|
|
2454
|
+
`expected one of terminal | json | mermaid | dot | html, got "${value}"`
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2159
2457
|
function printError(error, ctx2) {
|
|
2160
2458
|
if (error instanceof RippleError) {
|
|
2161
2459
|
ctx2.errorWriter.writeLine(`\u2716 ${error.message}`);
|
|
@@ -2189,7 +2487,7 @@ async function run({ argv, ctx: ctx2 }) {
|
|
|
2189
2487
|
|
|
2190
2488
|
// package.json
|
|
2191
2489
|
var package_default = {
|
|
2192
|
-
version: "0.
|
|
2490
|
+
version: "0.6.0"};
|
|
2193
2491
|
|
|
2194
2492
|
// src/cli/bin.ts
|
|
2195
2493
|
var ctx = {
|