diff --git a/HISTORY.md b/HISTORY.md index eb957566..cecac0f8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,21 +1,27 @@ History ======= +## UNRELEASED + +* Add `commonRoot` to `versions` metadata to indicate what installed paths are relative to. +* BUG: Detect hidden application roots for things like `yarn` workspaces that are completely flattened. + [#103](https://github.com/FormidableLabs/inspectpack/issues/103) + ## 4.1.2 -- BUG: Use `name` field to better process `identifier` to remove things like +* BUG: Use `name` field to better process `identifier` to remove things like `/PATH/TO/node_modules/font-awesome/font-awesome.css 0"`. May result in some `baseName`s being identical despite different `identifier`s because of loaders and generated code. ## 4.1.1 -- BUG: A loader in the `identifier` field would incorrectly have all modules inferred "as a `node_modules` file", even if not. Implements a naive loader stripping heuristic to correctly assess if `node_modules` or real application source. -- Optimizes internal calls to `_isNodeModules()` from 2 to 1 for better performance. +* BUG: A loader in the `identifier` field would incorrectly have all modules inferred "as a `node_modules` file", even if not. Implements a naive loader stripping heuristic to correctly assess if `node_modules` or real application source. +* Optimizes internal calls to `_isNodeModules()` from 2 to 1 for better performance. ## 4.1.0 -- Add `emitHandler` option to `DuplicatesPlugin` to allow customized output. +* Add `emitHandler` option to `DuplicatesPlugin` to allow customized output. ## 4.0.1 @@ -29,7 +35,7 @@ History * `--action=versions`: * _Reports_: The `tsv` and `text` reports have now changed to reflect dependencies hierarchies as _installed_ (e.g., `scoped@1.2.3 -> - flattened-foo@1.1.1 -> @scope/foo@1.1.1`) to a semever range meaning + flattened-foo@1.1.1 -> @scope/foo@1.1.1`) to a semver range meaning something like as _depended_ (e.g., `scoped@1.2.3 -> flattened-foo@^1.1.0 -> @scope/foo@^1.1.1`). We expect that this change will provide much more useful information as to how and why your dependency graph impacts what is @@ -57,9 +63,9 @@ History ### Miscellaneous -- Updated README.md with note that `--action=versions` is not filtered to only +* Updated README.md with note that `--action=versions` is not filtered to only packages that would have files show up in the `--action=duplicates` report. -- Update `--action=versions` logic to explicitly use `semver-compare` for sort +* Update `--action=versions` logic to explicitly use `semver-compare` for sort order. ## 3.0.0 diff --git a/README.md b/README.md index 0854af55..a7aeb4a5 100644 --- a/README.md +++ b/README.md @@ -486,8 +486,8 @@ Other tools that inspect Webpack bundles: [npm_img]: https://badge.fury.io/js/inspectpack.svg [npm_site]: http://badge.fury.io/js/inspectpack -[trav_img]: https://api.travis-ci.org/FormidableLabs/inspectpack.svg -[trav_site]: https://travis-ci.org/FormidableLabs/inspectpack +[trav_img]: https://api.travis-ci.com/FormidableLabs/inspectpack.svg +[trav_site]: https://travis-ci.com/FormidableLabs/inspectpack [appveyor_img]: https://ci.appveyor.com/api/projects/status/github/formidablelabs/inspectpack?branch=master&svg=true [appveyor_site]: https://ci.appveyor.com/project/FormidableLabs/inspectpack [cov]: https://codecov.io diff --git a/src/lib/actions/base.ts b/src/lib/actions/base.ts index 86c79f5e..bafd5924 100644 --- a/src/lib/actions/base.ts +++ b/src/lib/actions/base.ts @@ -51,6 +51,9 @@ export const _isNodeModules = (name: string): boolean => nodeModulesParts(name). // First, strip off anything before a `?` and `!`: // - `REMOVE?KEEP` // - `REMOVE!KEEP` +// +// TODO(106): Revise code and tests for `fullPath`. +// https://github.com/FormidableLabs/inspectpack/issues/106 export const _normalizeWebpackPath = (identifier: string, name?: string): string => { const bangLastIdx = identifier.lastIndexOf("!"); const questionLastIdx = identifier.lastIndexOf("?"); @@ -63,6 +66,9 @@ export const _normalizeWebpackPath = (identifier: string, name?: string): string candidate = candidate.substr(prefixEnd + 1); } + // Naive heuristic: remove known starting webpack tokens. + candidate = candidate.replace(/^(multi |ignored )/, ""); + // Assume a normalized then truncate to name if applicable. // // E.g., diff --git a/src/lib/actions/versions.ts b/src/lib/actions/versions.ts index c3782f76..614f3c9a 100644 --- a/src/lib/actions/versions.ts +++ b/src/lib/actions/versions.ts @@ -1,5 +1,5 @@ import chalk from "chalk"; -import { join, relative, sep } from "path"; +import { dirname, join, relative, sep } from "path"; import semverCompare = require("semver-compare"); import { IActionModule, IModule } from "../interfaces/modules"; @@ -11,8 +11,10 @@ import { mapDepsToPackageName, } from "../util/dependencies"; import { exists, toPosixPath } from "../util/files"; +import { serial } from "../util/promise"; import { numF, sort } from "../util/strings"; import { + _normalizeWebpackPath, Action, IAction, IActionConstructor, @@ -21,6 +23,18 @@ import { Template, } from "./base"; +// Node.js `require`-compliant sorted order, in the **reverse** of what will +// be looked up so that we can seed the cache with the found packages from +// roots early. +// +// E.g., +// - `/my-app/` +// - `/my-app/foo/` +// - `/my-app/foo/bar` +export const _requireSort = (vals: string[]) => { + return vals.sort(); +}; + /** * Webpack projects can have multiple "roots" of `node_modules` that can be * the source of installed versions, including things like: @@ -34,26 +48,79 @@ import { * of each `node_modules` installed module in a source bundle. * * @param mods {IModule[]} list of modules. - * @returns {string[]} list of package roots. + * @returns {Promise} list of package roots. */ -const packagesRoots = (mods: IModule[]): string[] => { - const roots: string[] = []; +export const _packageRoots = (mods: IModule[]): Promise => { + const depRoots: string[] = []; + const appRoots: string[] = []; // Iterate node_modules modules and add to list of roots. mods .filter((mod) => mod.isNodeModules) .forEach((mod) => { - const parts = mod.identifier.split(sep); + const parts = _normalizeWebpackPath(mod.identifier).split(sep); const nmIndex = parts.indexOf("node_modules"); const candidate = parts.slice(0, nmIndex).join(sep); - if (roots.indexOf(candidate) === -1) { + if (depRoots.indexOf(candidate) === -1) { // Add unique root. - roots.push(candidate); + depRoots.push(candidate); + } + }); + + // If there are no dependency roots, then we don't care about dependencies + // and don't need to find any application roots. Short-circuit. + if (!depRoots.length) { + return Promise.resolve(depRoots); + } + + // Now, the tricky part. Find "hidden roots" that don't have `node_modules` + // in the path, but still have a `package.json`. To limit the review of this + // we only check up to a pre-existing root above that _is_ a `node_modules`- + // based root, because that would have to exist if somewhere deeper in a + // project had a `package.json` that got flattened. + mods + .filter((mod) => !mod.isNodeModules && !mod.isSynthetic) + .forEach((mod) => { + // Start at full path. + // TODO(106): Revise code and tests for `fullPath`. + // https://github.com/FormidableLabs/inspectpack/issues/106 + let curPath: string|null = _normalizeWebpackPath(mod.identifier); + + // We can't ever go below the minimum dep root. + const depRootMinLength = depRoots + .map((depRoot) => depRoot.length) + .reduce((memo, len) => memo > 0 && memo < len ? memo : len, 0); + + // Iterate parts. + // tslint:disable-next-line no-conditional-assignment + while (curPath = curPath && dirname(curPath)) { + // Stop if (1) below all dep roots, (2) hit existing dep root, or + // (3) no longer _end_ at dep root + if ( + depRootMinLength > curPath.length || + depRoots.indexOf(curPath) > -1 || + !depRoots.some((d) => !!curPath && curPath.indexOf(d) === 0) + ) { + curPath = null; + } else if (appRoots.indexOf(curPath) === -1) { + // Add potential unique root. + appRoots.push(curPath); + } } }); - return roots.sort(); + // Check all the potential dep and app roots for the presence of a + // `package.json` file. This is a bit of disk I/O but saves us later I/O and + // processing to not have false roots in the list of potential roots. + const roots = depRoots.concat(appRoots); + return Promise.all( + roots.map((rootPath) => exists(join(rootPath, "package.json"))), + ) + .then((rootExists) => { + const foundRoots = roots.filter((_, i) => rootExists[i]); + return _requireSort(foundRoots); + }); }; // Simple helper to get package name from a base name. @@ -134,7 +201,7 @@ const modulesByPackageNameByPackagePath = ( // Insert package path. (All the different installs of package). const pkgMap = modsMap[pkgName]; - const modParts = mod.identifier.split(sep); + const modParts = _normalizeWebpackPath(mod.identifier).split(sep); const nmIndex = modParts.lastIndexOf("node_modules"); const pkgPath = modParts // Remove base name path suffix. @@ -183,6 +250,10 @@ export interface IVersionsMeta { interface IVersionsSummary extends IVersionsMeta { // Inferred base path of the project / node_modules. packageRoots: string[]; + + // Longest common path between package roots. + // Installed paths are relative to this. + commonRoot: string | null; } interface IVersionsPackages extends IDependenciesByPackageName { @@ -235,12 +306,33 @@ const createEmptyData = (): IVersionsData => ({ assets: {}, meta: { ...createEmptyMeta(), + commonRoot: null, packageRoots: [], }, }); +// Find largest common match for `node_module` dependencies. +const commonPath = (val1: string, val2: string) => { + // Find last common index. + let i = 0; + while (i < val1.length && val1.charAt(i) === val2.charAt(i)) { + i++; + } + + let candidate = val1.substring(0, i); + + // Remove trailing slash and trailing `node_modules` in order. + const parts = candidate.split(sep); + const nmIndex = parts.indexOf("node_modules"); + if (nmIndex > -1) { + candidate = parts.slice(0, nmIndex).join(sep); + } + + return candidate; +}; + const getAssetData = ( - pkgRoots: string[], + commonRoot: string, allDeps: Array, mods: IModule[], ): IVersionsAsset => { @@ -248,7 +340,7 @@ const getAssetData = ( const data = createEmptyAsset(); const modsMap = modulesByPackageNameByPackagePath(mods); - allDeps.forEach((deps, depsIdx) => { + allDeps.forEach((deps) => { // Skip nulls. if (deps === null) { return; } @@ -279,14 +371,21 @@ const getAssetData = ( if (!modules.length) { return; } // Need to posix-ify after call to `relative`. - const relPath = toPosixPath(relative(pkgRoots[depsIdx], filePath)); + const relPath = toPosixPath(relative(commonRoot, filePath)); // Late patch everything. data.packages[name] = data.packages[name] || {}; const dataVers = data.packages[name][version] = data.packages[name][version] || {}; const dataObj = dataVers[relPath] = dataVers[relPath] || {}; dataObj.skews = (dataObj.skews || []).concat(depsForPkgVers[filePath].skews); - dataObj.modules = (dataObj.modules || []).concat(modules); + + dataObj.modules = dataObj.modules || []; + // Add _new, unique_ modules. + // Note that `baseName` might have multiple matches for duplicate installs, but + // `fileName` won't. + const newMods = modules + .filter((newMod) => !dataObj.modules.some((mod) => mod.fileName === newMod.fileName)); + dataObj.modules = dataObj.modules.concat(newMods); }); }); }); @@ -299,95 +398,118 @@ class Versions extends Action { protected _getData(): Promise { const mods = this.modules; - // Infer the absolute paths to the package roots. - const pkgRoots = packagesRoots(mods); - - // If we don't have a package root, then we have no dependencies in the - // bundle and we can short circuit. - if (!pkgRoots.length) { - return Promise.resolve(createEmptyData()); - } + // Share a mutable package map cache across all dependency resolution. + const pkgMap = {}; - // We now have a guaranteed non-empty string. Get modules map and filter to - // limit I/O to only potential packages. - const pkgsFilter = allPackages(mods); - - // Recursively read in dependencies. - let allDeps: Array; - return Promise.all(pkgRoots.map((pkgRoot) => dependencies(pkgRoot, pkgsFilter))) - // Capture deps. - .then((all) => { allDeps = all; }) - // Check dependencies and validate. - .then(() => Promise.all(allDeps.map((deps, i) => { - // We're going to _mostly_ permissively handle uninstalled trees, but - // we will error if `node_modules` doesn't exist which means likely that - // an `npm install` is needed. - if (deps !== null && !deps.dependencies.length) { - const pkgNodeModules = join(pkgRoots[i], "node_modules"); - return exists(pkgNodeModules) - .then((nmExists) => { - if (!nmExists) { - throw new Error( - `Found ${mods.length} bundled files in 'node_modules', but ` + - `'${pkgNodeModules}' doesn't exist. ` + - `Do you need to run 'npm install'?`, - ); - } - }); - } - }))) - // Assemble data. - .then(() => { - // Short-circuit if all null or empty array. - // Really a belt-and-suspenders check, since we've already validated - // that package.json exists. - if (!allDeps.length || allDeps.every((deps) => deps === null)) { - return createEmptyData(); - } + // Infer the absolute paths to the package roots. + // + // The package roots come back in an order such that we cache things early + // that may be used later for nested directories that may need to search + // up higher for "flattened" dependencies. + return _packageRoots(mods).then((pkgRoots) => { + // If we don't have a package root, then we have no dependencies in the + // bundle and we can short circuit. + if (!pkgRoots.length) { + return Promise.resolve(createEmptyData()); + } - const { assets } = this; - const assetNames = Object.keys(assets).sort(sort); - - // Create root data without meta summary. - const data: IVersionsData = { - ...createEmptyData(), - assets: assetNames.reduce((memo, assetName) => ({ - ...memo, - [assetName]: getAssetData(pkgRoots, allDeps, assets[assetName].mods), - }), {}), - }; - - // Attach root-level meta. - data.meta.packageRoots = pkgRoots; - assetNames.forEach((assetName) => { - const { packages, meta } = data.assets[assetName]; - - Object.keys(packages).forEach((pkgName) => { - const pkgVersions = Object.keys(packages[pkgName]); - - meta.packages.num += 1; - meta.resolved.num += pkgVersions.length; - - data.meta.packages.num += 1; - data.meta.resolved.num += pkgVersions.length; - - pkgVersions.forEach((version) => { - const pkgVers = packages[pkgName][version]; - Object.keys(pkgVers).forEach((filePath) => { - meta.files.num += pkgVers[filePath].modules.length; - meta.depended.num += pkgVers[filePath].skews.length; - meta.installed.num += 1; - - data.meta.files.num += pkgVers[filePath].modules.length; - data.meta.depended.num += pkgVers[filePath].skews.length; - data.meta.installed.num += 1; + // We now have a guaranteed non-empty string. Get modules map and filter to + // limit I/O to only potential packages. + const pkgsFilter = allPackages(mods); + + // Recursively read in dependencies. + // + // However, since package roots rely on a properly seeded cache from earlier + // runs with a higher-up, valid traversal path, we start bottom up in serial + // rather than executing different roots in parallel. + let allDeps: Array; + return serial( + pkgRoots.map((pkgRoot) => () => dependencies(pkgRoot, pkgsFilter, pkgMap)), + ) + // Capture deps. + .then((all) => { allDeps = all; }) + // Check dependencies and validate. + .then(() => Promise.all(allDeps.map((deps) => { + // We're going to _mostly_ permissively handle uninstalled trees, but + // we will error if no `node_modules` exist which means likely that + // an `npm install` is needed. + if (deps !== null && !deps.dependencies.length) { + return Promise.all( + pkgRoots.map((pkgRoot) => exists(join(pkgRoot, "node_modules"))), + ) + .then((pkgRootsExist) => { + if (pkgRootsExist.indexOf(true) === -1) { + throw new Error( + `Found ${mods.length} bundled files in a project ` + + `'node_modules' directory, but none found on disk. ` + + `Do you need to run 'npm install'?`, + ); + } + }); + } + + return Promise.resolve(); + }))) + // Assemble data. + .then(() => { + // Short-circuit if all null or empty array. + // Really a belt-and-suspenders check, since we've already validated + // that package.json exists. + if (!allDeps.length || allDeps.every((deps) => deps === null)) { + return createEmptyData(); + } + + const { assets } = this; + const assetNames = Object.keys(assets).sort(sort); + + // Find largest-common-part of all roots for this version to do relative paths from. + // **Note**: No second memo argument. First `memo` is first array element. + const commonRoot = pkgRoots.reduce((memo, pkgRoot) => commonPath(memo, pkgRoot)); + + // Create root data without meta summary. + const data: IVersionsData = { + ...createEmptyData(), + assets: assetNames.reduce((memo, assetName) => ({ + ...memo, + [assetName]: getAssetData(commonRoot, allDeps, assets[assetName].mods), + }), {}), + }; + + // Attach root-level meta. + data.meta.packageRoots = pkgRoots; + data.meta.commonRoot = commonRoot; + + // Each asset. + assetNames.forEach((assetName) => { + const { packages, meta } = data.assets[assetName]; + + Object.keys(packages).forEach((pkgName) => { + const pkgVersions = Object.keys(packages[pkgName]); + + meta.packages.num += 1; + meta.resolved.num += pkgVersions.length; + + data.meta.packages.num += 1; + data.meta.resolved.num += pkgVersions.length; + + pkgVersions.forEach((version) => { + const pkgVers = packages[pkgName][version]; + Object.keys(pkgVers).forEach((filePath) => { + meta.files.num += pkgVers[filePath].modules.length; + meta.depended.num += pkgVers[filePath].skews.length; + meta.installed.num += 1; + + data.meta.files.num += pkgVers[filePath].modules.length; + data.meta.depended.num += pkgVers[filePath].skews.length; + data.meta.installed.num += 1; + }); }); }); + }); + return data; }); - - return data; }); } diff --git a/src/lib/util/dependencies.ts b/src/lib/util/dependencies.ts index 0f3b634d..b571f010 100644 --- a/src/lib/util/dependencies.ts +++ b/src/lib/util/dependencies.ts @@ -1,4 +1,4 @@ -import { dirname, join, resolve } from "path"; +import { dirname, join } from "path"; import { readDir, readJson, toPosixPath } from "./files"; export interface INpmPackageBase { @@ -123,7 +123,7 @@ export const readPackages = ( dirs // Filter to known packages. .filter(isIncludedPkg) - // Recurse + // Recurse. .map((dir) => readPackages(join(path, "node_modules", dir), pkgsFilter, _cache)), )) // The cache **is** our return value. @@ -142,51 +142,70 @@ export const _resolvePackageMap = ( {}, )); -const _findPackage = ({ +export const _findPackage = ({ filePath, name, pkgMap, - rootPath, }: { filePath: string, name: string, pkgMap: INpmPackageMap, - rootPath: string, }): { isFlattened: boolean, pkgPath: string | null; pkgObj: INpmPackage | null; } => { - const resolvedRoot = resolve(rootPath); + // We now check the existing package map which, if iterating in correct + // directory order, should already have higher up roots that may contain + // `node_modules` **within** the `require` resolution rules that would + // naturally be the "selected" module. + // + // Fixes https://github.com/FormidableLabs/inspectpack/issues/10 + const cachedRoots = Object.keys(pkgMap) + // Get directories. + .map((k) => dirname(k)) + // Limit to those that are a higher up directory from our root, which + // is fair game by Node.js `require` resolution rules, and not the current + // root because that already failed. + .filter((p) => p !== filePath && filePath.indexOf(p) === 0); + + const roots = [filePath].concat(cachedRoots); // Iterate down potential paths. - let curFilePath = filePath; + // If we find it as _first_ result, then it hasn't been flattened. let isFlattened = false; - while (resolvedRoot.length <= resolve(curFilePath).length) { - // Check at this level. - const pkgPath = join(curFilePath, "node_modules", name); - const pkgJsonPath = join(pkgPath, "package.json"); - const pkgObj = pkgMap[pkgJsonPath]; - - // Found a match. - if (pkgObj) { - // Validation: These should all be **real** npm packages, so we should - // **never** fail here. But, can't hurt to check. - if (!pkgObj.name) { - throw new Error(`Found package without name: ${JSON.stringify(pkgObj)}`); - } else if (!pkgObj.version) { - throw new Error(`Found package without version: ${JSON.stringify(pkgObj)}`); + + for (const curRoot of roots) { + // Reset to full path. This _will_ end up in some duplicate checks, but + // shouldn't be too expensive. + let curFilePath = filePath; + + while (curRoot.length <= curFilePath.length) { + // Check at this level. + const pkgPath = join(curFilePath, "node_modules", name); + const pkgJsonPath = join(pkgPath, "package.json"); + const pkgObj = pkgMap[pkgJsonPath]; + + // Found a match. + if (pkgObj) { + // Validation: These should all be **real** npm packages, so we should + // **never** fail here. But, can't hurt to check. + if (!pkgObj.name) { + throw new Error(`Found package without name: ${JSON.stringify(pkgObj)}`); + } else if (!pkgObj.version) { + throw new Error(`Found package without version: ${JSON.stringify(pkgObj)}`); + } + + return { isFlattened, pkgPath, pkgObj }; } - return { isFlattened, pkgPath, pkgObj }; + // Decrement path. If we find it now, it's flattened. + curFilePath = dirname(curFilePath); + isFlattened = true; } - - // Decrement path. If we find it now, it's flattened. - curFilePath = dirname(curFilePath); - isFlattened = true; } - return { isFlattened, pkgPath: null, pkgObj: null }; + return { isFlattened: false, pkgPath: null, pkgObj: null }; }; // - Populates `pkgMap` with installed `package.json`s @@ -198,14 +217,12 @@ const _recurseDependencies = ({ names, pkgMap, pkgsFilter, - rootPath, }: { filePath: string, foundMap?: { [filePath: string]: { [name: string]: IDependencies | null } }, names: string[], pkgMap: INpmPackageMap, pkgsFilter?: string[], - rootPath: string, }): IDependencies[] => { // Build up cache. const _foundMap = foundMap || {}; @@ -217,10 +234,12 @@ const _recurseDependencies = ({ // Inflated current level. .map((name): { pkg: IDependencies, pkgNames: string[] } | null => { // Find actual location. - const { isFlattened, pkgPath, pkgObj } = _findPackage({ filePath, name, rootPath, pkgMap }); + const { isFlattened, pkgPath, pkgObj } = _findPackage({ filePath, name, pkgMap }); // Short-circuit on not founds. - if (pkgPath === null || pkgObj === null) { return null; } + if (pkgPath === null || pkgObj === null) { + return null; + } // Build and check cache. const found = _foundMap[pkgPath] = _foundMap[pkgPath] || {}; @@ -267,7 +286,6 @@ const _recurseDependencies = ({ names: pkgNames, pkgMap, pkgsFilter, - rootPath, }); } @@ -446,7 +464,6 @@ export const dependencies = ( names, pkgMap, pkgsFilter, - rootPath: filePath, }), filePath, name: rootPkg.name || "ROOT", diff --git a/src/lib/util/promise.ts b/src/lib/util/promise.ts new file mode 100644 index 00000000..ec5adeaa --- /dev/null +++ b/src/lib/util/promise.ts @@ -0,0 +1,5 @@ +// Execute promises in serial. +export const serial = (proms: Array<() => Promise>) => proms.reduce( + (memo, prom) => memo.then((vals) => prom().then((val: any) => vals.concat(val))), + Promise.resolve([]), +); diff --git a/src/plugin/duplicates.ts b/src/plugin/duplicates.ts index 9be7493f..e3e44bfa 100644 --- a/src/plugin/duplicates.ts +++ b/src/plugin/duplicates.ts @@ -115,6 +115,7 @@ const getDuplicatesPackageNames = (data: IDuplicatesData): IPackageNames => { export const _getDuplicatesVersionsData = ( dupData: IDuplicatesData, pkgDataOrig: IVersionsData, + addWarning: (val: string) => number, ): IVersionsData => { // Start with a clone of the data. const pkgData: IVersionsData = JSON.parse(JSON.stringify(pkgDataOrig)); @@ -158,6 +159,46 @@ export const _getDuplicatesVersionsData = ( }); }); + // Validate mutated package data by checking we have matching number of + // sources (identical or not). + const extraSources = dupData.meta.extraSources.num; + + interface IFilesMap { [baseName: string]: number; } + const foundFilesMap: IFilesMap = {}; + Object.keys(pkgData.assets).forEach((assetName) => { + const pkgs = pkgData.assets[assetName].packages; + Object.keys(pkgs).forEach((pkgName) => { + Object.keys(pkgs[pkgName]).forEach((pkgVers) => { + const pkgInstalls = pkgs[pkgName][pkgVers]; + Object.keys(pkgInstalls).forEach((installPath) => { + pkgInstalls[installPath].modules.forEach((mod) => { + if (!mod.baseName) { return; } + foundFilesMap[mod.baseName] = (foundFilesMap[mod.baseName] || 0) + 1; + }); + }); + }); + }); + }); + const foundDupFilesMap: IFilesMap = Object.keys(foundFilesMap) + .reduce((memo: IFilesMap, baseName) => { + if (foundFilesMap[baseName] >= 2) { + memo[baseName] = foundFilesMap[baseName]; + } + + return memo; + }, {}); + const foundSources = Object.keys(foundDupFilesMap) + .reduce((memo, baseName) => { + return memo + foundDupFilesMap[baseName]; + }, 0); + + if (extraSources !== foundSources) { + addWarning(error( + `Missing sources: Expected ${numF(extraSources)}, found ${numF(foundSources)}.\n` + + chalk`{white Found map:} {gray ${JSON.stringify(foundDupFilesMap)}}\n`, + )); + } + return pkgData; }; @@ -212,7 +253,7 @@ export class DuplicatesPlugin { } // Filter versions/packages data to _just_ duplicates. - const pkgData = _getDuplicatesVersionsData(dupData, pkgDataOrig); + const pkgData = _getDuplicatesVersionsData(dupData, pkgDataOrig, addMsg); // Choose output format. const fmt = emitErrors ? error : warning; diff --git a/test/fixtures/config/scenarios.json b/test/fixtures/config/scenarios.json index 3bd6a250..7722ea64 100644 --- a/test/fixtures/config/scenarios.json +++ b/test/fixtures/config/scenarios.json @@ -8,5 +8,6 @@ { "WEBPACK_CWD": "../../test/fixtures/multiple-chunks" }, { "WEBPACK_CWD": "../../test/fixtures/scoped" }, { "WEBPACK_CWD": "../../test/fixtures/tree-shaking" }, - { "WEBPACK_CWD": "../../test/fixtures/multiple-resolved-no-duplicates" } + { "WEBPACK_CWD": "../../test/fixtures/multiple-resolved-no-duplicates" }, + { "WEBPACK_CWD": "../../test/fixtures/hidden-app-roots" } ] \ No newline at end of file diff --git a/test/fixtures/hidden-app-roots/node_modules/different-foo/index.js b/test/fixtures/hidden-app-roots/node_modules/different-foo/index.js new file mode 100644 index 00000000..b407063a --- /dev/null +++ b/test/fixtures/hidden-app-roots/node_modules/different-foo/index.js @@ -0,0 +1,11 @@ +const { foo } = require("foo"); +const { car } = require("foo/car"); + +module.exports = { + differentFoo() { + return foo(); + }, + car() { + return car(); + } +}; diff --git a/test/fixtures/hidden-app-roots/node_modules/different-foo/node_modules/foo/car.js b/test/fixtures/hidden-app-roots/node_modules/different-foo/node_modules/foo/car.js new file mode 100644 index 00000000..5335bb6f --- /dev/null +++ b/test/fixtures/hidden-app-roots/node_modules/different-foo/node_modules/foo/car.js @@ -0,0 +1,5 @@ +module.exports = { + car() { + return "I'm a car!"; + } +}; diff --git a/test/fixtures/hidden-app-roots/node_modules/different-foo/node_modules/foo/index.js b/test/fixtures/hidden-app-roots/node_modules/different-foo/node_modules/foo/index.js new file mode 100644 index 00000000..13a8686e --- /dev/null +++ b/test/fixtures/hidden-app-roots/node_modules/different-foo/node_modules/foo/index.js @@ -0,0 +1,5 @@ +module.exports = { + foo() { + return "different foo"; + } +}; diff --git a/test/fixtures/hidden-app-roots/node_modules/different-foo/node_modules/foo/package.json b/test/fixtures/hidden-app-roots/node_modules/different-foo/node_modules/foo/package.json new file mode 100644 index 00000000..206586e7 --- /dev/null +++ b/test/fixtures/hidden-app-roots/node_modules/different-foo/node_modules/foo/package.json @@ -0,0 +1,6 @@ +{ + "name": "foo", + "version": "3.3.3", + "description": "DUMMY MODULE, but different", + "main": "index.js" +} diff --git a/test/fixtures/hidden-app-roots/node_modules/different-foo/package.json b/test/fixtures/hidden-app-roots/node_modules/different-foo/package.json new file mode 100644 index 00000000..8fbfe93b --- /dev/null +++ b/test/fixtures/hidden-app-roots/node_modules/different-foo/package.json @@ -0,0 +1,9 @@ +{ + "name": "different-foo", + "version": "1.1.1", + "description": "has contained, _different_ source version of foo", + "main": "index.js", + "dependencies": { + "foo": "^3.0.1" + } +} diff --git a/test/fixtures/hidden-app-roots/node_modules/foo/index.js b/test/fixtures/hidden-app-roots/node_modules/foo/index.js new file mode 100644 index 00000000..d00aed11 --- /dev/null +++ b/test/fixtures/hidden-app-roots/node_modules/foo/index.js @@ -0,0 +1,5 @@ +module.exports = { + foo() { + return "foo"; + } +}; diff --git a/test/fixtures/hidden-app-roots/node_modules/foo/package.json b/test/fixtures/hidden-app-roots/node_modules/foo/package.json new file mode 100644 index 00000000..cf3e899a --- /dev/null +++ b/test/fixtures/hidden-app-roots/node_modules/foo/package.json @@ -0,0 +1,6 @@ +{ + "name": "foo", + "version": "1.1.1", + "description": "DUMMY MODULE", + "main": "index.js" +} diff --git a/test/fixtures/hidden-app-roots/package.json b/test/fixtures/hidden-app-roots/package.json new file mode 100644 index 00000000..dfe06eb7 --- /dev/null +++ b/test/fixtures/hidden-app-roots/package.json @@ -0,0 +1,7 @@ +{ + "name": "hidden-app-roots", + "version": "1.2.3", + "description": "DUMMY APP", + "main": "packages/hidden-app/src/index.js", + "dependencies": {} +} diff --git a/test/fixtures/hidden-app-roots/packages/hidden-app/package.json b/test/fixtures/hidden-app-roots/packages/hidden-app/package.json new file mode 100644 index 00000000..5e709388 --- /dev/null +++ b/test/fixtures/hidden-app-roots/packages/hidden-app/package.json @@ -0,0 +1,10 @@ +{ + "name": "package1", + "version": "1.1.1", + "description": "DUMMY PACKAGE", + "main": "index.js", + "dependencies": { + "different-foo": "^1.0.1", + "foo": "^1.0.0" + } +} diff --git a/test/fixtures/hidden-app-roots/packages/hidden-app/src/index.js b/test/fixtures/hidden-app-roots/packages/hidden-app/src/index.js new file mode 100644 index 00000000..4aa55b30 --- /dev/null +++ b/test/fixtures/hidden-app-roots/packages/hidden-app/src/index.js @@ -0,0 +1,8 @@ +/* eslint-disable no-console*/ + +// both packages are flattened into root `node_modules`. +const { foo } = require("foo"); +const { differentFoo } = require("different-foo"); + +console.log("foo", foo()); +console.log("differentFoo", differentFoo()); diff --git a/test/fixtures/hidden-app-roots/webpack.config.js b/test/fixtures/hidden-app-roots/webpack.config.js new file mode 100644 index 00000000..3341339e --- /dev/null +++ b/test/fixtures/hidden-app-roots/webpack.config.js @@ -0,0 +1,9 @@ +module.exports = (webpack, config) => { + if (webpack) { + config.entry = { + bundle: "./packages/hidden-app/src/index.js" + }; + } + + return config; +}; diff --git a/test/lib/actions/versions.spec.ts b/test/lib/actions/versions.spec.ts index 51bf99a8..1a47db6b 100644 --- a/test/lib/actions/versions.spec.ts +++ b/test/lib/actions/versions.spec.ts @@ -1,6 +1,8 @@ import { join, resolve, sep } from "path"; import { _packageName, + _packageRoots, + _requireSort, create, IVersionsData, IVersionsMeta, @@ -40,18 +42,21 @@ export const EMPTY_VERSIONS_DATA: IVersionsData = { assets: {}, meta: { ...EMPTY_VERSIONS_META, + commonRoot: null, packageRoots: [], }, }; const BASE_DUPS_CJS_DATA = merge(EMPTY_VERSIONS_DATA, { meta: { + commonRoot: resolve(__dirname, "../../fixtures/duplicates-cjs"), packageRoots: [resolve(__dirname, "../../fixtures/duplicates-cjs")], }, }); const BASE_SCOPED_DATA = merge(EMPTY_VERSIONS_DATA, { meta: { + commonRoot: resolve(__dirname, "../../fixtures/scoped"), packageRoots: [resolve(__dirname, "../../fixtures/scoped")], }, }); @@ -83,6 +88,85 @@ const patchAction = (name) => (instance) => { return instance; }; +// Complex roots from hidden roots regression sample. +const complexHiddenAppRoots = { + "node_modules": { + "fbjs": { + "package.json": JSON.stringify({ + name: "fbjs", + version: "1.1.1", + }, null, 2), + }, + "hoist-non-react-statics": { + "package.json": JSON.stringify({ + name: "hoist-non-react-statics", + version: "1.1.1", + }, null, 2), + }, + "prop-types": { + "package.json": JSON.stringify({ + name: "prop-types", + version: "1.1.1", + }, null, 2), + }, + "react-addons-shallow-compare": { + "node_modules": { + "fbjs/package.json": JSON.stringify({ + name: "fbjs", + version: "2.2.2", + }, null, 2), + }, + "package.json": JSON.stringify({ + dependencies: { + fbjs: "^2.0.0", + }, + name: "react-addons-shallow-compare", + version: "1.1.1", + }, null, 2), + }, + "react-apollo": { + "node_modules": { + "hoist-non-react-statics": { + "package.json": JSON.stringify({ + name: "hoist-non-react-statics", + version: "2.2.2", + }, null, 2), + }, + "prop-types": { + "package.json": JSON.stringify({ + name: "prop-types", + version: "2.2.2", + }, null, 2), + }, + }, + "package.json": JSON.stringify({ + dependencies: { + "hoist-non-react-statics": "^2.0.0", + "prop-types": "^2.0.0", + }, + name: "react-apollo", + version: "1.1.1", + }, null, 2), + }, + }, + "package.json": JSON.stringify({ + name: "complex-hidden-app-roots", + }, null, 2), + "packages": { + "hidden-app": { + "package.json": JSON.stringify({ + dependencies: { + "fbjs": "^1.0.0", + "hoist-non-react-statics": "^1.0.0", + "prop-types": "^1.0.0", + "react-apollo": "^1.0.0", + }, + name: "hidden-app", + }, null, 2), + }, + }, +}; + describe("lib/actions/versions", () => { let fixtures; let fixtureDirs; @@ -90,6 +174,7 @@ describe("lib/actions/versions", () => { let dupsCjsInstance; let scopedInstance; let multipleRootsInstance; + let hiddenAppRootsInstance; const getData = (name) => Promise.resolve() .then(() => create({ stats: fixtures[toPosixPath(name)] }).validate()) @@ -106,6 +191,7 @@ describe("lib/actions/versions", () => { "duplicates-cjs", "scoped", "multiple-roots", + "hidden-app-roots", ].map((name) => create({ stats: fixtures[toPosixPath(join(name, "dist-development-4"))], }).validate())) @@ -115,12 +201,14 @@ describe("lib/actions/versions", () => { dupsCjsInstance, scopedInstance, multipleRootsInstance, + hiddenAppRootsInstance, ] = instances; expect(simpleInstance).to.not.be.an("undefined"); expect(dupsCjsInstance).to.not.be.an("undefined"); expect(scopedInstance).to.not.be.an("undefined"); expect(multipleRootsInstance).to.not.be.an("undefined"); + expect(hiddenAppRootsInstance).to.not.be.an("undefined"); }), ); @@ -593,6 +681,106 @@ describe("lib/actions/versions", () => { expectProp.to.have.property("modules").that.has.length(2); }); }); + + // Regression test: https://github.com/FormidableLabs/inspectpack/issues/103 + it("displays versions skews correctly for hidden app roots", () => { + mock({ + "test/fixtures/hidden-app-roots": fixtureDirs["test/fixtures/hidden-app-roots"], + }); + + return hiddenAppRootsInstance.getData() + .then((data) => { + expect(data).to.have.keys("meta", "assets"); + expect(data).to.have.property("meta").that.eql(merge(EMPTY_VERSIONS_DATA.meta, { + commonRoot: resolve(__dirname, "../../fixtures/hidden-app-roots"), + depended: { + num: 2, + }, + files: { + num: 3, + }, + installed: { + num: 2, + }, + packageRoots: [ + resolve(__dirname, "../../fixtures/hidden-app-roots"), + resolve(__dirname, "../../fixtures/hidden-app-roots/packages/hidden-app"), + ], + packages: { + num: 1, + }, + resolved: { + num: 2, + }, + })); + + let expectProp; + + expectProp = expect(data).to.have.nested.property( + "assets.bundle\\.js.packages.foo.1\\.1\\.1.node_modules/foo", + ); + expectProp.to.have.property("skews").that.has.length(1); + expectProp.to.have.property("modules").that.has.length(1); + + expectProp = expect(data).to.have.nested.property( + "assets.bundle\\.js.packages.foo.3\\.3\\.3.node_modules/different-foo/node_modules/foo", + ); + expectProp.to.have.property("skews").that.has.length(1); + expectProp.to.have.property("modules").that.has.length(2); + }); + }); + + // Regression test: https://github.com/FormidableLabs/inspectpack/issues/103 + it("displays versions skews correctly for hidden app roots with empty node_modules", () => { + const curFixtures = JSON.parse(JSON.stringify(fixtureDirs["test/fixtures/hidden-app-roots"])); + // Add empty `node_modules` to hit different code path. + curFixtures.packages["hidden-app"].node_modules = {}; + + mock({ + "test/fixtures/hidden-app-roots": curFixtures, + }); + + return hiddenAppRootsInstance.getData() + .then((data) => { + expect(data).to.have.keys("meta", "assets"); + expect(data).to.have.property("meta").that.eql(merge(EMPTY_VERSIONS_DATA.meta, { + commonRoot: resolve(__dirname, "../../fixtures/hidden-app-roots"), + depended: { + num: 2, + }, + files: { + num: 3, + }, + installed: { + num: 2, + }, + packageRoots: [ + resolve(__dirname, "../../fixtures/hidden-app-roots"), + resolve(__dirname, "../../fixtures/hidden-app-roots/packages/hidden-app"), + ], + packages: { + num: 1, + }, + resolved: { + num: 2, + }, + })); + + let expectProp; + + expectProp = expect(data).to.have.nested.property( + "assets.bundle\\.js.packages.foo.1\\.1\\.1.node_modules/foo", + ); + expectProp.to.have.property("skews").that.has.length(1); + expectProp.to.have.property("modules").that.has.length(1); + + expectProp = expect(data).to.have.nested.property( + "assets.bundle\\.js.packages.foo.3\\.3\\.3.node_modules/different-foo/node_modules/foo", + ); + expectProp.to.have.property("skews").that.has.length(1); + expectProp.to.have.property("modules").that.has.length(2); + }); + }); }); }); @@ -726,16 +914,18 @@ inspectpack --action=versions ## Summary * Packages with skews: 1 * Total resolved versions: 2 -* Total installed packages: 2 +* Total installed packages: 3 * Total depended packages: 3 * Total bundled files: 4 ## \`bundle.js\` * foo * 1.1.1 - * ~/foo - * Num deps: 2, files: 2 + * packages/package1/~/foo + * Num deps: 1, files: 1 * package1@1.1.1 -> foo@^1.0.0 + * packages/package2/~/foo + * Num deps: 1, files: 1 * package2@2.2.2 -> foo@^1.0.0 * 3.3.3 * ~/different-foo/~/foo @@ -744,6 +934,39 @@ inspectpack --action=versions `.trim()); }); }); + + // Regression test: https://github.com/FormidableLabs/inspectpack/issues/103 + it("displays versions skews correctly for hidden app roots", () => { + mock({ + "test/fixtures/hidden-app-roots": fixtureDirs["test/fixtures/hidden-app-roots"], + }); + + return hiddenAppRootsInstance.template.text() + .then((textStr) => { + expect(textStr).to.eql(` +inspectpack --action=versions +============================= + +## Summary +* Packages with skews: 1 +* Total resolved versions: 2 +* Total installed packages: 2 +* Total depended packages: 2 +* Total bundled files: 3 + +## \`bundle.js\` +* foo + * 1.1.1 + * ~/foo + * Num deps: 1, files: 1 + * package1@1.1.1 -> foo@^1.0.0 + * 3.3.3 + * ~/different-foo/~/foo + * Num deps: 1, files: 2 + * package1@1.1.1 -> different-foo@^1.0.1 -> foo@^3.0.1 + `.trim()); + }); + }); }); describe("tsv", () => { @@ -766,6 +989,270 @@ bundle.js foo 4.3.3 ~/unscoped-foo/~/deeper-unscoped/~/foo scoped@1.2.3 -> unsco /*tslint:enable max-line-length*/ }); }); + + // Regression test: https://github.com/FormidableLabs/inspectpack/issues/103 + it("displays versions skews correctly for hidden app roots", () => { + mock({ + "test/fixtures/hidden-app-roots": fixtureDirs["test/fixtures/hidden-app-roots"], + }); + + return hiddenAppRootsInstance.template.tsv() + .then((tsvStr) => { + /*tslint:disable max-line-length*/ + expect(tsvStr).to.eql(` +Asset Package Version Installed Path Dependency Path +bundle.js foo 1.1.1 ~/foo package1@1.1.1 -> foo@^1.0.0 +bundle.js foo 3.3.3 ~/different-foo/~/foo package1@1.1.1 -> different-foo@^1.0.1 -> foo@^3.0.1 + `.trim()); + /*tslint:enable max-line-length*/ + }); + }); + }); + + describe("_requireSort", () => { + it("handles base cases", () => { + expect(_requireSort([])).to.eql([]); + }); + + it("handles simple roots", () => { + const vals = [ + "/BASE", + "/BASE/packages/hidden-app", + ]; + + expect(_requireSort(vals)).to.eql(vals); + }); + + it("handles complex roots", () => { + expect(_requireSort([ + "/foo/two/a", + "/foo/1/2", + "/bar/foo/one/b", + "/foo/one/b", + "/bar/foo/one", + "/bar/foo/one/a", + "/foo/one", + "/foo/one/a", + "/bar/", + "/foo/two", + "/foo/1", + "/bar/foo", + "/foo/two/d", + "/foo/", + ])).to.eql([ + "/bar/", + "/bar/foo", + "/bar/foo/one", + "/bar/foo/one/a", + "/bar/foo/one/b", + "/foo/", + "/foo/1", + "/foo/1/2", + "/foo/one", + "/foo/one/a", + "/foo/one/b", + "/foo/two", + "/foo/two/a", + "/foo/two/d", + ]); + }); + }); + + describe("_packageRoots", () => { + beforeEach(() => { + mock({}); + }); + + it("handles base cases", () => { + return _packageRoots([]).then((pkgRoots) => { + expect(pkgRoots).to.eql([]); + }); + }); + + it("handles no node_modules cases", () => { + return _packageRoots([ + { + identifier: resolve("src/baz/index.js"), + isNodeModules: false, + }, + { + identifier: resolve("src/baz/bug.js"), + isNodeModules: false, + }, + ]) + .then((pkgRoots) => { + expect(pkgRoots).to.eql([]); + }); + }); + + it("handles no node_modules with package.json cases", () => { + mock({ + "src/baz": { + "package.json": JSON.stringify({ + name: "baz", + }, null, 2), + }, + }); + + return _packageRoots([ + { + identifier: resolve("src/baz/index.js"), + isNodeModules: false, + }, + { + identifier: resolve("src/baz/bug.js"), + isNodeModules: false, + }, + ]) + .then((pkgRoots) => { + expect(pkgRoots).to.eql([]); + }); + }); + + it("handles simple cases", () => { + mock({ + "my-app": { + "package.json": JSON.stringify({ + name: "my-app", + }, null, 2), + }, + }); + + return _packageRoots([ + { + identifier: resolve("my-app/src/baz/index.js"), + isNodeModules: false, + }, + { + identifier: resolve("my-app/node_modules/foo/index.js"), + isNodeModules: true, + }, + { + identifier: resolve("my-app/node_modules/foo/node_modules/bug/bug.js"), + isNodeModules: true, + }, + ]).then((pkgRoots) => { + expect(pkgRoots).to.eql([ + resolve("my-app"), + ]); + }); + }); + + // Regression test: https://github.com/FormidableLabs/inspectpack/issues/103 + it("handles hidden application roots", () => { + mock({ + "test/fixtures/hidden-app-roots": fixtureDirs["test/fixtures/hidden-app-roots"], + }); + + const appRoot = resolve("test/fixtures/hidden-app-roots"); + const mods = [ + { + identifier: "node_modules/different-foo/index.js", + isNodeModules: true, + }, + { + identifier: "node_modules/different-foo/node_modules/foo/car.js", + isNodeModules: true, + }, + { + identifier: "node_modules/different-foo/node_modules/foo/index.js", + isNodeModules: true, + }, + { + identifier: "node_modules/foo/index.js", + isNodeModules: true, + }, + { + identifier: "packages/hidden-app/src/index.js", + isNodeModules: false, + }, + ].map(({ identifier, isNodeModules }) => ({ + identifier: join(appRoot, identifier), + isNodeModules, + })); + + return _packageRoots(mods).then((pkgRoots) => { + expect(pkgRoots).to.eql([ + appRoot, + join(appRoot, "packages/hidden-app"), + ]); + }); + }); + + // Regression test: https://github.com/FormidableLabs/inspectpack/issues/103 + it("handles complex hidden application roots", () => { + const appRoot = resolve("complex-hidden-app-roots"); + mock({ + "complex-hidden-app-roots": complexHiddenAppRoots, + }); + + // tslint:disable max-line-length + const mods = [ + { + identifier: "node_modules/prop-types/factoryWithThrowingShims.js", + isNodeModules: true, + }, + { + identifier: "node_modules/fbjs/lib/shallowEqual.js", + isNodeModules: true, + }, + { + identifier: "node_modules/react-addons-shallow-compare/node_modules/fbjs/lib/shallowEqual.js", + isNodeModules: true, + }, + { + identifier: "node_modules/react-apollo/node_modules/prop-types/factoryWithThrowingShims.js", + isNodeModules: true, + }, + { + identifier: "node_modules/hoist-non-react-statics/dist/hoist-non-react-statics.cjs.js", + isNodeModules: true, + }, + { + identifier: "node_modules/react-apollo/node_modules/hoist-non-react-statics/dist/hoist-non-react-statics.cjs.js", + isNodeModules: true, + }, + { + identifier: "node_modules/prop-types/lib/ReactPropTypesSecret.js", + isNodeModules: true, + }, + { + identifier: "node_modules/react-apollo/node_modules/prop-types/lib/ReactPropTypesSecret.js", + isNodeModules: true, + }, + { + identifier: "node_modules/css-in-js-utils/lib/hyphenateProperty.js", + isNodeModules: true, + }, + { + identifier: "node_modules/inline-style-prefixer/node_modules/css-in-js-utils/lib/hyphenateProperty.js", + isNodeModules: true, + }, + { + identifier: "node_modules/react-apollo/node_modules/prop-types/index.js", + isNodeModules: true, + }, + { + identifier: "node_modules/prop-types/index.js", + isNodeModules: true, + }, + { + identifier: "packages/hidden-app/src/index.js", + isNodeModules: false, + }, + ].map(({ identifier, isNodeModules }) => ({ + identifier: join(appRoot, identifier), + isNodeModules, + })); + // tslint:enable max-line-length + + return _packageRoots(mods).then((pkgRoots) => { + expect(pkgRoots).to.eql([ + "", + "packages/hidden-app", + ].map((id) => join(appRoot, id))); + }); + }); }); describe("_packageName", () => { diff --git a/test/lib/plugin/duplicates.spec.ts b/test/lib/plugin/duplicates.spec.ts index 46023445..96566d68 100644 --- a/test/lib/plugin/duplicates.spec.ts +++ b/test/lib/plugin/duplicates.spec.ts @@ -18,20 +18,11 @@ const MULTI_SCENARIO = "multiple-resolved-no-duplicates"; const EMPTY_DUPLICATES_DATA = { assets: {}, meta: { - depended: { + extraFiles: { num: 0, }, - files: { - num: 0, - }, - installed: { - num: 0, - }, - packageRoots: [], - packages: { - num: 0, - }, - resolved: { + extraSources: { + bytes: 0, num: 0, }, }, @@ -78,9 +69,18 @@ describe("plugin/duplicates", () => { }); describe("_getDuplicatesVersionsData", () => { + let warningSpy; + + beforeEach(() => { + warningSpy = sandbox.spy(); + }); + it("handles base cases", () => { - expect(_getDuplicatesVersionsData(EMPTY_DUPLICATES_DATA, EMPTY_VERSIONS_DATA)) - .to.eql(EMPTY_VERSIONS_DATA); + const actual = _getDuplicatesVersionsData( + EMPTY_DUPLICATES_DATA, EMPTY_VERSIONS_DATA, warningSpy, + ); + expect(actual).to.eql(EMPTY_VERSIONS_DATA); + expect(warningSpy).to.not.be.called; // tslint:disable-line no-unused-expression }); describe(`handles ${MULTI_SCENARIO}`, () => { @@ -90,6 +90,7 @@ describe("plugin/duplicates", () => { const noDupsVersions = _getDuplicatesVersionsData( multiDataDuplicates[vers - 1], origVersionsData, + warningSpy, ); // Should remove all of the no-duplicates bundle. @@ -112,6 +113,9 @@ describe("plugin/duplicates", () => { expect(noDupsVersions) .to.have.nested.property("assets.bundle\\.js") .that.eql(expectedBundle); + + // Expect no warnings. + expect(warningSpy).to.not.be.called; // tslint:disable-line no-unused-expression }); }); }); diff --git a/test/lib/util/dependencies.spec.ts b/test/lib/util/dependencies.spec.ts index d2ac839a..bfa9e569 100644 --- a/test/lib/util/dependencies.spec.ts +++ b/test/lib/util/dependencies.spec.ts @@ -1,6 +1,7 @@ -import { join, resolve } from "path"; +import { join, resolve, sep } from "path"; import { _files, + _findPackage, _resolvePackageMap, dependencies, readPackage, @@ -14,6 +15,10 @@ import { toPosixPath } from "../../../src/lib/util/files"; const posixifyKeys = (obj) => Object.keys(obj) .reduce((memo, key) => ({ ...memo, [toPosixPath(key)]: obj[key] }), {}); +const toNativePath = (filePath) => filePath.split("/").join(sep); +const nativifyKeys = (obj) => Object.keys(obj) + .reduce((memo, key) => ({ ...memo, [toNativePath(key)]: obj[key] }), {}); + describe("lib/util/dependencies", () => { let sandbox; @@ -228,6 +233,149 @@ describe("lib/util/dependencies", () => { }); }); }); + + it("includes multiple deps", () => { + const foo1 = { + name: "foo", + version: "1.0.0", + }; + const diffFoo = { + dependencies: { + foo: "^3.0.0", + }, + name: "different-foo", + version: "1.0.1", + }; + const foo3 = { + name: "foo", + version: "3.0.0", + }; + const base = { + dependencies: { + "different-foo": "1.0.0", + "foo": "^3.0.0", + }, + name: "base", + version: "1.0.2", + }; + + mock({ + "node_modules": { + "different-foo": { + "node_modules": { + foo: { + "package.json": JSON.stringify(foo3), + }, + }, + "package.json": JSON.stringify(diffFoo), + }, + "foo": { + "package.json": JSON.stringify(foo1), + }, + }, + "package.json": JSON.stringify(base), + }); + + return readPackages(".") + .then(_resolvePackageMap) + .then(posixifyKeys) + .then((pkgs) => { + expect(pkgs).to.eql({ + "node_modules/different-foo/node_modules/foo/package.json": foo3, + "node_modules/different-foo/package.json": diffFoo, + "node_modules/foo/package.json": foo1, + "package.json": base, + }); + }); + }); + }); + + describe("_findPackage", () => { + const _baseArgs = { filePath: "base", name: "foo", pkgMap: {} }; + const _emptyResp = { isFlattened: false, pkgObj: null, pkgPath: null }; + + it("handles empty cases", () => { + const base = { + dependencies: { + foo: "^3.0.0", + }, + name: "base", + version: "1.0.2", + }; + + expect(_findPackage(_baseArgs)).to.eql(_emptyResp); + + expect(_findPackage({ + ..._baseArgs, + name: "bar", + pkgMap: nativifyKeys({ + "base/node_modules/foo/package.json": { + name: "foo", + version: "1.0.0", + }, + "base/package.json": base, + }), + })).to.eql(_emptyResp); + }); + + it("finds unflattened packages", () => { + const base = { + dependencies: { + foo: "^3.0.0", + }, + name: "base", + version: "1.0.2", + }; + const foo = { + name: "foo", + version: "3.0.0", + }; + + expect(_findPackage({ + ..._baseArgs, + pkgMap: nativifyKeys({ + "base/node_modules/foo/package.json": foo, + "base/package.json": base, + }), + })).to.eql({ + isFlattened: false, + pkgObj: foo, + pkgPath: toNativePath("base/node_modules/foo"), + }); + }); + + it("finds hidden roots packages outside of file path", () => { + const myPkg = { + dependencies: { + foo: "^3.0.0", + }, + name: "my-pkg", + version: "1.0.2", + }; + const foo = { + name: "foo", + version: "3.0.0", + }; + // Note: Base _doesn't_ have `foo` dependency. + const base = { + name: "base", + version: "1.0.0", + }; + + expect(_findPackage({ + ..._baseArgs, + filePath: "base/packages/my-pkg", + pkgMap: nativifyKeys({ + "base/node_modules/foo/package.json": foo, + "base/package.json": base, + "base/packages/my-pkg/package.json": myPkg, + }), + })).to.eql({ + isFlattened: true, + pkgObj: foo, + pkgPath: toNativePath("base/node_modules/foo"), + }); + }); }); describe("dependencies", () => { diff --git a/test/lib/util/promise.spec.ts b/test/lib/util/promise.spec.ts new file mode 100644 index 00000000..52325b1a --- /dev/null +++ b/test/lib/util/promise.spec.ts @@ -0,0 +1,28 @@ +import { + serial, +} from "../../../src/lib/util/promise"; + +describe("lib/util/promise", () => { + + describe("serial", () => { + it("handles base cases", () => + serial([]) + .then((vals) => { + expect(vals).to.eql([]); + }), + ); + + it("handles arrays", () => + serial([ + () => Promise.resolve(10), + () => Promise.resolve(20), + ]) + .then((vals) => { + expect(vals).to.eql([ + 10, + 20, + ]); + }), + ); + }); +}); diff --git a/test/utils.ts b/test/utils.ts index 59d68491..bbb83f0f 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -37,6 +37,9 @@ const FIXTURES_STATS = FIXTURES_DIRS.map((f) => join(__dirname, "fixtures", f, " // Extra patches for webpack-config-driven stuff that doesn't fit within // node_modules-based traversals. const FIXTURES_EXTRA_DIRS = { + "hidden-app-roots": [ + "packages/hidden-app", + ], "multiple-roots": [ "packages/package1", "packages/package2",