dependency-tree.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import yargs from "yargs/yargs";
  2. import path from "path";
  3. import { hideBin } from "yargs/helpers";
  4. type FormatValue = "tree" | "csv";
  5. type Format = { header: string, apply: (node: PackageLockTree, parents: string[], audit: AuditResult) => string }
  6. type FilterValue = "none" | "oracle";
  7. type Filter = (node: PackageLockTree, parents: string[]) => boolean;
  8. type Audit = (node: PackageLockTree) => Promise<AuditResult>;
  9. const args = yargs(hideBin(process.argv))
  10. .option("package", {
  11. type: "string",
  12. describe: "The full or relative path to the package.json file",
  13. require: true
  14. })
  15. .option("format", {
  16. type: "string",
  17. describe: "Possible values are 'tree' and 'csv'",
  18. default: "csv",
  19. coerce: (value: FormatValue): Format =>
  20. value === "tree"
  21. ? {
  22. header: "",
  23. apply: (node, parents, audit) =>
  24. `${" ".repeat(parents.length)}${node.value.name}@${node.value.version}${node.value.dev ? " (dev)" : ""} audit:${audit}`
  25. }
  26. : {
  27. header: "name,version,root,parent,type,audit",
  28. apply: (node, parents, audit) =>
  29. `${node.value.name},${node.value.version},${parents[0] ?? ""},${parents[parents.length - 1] ?? ""},${node.value.dev ? "devDependency" : "dependency"},${audit}`
  30. }
  31. })
  32. .option("filter", {
  33. type: "string",
  34. describe: "Possible values are 'none' and 'oracle'",
  35. default: "oracle",
  36. coerce: (value: FilterValue): Filter =>
  37. value === "oracle"
  38. ? (node, parents) => (parents.length === 0 && !node.value.name.startsWith("@os")) || (!node.value.name.startsWith("@o") && parents.length > 1 && parents.every(p => p.startsWith("@os")))
  39. : (_, __) => true
  40. })
  41. .option("audit", {
  42. type: "boolean",
  43. describe: "Open Source Compliance Service audit",
  44. default: false,
  45. coerce: (value: boolean): Audit =>
  46. _ => Promise.resolve(AuditResult.NA)
  47. })
  48. .help()
  49. .argv;
  50. interface Dictionary<T> {
  51. [index: string]: T
  52. }
  53. interface Package {
  54. dependencies: Dictionary<string>;
  55. devDependencies?: Dictionary<string>;
  56. }
  57. type PackageLockDependencies = Dictionary<{
  58. version: string;
  59. dev: boolean;
  60. requires?: Dictionary<string>;
  61. dependencies?: PackageLockDependencies;
  62. }>
  63. interface PackageLock {
  64. dependencies: PackageLockDependencies;
  65. }
  66. type Tree<T> = Leaf<T> | Node<T>;
  67. type Node<T> = {
  68. type: "node";
  69. value: T;
  70. children: Tree<T>[];
  71. }
  72. type Leaf<T> = {
  73. type: "leaf";
  74. value: T;
  75. }
  76. type PackageLockTree = Tree<{ name: string, version: string, dev: boolean }>;
  77. const constructDepTree = (packageName: string, context: PackageLockDependencies, stack: string[] = []): PackageLockTree => {
  78. const dependency = context[packageName];
  79. const value = { name: packageName, version: dependency.version, dev: dependency.dev };
  80. if (!dependency.requires) {
  81. return { type: "leaf", value };
  82. }
  83. return {
  84. type: "node",
  85. value,
  86. children: Object.keys(dependency.requires).filter(k => stack.every(s => s !== k))
  87. .map(requiredDepName => constructDepTree(requiredDepName, { ...context, ...dependency.dependencies }, [...stack, packageName]))
  88. };
  89. }
  90. const enum AuditResult {
  91. OK = "registered",
  92. NOK = "not registered",
  93. NA = "n/a"
  94. }
  95. const displayDepTree = (format: Format) => {
  96. if (format.header) {
  97. console.log(format.header);
  98. }
  99. return (filter: Filter) => (action: Audit) => (tree: PackageLockTree) => {
  100. const iter = (tree: PackageLockTree, stack: string[] = []) => {
  101. if (filter(tree, stack)) {
  102. action(tree).then(auditResult => console.log(format.apply(tree, stack, auditResult)));
  103. }
  104. if (tree.type === "node") {
  105. tree.children.forEach(t => iter(t, [...stack, `${tree.value.name}@${tree.value.version}`]))
  106. }
  107. }
  108. iter(tree);
  109. };
  110. }
  111. Promise.all<Package, PackageLock>([
  112. import(args.package),
  113. import(`${path.dirname(args.package)}/package-lock.json`)
  114. ])
  115. .then(([json, jsonLock]) => {
  116. const dependencyTree = Object.keys(json.dependencies)
  117. .concat(Object.keys(json.devDependencies ?? {}))
  118. .map(depName => constructDepTree(depName, jsonLock.dependencies));
  119. const display = displayDepTree(args.format)(args.filter)(args.audit);
  120. dependencyTree.forEach(t => display(t));
  121. })
  122. .catch(reson => {
  123. console.error(reson);
  124. });