dependency-tree.ts 4.1 KB

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