Path: blob/main/package/src/common/cyclic-dependencies.ts
6450 views
/*1* cyclic-dependencies.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*5*/6import { basename, isAbsolute, join } from "../../../src/deno_ral/path.ts";7import { Command } from "cliffy/command/mod.ts";89import { runCmd } from "../util/cmd.ts";10import { Configuration, readConfiguration } from "./config.ts";11import { error, info } from "../../../src/deno_ral/log.ts";12import { progressBar } from "../../../src/core/console.ts";13import { md5HashSync } from "../../../src/core/hash.ts";1415export function cycleDependenciesCommand() {16return new Command()17.name("cycle-dependencies")18.description(19"Debugging tool for helping discover cyclic dependencies in quarto",20)21.option(22"-o, --output",23"Path to write json output",24)25// deno-lint-ignore no-explicit-any26.action(async (args: Record<string, any>) => {27const configuration = readConfiguration();28info("Using configuration:");29info(configuration);30info("");31await cyclicDependencies(args.output as string, configuration);32});33}3435export function parseSwcLogCommand() {36return new Command()37.name("parse-swc-log")38.description(39"Parses SWC bundler debug log to discover cyclic dependencies in quarto",40)41.option(42"-i, --input",43"Path to text file containing swc bundler debug output",44)45.option(46"-o, --output",47"Path to write json output",48)49// deno-lint-ignore no-explicit-any50.action((args: Record<string, any>) => {51const configuration = readConfiguration();52info("Using configuration:");53info(configuration);54info("");55parseSwcBundlerLog(args.input, args.output, configuration);56});57}5859export async function cyclicDependencies(60out: string,61config: Configuration,62) {63const modules = await loadModules(config);64findCyclicDependencies(modules, out, config);65}6667// Parses the debug output from the SWC bundler68// (enable --log-level debug when call deno bundle to emit this and redirect stderr/stdout to a file)69// Will create a table of module id and path as well as any circular dependencies70// that SWC complains about71function parseSwcBundlerLog(72log: string,73out: string,74config: Configuration,75) {76// TODO: This should accept a log output file from the swc bundler77// and a target results file (rather than being hard coded)7879// TODO: Consider just outputting this after prepare-dist8081// Read the debug output and create alases for module numbers82const logPath = isAbsolute(log) ? log : join(Deno.cwd(), log);83const outPath = isAbsolute(out) ? out : join(Deno.cwd(), out);8485const text = Deno.readTextFileSync(logPath);86if (text) {87const moduleRegex = /\(ModuleId\(([0-9]+)\)\) <.+:\/\/(.*)>/gm;88const circularRegex =89/DEBUG RS - swc_bundler::bundler::chunk.*Circular dep:*.ModuleId\(([0-9]+)\) => ModuleId\(([0-9]+)\)/gm;9091// Parse the modules and create a map92const moduleMap: Record<string, string> = {};93moduleRegex.lastIndex = 0;94let match = moduleRegex.exec(text);95while (match) {96const moduleId = match[1];97const moduleFile = match[2];98moduleMap[moduleId] = moduleFile;99match = moduleRegex.exec(text);100}101102// Function to make a pretty name for this module103const name = (moduleId: string) => {104const name = moduleMap[moduleId];105if (name) {106return name.replaceAll(config.directoryInfo.src, "");107} else {108return `?${moduleId}`;109}110};111112// Find any circular reference complains and read the modules, use the map113// to find the names of the circularlities, and then add to the cycle map114const cycleMap: Record<string, string> = {};115let circularMatch = circularRegex.exec(text);116while (circularMatch) {117// Add this match to the circular list118const baseModule = circularMatch[1];119const depModule = circularMatch[2];120cycleMap[name(baseModule)] = name(depModule);121122// Search again123circularMatch = circularRegex.exec(text);124}125126// Create a human readable map of circulars127const outputObj = {128modules: moduleMap,129circulars: cycleMap,130};131132// Write the output133Deno.writeTextFileSync(134outPath,135JSON.stringify(outputObj, undefined, 2),136);137info("Log written to: " + outPath);138}139}140async function loadModules(config: Configuration) {141info("Reading modules");142const denoExecPath = Deno.env.get("QUARTO_DENO")143if (! denoExecPath) {144throw Error("QUARTO_DENO is not defined");145}146const result = await runCmd(147denoExecPath,148[149"info",150join(config.directoryInfo.src, "quarto.ts"),151"--json",152"--unstable",153],154);155156const rawOutput = result.stdout;157const jsonOutput = JSON.parse(rawOutput || "");158159// module path, array of import paths160const modules: Record<string, string[]> = {};161for (const mod of jsonOutput["modules"]) {162modules[mod.specifier] = mod.dependencies.map((dep: { code: string }) => {163return dep.code;164}).filter((p: unknown) => p !== undefined);165}166167return modules;168}169170// Holds a detected cycle (with a handful of stacks / sample stacks)171interface Cycle {172cycle: [string, string];173stacks: [string[]];174}175176function findCyclicDependencies(177modules: Record<string, string[]>,178out: string,179_config: Configuration,180) {181const outPath = isAbsolute(out) ? out : join(Deno.cwd(), out);182183const cycles: Record<string, Cycle> = {};184185// creates a hash for a set of paths (a cycle)186const hash = (paths: string[]) => {187const string = paths.join(" ");188return md5HashSync(string);189};190191// The current import stack192const stack: string[] = [];193194const walkImports = (path: string, modules: Record<string, string[]>) => {195// See if this path is already in the stack.196const existingIndex = stack.findIndex((item) => item === path);197// If it is, stop looking and return198if (existingIndex !== -1) {199// Log the cycle200const substack = [...stack.slice(existingIndex), path];201const key = [stack[existingIndex], substack[substack.length - 1]];202const currentCycle = cycles[hash(key)] || { cycle: key, stacks: [] };203// Add the first 5 example stacks204if (currentCycle.stacks.length < 5) {205currentCycle.stacks.push(substack);206}207cycles[hash(key)] = currentCycle;208} else {209stack.push(path);210const dependencies = modules[path];211if (dependencies) {212for (const dependency of dependencies) {213walkImports(dependency, modules);214}215}216stack.pop();217}218};219220const paths = Object.keys(modules);221const prog = progressBar(paths.length, `Detecting cycles`);222let count = 0;223for (const path of paths) {224if (path.endsWith("quarto.ts")) {225continue;226}227const status = `scanning ${basename(path)} | total of ${228Object.keys(cycles).length229} cycles`;230prog.update(count, status);231try {232walkImports(path, modules);233} catch (er) {234error(er);235} finally {236stack.splice(0, stack.length);237}238239count = count + 1;240241if (Object.keys(cycles).length > 100) {242break;243}244}245prog.complete();246247Deno.writeTextFileSync(outPath, JSON.stringify(cycles, undefined, 2));248info("Log written to: " + outPath);249}250251252