import yargs from "yargs/yargs"; import path from "path"; import { hideBin } from "yargs/helpers"; type FormatValue = "tree" | "csv"; type Format = { header: string, apply: (node: PackageLockTree, parents: string[], audit: AuditResult) => string } type FilterValue = "none" | "oracle"; type Filter = (node: PackageLockTree, parents: string[]) => boolean; type Audit = (node: PackageLockTree) => Promise; 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", { type: "string", describe: "Possible values are 'tree' and 'csv'", default: "csv", coerce: (value: FormatValue): Format => value === "tree" ? { header: "", apply: (node, parents, audit) => `${" ".repeat(parents.length)}${node.value.name}@${node.value.version}${node.value.dev ? " (dev)" : ""} audit:${audit}` } : { header: "name,version,root,parent,type,audit", apply: (node, parents, audit) => `${node.value.name},${node.value.version},${parents[0] ?? ""},${parents[parents.length - 1] ?? ""},${node.value.dev ? "devDependency" : "dependency"},${audit}` } }) .option("filter", { type: "string", describe: "Possible values are 'none' and 'oracle'", default: "oracle", coerce: (value: FilterValue): Filter => value === "oracle" ? (node, parents) => (parents.length === 0 && !node.value.name.startsWith("@os")) || (!node.value.name.startsWith("@o") && parents.length > 1 && parents.every(p => p.startsWith("@os"))) : (_, __) => true }) .option("audit", { type: "boolean", describe: "Open Source Compliance Service audit", default: false, coerce: (value: boolean): Audit => _ => Promise.resolve(AuditResult.NA) }) .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 enum AuditResult { OK = "registered", NOK = "not registered", NA = "n/a" } const displayDepTree = (format: Format) => { if (format.header) { console.log(format.header); } return (filter: Filter) => (action: Audit) => (tree: PackageLockTree) => { const iter = (tree: PackageLockTree, stack: string[] = []) => { if (filter(tree, stack)) { action(tree).then(auditResult => console.log(format.apply(tree, stack, auditResult))); } 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)); const display = displayDepTree(args.format)(args.filter)(args.audit); dependencyTree.forEach(t => display(t)); }) .catch(reson => { console.error(reson); });