import yargs from "yargs/yargs"; import { hideBin } from "yargs/helpers"; type Format = "tree" | "csv"; const formats: ReadonlyArray = ["tree", "csv"]; const args = yargs(hideBin(process.argv)) .option("package", { type: "string", describe: "The full or relative path to the package.json file", require: true }) .option("packageLock", { alias: "package-lock", type: "string", describe: "The full or relative path to the package-lock.json file", require: true }) .option("format", { choices: formats, demandOption: true, default: "tree" }) .help() .argv; interface Dictionary { [index: string]: T } interface Package { dependencies: Dictionary; devDependencies?: Dictionary; } type PackageLockDependencies = Dictionary<{ version: string; requires?: Dictionary; dependencies?: PackageLockDependencies; }> interface PackageLock { dependencies: PackageLockDependencies; } type Tree = Leaf | Node; type Node = { type: "node"; value: T; children: Tree[]; } type Leaf = { type: "leaf"; value: T; } type PackageLockTree = Tree<{ name: string, version: string }>; const constructDepTree = (packageName: string, context: PackageLockDependencies, stack: string[] = []): PackageLockTree => { const dependency = context[packageName]; const value = { name: packageName, version: dependency.version }; if (!dependency.requires) { return { type: "leaf", value }; } return { type: "node", value, children: Object.keys(dependency.requires).filter(k => stack.every(s => s !== k)) .map(requiredDepName => constructDepTree(requiredDepName, { ...context, ...dependency.dependencies }, [...stack, packageName])) }; } const displayDepTree = (tree: PackageLockTree, level = 1): void => { console.log(`${" ".repeat(level - 1)}${tree.value.name}@${tree.value.version}`); if (tree.type === "node") { tree.children.forEach(t => displayDepTree(t, level + 1)); } } const displayDepTreeAsCsv = (tree: PackageLockTree, depType: "dependency" | "devDependency", stack: string[] = []): void => { console.log(`${tree.value.name},${tree.value.version},${stack[0] ?? ""},${stack[stack.length - 1] ?? ""},${depType}`); if (tree.type === "node") { tree.children.forEach(t => displayDepTreeAsCsv(t, depType, [...stack, `${tree.value.name}@${tree.value.version}`])) } } Promise.all([ import(args.package), import(args.packageLock) ]) .then(([json, jsonLock]) => { const dependencyTree = Object.keys(json.dependencies) .map(depName => constructDepTree(depName, jsonLock.dependencies)); if (args.format === "tree") { console.log("--------------------- dependencies --------------------"); dependencyTree.forEach(t => displayDepTree(t)); } if (args.format === "csv") { console.log("name,version,root,parent,type"); dependencyTree.forEach(t => displayDepTreeAsCsv(t, "dependency")); } const devDependencyTree = Object.keys(json.devDependencies ?? {}) .map(devDepName => constructDepTree(devDepName, jsonLock.dependencies)); if (args.format === "tree") { console.log("-------------------- devDependencies --------------------"); devDependencyTree.forEach(t => displayDepTree(t)); } if (args.format === "csv") { devDependencyTree.forEach(t => displayDepTreeAsCsv(t, "devDependency")); } }) .catch(reson => { console.error(reson); });