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-import-chain.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 explain(
14
graph: DependencyGraph,
15
source: string,
16
target: string,
17
): Edge[] {
18
const result: Edge[] = [];
19
const deps = reachability(graph, source);
20
if (!deps[target]) {
21
const [ss, st] = [source, target].map((s) =>
22
s.slice(longestCommonDirPrefix([source, target]).length)
23
);
24
console.error(`${ss} does not depend on ${st}`);
25
Deno.exit(1);
26
}
27
if (!deps[target].has(source)) {
28
return result;
29
}
30
const transpose = graphTranspose(graph);
31
const visited: Set<string> = new Set();
32
const inner = (node: string) => {
33
if (visited.has(node)) {
34
return;
35
}
36
visited.add(node);
37
for (const pred of transpose[node]) {
38
if (deps[pred].has(source) || pred === source) {
39
result.push({ "from": pred, "to": node });
40
inner(pred);
41
}
42
}
43
};
44
inner(target);
45
return result;
46
}
47
48
function edgeColor(source: string, target: string) {
49
if (
50
source.indexOf("src/core/") !== -1 &&
51
target.indexOf("src/core/") === -1
52
) {
53
return "red";
54
}
55
if (
56
source.indexOf("src/core/lib") !== -1 &&
57
target.indexOf("src/core/lib") === -1
58
) {
59
return "red";
60
}
61
return undefined;
62
}
63
64
function generateGraph(edges: Edge[], source: string, target: string): string {
65
// https://stackoverflow.com/a/68702966
66
const strs: Set<string> = new Set();
67
for (const { "from": edgeFrom, to } of edges) {
68
strs.add(edgeFrom);
69
strs.add(to);
70
}
71
const p = longestCommonDirPrefix(Array.from(strs)).length;
72
73
const qmdOut: string[] = [];
74
qmdOut.push("digraph G {");
75
const m: Record<string, number> = {};
76
let ix = 1;
77
for (const { "from": edgeFrom, to } of edges) {
78
if (m[edgeFrom] === undefined) {
79
m[edgeFrom] = ix++;
80
}
81
if (m[to] === undefined) {
82
m[to] = ix++;
83
}
84
85
const c = edgeColor(edgeFrom, to);
86
if (c) {
87
qmdOut.push(` ${m[edgeFrom]} -> ${m[to]} [color="${c}"];`);
88
} else {
89
qmdOut.push(` ${m[edgeFrom]} -> ${m[to]};`);
90
}
91
}
92
for (const [name, ix] of Object.entries(m)) {
93
if (name.slice(p) === source.slice(p)) {
94
qmdOut.push(
95
` ${ix} [ label = "${
96
name.slice(p)
97
}", shape = "box", fillcolor = "#1f77b4", style = "filled", fontcolor = "white" ];`,
98
);
99
} else if (name.slice(p) === target.slice(p)) {
100
qmdOut.push(
101
` ${ix} [ label = "${
102
name.slice(p)
103
}", shape = "box", fillcolor = "#ff7f0e", style = "filled" ];`,
104
);
105
} else {
106
qmdOut.push(` ${ix} [ label = "${name.slice(p)}", shape = "none" ];`);
107
}
108
}
109
qmdOut.push("}\n");
110
return qmdOut.join("");
111
}
112
113
async function buildOutput(edges: Edge[], source: string, target: string) {
114
const strs: Set<string> = new Set();
115
for (const { "from": edgeFrom, to } of edges) {
116
strs.add(edgeFrom);
117
strs.add(to);
118
}
119
const p = longestCommonDirPrefix(Array.from(strs)).length;
120
121
const qmdOut: string[] = [`---
122
title: explain.ts
123
format: html
124
---
125
`];
126
qmdOut.push(
127
"This graph shows all import chains from `" + source.slice(p) +
128
"` (a <span style='color:#1f77b4'>blue node</span>) to `" +
129
target.slice(p) +
130
"` (an <span style='color:#ff7f0e'>orange node</span>).\n\n",
131
);
132
qmdOut.push("```{ojs}\n//| echo: false\n\ndot`");
133
qmdOut.push(generateGraph(edges, source, target));
134
qmdOut.push("`\n");
135
qmdOut.push("```\n");
136
137
const filename = await Deno.makeTempFile({ suffix: ".qmd" });
138
Deno.writeTextFileSync(filename, qmdOut.join("\n"));
139
140
const process = Deno.run({
141
cmd: ["quarto", "preview", filename],
142
});
143
await process.status();
144
145
Deno.remove(filename);
146
}
147
148
if (import.meta.main) {
149
if (Deno.args.length === 0) {
150
console.log(
151
`explain-import-chain.ts: generate a graph visualization of import paths
152
between files in quarto.
153
154
Usage:
155
$ quarto run --dev explain-import-chain.ts <source-file.ts> <target-file.ts> [--simplify] [--graph|--toon [filename]]
156
157
Options:
158
--simplify Simplify paths by removing common prefix
159
--graph [file] Output .dot specification (default: import-chain.dot)
160
--toon [file] Output edges in TOON format (default: import-chain.toon)
161
162
Examples:
163
164
From project root:
165
166
$ quarto run --dev package/src/common/import-report/explain-import-chain.ts src/command/render/render.ts src/core/esbuild.ts
167
$ quarto run --dev package/src/common/import-report/explain-import-chain.ts src/command/check/cmd.ts src/core/lib/external/regexpp.mjs
168
$ quarto run --dev package/src/common/import-report/explain-import-chain.ts src/command/render/render.ts src/core/esbuild.ts --simplify --toon
169
170
If no dependencies exist, this script will report that:
171
172
$ quarto run --dev package/src/common/import-report/explain-import-chain.ts package/src/bld.ts src/core/lib/external/tree-sitter-deno.js
173
174
package/src/bld.ts does not depend on src/core/lib/external/tree-sitter-deno.js
175
176
If no output option is given, opens an interactive preview.
177
`,
178
);
179
Deno.exit(1);
180
}
181
// Parse arguments
182
let simplify = false;
183
let args = Deno.args;
184
185
if (args[2] === "--simplify") {
186
simplify = true;
187
args = [args[0], args[1], ...args.slice(3)];
188
}
189
190
const json = await getDenoInfo(args[0]);
191
const { graph } = moduleGraph(json);
192
193
const targetName = args[1];
194
const target = toFileUrl(resolve(targetName)).href;
195
let result = explain(graph, json.roots[0], target);
196
197
// Apply simplification if requested
198
if (simplify && result.length > 0) {
199
const allPaths = result.map(e => [e.from, e.to]).flat();
200
const prefix = longestCommonDirPrefix(allPaths);
201
result = result.map(({ from, to }) => ({
202
from: from.slice(prefix.length),
203
to: to.slice(prefix.length),
204
}));
205
}
206
207
if (args[2] === "--graph") {
208
Deno.writeTextFileSync(
209
args[3] || "import-chain.dot",
210
generateGraph(result, json.roots[0], target),
211
);
212
} else if (args[2] === "--toon") {
213
const lines = [`edges[${result.length}]{from,to}:`];
214
for (const { from, to } of result) {
215
lines.push(` ${from},${to}`);
216
}
217
Deno.writeTextFileSync(
218
args[3] || "import-chain.toon",
219
lines.join("\n") + "\n",
220
);
221
} else {
222
await buildOutput(result, json.roots[0], target);
223
}
224
}
225
226