UNPKG

8.46 kBJavaScriptView Raw
1"use strict";
2/* -----------------------------------------------------------------------------
3| Copyright (c) Jupyter Development Team.
4| Distributed under the terms of the Modified BSD License.
5|----------------------------------------------------------------------------*/
6var __importStar = (this && this.__importStar) || function (mod) {
7 if (mod && mod.__esModule) return mod;
8 var result = {};
9 if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
10 result["default"] = mod;
11 return result;
12};
13var __importDefault = (this && this.__importDefault) || function (mod) {
14 return (mod && mod.__esModule) ? mod : { "default": mod };
15};
16Object.defineProperty(exports, "__esModule", { value: true });
17const fs = __importStar(require("fs-extra"));
18const lockfile = __importStar(require("@yarnpkg/lockfile"));
19const path = __importStar(require("path"));
20const utils = __importStar(require("./utils"));
21const commander_1 = __importDefault(require("commander"));
22/**
23 * Flatten a nested array one level.
24 */
25function flat(arr) {
26 return arr.reduce((acc, val) => acc.concat(val), []);
27}
28/**
29 * Parse the yarn file at the given path.
30 */
31function readYarn(basePath = '.') {
32 const file = fs.readFileSync(path.join(basePath, 'yarn.lock'), 'utf8');
33 const json = lockfile.parse(file);
34 if (json.type !== 'success') {
35 throw new Error('Error reading file');
36 }
37 return json.object;
38}
39/**
40 * Get a node name corresponding to package@versionspec.
41 *
42 * The nodes names are of the form "<package>@<resolved version>".
43 *
44 * Returns undefined if the package is not fund
45 */
46function getNode(yarnData, pkgName) {
47 if (!(pkgName in yarnData)) {
48 console.error(`Could not find ${pkgName} in yarn.lock file. Ignore if this is a top-level package.`);
49 return undefined;
50 }
51 const name = pkgName[0] + pkgName.slice(1).split('@')[0];
52 const version = yarnData[pkgName].version;
53 const pkgNode = `${name}@${version}`;
54 return pkgNode;
55}
56/**
57 * Build a dependency graph based on the yarn data.
58 */
59function buildYarnGraph(yarnData) {
60 // 'a': ['b', 'c'] means 'a' depends on 'b' and 'c'
61 const dependsOn = Object.create(null);
62 Object.keys(yarnData).forEach(pkgName => {
63 const pkg = yarnData[pkgName];
64 const pkgNode = getNode(yarnData, pkgName);
65 // If multiple version specs resolve to the same actual package version, we
66 // only want to record the dependency once.
67 if (dependsOn[pkgNode] !== undefined) {
68 return;
69 }
70 dependsOn[pkgNode] = [];
71 const deps = pkg.dependencies;
72 if (deps) {
73 Object.keys(deps).forEach(depName => {
74 const depNode = getNode(yarnData, `${depName}@${deps[depName]}`);
75 dependsOn[pkgNode].push(depNode);
76 });
77 }
78 });
79 return dependsOn;
80}
81/**
82 * Construct a subgraph of all nodes reachable from the given nodes.
83 */
84function subgraph(graph, nodes) {
85 const sub = Object.create(null);
86 // Seed the graph
87 let newNodes = nodes;
88 while (newNodes.length > 0) {
89 const old = newNodes;
90 newNodes = [];
91 old.forEach(i => {
92 if (!(i in sub)) {
93 sub[i] = graph[i];
94 newNodes.push(...sub[i]);
95 }
96 });
97 }
98 return sub;
99}
100/**
101 * Return the package.json data at the given path
102 */
103function pkgData(packagePath) {
104 packagePath = path.join(packagePath, 'package.json');
105 let data;
106 try {
107 data = utils.readJSONFile(packagePath);
108 }
109 catch (e) {
110 console.error('Skipping package ' + packagePath);
111 return {};
112 }
113 return data;
114}
115function convertDot(g, graphOptions, distinguishRoots = false, distinguishLeaves = false) {
116 const edges = flat(Object.keys(g).map(a => g[a].map(b => [a, b]))).sort();
117 const nodes = Object.keys(g).sort();
118 // let leaves = Object.keys(g).filter(i => g[i].length === 0);
119 // let roots = Object.keys(g).filter(i => g[i].length === 0);
120 const dot = `
121digraph DEPS {
122 ${graphOptions || ''}
123 ${nodes.map(node => `"${node}";`).join(' ')}
124 ${edges.map(([a, b]) => `"${a}" -> "${b}"`).join('\n ')}
125}
126`;
127 return dot;
128}
129function main({ dependencies, devDependencies, jupyterlab, lerna, lernaExclude, lernaInclude, path, lumino, topLevel }) {
130 const yarnData = readYarn(path);
131 const graph = buildYarnGraph(yarnData);
132 const paths = [path];
133 if (lerna !== false) {
134 paths.push(...utils.getLernaPaths(path).sort());
135 }
136 // Get all package data
137 let data = paths.map(p => pkgData(p));
138 // Get top-level package names (these won't be listed in yarn)
139 const topLevelNames = new Set(data.map(d => d.name));
140 // Filter lerna packages if a regex was supplied
141 if (lernaInclude) {
142 const re = new RegExp(lernaInclude);
143 data = data.filter(d => d.name && d.name.match(re));
144 }
145 if (lernaExclude) {
146 const re = new RegExp(lernaExclude);
147 data = data.filter(d => d.name && !d.name.match(re));
148 }
149 const depKinds = [];
150 if (devDependencies) {
151 depKinds.push('devDependencies');
152 }
153 if (dependencies) {
154 depKinds.push('dependencies');
155 }
156 /**
157 * All dependency roots *except* other packages in this repo.
158 */
159 const dependencyRoots = data.map(d => {
160 const roots = [];
161 for (const depKind of depKinds) {
162 const deps = d[depKind];
163 if (deps === undefined) {
164 continue;
165 }
166 const nodes = Object.keys(deps)
167 .map(i => {
168 // Do not get a package if it is a top-level package (and this is
169 // not in yarn).
170 if (!topLevelNames.has(i)) {
171 return getNode(yarnData, `${i}@${deps[i]}`);
172 }
173 })
174 .filter(i => i !== undefined);
175 roots.push(...nodes);
176 }
177 return roots;
178 });
179 // Find the subgraph
180 const sub = subgraph(graph, flat(dependencyRoots));
181 // Add in top-level lerna packages if desired
182 if (topLevel) {
183 data.forEach((d, i) => {
184 sub[`${d.name}@${d.version}`] = dependencyRoots[i];
185 });
186 }
187 // Filter out *all* lumino nodes
188 if (!lumino) {
189 Object.keys(sub).forEach(v => {
190 sub[v] = sub[v].filter(w => !w.startsWith('@lumino/'));
191 });
192 Object.keys(sub).forEach(v => {
193 if (v.startsWith('@lumino/')) {
194 delete sub[v];
195 }
196 });
197 }
198 // Filter for any edges going into a jlab package, and then for any
199 // disconnected jlab packages. This preserves jlab packages in the graph that
200 // point to other packages, so we can see where third-party packages come
201 // from.
202 if (!jupyterlab) {
203 Object.keys(sub).forEach(v => {
204 sub[v] = sub[v].filter(w => !w.startsWith('@jupyterlab/'));
205 });
206 Object.keys(sub).forEach(v => {
207 if (v.startsWith('@jupyterlab/') && sub[v].length === 0) {
208 delete sub[v];
209 }
210 });
211 }
212 return sub;
213}
214commander_1.default
215 .description(`Print out the dependency graph in dot graph format.`)
216 .option('--lerna', 'Include dependencies in all lerna packages')
217 .option('--lerna-include <regex>', 'A regex for package names to include in dependency roots')
218 .option('--lerna-exclude <regex>', 'A regex for lerna package names to exclude from dependency roots (can override the include regex)')
219 .option('--path [path]', 'Path to package or monorepo to investigate', '.')
220 .option('--no-jupyterlab', 'Do not include dependency connections TO @jupyterlab org packages nor isolated @jupyterlab org packages')
221 .option('--no-lumino', 'Do not include @lumino org packages')
222 .option('--no-devDependencies', 'Do not include dev dependencies')
223 .option('--no-dependencies', 'Do not include normal dependencies')
224 .option('--no-top-level', 'Do not include the top-level packages')
225 .option('--graph-options <options>', 'dot graph options (such as "ratio=0.25; concentrate=true;")')
226 .action(args => {
227 const graph = main(args);
228 console.debug(convertDot(graph, args.graphOptions));
229 console.error(`Nodes: ${Object.keys(graph).length}`);
230});
231commander_1.default.parse(process.argv);
232//# sourceMappingURL=dependency-graph.js.map
\No newline at end of file