Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/package/src/common/cyclic-dependencies.ts
6450 views
1
/*
2
* cyclic-dependencies.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*
6
*/
7
import { basename, isAbsolute, join } from "../../../src/deno_ral/path.ts";
8
import { Command } from "cliffy/command/mod.ts";
9
10
import { runCmd } from "../util/cmd.ts";
11
import { Configuration, readConfiguration } from "./config.ts";
12
import { error, info } from "../../../src/deno_ral/log.ts";
13
import { progressBar } from "../../../src/core/console.ts";
14
import { md5HashSync } from "../../../src/core/hash.ts";
15
16
export function cycleDependenciesCommand() {
17
return new Command()
18
.name("cycle-dependencies")
19
.description(
20
"Debugging tool for helping discover cyclic dependencies in quarto",
21
)
22
.option(
23
"-o, --output",
24
"Path to write json output",
25
)
26
// deno-lint-ignore no-explicit-any
27
.action(async (args: Record<string, any>) => {
28
const configuration = readConfiguration();
29
info("Using configuration:");
30
info(configuration);
31
info("");
32
await cyclicDependencies(args.output as string, configuration);
33
});
34
}
35
36
export function parseSwcLogCommand() {
37
return new Command()
38
.name("parse-swc-log")
39
.description(
40
"Parses SWC bundler debug log to discover cyclic dependencies in quarto",
41
)
42
.option(
43
"-i, --input",
44
"Path to text file containing swc bundler debug output",
45
)
46
.option(
47
"-o, --output",
48
"Path to write json output",
49
)
50
// deno-lint-ignore no-explicit-any
51
.action((args: Record<string, any>) => {
52
const configuration = readConfiguration();
53
info("Using configuration:");
54
info(configuration);
55
info("");
56
parseSwcBundlerLog(args.input, args.output, configuration);
57
});
58
}
59
60
export async function cyclicDependencies(
61
out: string,
62
config: Configuration,
63
) {
64
const modules = await loadModules(config);
65
findCyclicDependencies(modules, out, config);
66
}
67
68
// Parses the debug output from the SWC bundler
69
// (enable --log-level debug when call deno bundle to emit this and redirect stderr/stdout to a file)
70
// Will create a table of module id and path as well as any circular dependencies
71
// that SWC complains about
72
function parseSwcBundlerLog(
73
log: string,
74
out: string,
75
config: Configuration,
76
) {
77
// TODO: This should accept a log output file from the swc bundler
78
// and a target results file (rather than being hard coded)
79
80
// TODO: Consider just outputting this after prepare-dist
81
82
// Read the debug output and create alases for module numbers
83
const logPath = isAbsolute(log) ? log : join(Deno.cwd(), log);
84
const outPath = isAbsolute(out) ? out : join(Deno.cwd(), out);
85
86
const text = Deno.readTextFileSync(logPath);
87
if (text) {
88
const moduleRegex = /\(ModuleId\(([0-9]+)\)\) <.+:\/\/(.*)>/gm;
89
const circularRegex =
90
/DEBUG RS - swc_bundler::bundler::chunk.*Circular dep:*.ModuleId\(([0-9]+)\) => ModuleId\(([0-9]+)\)/gm;
91
92
// Parse the modules and create a map
93
const moduleMap: Record<string, string> = {};
94
moduleRegex.lastIndex = 0;
95
let match = moduleRegex.exec(text);
96
while (match) {
97
const moduleId = match[1];
98
const moduleFile = match[2];
99
moduleMap[moduleId] = moduleFile;
100
match = moduleRegex.exec(text);
101
}
102
103
// Function to make a pretty name for this module
104
const name = (moduleId: string) => {
105
const name = moduleMap[moduleId];
106
if (name) {
107
return name.replaceAll(config.directoryInfo.src, "");
108
} else {
109
return `?${moduleId}`;
110
}
111
};
112
113
// Find any circular reference complains and read the modules, use the map
114
// to find the names of the circularlities, and then add to the cycle map
115
const cycleMap: Record<string, string> = {};
116
let circularMatch = circularRegex.exec(text);
117
while (circularMatch) {
118
// Add this match to the circular list
119
const baseModule = circularMatch[1];
120
const depModule = circularMatch[2];
121
cycleMap[name(baseModule)] = name(depModule);
122
123
// Search again
124
circularMatch = circularRegex.exec(text);
125
}
126
127
// Create a human readable map of circulars
128
const outputObj = {
129
modules: moduleMap,
130
circulars: cycleMap,
131
};
132
133
// Write the output
134
Deno.writeTextFileSync(
135
outPath,
136
JSON.stringify(outputObj, undefined, 2),
137
);
138
info("Log written to: " + outPath);
139
}
140
}
141
async function loadModules(config: Configuration) {
142
info("Reading modules");
143
const denoExecPath = Deno.env.get("QUARTO_DENO")
144
if (! denoExecPath) {
145
throw Error("QUARTO_DENO is not defined");
146
}
147
const result = await runCmd(
148
denoExecPath,
149
[
150
"info",
151
join(config.directoryInfo.src, "quarto.ts"),
152
"--json",
153
"--unstable",
154
],
155
);
156
157
const rawOutput = result.stdout;
158
const jsonOutput = JSON.parse(rawOutput || "");
159
160
// module path, array of import paths
161
const modules: Record<string, string[]> = {};
162
for (const mod of jsonOutput["modules"]) {
163
modules[mod.specifier] = mod.dependencies.map((dep: { code: string }) => {
164
return dep.code;
165
}).filter((p: unknown) => p !== undefined);
166
}
167
168
return modules;
169
}
170
171
// Holds a detected cycle (with a handful of stacks / sample stacks)
172
interface Cycle {
173
cycle: [string, string];
174
stacks: [string[]];
175
}
176
177
function findCyclicDependencies(
178
modules: Record<string, string[]>,
179
out: string,
180
_config: Configuration,
181
) {
182
const outPath = isAbsolute(out) ? out : join(Deno.cwd(), out);
183
184
const cycles: Record<string, Cycle> = {};
185
186
// creates a hash for a set of paths (a cycle)
187
const hash = (paths: string[]) => {
188
const string = paths.join(" ");
189
return md5HashSync(string);
190
};
191
192
// The current import stack
193
const stack: string[] = [];
194
195
const walkImports = (path: string, modules: Record<string, string[]>) => {
196
// See if this path is already in the stack.
197
const existingIndex = stack.findIndex((item) => item === path);
198
// If it is, stop looking and return
199
if (existingIndex !== -1) {
200
// Log the cycle
201
const substack = [...stack.slice(existingIndex), path];
202
const key = [stack[existingIndex], substack[substack.length - 1]];
203
const currentCycle = cycles[hash(key)] || { cycle: key, stacks: [] };
204
// Add the first 5 example stacks
205
if (currentCycle.stacks.length < 5) {
206
currentCycle.stacks.push(substack);
207
}
208
cycles[hash(key)] = currentCycle;
209
} else {
210
stack.push(path);
211
const dependencies = modules[path];
212
if (dependencies) {
213
for (const dependency of dependencies) {
214
walkImports(dependency, modules);
215
}
216
}
217
stack.pop();
218
}
219
};
220
221
const paths = Object.keys(modules);
222
const prog = progressBar(paths.length, `Detecting cycles`);
223
let count = 0;
224
for (const path of paths) {
225
if (path.endsWith("quarto.ts")) {
226
continue;
227
}
228
const status = `scanning ${basename(path)} | total of ${
229
Object.keys(cycles).length
230
} cycles`;
231
prog.update(count, status);
232
try {
233
walkImports(path, modules);
234
} catch (er) {
235
error(er);
236
} finally {
237
stack.splice(0, stack.length);
238
}
239
240
count = count + 1;
241
242
if (Object.keys(cycles).length > 100) {
243
break;
244
}
245
}
246
prog.complete();
247
248
Deno.writeTextFileSync(outPath, JSON.stringify(cycles, undefined, 2));
249
info("Log written to: " + outPath);
250
}
251
252