import yargs from "yargs/yargs"; import path from "path"; import { hideBin } from "yargs/helpers"; const args = yargs(hideBin(process.argv)) .option("package", { type: "string", describe: "The full or relative path to the package.json file", require: true }) .option("format", { choices: ["tree", "csv"], demandOption: true, default: "tree" }) .option("filter", { choices: ["none", "oracle"], demandOption: true, default: "none" }) .help() .argv; interface Dictionary { [index: string]: T } interface Package { dependencies: Dictionary; devDependencies?: Dictionary; } type PackageLockDependencies = Dictionary<{ version: string; dev: boolean; 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, dev: boolean }>; const constructDepTree = (packageName: string, context: PackageLockDependencies, stack: string[] = []): PackageLockTree => { const dependency = context[packageName]; const value = { name: packageName, version: dependency.version, dev: dependency.dev }; 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 = (format: (node: PackageLockTree, parents: string[]) => string) => (filter: (node: PackageLockTree, parents: string[]) => boolean) => (tree: PackageLockTree) => { const iter = (tree: PackageLockTree, stack: string[] = []) => { if (filter(tree, stack)) { console.log(format(tree, stack)); } if (tree.type === "node") { tree.children.forEach(t => iter(t, [...stack, `${tree.value.name}@${tree.value.version}`])) } } iter(tree); } Promise.all([ import(args.package), import(`${path.dirname(args.package)}/package-lock.json`) ]) .then(([json, jsonLock]) => { const dependencyTree = Object.keys(json.dependencies) .concat(Object.keys(json.devDependencies ?? {})) .map(depName => constructDepTree(depName, jsonLock.dependencies)); if (args.format === "csv") { console.log("name,version,root,parent,type"); } const format = args.format === "tree" ? (node: PackageLockTree, parents: string[]) => `${" ".repeat(parents.length)}${node.value.name}@${node.value.version}${node.value.dev ? " (dev)" : ""}` : (node: PackageLockTree, parents: string[]) => `${node.value.name},${node.value.version},${parents[0] ?? ""},${parents[parents.length - 1] ?? ""},${node.value.dev ? "devDependency" : "dependency"}`; const filter = args.filter === "oracle" ? (node: PackageLockTree, parents: string[]) => (parents.length === 0 && !node.value.name.startsWith("@os")) || (!node.value.name.startsWith("@o") && parents.length > 1 && parents.every(p => p.startsWith("@os"))) : (node: PackageLockTree, parents: string[]) => true; const display = displayDepTree(format)(filter); dependencyTree.forEach(t => display(t)); }) .catch(reson => { console.error(reson); });