UNPKG

21 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.create = exports._packageName = exports._packageRoots = exports._requireSort = void 0;
4const chalk = require("chalk");
5const path_1 = require("path");
6const semverCompare = require("semver-compare");
7const dependencies_1 = require("../util/dependencies");
8const files_1 = require("../util/files");
9const promise_1 = require("../util/promise");
10const strings_1 = require("../util/strings");
11const base_1 = require("./base");
12// Node.js `require`-compliant sorted order, in the **reverse** of what will
13// be looked up so that we can seed the cache with the found packages from
14// roots early.
15//
16// E.g.,
17// - `/my-app/`
18// - `/my-app/foo/`
19// - `/my-app/foo/bar`
20const _requireSort = (vals) => {
21 return vals.sort();
22};
23exports._requireSort = _requireSort;
24/**
25 * Webpack projects can have multiple "roots" of `node_modules` that can be
26 * the source of installed versions, including things like:
27 *
28 * - Node library deps: `/PATH/TO/node/v6.5.0/lib`
29 * - Monorepo projects: `/PATH/TO/MY_PROJECT/package1`, `/PATH/TO/MY_PROJECT/package2`
30 * - ... or the simple version of just one root for a project.
31 *
32 * The webpack stats object doesn't contain any information about what the root
33 * / roots are, so we have to infer it, which we do by pulling apart the paths
34 * of each `node_modules` installed module in a source bundle.
35 *
36 * @param mods {IModule[]} list of modules.
37 * @returns {Promise<string[]>} list of package roots.
38 */
39const _packageRoots = (mods) => {
40 const depRoots = [];
41 const appRoots = [];
42 // Iterate node_modules modules and add to list of roots.
43 mods
44 .filter((mod) => mod.isNodeModules)
45 .forEach((mod) => {
46 const parts = base_1._normalizeWebpackPath(mod.identifier).split(path_1.sep);
47 const nmIndex = parts.indexOf("node_modules");
48 const candidate = parts.slice(0, nmIndex).join(path_1.sep);
49 if (depRoots.indexOf(candidate) === -1) {
50 // Add unique root.
51 depRoots.push(candidate);
52 }
53 });
54 // If there are no dependency roots, then we don't care about dependencies
55 // and don't need to find any application roots. Short-circuit.
56 if (!depRoots.length) {
57 return Promise.resolve(depRoots);
58 }
59 // Now, the tricky part. Find "hidden roots" that don't have `node_modules`
60 // in the path, but still have a `package.json`. To limit the review of this
61 // we only check up to a pre-existing root above that _is_ a `node_modules`-
62 // based root, because that would have to exist if somewhere deeper in a
63 // project had a `package.json` that got flattened.
64 mods
65 .filter((mod) => !mod.isNodeModules && !mod.isSynthetic)
66 .forEach((mod) => {
67 // Start at full path.
68 // TODO(106): Revise code and tests for `fullPath`.
69 // https://github.com/FormidableLabs/inspectpack/issues/106
70 let curPath = base_1._normalizeWebpackPath(mod.identifier);
71 // We can't ever go below the minimum dep root.
72 const depRootMinLength = depRoots
73 .map((depRoot) => depRoot.length)
74 .reduce((memo, len) => memo > 0 && memo < len ? memo : len, 0);
75 // Iterate parts.
76 // tslint:disable-next-line no-conditional-assignment
77 while (curPath = curPath && path_1.dirname(curPath)) {
78 // Stop if (1) below all dep roots, (2) hit existing dep root, or
79 // (3) no longer _end_ at dep root
80 if (depRootMinLength > curPath.length ||
81 depRoots.indexOf(curPath) > -1 ||
82 !depRoots.some((d) => !!curPath && curPath.indexOf(d) === 0)) {
83 curPath = null;
84 }
85 else if (appRoots.indexOf(curPath) === -1) {
86 // Add potential unique root.
87 appRoots.push(curPath);
88 }
89 }
90 });
91 // Check all the potential dep and app roots for the presence of a
92 // `package.json` file. This is a bit of disk I/O but saves us later I/O and
93 // processing to not have false roots in the list of potential roots.
94 const roots = depRoots.concat(appRoots);
95 return Promise.all(roots.map((rootPath) => files_1.exists(path_1.join(rootPath, "package.json"))))
96 .then((rootExists) => {
97 const foundRoots = roots.filter((_, i) => rootExists[i]);
98 return exports._requireSort(foundRoots);
99 });
100};
101exports._packageRoots = _packageRoots;
102// Simple helper to get package name from a base name.
103const _packageName = (baseName) => {
104 const base = files_1.toPosixPath(baseName.trim());
105 if (!base) {
106 throw new Error(`No package name was provided`);
107 }
108 const parts = base.split("/");
109 if (parts[0].startsWith("@")) {
110 if (parts.length >= 2) {
111 // Scoped. Always use posix '/' separator.
112 return [parts[0], parts[1]].join("/");
113 }
114 throw new Error(`${baseName} is scoped, but is missing package name`);
115 }
116 return parts[0]; // Normal.
117};
118exports._packageName = _packageName;
119// Create list of **all** packages potentially at issue, including intermediate
120// ones
121const allPackages = (mods) => {
122 // Intermediate map.
123 const pkgs = {};
124 mods
125 .filter((mod) => mod.isNodeModules)
126 .forEach((mod) => {
127 // Posixified array of:
128 // ["/PATH/TO", "/", "node_modules", "/", "package1", "/", "node_modules", ...]
129 const parts = base_1.nodeModulesParts(mod.identifier)
130 // Remove prefix and any intermediate "node_modules" or "/".
131 .filter((part, i) => i > 0 && part !== "/" && part !== "node_modules");
132 // Convert last part to a package name.
133 const lastIdx = parts.length - 1;
134 parts[lastIdx] = exports._packageName(parts[lastIdx]);
135 parts.forEach((pkgName) => {
136 pkgs[pkgName] = true;
137 });
138 });
139 // Convert to list.
140 return Object.keys(pkgs).sort(strings_1.sort);
141};
142/**
143 * Create map of `basename` -> `IModule`.
144 *
145 * @param mods {IModule[]} array of module objects.
146 * @returns {IModulesByBaseName} map
147 */
148const modulesByPackageNameByPackagePath = (mods) => {
149 // Mutable, empty object to group base names with.
150 const modsMap = {};
151 // Iterate node_modules modules and add to keyed object.
152 mods.forEach((mod) => {
153 if (!mod.isNodeModules) {
154 return;
155 }
156 if (mod.baseName === null) { // Programming error.
157 throw new Error(`Encountered non-node_modules null baseName: ${JSON.stringify(mod)}`);
158 }
159 // Insert package.
160 const pkgName = exports._packageName(mod.baseName);
161 modsMap[pkgName] = modsMap[pkgName] || {};
162 // Insert package path. (All the different installs of package).
163 const pkgMap = modsMap[pkgName];
164 const modParts = base_1._normalizeWebpackPath(mod.identifier).split(path_1.sep);
165 const nmIndex = modParts.lastIndexOf("node_modules");
166 const pkgPath = modParts
167 // Remove base name path suffix.
168 .slice(0, nmIndex + 1)
169 // Add in parts of the package name (split with "/" because posixified).
170 .concat(pkgName.split("/"))
171 // Back to string.
172 .join(path_1.sep);
173 pkgMap[pkgPath] = (pkgMap[pkgPath] || []).concat(mod);
174 });
175 // Now, remove any single item keys (no duplicates).
176 Object.keys(modsMap).forEach((pkgName) => {
177 if (Object.keys(modsMap[pkgName]).length === 1) {
178 delete modsMap[pkgName];
179 }
180 });
181 return modsMap;
182};
183const createEmptyMeta = () => ({
184 depended: {
185 num: 0,
186 },
187 files: {
188 num: 0,
189 },
190 installed: {
191 num: 0,
192 },
193 packages: {
194 num: 0,
195 },
196 resolved: {
197 num: 0,
198 },
199});
200const createEmptyAsset = () => ({
201 meta: createEmptyMeta(),
202 packages: {},
203});
204const createEmptyData = () => ({
205 assets: {},
206 meta: Object.assign(createEmptyMeta(), {
207 commonRoot: null,
208 packageRoots: [],
209 }),
210});
211// Find largest common match for `node_module` dependencies.
212const commonPath = (val1, val2) => {
213 // Find last common index.
214 let i = 0;
215 while (i < val1.length && val1.charAt(i) === val2.charAt(i)) {
216 i++;
217 }
218 let candidate = val1.substring(0, i);
219 // Remove trailing slash and trailing `node_modules` in order.
220 const parts = candidate.split(path_1.sep);
221 const nmIndex = parts.indexOf("node_modules");
222 if (nmIndex > -1) {
223 candidate = parts.slice(0, nmIndex).join(path_1.sep);
224 }
225 return candidate;
226};
227const getAssetData = (commonRoot, allDeps, mods) => {
228 // Start assembling and merging in deps for each package root.
229 const data = createEmptyAsset();
230 const modsMap = modulesByPackageNameByPackagePath(mods);
231 allDeps.forEach((deps) => {
232 // Skip nulls.
233 if (deps === null) {
234 return;
235 }
236 // Add in dependencies skews for all duplicates.
237 // Get map of `name -> version -> IDependenciesByPackageName[] | [{ filePath }]`.
238 const depsToPackageName = dependencies_1.mapDepsToPackageName(deps);
239 // Go through and match to our map of `name -> filePath -> IModule[]`.
240 Object.keys(modsMap).sort(strings_1.sort).forEach((name) => {
241 // Use the modules as an "is present" lookup table.
242 const modsToFilePath = modsMap[name] || {};
243 Object.keys(depsToPackageName[name] || {}).sort(semverCompare).forEach((version) => {
244 // Have potential `filePath` match across mods and deps.
245 // Filter to just these file paths.
246 const depsForPkgVers = depsToPackageName[name][version] || {};
247 Object.keys(depsForPkgVers).sort(strings_1.sort).forEach((filePath) => {
248 // Get applicable modules.
249 const modules = (modsToFilePath[filePath] || []).map((mod) => ({
250 baseName: mod.baseName,
251 fileName: mod.identifier,
252 size: {
253 full: mod.size,
254 },
255 }));
256 // Short-circuit -- need to actually **have** modules to add.
257 if (!modules.length) {
258 return;
259 }
260 // Need to posix-ify after call to `relative`.
261 const relPath = files_1.toPosixPath(path_1.relative(commonRoot, filePath));
262 // Late patch everything.
263 data.packages[name] = data.packages[name] || {};
264 const dataVers = data.packages[name][version] = data.packages[name][version] || {};
265 const dataObj = dataVers[relPath] = dataVers[relPath] || {};
266 dataObj.skews = (dataObj.skews || []).concat(depsForPkgVers[filePath].skews);
267 dataObj.modules = dataObj.modules || [];
268 // Add _new, unique_ modules.
269 // Note that `baseName` might have multiple matches for duplicate installs, but
270 // `fileName` won't.
271 const newMods = modules
272 .filter((newMod) => !dataObj.modules.some((mod) => mod.fileName === newMod.fileName));
273 dataObj.modules = dataObj.modules.concat(newMods);
274 });
275 });
276 });
277 });
278 return data;
279};
280class Versions extends base_1.Action {
281 shouldBail() {
282 return this.getData().then((data) => data.meta.packages.num !== 0);
283 }
284 _getData() {
285 const mods = this.modules;
286 // Share a mutable package map cache across all dependency resolution.
287 const pkgMap = {};
288 // Infer the absolute paths to the package roots.
289 //
290 // The package roots come back in an order such that we cache things early
291 // that may be used later for nested directories that may need to search
292 // up higher for "flattened" dependencies.
293 return exports._packageRoots(mods).then((pkgRoots) => {
294 // If we don't have a package root, then we have no dependencies in the
295 // bundle and we can short circuit.
296 if (!pkgRoots.length) {
297 return Promise.resolve(createEmptyData());
298 }
299 // We now have a guaranteed non-empty string. Get modules map and filter to
300 // limit I/O to only potential packages.
301 const pkgsFilter = allPackages(mods);
302 // Recursively read in dependencies.
303 //
304 // However, since package roots rely on a properly seeded cache from earlier
305 // runs with a higher-up, valid traversal path, we start bottom up in serial
306 // rather than executing different roots in parallel.
307 let allDeps;
308 return promise_1.serial(pkgRoots.map((pkgRoot) => () => dependencies_1.dependencies(pkgRoot, pkgsFilter, pkgMap)))
309 // Capture deps.
310 .then((all) => { allDeps = all; })
311 // Check dependencies and validate.
312 .then(() => Promise.all(allDeps.map((deps) => {
313 // We're going to _mostly_ permissively handle uninstalled trees, but
314 // we will error if no `node_modules` exist which means likely that
315 // an `npm install` is needed.
316 if (deps !== null && !deps.dependencies.length) {
317 return Promise.all(pkgRoots.map((pkgRoot) => files_1.exists(path_1.join(pkgRoot, "node_modules"))))
318 .then((pkgRootsExist) => {
319 if (pkgRootsExist.indexOf(true) === -1) {
320 throw new Error(`Found ${mods.length} bundled files in a project ` +
321 `'node_modules' directory, but none found on disk. ` +
322 `Do you need to run 'npm install'?`);
323 }
324 });
325 }
326 return Promise.resolve();
327 })))
328 // Assemble data.
329 .then(() => {
330 // Short-circuit if all null or empty array.
331 // Really a belt-and-suspenders check, since we've already validated
332 // that package.json exists.
333 if (!allDeps.length || allDeps.every((deps) => deps === null)) {
334 return createEmptyData();
335 }
336 const { assets } = this;
337 const assetNames = Object.keys(assets).sort(strings_1.sort);
338 // Find largest-common-part of all roots for this version to do relative paths from.
339 // **Note**: No second memo argument. First `memo` is first array element.
340 const commonRoot = pkgRoots.reduce((memo, pkgRoot) => commonPath(memo, pkgRoot));
341 // Create root data without meta summary.
342 const assetsData = {};
343 assetNames.forEach((assetName) => {
344 assetsData[assetName] = getAssetData(commonRoot, allDeps, assets[assetName].mods);
345 });
346 const data = Object.assign(createEmptyData(), {
347 assets: assetsData,
348 });
349 // Attach root-level meta.
350 data.meta.packageRoots = pkgRoots;
351 data.meta.commonRoot = commonRoot;
352 // Each asset.
353 assetNames.forEach((assetName) => {
354 const { packages, meta } = data.assets[assetName];
355 Object.keys(packages).forEach((pkgName) => {
356 const pkgVersions = Object.keys(packages[pkgName]);
357 meta.packages.num += 1;
358 meta.resolved.num += pkgVersions.length;
359 data.meta.packages.num += 1;
360 data.meta.resolved.num += pkgVersions.length;
361 pkgVersions.forEach((version) => {
362 const pkgVers = packages[pkgName][version];
363 Object.keys(pkgVers).forEach((filePath) => {
364 meta.files.num += pkgVers[filePath].modules.length;
365 meta.depended.num += pkgVers[filePath].skews.length;
366 meta.installed.num += 1;
367 data.meta.files.num += pkgVers[filePath].modules.length;
368 data.meta.depended.num += pkgVers[filePath].skews.length;
369 data.meta.installed.num += 1;
370 });
371 });
372 });
373 });
374 return data;
375 });
376 });
377 }
378 _createTemplate() {
379 return new VersionsTemplate({ action: this });
380 }
381}
382// `~/different-foo/~/foo`
383const shortPath = (filePath) => filePath.replace(/node_modules/g, "~");
384// `duplicates-cjs@1.2.3 -> different-foo@^1.0.1 -> foo@^2.2.0`
385const pkgNamePath = (pkgParts) => pkgParts.reduce((m, part) => `${m}${m ? " -> " : ""}${part.name}@${part.range}`, "");
386class VersionsTemplate extends base_1.Template {
387 text() {
388 return Promise.resolve()
389 .then(() => this.action.getData())
390 .then(({ meta, assets }) => {
391 const versAsset = (name) => chalk `{gray ## \`${name}\`}`;
392 const versPkgs = (name) => Object.keys(assets[name].packages)
393 .sort(strings_1.sort)
394 .map((pkgName) => this.trim(chalk `
395 * {cyan ${pkgName}}
396 ${Object.keys(assets[name].packages[pkgName])
397 .sort(semverCompare)
398 .map((version) => this.trim(chalk `
399 * {gray ${version}}
400 ${Object.keys(assets[name].packages[pkgName][version])
401 .sort(strings_1.sort)
402 .map((filePath) => {
403 const { skews, modules, } = assets[name].packages[pkgName][version][filePath];
404 return this.trim(chalk `
405 * {green ${shortPath(filePath)}}
406 * Num deps: ${strings_1.numF(skews.length)}, files: ${strings_1.numF(modules.length)}
407 ${skews
408 .map((pkgParts) => pkgParts.map((part, i) => Object.assign({}, part, {
409 name: chalk[i < pkgParts.length - 1 ? "gray" : "cyan"](part.name),
410 })))
411 .map(pkgNamePath)
412 .sort(strings_1.sort)
413 .map((pkgStr) => this.trim(`
414 * ${pkgStr}
415 `, 24))
416 .join("\n ")}
417 `, 20);
418 })
419 .join("\n ")}
420 `, 16))
421 .join("\n ")}
422 `, 12))
423 .join("\n");
424 const versions = (name) => `${versAsset(name)}\n${versPkgs(name)}\n`;
425 const report = this.trim(chalk `
426 {cyan inspectpack --action=versions}
427 {gray =============================}
428
429 {gray ## Summary}
430 * Packages with skews: ${strings_1.numF(meta.packages.num)}
431 * Total resolved versions: ${strings_1.numF(meta.resolved.num)}
432 * Total installed packages: ${strings_1.numF(meta.installed.num)}
433 * Total depended packages: ${strings_1.numF(meta.depended.num)}
434 * Total bundled files: ${strings_1.numF(meta.files.num)}
435
436 ${Object.keys(assets)
437 .filter((name) => Object.keys(assets[name].packages).length)
438 .map(versions)
439 .join("\n")}
440 `, 10);
441 return report;
442 });
443 }
444 tsv() {
445 return Promise.resolve()
446 .then(() => this.action.getData())
447 .then(({ assets }) => ["Asset\tPackage\tVersion\tInstalled Path\tDependency Path"]
448 .concat(Object.keys(assets)
449 .filter((name) => Object.keys(assets[name].packages).length)
450 .map((name) => Object.keys(assets[name].packages)
451 .sort(strings_1.sort)
452 .map((pkgName) => Object.keys(assets[name].packages[pkgName])
453 .sort(semverCompare)
454 .map((version) => Object.keys(assets[name].packages[pkgName][version])
455 .sort(strings_1.sort)
456 .map((filePath) => assets[name].packages[pkgName][version][filePath].skews
457 .map(pkgNamePath)
458 .sort(strings_1.sort)
459 .map((pkgStr) => [
460 name,
461 pkgName,
462 version,
463 shortPath(filePath),
464 pkgStr,
465 ].join("\t"))
466 .join("\n"))
467 .join("\n"))
468 .join("\n"))
469 .join("\n"))
470 .join("\n"))
471 .join("\n"));
472 }
473}
474const create = (opts) => {
475 return new Versions(opts);
476};
477exports.create = create;