Skip to content

Commit 3a9e8fd

Browse files
claude: tool to find cycles with transitive dependencies to async modules
If you have a dependency cycle where all of the modules are async (transitively because of their dependencies) then it would probably deadlock if you ran it. esbuild gives up and emits invalid JS. So you must either break these cycles, or prevent the async from propagating to the cycles. This tool uses ILP to show - Minimal places to break remove static imports to stop transitive dependencies before they hit cycles (minimal hitting set) - Minimal places to remove static imports to break these cycles (minimum feedback arc set) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4b2388e commit 3a9e8fd

File tree

7 files changed

+1356
-22
lines changed

7 files changed

+1356
-22
lines changed

package/src/common/import-report/deno-info.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
*
88
*/
99

10+
import { architectureToolsPath } from "../../../../src/core/resources.ts";
11+
1012
////////////////////////////////////////////////////////////////////////////////
1113

1214
export interface DenoInfoDependency {
@@ -61,8 +63,9 @@ export interface Edge {
6163
////////////////////////////////////////////////////////////////////////////////
6264

6365
export async function getDenoInfo(_root: string): Promise<DenoInfoJSON> {
66+
const denoBinary = Deno.env.get("QUARTO_DENO") || architectureToolsPath("deno");
6467
const process = Deno.run({
65-
cmd: ["deno", "info", Deno.args[0], "--json"],
68+
cmd: [denoBinary, "info", Deno.args[0], "--json"],
6669
stdout: "piped",
6770
});
6871
const rawOutput = await process.output();

package/src/common/import-report/explain-all-cycles.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,15 +156,19 @@ if (import.meta.main) {
156156
between all files reachable from some source file.
157157
158158
Usage:
159-
$ quarto run explain-all-cycles.ts <entry-point.ts>
159+
$ quarto run --dev explain-all-cycles.ts <entry-point.ts> [--simplify <prefixes...>] [--graph|--toon [filename]]
160160
161-
Examples:
162-
163-
From ./src:
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)
164165
165-
$ quarto run quarto.ts
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
166170
167-
If the second parameter is "--graph", then this program outputs the .dot specification to the file given by the third parameter, rather opening a full report.
171+
If no output option is given, opens an interactive preview.
168172
`,
169173
);
170174
Deno.exit(1);
@@ -187,7 +191,17 @@ If the second parameter is "--graph", then this program outputs the .dot specifi
187191

188192
result = dropTypesFiles(result);
189193

190-
if (args[1] === "--graph") {
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") {
191205
Deno.writeTextFileSync(
192206
args[2] ?? "graph.dot",
193207
generateGraph(result),

package/src/common/import-report/explain-import-chain.ts

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -152,37 +152,72 @@ if (import.meta.main) {
152152
between files in quarto.
153153
154154
Usage:
155-
$ quarto run explain-import-chain.ts <source-file.ts> <target-file.ts>
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)
156161
157162
Examples:
158163
159-
From ./src:
164+
From project root:
160165
161-
$ quarto run ../package/scripts/common/explain-import-chain.ts command/render/render.ts core/esbuild.ts
162-
$ quarto run ../package/scripts/common/explain-import-chain.ts command/check/cmd.ts core/lib/external/regexpp.mjs
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
163169
164170
If no dependencies exist, this script will report that:
165171
166-
$ quarto run ../package/scripts/common/explain-import-chain.ts ../package/src/bld.ts core/lib/external/tree-sitter-deno.js
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
167173
168174
package/src/bld.ts does not depend on src/core/lib/external/tree-sitter-deno.js
169175
170-
If the third parameter is "--graph", then this program outputs the .dot specification to the file given by the fourth parameter, rather opening a full report.
176+
If no output option is given, opens an interactive preview.
171177
`,
172178
);
173179
Deno.exit(1);
174180
}
175-
const json = await getDenoInfo(Deno.args[0]);
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]);
176191
const { graph } = moduleGraph(json);
177192

178-
const targetName = Deno.args[1];
193+
const targetName = args[1];
179194
const target = toFileUrl(resolve(targetName)).href;
180-
const result = explain(graph, json.roots[0], target);
181-
if (Deno.args[2] === "--graph") {
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") {
182208
Deno.writeTextFileSync(
183-
Deno.args[3],
209+
args[3] || "import-chain.dot",
184210
generateGraph(result, json.roots[0], target),
185211
);
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+
);
186221
} else {
187222
await buildOutput(result, json.roots[0], target);
188223
}

package/src/common/import-report/package_report.qmd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import subprocess
1111
def run(what):
1212
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
1313
str = subprocess.run(
14-
("quarto run %s ../../../../src/quarto.ts" % what).split(" "),
14+
("quarto run --dev %s ../../../../src/quarto.ts" % what).split(" "),
1515
capture_output = True
1616
).stdout.decode('utf-8').replace("Bad import from ", "").replace(" to ", " -> ")
1717
return ansi_escape.sub("", str)

0 commit comments

Comments
 (0)