Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/package/src/common/import-report/explain-all-cycles.ts
6452 views
1
import { resolve, toFileUrl } from "../../../../src/deno_ral/path.ts";
2
import {
3
DependencyGraph,
4
Edge,
5
getDenoInfo,
6
graphTranspose,
7
moduleGraph,
8
reachability,
9
} from "./deno-info.ts";
10
11
import { longestCommonDirPrefix } from "./utils.ts";
12
13
function dropTypesFiles(edges: Edge[])
14
{
15
return edges.filter(({
16
"from": edgeFrom,
17
to,
18
}) => !(to.endsWith("types.ts") || edgeFrom.endsWith("types.ts")));
19
}
20
21
function trimCommonPrefix(edges: Edge[])
22
{
23
// https://stackoverflow.com/a/68702966
24
const strs: Set<string> = new Set();
25
for (const { "from": edgeFrom, to } of edges) {
26
strs.add(edgeFrom);
27
strs.add(to);
28
}
29
const p = longestCommonDirPrefix(Array.from(strs)).length;
30
return edges.map(({ "from": edgeFrom, to }) => ({
31
"from": edgeFrom.slice(p),
32
to: to.slice(p),
33
}));
34
}
35
36
function simplify(
37
edges: Edge[],
38
prefixes: string[],
39
): Edge[]
40
{
41
edges = trimCommonPrefix(edges);
42
43
const result: Edge[] = [];
44
const keepPrefix = (s: string) => {
45
for (const prefix of prefixes) {
46
if (s.startsWith(prefix)) {
47
return prefix + "...";
48
}
49
}
50
return s;
51
}
52
const edgeSet = new Set<string>();
53
for (const edge of edges) {
54
const from = keepPrefix(edge.from);
55
const to = keepPrefix(edge.to);
56
if (from === to) {
57
continue;
58
}
59
const key = `${from} -> ${to}`;
60
if (edgeSet.has(key)) {
61
continue;
62
}
63
edgeSet.add(key);
64
result.push({ from, to });
65
}
66
return result;
67
}
68
69
function explain(
70
graph: DependencyGraph,
71
): Edge[] {
72
const result: Edge[] = [];
73
const transpose = graphTranspose(graph);
74
const visited: Set<string> = new Set();
75
let found = false;
76
let count = 0;
77
78
const deps = reachability(graph);
79
80
for (const source of Object.keys(graph)) {
81
if (!deps[source]) {
82
continue;
83
}
84
found = true;
85
const inner = (node: string) => {
86
if (visited.has(node)) {
87
return;
88
}
89
visited.add(node);
90
for (const pred of transpose[node]) {
91
if (deps[pred].has(source) || pred === source) {
92
result.push({ "from": pred, "to": node });
93
inner(pred);
94
}
95
}
96
};
97
inner(source);
98
}
99
if (!found) {
100
console.error(`graph does not have cyclic imports`);
101
Deno.exit(1);
102
}
103
return result;
104
}
105
106
function generateGraph(edges: Edge[]): string {
107
const qmdOut: string[] = [];
108
qmdOut.push("digraph G {");
109
const m: Record<string, number> = {};
110
let ix = 1;
111
for (const { "from": edgeFrom, to } of edges) {
112
if (m[edgeFrom] === undefined) {
113
m[edgeFrom] = ix++;
114
}
115
if (m[to] === undefined) {
116
m[to] = ix++;
117
}
118
qmdOut.push(` ${m[edgeFrom]} -> ${m[to]};`);
119
}
120
for (const [name, ix] of Object.entries(m)) {
121
qmdOut.push(` ${ix} [ label = "${name}", shape = "none" ];`);
122
}
123
qmdOut.push("}\n");
124
return qmdOut.join("");
125
}
126
127
async function buildOutput(edges: Edge[]) {
128
const qmdOut: string[] = [`---
129
title: explain.ts
130
format: html
131
---
132
`];
133
qmdOut.push(
134
"This graph shows all cyclic import chains.\n\n",
135
);
136
qmdOut.push("```{ojs}\n//| echo: false\n\ndot`");
137
qmdOut.push(generateGraph(edges));
138
qmdOut.push("`\n");
139
qmdOut.push("```\n");
140
141
const filename = await Deno.makeTempFile({ suffix: ".qmd" });
142
Deno.writeTextFileSync(filename, qmdOut.join("\n"));
143
144
const process = Deno.run({
145
cmd: ["quarto", "preview", filename],
146
});
147
await process.status();
148
149
Deno.remove(filename);
150
}
151
152
if (import.meta.main) {
153
if (Deno.args.length === 0) {
154
console.log(
155
`explain-all-cycles.ts: generate a graph visualization of cyclic imports
156
between all files reachable from some source file.
157
158
Usage:
159
$ quarto run --dev explain-all-cycles.ts <entry-point.ts> [--simplify <prefixes...>] [--graph|--toon [filename]]
160
161
Options:
162
--simplify <prefixes...> Collapse paths with given prefixes (must be first if used)
163
--graph [filename] Output .dot specification (default: graph.dot)
164
--toon [filename] Output edges in TOON format (default: cycles.toon)
165
166
Examples:
167
$ quarto run --dev package/src/common/import-report/explain-all-cycles.ts src/quarto.ts
168
$ quarto run --dev package/src/common/import-report/explain-all-cycles.ts src/quarto.ts --simplify core/ command/ --toon
169
$ quarto run --dev package/src/common/import-report/explain-all-cycles.ts src/quarto.ts --graph cycles.dot
170
171
If no output option is given, opens an interactive preview.
172
`,
173
);
174
Deno.exit(1);
175
}
176
const json = await getDenoInfo(Deno.args[0]);
177
const { graph } = moduleGraph(json);
178
179
let args = Array.from(Deno.args);
180
181
let result = explain(graph);
182
if (args[1] === "--simplify") {
183
args.splice(1, 1);
184
const prefixes = [];
185
while (!args[1].startsWith("--")) {
186
prefixes.push(args[1]);
187
args.splice(1, 1);
188
}
189
result = simplify(result, prefixes);
190
}
191
192
result = dropTypesFiles(result);
193
194
if (args[1] === "--toon") {
195
// Output in TOON format
196
const lines = [`edges[${result.length}]{from,to}:`];
197
for (const { from, to } of result) {
198
lines.push(` ${from},${to}`);
199
}
200
Deno.writeTextFileSync(
201
args[2] ?? "cycles.toon",
202
lines.join("\n") + "\n",
203
);
204
} else if (args[1] === "--graph") {
205
Deno.writeTextFileSync(
206
args[2] ?? "graph.dot",
207
generateGraph(result),
208
);
209
} else {
210
await buildOutput(result);
211
}
212
}
213
214