diff --git a/HISTORY.md b/HISTORY.md index 897bdae3..0c4927e3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,13 @@ History ======= +## UNRELEASED + +* Feature: Add webpack5 support. + [#156](https://github.com/FormidableLabs/inspectpack/issues/156) +* Test: Change handling of tree-shaking supported fixtures to compare production on v4+ and dev vs prod on v3-. +* Test: Remove `expose-loader` from `loaders` test scenario as just wasn't working on windows + v5. + ## 4.5.2 * Internal: Optimize `shouldBail` to used cached `getData()`. diff --git a/README.md b/README.md index 0a9e28dd..e3258945 100644 --- a/README.md +++ b/README.md @@ -314,7 +314,10 @@ module.exports = { // ... plugins: [ new StatsWriterPlugin({ - fields: ["assets", "modules"] + fields: ["assets", "modules"], + stats: { + source: true // Needed for webpack5+ + } }) ] }; @@ -322,6 +325,8 @@ module.exports = { This uses the [`webpack-stats-plugin`](https://github.com/FormidableLabs/webpack-stats-plugin) to output at least the `assets` and `modules` fields of the stats object to a file named `stats.json` in the directory specified in `output.path`. There are lots of various [options](https://github.com/FormidableLabs/webpack-stats-plugin#statswriterpluginopts) for the `webpack-stats-plugin` that may suit your particular webpack config better than this example. +> ℹ️ **Webpack 5+ Note**: If you are using webpack5+ you will need to enable the `{ source: true }` options for the `stats` field for the plugin to include sources in stats output. In webpack versions previous to 5, this was enabled by default. The field is needed for internal determination as to whether or not a module is a real source file or a "synthetic" webpack added entry. + #### _Note_: Multiple entry points If you configure `entry` with multiple entry points like: diff --git a/package.json b/package.json index 86ac75df..b669a058 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "build-test-wp": "node test/fixtures/packages/webpack.js --config ../../test/fixtures/config/webpack.config.js && node test/fixtures/packages/check-bundle.js", "build-test-scenarios": "builder envs --envs-path=test/fixtures/config/scenarios.json build-test-wp --buffer --queue=1", "build-test-modes": "builder envs build-test-scenarios \"[{\\\"WEBPACK_MODE\\\":\\\"development\\\"},{\\\"WEBPACK_MODE\\\":\\\"production\\\"}]\" --queue=2", - "build-test-versions": "builder envs build-test-modes \"[{\\\"WEBPACK_VERSION\\\":\\\"1\\\"},{\\\"WEBPACK_VERSION\\\":\\\"2\\\"},{\\\"WEBPACK_VERSION\\\":\\\"3\\\"},{\\\"WEBPACK_VERSION\\\":\\\"4\\\"}]\" --queue=4", + "build-test-versions": "builder envs --envs-path=test/fixtures/config/versions.json build-test-modes --queue=4", "build-test": "builder run build-test-versions", "build": "yarn run clean-lib && yarn run build-lib", "watch": "tsc -w", @@ -83,7 +83,6 @@ "eslint-plugin-import": "^2.20.2", "eslint-plugin-promise": "^4.2.1", "execa": "^5.0.0", - "expose-loader": "^0.7.5", "inspectpack-test-fixtures": "file:test/fixtures", "mocha": "^8.2.1", "mock-fs": "^4.12.0", diff --git a/src/lib/actions/base.ts b/src/lib/actions/base.ts index b0e66173..42adfaad 100644 --- a/src/lib/actions/base.ts +++ b/src/lib/actions/base.ts @@ -11,10 +11,12 @@ import { IWebpackStatsModuleModules, IWebpackStatsModules, IWebpackStatsModuleSource, + IWebpackStatsModuleOrphan, IWebpackStatsModuleSynthetic, RWebpackStats, RWebpackStatsModuleModules, RWebpackStatsModuleSource, + RWebpackStatsModuleOrphan, RWebpackStatsModuleSynthetic, } from "../interfaces/webpack-stats"; import { toPosixPath } from "../util/files"; @@ -217,7 +219,15 @@ export abstract class Action { return list.concat(this.getSourceMods(modsMod.modules, chunks)); } else if (isRight(RWebpackStatsModuleSource.decode(mod))) { - // Easy case -- a normal source code module. + // webpack5+: Check if an orphan and just skip entirely. + if ( + isRight(RWebpackStatsModuleOrphan.decode(mod)) && + (mod as IWebpackStatsModuleOrphan).orphan + ) { + return list; + } + + // Base case -- a normal source code module that is **not** an orphan. const srcMod = mod as IWebpackStatsModuleSource; identifier = srcMod.identifier; name = srcMod.name; diff --git a/src/lib/interfaces/webpack-stats.ts b/src/lib/interfaces/webpack-stats.ts index 95b16e9d..f368fa41 100644 --- a/src/lib/interfaces/webpack-stats.ts +++ b/src/lib/interfaces/webpack-stats.ts @@ -48,11 +48,10 @@ export type IWebpackStatsAssets = t.TypeOf; const RWebpackStatsModuleBase = t.type({ // Chunk identifiers. chunks: t.array(RWebpackStatsChunk), - // Full path to file on disk (with extra hash stuff if `modules` module). - // Full path to file on disk (with extra hash stuff if `modules` module and + // Full path to file on disk (with extra hash stuff if `modules` module and // loader prefixes, etc.). identifier: t.string, - // Estimated byte size of module. + // Estimated byte size of module. size: t.number, }); @@ -83,6 +82,21 @@ export const RWebpackStatsModuleSource = t.intersection([ export type IWebpackStatsModuleSource = t.TypeOf; +// ---------------------------------------------------------------------------- +// Module: Orphaned code **source** +// +// Introduced in webpack5 as a stat field, ignore these as not in any chunk. +// See: https://webpack.js.org/configuration/stats/#statsorphanmodules +// ---------------------------------------------------------------------------- +export const RWebpackStatsModuleOrphan = t.intersection([ + RWebpackStatsModuleSource, + t.type({ + orphan: t.boolean, + }), +]); + +export type IWebpackStatsModuleOrphan = t.TypeOf; + // ---------------------------------------------------------------------------- // Module: Single "synthetic" module // @@ -117,6 +131,7 @@ export const RWebpackStatsModuleSynthetic = t.intersection([ RWebpackStatsModuleBase, RWebpackStatsModuleWithName, ]); + export type IWebpackStatsModuleSynthetic = t.TypeOf; // ---------------------------------------------------------------------------- diff --git a/src/plugin/duplicates.ts b/src/plugin/duplicates.ts index 00316464..90c95bff 100644 --- a/src/plugin/duplicates.ts +++ b/src/plugin/duplicates.ts @@ -22,7 +22,7 @@ export interface ICompilation { errors: Error[]; warnings: Error[]; getStats: () => { - toJson: () => IWebpackStats; + toJson: (opts: object) => IWebpackStats; }; } @@ -218,7 +218,7 @@ export class DuplicatesPlugin { public apply(compiler: ICompiler) { if (compiler.hooks) { - // Webpack4 integration + // Webpack4+ integration compiler.hooks.emit.tapPromise("inspectpack-duplicates-plugin", this.analyze.bind(this)); } else { // Webpack1-3 integration @@ -228,7 +228,11 @@ export class DuplicatesPlugin { public analyze(compilation: ICompilation, callback?: () => void) { const { errors, warnings } = compilation; - const stats = compilation.getStats().toJson(); + const stats = compilation + .getStats() + .toJson({ + source: true // Needed for webpack5+ + }); const { emitErrors, emitHandler, ignoredPackages, verbose } = this.opts; diff --git a/test/bin/inspectpack.spec.ts b/test/bin/inspectpack.spec.ts index 3930d2fe..08de425c 100644 --- a/test/bin/inspectpack.spec.ts +++ b/test/bin/inspectpack.spec.ts @@ -3,8 +3,8 @@ import execa = require("execa"); // Have to use transpiled bin to exec in Node.js const IP_PATH = require.resolve("../../bin/inspectpack.js"); -const SIMPLE_STATS = require.resolve("../fixtures/simple/dist-development-4/stats.json"); -const DUP_ESM_STATS = require.resolve("../fixtures/duplicates-esm/dist-development-4/stats.json"); +const SIMPLE_STATS = require.resolve("../fixtures/simple/dist-development-5/stats.json"); +const DUP_ESM_STATS = require.resolve("../fixtures/duplicates-esm/dist-development-5/stats.json"); const exec = (args: string[]) => execa("node", [IP_PATH].concat(args), { env: { ...process.env, FORCE_COLOR: "0" } diff --git a/test/fixtures/config/versions.json b/test/fixtures/config/versions.json index 70fdf8a8..386eedeb 100644 --- a/test/fixtures/config/versions.json +++ b/test/fixtures/config/versions.json @@ -2,5 +2,6 @@ { "WEBPACK_VERSION": "1" }, { "WEBPACK_VERSION": "2" }, { "WEBPACK_VERSION": "3" }, - { "WEBPACK_VERSION": "4" } + { "WEBPACK_VERSION": "4" }, + { "WEBPACK_VERSION": "5" } ] \ No newline at end of file diff --git a/test/fixtures/config/webpack.config.js b/test/fixtures/config/webpack.config.js index 99ac3936..79d291fa 100644 --- a/test/fixtures/config/webpack.config.js +++ b/test/fixtures/config/webpack.config.js @@ -6,9 +6,9 @@ * Example usage (make sure to be in project root): * * ```sh - * $ export WEBPACK_VERSION=4; \ + * $ export WEBPACK_VERSION=5; \ * WEBPACK_MODE=development \ - * WEBPACK_CWD=../../test/fixtures/duplicates-esm \ + * WEBPACK_CWD=../../test/fixtures/hidden-app-roots \ * NODE_PATH="${PWD}/node_modules/webpack${WEBPACK_VERSION}/node_modules:${PWD}/node_modules" \ * node test/fixtures/packages/webpack.js \ * --config ../../test/fixtures/config/webpack.config.js @@ -39,6 +39,7 @@ const vers = process.env.WEBPACK_VERSION; if (!vers) { throw new Error("WEBPACK_VERSION is required"); } +const versNum = parseInt(vers, 10); const cwd = process.env.WEBPACK_CWD; if (!cwd) { @@ -46,7 +47,9 @@ if (!cwd) { } const outputPath = resolve(cwd, `dist-${mode}-${vers}`); -const webpack4 = { + +// Webpack 4+ +let configModern = { mode, devtool: false, context: resolve(cwd), @@ -72,7 +75,10 @@ const webpack4 = { }, plugins: [ new StatsWriterPlugin({ - fields: ["assets", "modules"] + fields: ["assets", "modules"], + stats: { + source: true // Needed for webpack5+ + } }), DuplicatesPlugin ? new DuplicatesPlugin({ verbose: true, @@ -81,36 +87,39 @@ const webpack4 = { ].filter(Boolean) }; +// Dynamically try to import a custom override from `CWD/webpack.config.js` +try { + const webpack = require(`webpack${vers}/lib`); // eslint-disable-line global-require + const override = require(resolve(cwd, "webpack.config.js")); // eslint-disable-line global-require + configModern = override(webpack, configModern, versNum); +} catch (err) { + if (err.code !== "MODULE_NOT_FOUND") { + throw err; + } +} + const webpack1Module = { - loaders: webpack4.module.rules.map((rule) => ({ + loaders: configModern.module.rules.map((rule) => ({ test: rule.test, loader: rule.use })) }; -const webpackOld = { - devtool: webpack4.devtool, - context: webpack4.context, - entry: webpack4.entry, - output: webpack4.output, +// Webpack 2-3 +const configLegacy = { + devtool: configModern.devtool, + context: configModern.context, + entry: configModern.entry, + output: configModern.output, // TODO(66): Add minify thing here -- mode === "production", // https://github.com/FormidableLabs/inspectpack/issues/66 - module: vers === "1" ? webpack1Module : webpack4.module, - plugins: webpack4.plugins + module: versNum === 1 ? webpack1Module : configModern.module, + plugins: configModern.plugins, + resolve: configModern.resolve }; // Choose appropriate version. -let config = vers === "4" ? webpack4 : webpackOld; - -// Dynamically try to import a custom override from `CWD/webpack.config.js` -try { - const webpack = require(`webpack${vers}/lib`); // eslint-disable-line global-require - const override = require(resolve(cwd, "webpack.config.js")); // eslint-disable-line global-require - config = override(webpack, config); -} catch (err) { - if (err.code !== "MODULE_NOT_FOUND") { - throw err; - } -} +// eslint-disable-next-line no-magic-numbers +const config = versNum >= 4 ? configModern : configLegacy; module.exports = config; diff --git a/test/fixtures/index.html b/test/fixtures/index.html index 48ee3da3..03e8cb70 100644 --- a/test/fixtures/index.html +++ b/test/fixtures/index.html @@ -5,19 +5,19 @@

Test Fixture Loader

- As a debugging helpder, this page provides a general loader into known paths to built fixtures. + As a debugging helper, this page provides a general loader into known paths to built fixtures.

Examples:

diff --git a/test/fixtures/loaders/src/index.js b/test/fixtures/loaders/src/index.js index 53c2da5f..333eb588 100644 --- a/test/fixtures/loaders/src/index.js +++ b/test/fixtures/loaders/src/index.js @@ -1,22 +1,15 @@ /* eslint-disable no-console */ -/* globals global */ import text from "./hello.txt"; import style from "./style.css"; -// Use expose loader to make global -require("expose-loader?BunBun!./bunny"); // eslint-disable-line import/no-unresolved +// Legacy: just require a file (gave up on `expose-loader` global in webpack5 +// upgrade). +require("./bunny"); const hello = () => "hello world"; console.log("hello", hello()); console.log("text", text); console.log("style", style.toString()); - -let root = typeof window !== "undefined" && window; -if (!root && typeof global !== "undefined") { - root = global; -} - -console.log("global", root.BunBun); diff --git a/test/lib/actions/duplicates.spec.ts b/test/lib/actions/duplicates.spec.ts index 01af6a82..5b8b16e2 100644 --- a/test/lib/actions/duplicates.spec.ts +++ b/test/lib/actions/duplicates.spec.ts @@ -7,16 +7,18 @@ import { create, IDuplicatesData } from "../../../src/lib/actions/duplicates"; import { toPosixPath } from "../../../src/lib/util/files"; import { FIXTURES, - FIXTURES_WEBPACK1_BLACKLIST, - FIXTURES_WEBPACK4_BLACKLIST, + FIXTURES_WEBPACK1_SKIPLIST, IFixtures, JSON_PATH_RE, loadFixtures, normalizeOutput, patchAllMods, + treeShakingWorks, TEXT_PATH_RE, TSV_PATH_RE, VERSIONS, + VERSIONS_LATEST, + VERSIONS_LATEST_IDX, } from "../../utils"; // Keyed off `scenario`. Remap chunk names. @@ -35,7 +37,7 @@ const PATCHED_ASSETS: IPatchedAsset = { // **Note**: Some egregious TS `any`-ing to get patches hooked up. const patchAction = (name: string) => (instance: IAction) => { // Patch all modules. - (instance as any)._modules = instance.modules.map(patchAllMods(name)); + (instance as any)._modules = instance.modules.map(patchAllMods); // Patch assets scenarios via a rename LUT. const patches = PATCHED_ASSETS[name.split(sep)[0]]; @@ -66,7 +68,7 @@ describe("lib/actions/duplicates", () => { beforeEach(() => Promise.all([ "scoped", ].map((name) => create({ - stats: fixtures[toPosixPath(join(name, "dist-development-4"))], + stats: fixtures[toPosixPath(join(name, `dist-development-${VERSIONS[VERSIONS.length - 1]}`))], }).validate())) .then((instances) => { [ @@ -76,9 +78,8 @@ describe("lib/actions/duplicates", () => { ); describe("getData", () => { - describe("all versions", () => { + describe("all development versions", () => { FIXTURES.map((scenario: string) => { - const lastIdx = VERSIONS.length - 1; let datas: IDuplicatesData[]; before(() => { @@ -89,23 +90,17 @@ describe("lib/actions/duplicates", () => { }); VERSIONS.map((vers: string, i: number) => { - if (i === lastIdx) { return; } // Skip last index, version "current". + if (i === VERSIONS_LATEST_IDX) { return; } // Skip last index, version "current". - // Blacklist `import` + webpack@1 and skip test. - if (i === 0 && FIXTURES_WEBPACK1_BLACKLIST.indexOf(scenario) > -1) { - it(`should match v${vers}-v${lastIdx + 1} for ${scenario} (SKIP v1)`); + // Skip `import` + webpack@1. + if (i === 0 && FIXTURES_WEBPACK1_SKIPLIST.indexOf(scenario) > -1) { + it(`should match v${vers}-v${VERSIONS_LATEST} for ${scenario} (SKIP v1)`); return; } - // Blacklist `import` + webpack@4 and skip test. - if (lastIdx + 1 === 4 && FIXTURES_WEBPACK4_BLACKLIST.indexOf(scenario) > -1) { - it(`should match v${vers}-v${lastIdx + 1} for ${scenario} (SKIP v4)`); - return; - } - - it(`should match v${vers}-v${lastIdx + 1} for ${scenario}`, () => { - expect(datas[i], `version mismatch for v${vers}-v${lastIdx + 1} ${scenario}`) - .to.eql(datas[lastIdx]); + it(`should match v${vers}-v${VERSIONS_LATEST} for ${scenario}`, () => { + expect(datas[i], `version mismatch for v${vers}-v${VERSIONS_LATEST} ${scenario}`) + .to.eql(datas[VERSIONS_LATEST_IDX]); }); }); }); @@ -114,6 +109,11 @@ describe("lib/actions/duplicates", () => { describe("development vs production", () => { FIXTURES.map((scenario: string) => { VERSIONS.map((vers: string) => { + if (treeShakingWorks({ scenario, vers })) { + it(`v${vers} scenario '${scenario}' should match (SKIP TREE-SHAKING)`); + return; + } + it(`v${vers} scenario '${scenario}' should match`, () => { return Promise.all([ getData(join(scenario, `dist-development-${vers}`)), @@ -133,6 +133,38 @@ describe("lib/actions/duplicates", () => { }); }); }); + + describe("all production", () => { + FIXTURES.map((scenario: string) => { + VERSIONS.map((vers: string, i) => { + // Skip latest version + limit to tree-shaking scenarios. + if (i === VERSIONS_LATEST_IDX || !treeShakingWorks({ scenario, vers })) { + return; + } + + let latestProd: IDuplicatesData; + + before(() => { + return getData(join(scenario, `dist-production-${VERSIONS_LATEST}`)) + .then((data) => { latestProd = data; }) + }); + + it(`should match v${vers}-v${VERSIONS_LATEST} for ${scenario}`, () => { + return getData(join(scenario, `dist-production-${vers}`)) + .then((curProd) => { + expect(curProd, `prod is empty for v${vers} ${scenario}`) + .to.not.equal(null).and + .to.not.equal(undefined).and + .to.not.eql([]).and + .to.not.eql({}); + + expect(curProd, `prod mismatch for v${vers}-v${VERSIONS_LATEST} ${scenario}`) + .to.eql(latestProd); + }); + }); + }); + }); + }); }); describe("json", () => { diff --git a/test/lib/actions/sizes.spec.ts b/test/lib/actions/sizes.spec.ts index 0dc05b9e..ac74193f 100644 --- a/test/lib/actions/sizes.spec.ts +++ b/test/lib/actions/sizes.spec.ts @@ -9,21 +9,23 @@ import { IWebpackStatsChunk } from "../../../src/lib/interfaces/webpack-stats"; import { toPosixPath } from "../../../src/lib/util/files"; import { FIXTURES, - FIXTURES_WEBPACK1_BLACKLIST, - FIXTURES_WEBPACK4_BLACKLIST, + FIXTURES_WEBPACK1_SKIPLIST, IFixtures, JSON_PATH_RE, loadFixtures, normalizeOutput, patchAllMods, + treeShakingWorks, TEXT_PATH_RE, TSV_PATH_RE, VERSIONS, + VERSIONS_LATEST, + VERSIONS_LATEST_IDX, } from "../../utils"; const PATCHED_MOMENT_LOCALE_ES = { - baseName: "moment/locale sync /es/", - identifier: resolve(__dirname, "../../../node_modules/moment/locale sync /es/"), + baseName: "moment/locale|sync|/es/", + identifier: resolve(__dirname, "../../../node_modules/moment/locale|sync|/es/"), size: 100, source: "REMOVED", }; @@ -32,20 +34,11 @@ const PATCHED_MOMENT_LOCALE_ES = { // Should be `IWebpackStatsModuleBase`, but want subset to merge and override. interface IPatchedMods { [id: string]: any; } const PATCHED_MODS: IPatchedMods = { + // Normalize legacy/modern moment synthetic module names. "moment/locale /es/": PATCHED_MOMENT_LOCALE_ES, + "moment/locale|/es/": PATCHED_MOMENT_LOCALE_ES, "moment/locale sync /es/": PATCHED_MOMENT_LOCALE_ES, - "webpack/buildin/global.js": { - baseName: "webpack/buildin/global.js", - identifier: resolve(__dirname, "../../../node_modules/webpack/buildin/global.js"), - size: 300, - source: "REMOVED", - }, - "webpack/buildin/module.js": { - baseName: "webpack/buildin/module.js", - identifier: resolve(__dirname, "../../../node_modules/webpack/buildin/module.js"), - size: 200, - source: "REMOVED", - }, + "moment/locale|sync|/es/": PATCHED_MOMENT_LOCALE_ES, }; // Patch in _all_ assets. @@ -63,10 +56,24 @@ const patchAction = (name: string) => (instance: IAction) => { // **Note**: Patch modules **first** since memoized, then used by assets. (instance as any)._modules = instance.modules .map((mod) => { - // - `circular-deps`: Using `global` in v1 didn't include an extra file, - // but v2 includes `webpack/buildin/global.js` so, manually remove. - if (name.startsWith("circular-deps") && - mod.baseName === "webpack/buildin/global.js") { + // Ignore webpack5+ runtime helpers + if (mod.isSynthetic && mod.identifier.startsWith("webpack/runtime/")) { + return null; + } + + // Normalize / remove internal additions. + if ( + [ + // webpack5+ doesn't add polyfills. + "process/browser.js", + "setimmediate/setImmediate.js", + "timers-browserify/main.js", + + // webpack5+ doesn't always add these built-ins. + "webpack/buildin/global.js", + "webpack/buildin/module.js", + ].includes(mod.baseName || "") + ) { return null; } @@ -75,7 +82,9 @@ const patchAction = (name: string) => (instance: IAction) => { return patched ? { ...mod, ...patched } : mod; }) .filter(Boolean) - .map(patchAllMods(name)); + .map(patchAllMods) + // Re-sort as `identifier` string may have been changed. + .sort((a, b) => a.identifier.localeCompare(b.identifier)); // Patch assets scenarios manually. // - `multiple-chunks`: just use the normal bundle, not the split stuff. @@ -127,6 +136,28 @@ const patchData = (data: ISizesData) => { // - `chunks` are emptied because different by webpack version. const normalizeModules = (modules: IModule[]) => modules.map((mod) => ({ ...mod, chunks: [] })); +const normalizeAsset = (asset: object) => { + const normAsset = JSON.parse(JSON.stringify(asset)); + + // Remove new fields not needed for tests. + [ + "auxiliaryChunkIdHints", + "auxiliaryChunkNames", + "auxiliaryChunks", + "cached", + "chunkIdHints", + "comparedForEmit", + "filteredRelated", + "isOverSizeLimit", + "related", + "type" + ].forEach((field) => { + delete normAsset[field]; + }); + + return normAsset; +} + // Normalize assets for comparison. // - `size` is hard-coded because different by webpack version's boilerplate / generated // code. @@ -138,7 +169,7 @@ const normalizeAssets = (modulesByAsset: IModulesByAsset) => Object.keys(modules [name]: { ...modulesByAsset[name], asset: { - ...modulesByAsset[name].asset, + ...normalizeAsset(modulesByAsset[name].asset), chunks: [], size: 600, }, @@ -159,9 +190,8 @@ describe("lib/actions/base", () => { return loadFixtures().then((f) => { fixtures = f; }); }); - describe("all versions", () => { + describe("all development versions", () => { FIXTURES.map((scenario) => { - const lastIdx = VERSIONS.length - 1; let instances: IAction[]; before(() => { @@ -172,30 +202,24 @@ describe("lib/actions/base", () => { }); VERSIONS.map((vers, i) => { - if (i === lastIdx) { return; } // Skip last index, version "current". - - // Blacklist `import` + webpack@1 and skip test. - if (i === 0 && FIXTURES_WEBPACK1_BLACKLIST.indexOf(scenario) > -1) { - it(`should match modules/assets v${vers}-v${lastIdx + 1} for ${scenario} (SKIP v1)`); - return; - } + if (i === VERSIONS_LATEST_IDX) { return; } // Skip last index, version "current". - // Blacklist `import` + webpack@4 and skip test. - if (lastIdx + 1 === 4 && FIXTURES_WEBPACK4_BLACKLIST.indexOf(scenario) > -1) { - it(`should match modules/assets v${vers}-v${lastIdx + 1} for ${scenario} (SKIP v4)`); + // Skip `import` + webpack@1. + if (i === 0 && FIXTURES_WEBPACK1_SKIPLIST.indexOf(scenario) > -1) { + it(`should match modules/assets v${vers}-v${VERSIONS_LATEST} for ${scenario} (SKIP v1)`); return; } - it(`should match modules v${vers}-v${lastIdx + 1} for ${scenario}`, () => { + it(`should match modules v${vers}-v${VERSIONS_LATEST} for ${scenario}`, () => { expect(normalizeModules(instances[i].modules), - `version mismatch for v${vers}-v${lastIdx + 1} ${scenario}`) - .to.eql(normalizeModules(instances[lastIdx].modules)); + `version mismatch for v${vers}-v${VERSIONS_LATEST} ${scenario}`) + .to.eql(normalizeModules(instances[VERSIONS_LATEST_IDX].modules)); }); - it(`should match assets v${vers}-v${lastIdx + 1} for ${scenario}`, () => { + it(`should match assets v${vers}-v${VERSIONS_LATEST} for ${scenario}`, () => { expect(normalizeAssets(instances[i].assets), - `version mismatch for v${vers}-v${lastIdx + 1} ${scenario}`) - .to.eql(normalizeAssets(instances[lastIdx].assets)); + `version mismatch for v${vers}-v${VERSIONS_LATEST} ${scenario}`) + .to.eql(normalizeAssets(instances[VERSIONS_LATEST_IDX].assets)); }); }); }); @@ -204,8 +228,12 @@ describe("lib/actions/base", () => { describe("development vs production", () => { FIXTURES.map((scenario) => { VERSIONS.map((vers) => { - it(`v${vers} scenario '${scenario}' should match`, () => { + if (treeShakingWorks({ scenario, vers })) { + it(`v${vers} scenario '${scenario}' should match (SKIP TREE-SHAKING)`); + return; + } + it(`v${vers} scenario '${scenario}' should match`, () => { return Promise.all([ getInstance(join(scenario, `dist-development-${vers}`)), getInstance(join(scenario, `dist-production-${vers}`)), @@ -224,6 +252,43 @@ describe("lib/actions/base", () => { }); }); }); + + describe("all production", () => { + FIXTURES.map((scenario: string) => { + let latestProdAssets: IModulesByAsset; + + before(() => { + return getInstance(join(scenario, `dist-production-${VERSIONS_LATEST}`)) + .then((instance) => { + latestProdAssets = normalizeAssets(instance.assets); + }); + }); + + VERSIONS.map((vers: string, i) => { + // Skip latest version + limit to tree-shaking scenarios. + if (i === VERSIONS_LATEST_IDX || !treeShakingWorks({ scenario, vers })) { + return; + } + + let curProdAssets: IModulesByAsset; + + before(() => { + return getInstance(join(scenario, `dist-production-${vers}`)) + .then((instance) => { + curProdAssets = normalizeAssets(instance.assets); + }); + }); + + // Note: We _don't_ match modules like above because orphaned modules + // (e.g., `chunks = []` are treated differently in webpack4 vs 5). + + it(`should match assets v${vers}-v${VERSIONS_LATEST} for ${scenario}`, () => { + expect(curProdAssets, `prod mismatch for v${vers}-v${VERSIONS_LATEST} ${scenario}`) + .to.eql(latestProdAssets); + }); + }); + }); + }); }); }); @@ -243,7 +308,7 @@ describe("lib/actions/sizes", () => { "scoped", ].map((name) => create({ - stats: fixtures[toPosixPath(join(name, "dist-development-4"))], + stats: fixtures[toPosixPath(join(name, `dist-development-${VERSIONS[VERSIONS.length - 1]}`))], }) .validate() .then(patchAction(name)), @@ -256,9 +321,8 @@ describe("lib/actions/sizes", () => { ); describe("getData", () => { - describe("all versions", () => { + describe("all development versions", () => { FIXTURES.map((scenario) => { - const lastIdx = VERSIONS.length - 1; let datas: ISizesData[]; before(() => { @@ -269,23 +333,17 @@ describe("lib/actions/sizes", () => { }); VERSIONS.map((vers, i) => { - if (i === lastIdx) { return; } // Skip last index, version "current". + if (i === VERSIONS_LATEST_IDX) { return; } // Skip last index, version "current". - // Blacklist `import` + webpack@1 and skip test. - if (i === 0 && FIXTURES_WEBPACK1_BLACKLIST.indexOf(scenario) > -1) { - it(`should match v${vers}-v${lastIdx + 1} for ${scenario} (SKIP v1)`); + // Skip `import` + webpack@1. + if (i === 0 && FIXTURES_WEBPACK1_SKIPLIST.indexOf(scenario) > -1) { + it(`should match v${vers}-v${VERSIONS_LATEST} for ${scenario} (SKIP v1)`); return; } - // Blacklist `import` + webpack@4 and skip test. - if (lastIdx + 1 === 4 && FIXTURES_WEBPACK4_BLACKLIST.indexOf(scenario) > -1) { - it(`should match v${vers}-v${lastIdx + 1} for ${scenario} (SKIP v4)`); - return; - } - - it(`should match v${vers}-v${lastIdx + 1} for ${scenario}`, () => { - expect(datas[i], `version mismatch for v${vers}-v${lastIdx + 1} ${scenario}`) - .to.eql(datas[lastIdx]); + it(`should match v${vers}-v${VERSIONS_LATEST} for ${scenario}`, () => { + expect(datas[i], `version mismatch for v${vers}-v${VERSIONS_LATEST} ${scenario}`) + .to.eql(datas[VERSIONS_LATEST_IDX]); }); }); }); @@ -294,6 +352,11 @@ describe("lib/actions/sizes", () => { describe("development vs production", () => { FIXTURES.map((scenario) => { VERSIONS.map((vers) => { + if (treeShakingWorks({ scenario, vers })) { + it(`v${vers} scenario '${scenario}' should match (SKIP TREE-SHAKING)`); + return; + } + it(`v${vers} scenario '${scenario}' should match`, () => { return Promise.all([ getData(join(scenario, `dist-development-${vers}`)), @@ -313,6 +376,38 @@ describe("lib/actions/sizes", () => { }); }); }); + + describe("all production", () => { + FIXTURES.map((scenario: string) => { + VERSIONS.map((vers: string, i) => { + // Skip latest version + limit to tree-shaking scenarios. + if (i === VERSIONS_LATEST_IDX || !treeShakingWorks({ scenario, vers })) { + return; + } + + let latestProd: ISizesData; + + before(() => { + return getData(join(scenario, `dist-production-${VERSIONS_LATEST}`)) + .then((data) => { latestProd = data; }) + }); + + it(`should match v${vers}-v${VERSIONS_LATEST} for ${scenario}`, () => { + return getData(join(scenario, `dist-production-${vers}`)) + .then((curProd) => { + expect(curProd, `prod is empty for v${vers} ${scenario}`) + .to.not.equal(null).and + .to.not.equal(undefined).and + .to.not.eql([]).and + .to.not.eql({}); + + expect(curProd, `prod mismatch for v${vers}-v${VERSIONS_LATEST} ${scenario}`) + .to.eql(latestProd); + }); + }); + }); + }); + }); }); describe("json", () => { diff --git a/test/lib/actions/versions.spec.ts b/test/lib/actions/versions.spec.ts index dcfb7b92..383af48d 100644 --- a/test/lib/actions/versions.spec.ts +++ b/test/lib/actions/versions.spec.ts @@ -18,13 +18,15 @@ import { IModule } from "../../../src/lib/interfaces/modules"; import { toPosixPath } from "../../../src/lib/util/files"; import { FIXTURES, - FIXTURES_WEBPACK1_BLACKLIST, - FIXTURES_WEBPACK4_BLACKLIST, + FIXTURES_WEBPACK1_SKIPLIST, IFixtures, loadFixtureDirs, loadFixtures, patchAllMods, + treeShakingWorks, VERSIONS, + VERSIONS_LATEST, + VERSIONS_LATEST_IDX, } from "../../utils"; export const EMPTY_VERSIONS_META: IVersionsMeta = { @@ -82,7 +84,7 @@ const PATCHED_ASSETS: IPatchedAsset = { // Mutates. const patchAction = (name: string) => (instance: IAction) => { // Patch all modules. - (instance as any)._modules = instance.modules.map(patchAllMods(name)); + (instance as any)._modules = instance.modules.map(patchAllMods); // Patch assets scenarios via a rename LUT. const patches = PATCHED_ASSETS[name.split(sep)[0]]; @@ -127,7 +129,7 @@ describe("lib/actions/versions", () => { "hidden-app-roots", "circular-deps", ].map((name) => create({ - stats: fixtures[toPosixPath(join(name, "dist-development-4"))], + stats: fixtures[toPosixPath(join(name, `dist-development-${VERSIONS[VERSIONS.length - 1]}`))], }).validate())) .then((instances) => { [ @@ -153,9 +155,8 @@ describe("lib/actions/versions", () => { }); describe("getData", () => { - describe("all versions", () => { + describe("all development versions", () => { FIXTURES.map((scenario) => { - const lastIdx = VERSIONS.length - 1; let datas: IVersionsData[]; before(() => { @@ -166,23 +167,17 @@ describe("lib/actions/versions", () => { }); VERSIONS.map((vers, i) => { - if (i === lastIdx) { return; } // Skip last index, version "current". + if (i === VERSIONS_LATEST_IDX) { return; } // Skip last index, version "current". - // Blacklist `import` + webpack@1 and skip test. - if (i === 0 && FIXTURES_WEBPACK1_BLACKLIST.indexOf(scenario) > -1) { - it(`should match v${vers}-v${lastIdx + 1} for ${scenario} (SKIP v1)`); + // Skip `import` + webpack@1. + if (i === 0 && FIXTURES_WEBPACK1_SKIPLIST.indexOf(scenario) > -1) { + it(`should match v${vers}-v${VERSIONS_LATEST} for ${scenario} (SKIP v1)`); return; } - // Blacklist `import` + webpack@4 and skip test. - if (lastIdx + 1 === 4 && FIXTURES_WEBPACK4_BLACKLIST.indexOf(scenario) > -1) { - it(`should match v${vers}-v${lastIdx + 1} for ${scenario} (SKIP v4)`); - return; - } - - it(`should match v${vers}-v${lastIdx + 1} for ${scenario}`, () => { - expect(datas[i], `version mismatch for v${vers}-v${lastIdx + 1} ${scenario}`) - .to.eql(datas[lastIdx]); + it(`should match v${vers}-v${VERSIONS_LATEST} for ${scenario}`, () => { + expect(datas[i], `version mismatch for v${vers}-v${VERSIONS_LATEST} ${scenario}`) + .to.eql(datas[VERSIONS_LATEST_IDX]); }); }); }); @@ -191,6 +186,11 @@ describe("lib/actions/versions", () => { describe("development vs production", () => { FIXTURES.map((scenario) => { VERSIONS.map((vers) => { + if (treeShakingWorks({ scenario, vers })) { + it(`v${vers} scenario '${scenario}' should match (SKIP TREE-SHAKING)`); + return; + } + it(`v${vers} scenario '${scenario}' should match`, () => { return Promise.all([ getData(join(scenario, `dist-development-${vers}`)), @@ -211,6 +211,38 @@ describe("lib/actions/versions", () => { }); }); + describe("all production", () => { + FIXTURES.map((scenario: string) => { + VERSIONS.map((vers: string, i) => { + // Skip latest version + limit to tree-shaking scenarios. + if (i === VERSIONS_LATEST_IDX || !treeShakingWorks({ scenario, vers })) { + return; + } + + let latestProd: IVersionsData; + + before(() => { + return getData(join(scenario, `dist-production-${VERSIONS_LATEST}`)) + .then((data) => { latestProd = data; }) + }); + + it(`should match v${vers}-v${VERSIONS_LATEST} for ${scenario}`, () => { + return getData(join(scenario, `dist-production-${vers}`)) + .then((curProd) => { + expect(curProd, `prod is empty for v${vers} ${scenario}`) + .to.not.equal(null).and + .to.not.equal(undefined).and + .to.not.eql([]).and + .to.not.eql({}); + + expect(curProd, `prod mismatch for v${vers}-v${VERSIONS_LATEST} ${scenario}`) + .to.eql(latestProd); + }); + }); + }); + }); + }); + describe("node_modules scenarios", () => { it("errors on malformed root package.json", () => { mock({ diff --git a/test/utils.ts b/test/utils.ts index 89eca223..a47a48cd 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -19,23 +19,38 @@ export const FIXTURES = scenarios.map((s) => s.WEBPACK_CWD.replace("../../test/f // // Rather than deal with all these complexities, we just skip the subset of // tests for webpack@1 that involve `import` or loaders. -export const FIXTURES_WEBPACK1_BLACKLIST = [ +export const FIXTURES_WEBPACK1_SKIPLIST = [ "duplicates-esm", "loaders", "multiple-chunks", "tree-shaking", ]; -// Skip testing webpack4 vs. webpack1-3 because tree shaking appears to work -// in this scenario now-ish... +// Tree-shaking has only been working since webpack4+ (the webpack4 version we +// test against at least). With working tree-shaking, the production and +// development stats legitimately differ with removed/orphaned modules in +// production. // -// See: https://github.com/FormidableLabs/inspectpack/issues/77 -export const FIXTURES_WEBPACK4_BLACKLIST = [ +// Identify scenarios that are affect by tree-shaking. +export const TREE_SHAKING_FIXTURES = [ "tree-shaking", ]; +// Tree-shaking starts working here. +export const TREE_SHAKING_VERSION_MIN = 4; + +// Returns true if scenario results change if tree-shaking actually works. +export const treeShakingWorks = ({ scenario, vers }: { scenario: string, vers: string }) => { + const versNum = parseInt(vers, 10); + return versNum >= TREE_SHAKING_VERSION_MIN && TREE_SHAKING_FIXTURES.includes(scenario); +}; + export const VERSIONS = versions.map((v) => v.WEBPACK_VERSION); +export const VERSIONS_LATEST_IDX = VERSIONS.length - 1; + +export const VERSIONS_LATEST = VERSIONS[VERSIONS_LATEST_IDX]; + const FIXTURES_DIRS = FIXTURES .map((f) => VERSIONS.reduce((m: string[], v) => m.concat([ join("../../test/fixtures", f, `dist-development-${v}`), @@ -172,19 +187,23 @@ export const loadFixtureDirs = (): Promise => { return _fixtureDirsProm; }; -// General action patching -export const patchAllMods = (name: string) => (mod: IModule) => { - // Looks like tree-shaking **does** work in updated webpack4. - // Manually adjust just `foo/green.js` which is DCE'd to normalize dev vs prod - // - // **Side Effect**: Relies on populated `_assets` from above. - // - // See: https://github.com/FormidableLabs/inspectpack/issues/77 - if (name === join("tree-shaking", "dist-development-4") && - mod.baseName === "foo/green.js") { - mod.chunks = []; +// Normalize name fields. +const patchModName = (name: string | null) => { + if (name === null) { + return null; } + return name + // webpack5+ does some unicode normalizations that we unwind. + .replace("\u0000#", "#"); +}; + +// General action patching +export const patchAllMods = (mod: IModule) => { + // Name field normalization. + mod.baseName = patchModName(mod.baseName); + mod.identifier = patchModName(mod.identifier) || ""; + return mod; }; diff --git a/yarn.lock b/yarn.lock index 64e58f88..b69ae465 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2547,11 +2547,6 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -expose-loader@^0.7.5: - version "0.7.5" - resolved "https://registry.yarnpkg.com/expose-loader/-/expose-loader-0.7.5.tgz#e29ea2d9aeeed3254a3faa1b35f502db9f9c3f6f" - integrity sha512-iPowgKUZkTPX5PznYsmifVj9Bob0w2wTHVkt/eYNPSzyebkUgIedmskf/kcfEIWpiWjg3JRjnW+a17XypySMuw== - ext@^1.1.2: version "1.4.0" resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"