From dcf2eb654e28a9e77509250121494f86be4e140f Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Thu, 28 Apr 2022 14:34:46 -0400 Subject: [PATCH 01/83] Upgrade dependencies for Webpack 5 / Node 14-16 --- package.json | 74 ++++++++++++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 0ee394b..b0d8b3a 100644 --- a/package.json +++ b/package.json @@ -29,53 +29,53 @@ ] }, "devDependencies": { - "eslint": "^7.17.0", + "eslint": "^7.32.0", "jest": "^26.6.3", - "sass": "^1.32.4", - "typescript": "^4.0.2", - "webpack": "^4.46.0", - "webpack-cli": "^3.3.12", - "webpack-dev-server": "^3.11.2" + "sass": "^1.51.0", + "typescript": "^4.6.3", + "webpack": "^5.72.0", + "webpack-cli": "^4.9.2", + "webpack-dev-server": "^4.8.1" }, "dependencies": { - "@babel/core": "^7.10.3", - "@babel/plugin-proposal-class-properties": "^7.10.1", - "@wordpress/babel-preset-default": "^4.16.0", - "babel-loader": "^8.0.6", - "babel-plugin-transform-react-jsx": "^6.24.1", + "@babel/core": "^7.17.9", + "@babel/plugin-proposal-class-properties": "^7.16.7", + "@wordpress/babel-preset-default": "^6.9.0", + "babel-loader": "^8.2.5", "bell-on-bundler-error-plugin": "^2.0.0", - "block-editor-hmr": "^0.6.1", - "chalk": "^4.1.0", - "clean-webpack-plugin": "^3.0.0", - "copy-webpack-plugin": "^6.0.2", - "css-loader": "^5.0.1", + "block-editor-hmr": "^0.6.3", + "chalk": "^5.0.1", + "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^10.2.4", + "css-loader": "^6.7.1", + "css-minimizer-webpack-plugin": "^3.4.1", "detect-port-alt": "^1.1.6", "eslint-loader": "^4.0.2", - "file-loader": "^6.0.0", - "inquirer": "^7.2.0", - "is-root": "^2.1.0", - "mini-css-extract-plugin": "^1.3.4", - "optimize-css-assets-webpack-plugin": "^5.0.1", - "postcss": "^8.2.4", + "file-loader": "^6.2.0", + "inquirer": "^8.2.4", + "is-root": "^3.0.0", + "mini-css-extract-plugin": "^2.6.0", + "postcss": "^8.4.12", "postcss-flexbugs-fixes": "^5.0.2", - "postcss-loader": "^4.1.0", - "postcss-preset-env": "^6.7.0", - "run-parallel": "1.1.9", - "sass-loader": "^10.0.2", - "signal-exit": "^3.0.2", - "style-loader": "^2.0.0", - "terser-webpack-plugin": "^4.2.0", - "ts-loader": "^8.0.1", - "url-loader": "^4.1.0", - "webpack-bundle-analyzer": "^4.3.0", - "webpack-fix-style-only-entries": "^0.6.0", - "webpack-manifest-plugin": "^3.0.0" + "postcss-loader": "^6.2.1", + "postcss-preset-env": "^7.4.4", + "run-parallel": "^1.2.0", + "sass-loader": "^12.6.0", + "signal-exit": "^3.0.7", + "style-loader": "^3.3.1", + "terser-webpack-plugin": "^5.3.1", + "ts-loader": "^9.2.9", + "webpack": "^5.0.0", + "webpack-bundle-analyzer": "^4.5.0", + "webpack-dev-server": "^4.0.0", + "webpack-fix-style-only-entries": "^0.6.1", + "webpack-manifest-plugin": "^5.0.0" }, "peerDependencies": { "sass": "*", "typescript": "*", - "webpack": "^4.0.0", - "webpack-cli": "^3.0.0", - "webpack-dev-server": "^3.0.0" + "webpack": "^5.0.0", + "webpack-cli": "^4.0.0", + "webpack-dev-server": "^4.0.0" } } From 4cb27d0c5664fe34593dc2fb1a18ab4fcf1488ba Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Thu, 28 Apr 2022 14:37:33 -0400 Subject: [PATCH 02/83] Pin Chalk to v4 for node compatibility (see note) https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b0d8b3a..ef6dee4 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "babel-loader": "^8.2.5", "bell-on-bundler-error-plugin": "^2.0.0", "block-editor-hmr": "^0.6.3", - "chalk": "^5.0.1", + "chalk": "^4.1.2", "clean-webpack-plugin": "^4.0.0", "copy-webpack-plugin": "^10.2.4", "css-loader": "^6.7.1", From f6cc586b329aa8e3d5ed2e060420d5cd2efaf40e Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Thu, 28 Apr 2022 14:54:20 -0400 Subject: [PATCH 03/83] Update choose-port to latest react-dev-utils version --- package.json | 4 ++-- src/helpers/choose-port.js | 46 +++++++++++++++++++++----------------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index ef6dee4..2b235f0 100644 --- a/package.json +++ b/package.json @@ -52,13 +52,13 @@ "detect-port-alt": "^1.1.6", "eslint-loader": "^4.0.2", "file-loader": "^6.2.0", - "inquirer": "^8.2.4", - "is-root": "^3.0.0", + "is-root": "^2.1.0", "mini-css-extract-plugin": "^2.6.0", "postcss": "^8.4.12", "postcss-flexbugs-fixes": "^5.0.2", "postcss-loader": "^6.2.1", "postcss-preset-env": "^7.4.4", + "prompts": "^2.4.2", "run-parallel": "^1.2.0", "sass-loader": "^12.6.0", "signal-exit": "^3.0.7", diff --git a/src/helpers/choose-port.js b/src/helpers/choose-port.js index d4c02de..ce1b826 100644 --- a/src/helpers/choose-port.js +++ b/src/helpers/choose-port.js @@ -8,7 +8,7 @@ * */ const chalk = require( 'chalk' ); -const inquirer = require( 'inquirer' ); +const prompts = require( 'prompts' ); const detect = require( 'detect-port-alt' ); const isRoot = require( 'is-root' ); @@ -17,6 +17,8 @@ const getProcessForPort = require( '../vendor/get-process-for-port' ); const DEFAULT_PORT = process.env.PORT || 8080; const HOST = process.env.HOST || '0.0.0.0'; +const isInteractive = process.stdout.isTTY; + /** * choosePort method adapted from create-react-app's `react-dev-utils`. * @@ -33,25 +35,29 @@ const choosePort = ( host, defaultPort ) => detect( defaultPort, host ).then( const message = process.platform !== 'win32' && defaultPort < 1024 && ! isRoot() ? 'Admin permissions are required to run a server on a port below 1024.' : `Something is already running on port ${ defaultPort }.`; - // clearConsole(); - const existingProcess = getProcessForPort( defaultPort ); - const question = { - type: 'confirm', - name: 'shouldChangePort', - message: `${ - chalk.yellow( `${ message }${ - existingProcess ? ` Probably:\n ${ existingProcess }` : '' - }` ) - }\n\nWould you like to run the app on another port instead?`, - default: true, - }; - inquirer.prompt( question ).then( answer => { - if ( answer.shouldChangePort ) { - resolve( port ); - } else { - resolve( null ); - } - } ); + if ( isInteractive ) { + const existingProcess = getProcessForPort( defaultPort ); + const question = { + type: 'confirm', + name: 'shouldChangePort', + message: `${ + chalk.yellow( `${ message }${ + existingProcess ? ` Probably:\n ${ existingProcess }` : '' + }` ) + }\n\nWould you like to run the app on another port instead?`, + initial: true, + }; + prompts( question ).then( answer => { + if ( answer.shouldChangePort ) { + resolve( port ); + } else { + resolve( null ); + } + } ); + } else { + console.log( chalk.red( message ) ); + resolve( null ); + } } ), err => { throw new Error( `${ From c697a404d9add8154ea382d62ab0947aea398557 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Thu, 28 Apr 2022 15:01:29 -0400 Subject: [PATCH 04/83] Use Asset Modules over url-loader and file-loader --- docs/changelog.md | 5 ++++- docs/modules/loaders.md | 4 ++-- docs/modules/plugins.md | 1 + package.json | 1 - src/loaders.js | 33 +++++++++++++++++++-------------- src/presets.js | 24 ++++++++++++------------ 6 files changed, 38 insertions(+), 30 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 5b4df9b..25889b3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,8 +6,11 @@ nav_order: 10 # Changelog -## Next +## v1.0 +- **Breaking**: Switch to Webpack 5 and Webpack DevServer 4 +- **Potentially Breaking**: Remove `loaders.url()` and `loaders.file()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` (for assets which can be inlined) and `loaders.resource()` (as a catch-all for other types) in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` or `loaders.file()` was used directly. +- **Potentially Breaking**: Remove `loaders.url()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` was used directly. - Include the `contenthash` in generated CSS filenames. [#204](https://github.com/humanmade/webpack-helpers/pull/204) ## v0.11.1 diff --git a/docs/modules/loaders.md b/docs/modules/loaders.md index f036aa2..ab0e970 100644 --- a/docs/modules/loaders.md +++ b/docs/modules/loaders.md @@ -10,15 +10,15 @@ nav_order: 3 This module provides functions that generate configurations for commonly-needed Webpack loaders. Use them within the `.module.rules` array, or use `presets.development()`/`presets.production()` to opt-in to some opinionated defaults. +- `loaders.assets()`: Return a configured Webpack module loader rule for [`asset` modules](https://webpack.js.org/guides/asset-modules/#inlining-assets) which will be inlined if small enough. - `loaders.eslint()`: Return a configured Webpack module loader rule for `eslint-loader`. - `loaders.js()`: Return a configured Webpack module loader rule for `js-loader`. - `loaders.ts()`: Return a configured Webpack module loader rule for `ts-loader`. -- `loaders.url()`: Return a configured Webpack module loader rule for `url-loader`. - `loaders.style()`: Return a configured Webpack module loader rule for `style-loader`. - `loaders.css()`: Return a configured Webpack module loader rule for `css-loader`. - `loaders.postcss()`: Return a configured Webpack module loader rule for `postcss-loader`. - `loaders.sass()`: Return a configured Webpack module loader rule for `sass-loader`. -- `loaders.file()`: Return a configured Webpack module loader rule for `file-loader`. +- `loaders.resource()`: Return a configured Webpack module loader rule for [`asset/resource` modules](https://webpack.js.org/guides/asset-modules/#resource-assets). ## Customizing Loaders diff --git a/docs/modules/plugins.md b/docs/modules/plugins.md index ce31a83..d0c996e 100644 --- a/docs/modules/plugins.md +++ b/docs/modules/plugins.md @@ -21,6 +21,7 @@ This module provides methods which create new instances of commonly-needed Webpa   | `plugins.manifest()` | : Create and return a new [`webpack-manifest-plugin`](https://github.com/danethurber/webpack-manifest-plugin) instance, preconfigured to write the manifest file while running from a dev server. **P** | `plugins.miniCssExtract()` | Create and return a new [`mini-css-extract-plugin`](https://github.com/webpack-contrib/mini-css-extract-plugin) instance. **P** | `plugins.terser()` | Create and return a new [`terser-webpack-plugin`](https://github.com/webpack-contrib/terser-webpack-plugin) instance, preconfigured with defaults based on `create-react-app`. +**P** | `plugins.cssMinimizer()` | Create and return a new [`css-minimizer-webpack-plugin`](https://webpack.js.org/plugins/css-minimizer-webpack-plugin/) instance. **P**: Included in `presets.production()` diff --git a/package.json b/package.json index 2b235f0..825e2c2 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "css-minimizer-webpack-plugin": "^3.4.1", "detect-port-alt": "^1.1.6", "eslint-loader": "^4.0.2", - "file-loader": "^6.2.0", "is-root": "^2.1.0", "mini-css-extract-plugin": "^2.6.0", "postcss": "^8.4.12", diff --git a/src/loaders.js b/src/loaders.js index 0a23115..9e8673e 100644 --- a/src/loaders.js +++ b/src/loaders.js @@ -28,10 +28,21 @@ const createLoaderFactory = loaderKey => { }; // Define all supported loader factories within the loaders object. -[ 'eslint', 'js', 'ts', 'url', 'style', 'css', 'postcss', 'sass', 'file' ].forEach( loaderKey => { +[ 'assets', 'eslint', 'js', 'ts', 'style', 'css', 'postcss', 'sass', 'resource' ].forEach( loaderKey => { loaders[ loaderKey ] = createLoaderFactory( loaderKey ); } ); +loaders.assets.defaults = { + test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|eot|ttf)$/, + type: 'asset', + parser: { + dataUrlCondition: { + // Inline if less than 10kb. + maxSize: 10 * 1024, + }, + }, +}; + loaders.eslint.defaults = { test: /\.jsx?$/, exclude: /(node_modules|bower_components)/, @@ -56,14 +67,6 @@ loaders.ts.defaults = { loader: require.resolve( 'ts-loader' ), }; -loaders.url.defaults = { - test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|eot|ttf)$/, - loader: require.resolve( 'url-loader' ), - options: { - limit: 10000, - }, -}; - loaders.style.defaults = { loader: require.resolve( 'style-loader' ), options: {}, @@ -102,11 +105,13 @@ loaders.sass.defaults = { }, }; -loaders.file.defaults = { - // Exclude `js`, `html` and `json`, but match anything else. - exclude: /\.(js|html|json)$/, - loader: require.resolve( 'file-loader' ), - options: {}, +loaders.resource.defaults = { + // Exclude `js` files to keep "css" loader working as it injects + // its runtime that would otherwise be processed through "file" loader. + // Also exclude `html` and `json` extensions so they get processed + // by webpacks internal loaders. + exclude: [ /^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/ ], + type: 'asset/resource', }; module.exports = loaders; diff --git a/src/presets.js b/src/presets.js index 9f3a28a..d651cbb 100644 --- a/src/presets.js +++ b/src/presets.js @@ -147,14 +147,14 @@ const development = ( config = {}, options = {} ) => { { // "oneOf" will traverse all following loaders until one will // match the requirements. If no loader matches, it will fall - // back to the "file" loader at the end of the loader list. + // back to the resource loader at the end of the loader list. oneOf: [ // Enable processing TypeScript, if installed. ...ifInstalled( 'typescript', getFilteredLoader( 'ts' ) ), // Process JS with Babel. getFilteredLoader( 'js' ), - // Convert small files to data URIs. - getFilteredLoader( 'url' ), + // Handle static asset files. + getFilteredLoader( 'assets' ), // Parse styles using SASS, then PostCSS. filterLoaders( { test: /\.s?css$/, @@ -177,9 +177,9 @@ const development = ( config = {}, options = {} ) => { } ), ], }, 'stylesheet' ), - // "file" loader makes sure any non-matching assets still get served. - // When you `import` an asset you get its filename. - getFilteredLoader( 'file' ), + // Resource loader makes sure any non-matching assets still get served. + // When you `import` an asset, you get its (virtual) filename. + getFilteredLoader( 'resource' ), ], }, ], @@ -298,14 +298,14 @@ const production = ( config = {}, options = {} ) => { { // "oneOf" will traverse all following loaders until one will // match the requirements. If no loader matches, it will fall - // back to the "file" loader at the end of the loader list. + // back to the resource loader at the end of the loader list. oneOf: [ // Enable processing TypeScript, if installed. ...ifInstalled( 'typescript', getFilteredLoader( 'ts' ) ), // Process JS with Babel. getFilteredLoader( 'js' ), - // Convert small files to data URIs. - getFilteredLoader( 'url' ), + // Handle static asset files. + getFilteredLoader( 'assets' ), // Parse styles using SASS, then PostCSS. filterLoaders( { test: /\.s?css$/, @@ -318,9 +318,9 @@ const production = ( config = {}, options = {} ) => { getFilteredLoader( 'sass', cssOptions ), ], }, 'stylesheet' ), - // "file" loader makes sure any non-matching assets still get served. - // When you `import` an asset you get its filename. - getFilteredLoader( 'file' ), + // Resource loader makes sure any non-matching assets still get served. + // When you `import` an asset, you get its (virtual) filename. + getFilteredLoader( 'resource' ), ], }, ], From b3c8d52e90d1fd84d200e63466abeb4e81300c9a Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Thu, 28 Apr 2022 15:48:55 -0400 Subject: [PATCH 05/83] Switch to Webpack 5-compatible CSS optimizer plugin --- docs/changelog.md | 1 + src/plugins.js | 10 +++++----- src/presets.js | 15 ++------------- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 25889b3..20829a1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -11,6 +11,7 @@ nav_order: 10 - **Breaking**: Switch to Webpack 5 and Webpack DevServer 4 - **Potentially Breaking**: Remove `loaders.url()` and `loaders.file()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` (for assets which can be inlined) and `loaders.resource()` (as a catch-all for other types) in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` or `loaders.file()` was used directly. - **Potentially Breaking**: Remove `loaders.url()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` was used directly. +- Remove `OptimizeCssAssetsPlugin` (`plugins.optimizeCssAssets()`) in favor of Webpack 5-compatible [CssMinimizerPlugin](https://github.com/webpack-contrib/css-minimizer-webpack-plugin) (`plugins.cssMinimizer()`) - Include the `contenthash` in generated CSS filenames. [#204](https://github.com/humanmade/webpack-helpers/pull/204) ## v0.11.1 diff --git a/src/plugins.js b/src/plugins.js index 3de1ad3..04fd5df 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -7,7 +7,7 @@ const FixStyleOnlyEntriesPlugin = require( 'webpack-fix-style-only-entries' ); const { WebpackManifestPlugin: ManifestPlugin } = require( 'webpack-manifest-plugin' ); const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' ); const TerserPlugin = require( 'terser-webpack-plugin' ); -const OptimizeCssAssetsPlugin = require( 'optimize-css-assets-webpack-plugin' ); +const CssMinimizerPlugin = require( 'css-minimizer-webpack-plugin' ); const deepMerge = require( './helpers/deep-merge' ); @@ -26,7 +26,7 @@ module.exports = { HotModuleReplacementPlugin, ManifestPlugin, MiniCssExtractPlugin, - OptimizeCssAssetsPlugin, + CssMinimizerPlugin, TerserPlugin, }, @@ -154,12 +154,12 @@ module.exports = { } ), /** - * Create a new OptimizeCssAssetsPlugin instance. + * Create a new CssMinimizerPlugin instance. * * @param {Object} [options] Optional plugin configuration options. - * @returns {OptimizeCssAssetsPlugin} A configured OptimizeCssAssetsPlugin instance. + * @returns {CssMinimizerPlugin} A configured CssMinimizerPlugin instance. */ - optimizeCssAssets: ( options = {} ) => new OptimizeCssAssetsPlugin( options ), + cssMinimizer: ( options = {} ) => new CssMinimizerPlugin( options ), /** * Create a new TerserPlugin instance, defaulting to a set of options diff --git a/src/presets.js b/src/presets.js index d651cbb..15fd420 100644 --- a/src/presets.js +++ b/src/presets.js @@ -327,22 +327,11 @@ const production = ( config = {}, options = {} ) => { }, optimization: { + minimize: true, minimizer: [ plugins.terser(), - plugins.optimizeCssAssets( ( - // Set option to output source maps if devtool is set. - config.devtool && ! ( /inline-/ ).test( config.devtool ) ? - { - cssProcessorOptions: { - map: { - inline: false, - }, - }, - } : - undefined - ) ), + plugins.cssMinimizer(), ], - nodeEnv: 'production', noEmitOnErrors: true, }, From a7fbc9bd69252a9fe51cb7ce069efd8c6b4e2953 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Thu, 28 Apr 2022 15:22:23 -0400 Subject: [PATCH 06/83] Add source-map-loader, borrowed from latest CRA --- docs/modules/loaders.md | 1 + package.json | 1 + src/loaders.js | 10 +++++++++- src/presets.js | 2 ++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/modules/loaders.md b/docs/modules/loaders.md index ab0e970..e211e41 100644 --- a/docs/modules/loaders.md +++ b/docs/modules/loaders.md @@ -18,6 +18,7 @@ This module provides functions that generate configurations for commonly-needed - `loaders.css()`: Return a configured Webpack module loader rule for `css-loader`. - `loaders.postcss()`: Return a configured Webpack module loader rule for `postcss-loader`. - `loaders.sass()`: Return a configured Webpack module loader rule for `sass-loader`. +- `loaders.sourcemaps()`: Return a configured Webpack module loader rule for `source-map-loader`. - `loaders.resource()`: Return a configured Webpack module loader rule for [`asset/resource` modules](https://webpack.js.org/guides/asset-modules/#resource-assets). ## Customizing Loaders diff --git a/package.json b/package.json index 825e2c2..c66d030 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "run-parallel": "^1.2.0", "sass-loader": "^12.6.0", "signal-exit": "^3.0.7", + "source-map-loader": "^3.0.1", "style-loader": "^3.3.1", "terser-webpack-plugin": "^5.3.1", "ts-loader": "^9.2.9", diff --git a/src/loaders.js b/src/loaders.js index 9e8673e..05bbc3e 100644 --- a/src/loaders.js +++ b/src/loaders.js @@ -28,7 +28,7 @@ const createLoaderFactory = loaderKey => { }; // Define all supported loader factories within the loaders object. -[ 'assets', 'eslint', 'js', 'ts', 'style', 'css', 'postcss', 'sass', 'resource' ].forEach( loaderKey => { +[ 'assets', 'eslint', 'js', 'ts', 'style', 'css', 'postcss', 'sass', 'sourcemaps', 'resource' ].forEach( loaderKey => { loaders[ loaderKey ] = createLoaderFactory( loaderKey ); } ); @@ -105,6 +105,14 @@ loaders.sass.defaults = { }, }; +loaders.sourcemaps.defaults = { + test: /\.(js|mjs|jsx|ts|tsx|css)$/, + exclude: /@babel(?:\/|\\{1,2})runtime/, + enforce: 'pre', + loader: require.resolve( 'source-map-loader' ), + options: {}, +}; + loaders.resource.defaults = { // Exclude `js` files to keep "css" loader working as it injects // its runtime that would otherwise be processed through "file" loader. diff --git a/src/presets.js b/src/presets.js index 15fd420..64d2bc6 100644 --- a/src/presets.js +++ b/src/presets.js @@ -138,6 +138,8 @@ const development = ( config = {}, options = {} ) => { module: { strictExportPresence: true, rules: [ + // Handle node_modules packages that contain sourcemaps. + getFilteredLoader( 'sourcemaps' ), // Run all JS files through ESLint, if installed. ...ifInstalled( 'eslint', getFilteredLoader( 'eslint', { options: { From 287723c16c36a29cd62b7767a0659fb894cbe134 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Thu, 28 Apr 2022 15:24:31 -0400 Subject: [PATCH 07/83] JSX pragma is now baked into WordPress babel preset --- babel-preset.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/babel-preset.js b/babel-preset.js index ae44199..6028904 100644 --- a/babel-preset.js +++ b/babel-preset.js @@ -2,12 +2,7 @@ module.exports = ( api ) => { api.cache.forever(); return { - presets: [ '@wordpress/default' ], - plugins: [ - '@babel/plugin-proposal-class-properties', - [ 'transform-react-jsx', { - pragma: 'wp.element.createElement', - } ], - ], + presets: [ '@wordpress/babel-preset-default' ], + plugins: [ '@babel/plugin-proposal-class-properties' ], }; }; From 069375f47806bfba1c372beb14290dbddd5c5f0b Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Thu, 28 Apr 2022 15:27:35 -0400 Subject: [PATCH 08/83] Update to latest TerserPlugin config from CRA --- src/plugins.js | 12 ++++++++---- src/presets.js | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/plugins.js b/src/plugins.js index 04fd5df..7fdc4d4 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -11,6 +11,8 @@ const CssMinimizerPlugin = require( 'css-minimizer-webpack-plugin' ); const deepMerge = require( './helpers/deep-merge' ); +const isProfileMode = process.argv.includes( '--profile' ); + module.exports = { /** * Expose plugin constructor functions for use in consuming applications. @@ -173,8 +175,8 @@ module.exports = { terser: ( options = {} ) => new TerserPlugin( deepMerge( { terserOptions: { parse: { - // we want terser to parse ecma 8 code. However, we don't want it - // to apply any minfication steps that turns valid ecma 5 code + // We want terser to parse ecma 8 code. However, we don't want it + // to apply any minification steps that turns valid ecma 5 code // into invalid ecma 5 code. This is why the 'compress' and 'output' // sections only apply transformations that are ecma 5 safe // https://github.com/facebook/create-react-app/pull/4234 @@ -190,13 +192,16 @@ module.exports = { comparisons: false, // Disabled because of an issue with Terser breaking valid code: // https://github.com/facebook/create-react-app/issues/5250 - // Pending futher investigation: + // Pending further investigation: // https://github.com/terser-js/terser/issues/120 inline: 2, }, mangle: { safari10: true, }, + // Added for profiling in devtools + keep_classnames: isProfileMode, + keep_fnames: isProfileMode, output: { ecma: 5, comments: false, @@ -205,6 +210,5 @@ module.exports = { ascii_only: true, }, }, - extractComments: false, }, options ) ), }; diff --git a/src/presets.js b/src/presets.js index 64d2bc6..9667c7b 100644 --- a/src/presets.js +++ b/src/presets.js @@ -334,7 +334,7 @@ const production = ( config = {}, options = {} ) => { plugins.terser(), plugins.cssMinimizer(), ], - noEmitOnErrors: true, + emitOnErrors: false, }, stats, From d866674639d42c29e28d4fce62c200216bddbf72 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Thu, 28 Apr 2022 15:39:56 -0400 Subject: [PATCH 09/83] Update webpack devserver config for devServer v4 --- docs/changelog.md | 1 + docs/modules/plugins.md | 3 --- src/config.js | 11 ++++++----- src/plugins.js | 10 ---------- src/presets.js | 9 ++------- 5 files changed, 9 insertions(+), 25 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 20829a1..62602a9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -12,6 +12,7 @@ nav_order: 10 - **Potentially Breaking**: Remove `loaders.url()` and `loaders.file()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` (for assets which can be inlined) and `loaders.resource()` (as a catch-all for other types) in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` or `loaders.file()` was used directly. - **Potentially Breaking**: Remove `loaders.url()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` was used directly. - Remove `OptimizeCssAssetsPlugin` (`plugins.optimizeCssAssets()`) in favor of Webpack 5-compatible [CssMinimizerPlugin](https://github.com/webpack-contrib/css-minimizer-webpack-plugin) (`plugins.cssMinimizer()`) +- Remove `plugins.hotModuleReplacement()`, which is now handled automatically by the DevServer in `hot` mode. - Include the `contenthash` in generated CSS filenames. [#204](https://github.com/humanmade/webpack-helpers/pull/204) ## v0.11.1 diff --git a/docs/modules/plugins.md b/docs/modules/plugins.md index d0c996e..d7b59ed 100644 --- a/docs/modules/plugins.md +++ b/docs/modules/plugins.md @@ -17,12 +17,9 @@ This module provides methods which create new instances of commonly-needed Webpa   | `plugins.copy()` | Create and return a new [`copy-webpack-plugin`](https://github.com/webpack-contrib/copy-webpack-plugin) instance.   | `plugins.errorBell()` | Create and return a new [`bell-on-bundle-error-plugin`](https://www.npmjs.com/package/bell-on-bundler-error-plugin) instance.   | `plugins.fixStyleOnlyEntries()` | Create and return a [`webpack-fix-style-only-entries`](https://github.com/fqborges/webpack-fix-style-only-entries) instance to remove empty JS bundles for style-only entrypoints. -**D** | `plugins.hotModuleReplacement()` | Create and return a new [`webpack.HotModuleReplacementPlugin`](https://webpack.js.org/plugins/hot-module-replacement-plugin/) instance.   | `plugins.manifest()` | : Create and return a new [`webpack-manifest-plugin`](https://github.com/danethurber/webpack-manifest-plugin) instance, preconfigured to write the manifest file while running from a dev server. **P** | `plugins.miniCssExtract()` | Create and return a new [`mini-css-extract-plugin`](https://github.com/webpack-contrib/mini-css-extract-plugin) instance. **P** | `plugins.terser()` | Create and return a new [`terser-webpack-plugin`](https://github.com/webpack-contrib/terser-webpack-plugin) instance, preconfigured with defaults based on `create-react-app`. **P** | `plugins.cssMinimizer()` | Create and return a new [`css-minimizer-webpack-plugin`](https://webpack.js.org/plugins/css-minimizer-webpack-plugin/) instance. **P**: Included in `presets.production()` - -**D**: Included in `presets.development()` diff --git a/src/config.js b/src/config.js index 144df73..fa6c774 100644 --- a/src/config.js +++ b/src/config.js @@ -7,14 +7,15 @@ module.exports = { * @type {Object} */ devServer: { - disableHostCheck: true, + allowedHosts: 'all', headers: { 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': '*', + 'Access-Control-Allow-Headers': '*', }, - hotOnly: true, - watchOptions: { - aggregateTimeout: 300, - }, + // Enable gzip compression of generated files. + compress: true, + hot: 'only', }, /** diff --git a/src/plugins.js b/src/plugins.js index 7fdc4d4..112ee19 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -1,6 +1,5 @@ const { BundleAnalyzerPlugin } = require( 'webpack-bundle-analyzer' ); const { CleanWebpackPlugin } = require( 'clean-webpack-plugin' ); -const { HotModuleReplacementPlugin } = require( 'webpack' ); const BellOnBundleErrorPlugin = require( 'bell-on-bundler-error-plugin' ); const CopyPlugin = require( 'copy-webpack-plugin' ); const FixStyleOnlyEntriesPlugin = require( 'webpack-fix-style-only-entries' ); @@ -25,7 +24,6 @@ module.exports = { CleanWebpackPlugin, CopyPlugin, FixStyleOnlyEntriesPlugin, - HotModuleReplacementPlugin, ManifestPlugin, MiniCssExtractPlugin, CssMinimizerPlugin, @@ -110,14 +108,6 @@ module.exports = { */ fixStyleOnlyEntries: ( options ) => new FixStyleOnlyEntriesPlugin( options ), - /** - * Create a webpack.HotModuleReplacementPlugin instance. - * - * @param {Object} [options] Optional plugin options object. - * @returns {HotModuleReplacementPlugin} A configured HMR Plugin instance. - */ - hotModuleReplacement: ( options = {} ) => new HotModuleReplacementPlugin( options ), - /** * Create a new ManifestPlugin instance to output an asset-manifest.json * file, which can be consumed by the PHP server to auto-load generated diff --git a/src/presets.js b/src/presets.js index 9667c7b..a4ac60c 100644 --- a/src/presets.js +++ b/src/presets.js @@ -191,14 +191,9 @@ const development = ( config = {}, options = {} ) => { nodeEnv: 'development', }, - devServer: { - ...devServer, - stats, - }, + devServer, - plugins: [ - plugins.hotModuleReplacement(), - ], + plugins: [], }; // If no entry was provided, inject a default entry value. From 6898a3bcf752e51cabf995e346a3286b6297ce03 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Thu, 28 Apr 2022 15:54:00 -0400 Subject: [PATCH 10/83] Update unit tests for asset module changes Fix error in presets test "process" mocking --- src/loaders.js | 1 + src/loaders.test.js | 6 ++--- src/presets.test.js | 55 ++++++++++++++++++++++++++------------------- 3 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/loaders.js b/src/loaders.js index 05bbc3e..c5ddd77 100644 --- a/src/loaders.js +++ b/src/loaders.js @@ -83,6 +83,7 @@ loaders.postcss.defaults = { loader: require.resolve( 'postcss-loader' ), options: { postcssOptions: { + ident: 'postcss', plugins: [ postcssFlexbugsFixes, postcssPresetEnv( { diff --git a/src/loaders.test.js b/src/loaders.test.js index deb445d..a11d2e1 100644 --- a/src/loaders.test.js +++ b/src/loaders.test.js @@ -38,7 +38,7 @@ describe( 'loaders', () => { } ); } ); - describe( '.url()', () => { + describe( '.assets()', () => { it( 'tests for static assets', () => { [ 'file.png', @@ -51,14 +51,14 @@ describe( 'loaders', () => { 'file.eot', 'file.ttf', ].forEach( acceptedFileType => { - expect( acceptedFileType.match( loaders.url().test ) ).not.toBeNull(); + expect( acceptedFileType.match( loaders.assets().test ) ).not.toBeNull(); } ); [ 'file.js', 'file.css', 'file.html', ].forEach( unacceptedFileType => { - expect( unacceptedFileType.match( loaders.url().test ) ).toBeNull(); + expect( unacceptedFileType.match( loaders.assets().test ) ).toBeNull(); } ); } ); } ); diff --git a/src/presets.test.js b/src/presets.test.js index b623b2a..2392a71 100644 --- a/src/presets.test.js +++ b/src/presets.test.js @@ -4,7 +4,10 @@ const { } = require( './presets' ); const plugins = require( './plugins' ); -jest.mock( 'process', () => ( { cwd: () => 'cwd' } ) ); +jest.mock( 'process', () => ( { + cwd: () => 'cwd', + versions: {}, +} ) ); /** * Filter an array of plugins to only contain plugins of the provided type. @@ -26,6 +29,10 @@ const getLoaderByName = ( rules, loaderType ) => { if ( rule.loader && rule.loader.indexOf( loaderType ) > -1 ) { return rule; } + // Permit selection of asset module loaders with no .loader property. + if ( rule.type && rule.type === loaderType ) { + return rule; + } if ( rule.oneOf ) { const nestedMatch = getLoaderByName( rule.oneOf, loaderType ); if ( nestedMatch ) { @@ -267,19 +274,20 @@ describe( 'presets', () => { return loader; }, } ); - const fileLoader = getLoaderByName( config.module.rules, 'file-loader' ); - const urlLoader = getLoaderByName( config.module.rules, 'url-loader' ); + const resourceLoader = getLoaderByName( config.module.rules, 'asset/resource' ); + const assetsLoader = getLoaderByName( config.module.rules, 'asset' ); const jsLoader = getLoaderByName( config.module.rules, 'babel-loader' ); - expect( fileLoader ).toEqual( expect.objectContaining( { - exclude: /\.(js|html|json)$/, - options: { - publicPath: '../../', - }, + expect( resourceLoader ).toEqual( expect.objectContaining( { + exclude: [ /^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/ ], + type: 'asset/resource', } ) ); - expect( urlLoader ).toEqual( expect.objectContaining( { - test: /\.(png|jpg|jpeg|gif|svg)$/, - options: { - limit: 10000, + expect( assetsLoader ).toEqual( expect.objectContaining( { + test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|eot|ttf)$/, + type: 'asset', + parser: { + dataUrlCondition: { + maxSize: 10 * 1024, + }, }, } ) ); expect( jsLoader ).not.toBeNull(); @@ -476,19 +484,20 @@ describe( 'presets', () => { return loader; }, } ); - const fileLoader = getLoaderByName( config.module.rules, 'file-loader' ); - const urlLoader = getLoaderByName( config.module.rules, 'url-loader' ); + const resourceLoader = getLoaderByName( config.module.rules, 'asset/resource' ); + const assetsLoader = getLoaderByName( config.module.rules, 'asset' ); const jsLoader = getLoaderByName( config.module.rules, 'babel-loader' ); - expect( fileLoader ).toEqual( expect.objectContaining( { - exclude: /\.(js|html|json)$/, - options: { - publicPath: '../../', - }, + expect( resourceLoader ).toEqual( expect.objectContaining( { + exclude: [ /^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/ ], + type: 'asset/resource', } ) ); - expect( urlLoader ).toEqual( expect.objectContaining( { - test: /\.(png|jpg|jpeg|gif|svg)$/, - options: { - limit: 10000, + expect( assetsLoader ).toEqual( expect.objectContaining( { + test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|eot|ttf)$/, + type: 'asset', + parser: { + dataUrlCondition: { + maxSize: 10 * 1024, + }, }, } ) ); expect( jsLoader ).not.toBeNull(); From 880aaef9c96b9b789bcf6ef8f7bf9a19de793a8e Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Thu, 28 Apr 2022 15:59:56 -0400 Subject: [PATCH 11/83] Remove duplicative dependencies from package.json --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index c66d030..125ecff 100644 --- a/package.json +++ b/package.json @@ -65,9 +65,7 @@ "style-loader": "^3.3.1", "terser-webpack-plugin": "^5.3.1", "ts-loader": "^9.2.9", - "webpack": "^5.0.0", "webpack-bundle-analyzer": "^4.5.0", - "webpack-dev-server": "^4.0.0", "webpack-fix-style-only-entries": "^0.6.1", "webpack-manifest-plugin": "^5.0.0" }, From d75ca2319231c6f6805ed77cd0d90685a845b217 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Thu, 28 Apr 2022 16:18:23 -0400 Subject: [PATCH 12/83] Bump version to 1.0.0-beta.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 125ecff..1f9ec4a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "0.11.1", + "version": "1.0.0-beta.0", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made", From d3256e61e33ea40b426faec12f7e8cb3359323b3 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Thu, 28 Apr 2022 19:01:18 -0400 Subject: [PATCH 13/83] Replace filterLoaders system with a more generic addFilter hook system --- .eslintrc.js | 4 +- docs/changelog.md | 1 + docs/modules/presets.md | 43 ++++++----- index.js | 1 + src/helpers/filters.js | 102 ++++++++++++++++++++++++++ src/helpers/filters.test.js | 142 ++++++++++++++++++++++++++++++++++++ src/loaders.js | 34 +++++---- src/presets.js | 102 +++++++------------------- src/presets.test.js | 91 +++++++++++------------ 9 files changed, 364 insertions(+), 156 deletions(-) create mode 100644 src/helpers/filters.js create mode 100644 src/helpers/filters.test.js diff --git a/.eslintrc.js b/.eslintrc.js index 5837717..77aa5f6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -105,7 +105,7 @@ module.exports = { 'new-parens': 'error', 'newline-after-var': 'off', 'newline-before-return': 'off', - 'newline-per-chained-call': 'error', + 'newline-per-chained-call': 'off', 'no-alert': 'error', 'no-array-constructor': 'error', 'no-async-promise-executor': 'error', @@ -127,7 +127,7 @@ module.exports = { 'no-extra-label': 'error', 'no-extra-parens': 'off', 'no-floating-decimal': 'error', - 'no-implicit-coercion': 'error', + 'no-implicit-coercion': 'off', 'no-implicit-globals': 'error', 'no-implied-eval': 'error', 'no-inline-comments': 'error', diff --git a/docs/changelog.md b/docs/changelog.md index 62602a9..8a3010a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -9,6 +9,7 @@ nav_order: 10 ## v1.0 - **Breaking**: Switch to Webpack 5 and Webpack DevServer 4 +- **Breaking**: Replace `filterLoaders` system with [individual hooks accesible via the new `addFilter` helper](https://humanmade.github.io/webpack-helpers/modules/presets.html#customizing-presets). - **Potentially Breaking**: Remove `loaders.url()` and `loaders.file()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` (for assets which can be inlined) and `loaders.resource()` (as a catch-all for other types) in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` or `loaders.file()` was used directly. - **Potentially Breaking**: Remove `loaders.url()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` was used directly. - Remove `OptimizeCssAssetsPlugin` (`plugins.optimizeCssAssets()`) in favor of Webpack 5-compatible [CssMinimizerPlugin](https://github.com/webpack-contrib/css-minimizer-webpack-plugin) (`plugins.cssMinimizer()`) diff --git a/docs/modules/presets.md b/docs/modules/presets.md index 205d671..ed4747a 100644 --- a/docs/modules/presets.md +++ b/docs/modules/presets.md @@ -91,7 +91,6 @@ module.exports = { [Read up on the `...` "spread operator"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals) if this syntax is unfamiliar to you; it's quite useful! - Customize a generated production configuration by object mutation: ```js @@ -109,27 +108,37 @@ module.exports.optimization.minimizer = [ ]; ``` -Note that array values are _merged_, not overwritten. This allows you to easily add plugins, but it can make it hard to _remove_ values from array properties like the module loader rules. To change loader configuration options without completely removing a loader, we recommend the approach described below in "Customizing Loaders". However, if you do need to completely change or remove the loaders from a default, you may overwrite the `.module.rules` array using one of the above methods. - -### Modify loaders within a generated configuration +### Filtering values -Adjusting loaders within a generated configuration tree is difficult because loader arrays are not keyed and module rules may be nested. Instead, each `preset` generator accepts a second argument in which you can pass a callback function that will be run on the output of each computed loader definition. +Array values are _merged_ when processing a preset, not overwritten. This allows you to easily add plugins, but it can make it hard to _remove_ values from array properties like the module loader rules or plugins list. To change loader configuration options without completely removing a loader, or adjust loaders deep within a generated configuration tree, this library provides a `addFilter` helper function. `addFilter()` lets you register a callback which can transform the value of each computed loader, as well as some other useful configuration values. ```js -// Alter the publicPath value of the files-loader and url-loader. +const { addFilter } = require( 'helpers' ); + +// Intercept the "sass" loader and replace it with a Stylus loader. +addFilter( 'loader/sass', () => { + return { + loader: require.resolve( 'stylus-loader' ), + }; +} ); +// Alter presets to use a different targeting regular expression. +addFilter( 'preset/stylesheet-loaders', ( loader ) => { + loader.test = /.\styl$/; + return loader; +} ); + +// Now, use the preset as normal and it will pick up Stylus files instead! const config = production.preset( - { /* ...configuration options described above ... */ }, - { - filterLoaders: ( loader, loaderType ) => { - if ( loaderType === 'file' || loaderType === 'url' ) { - loader.options.publicPath = '../../'; - } - return loader; - } - } + { /* ...normal configuration options as described above ... */ } ); ``` -The `loaderType` option will be [one of the keys in the `loaders` object](./loaders.html), or else the special key `stylesheet` which will be passed alongside the entire computed stylesheet loader chain. Filter on `stylesheet` to, for example, completely replace the Sass stylesheet support with a different preprocessor like Stylus. +Available hooks: -The values of the `loaderType` argument in this callback correspond to the names of the available [loader factory functions](https://humanmade.github.io/webpack-helpers/modules/loaders.html). +- `loader/{loader name}`: Adjust the final output of [any of the methods on the `loaders` object](./loaders.html), for example `loader/sass` or `loader/js`. +- `loader/{loader name}/default`: Alter the default values before passing it to the loader configuration merge function. +- `loader/postcss/plugins`: Filter the list of PostCSS plugins used by that specific loader. +- `loader/postcss/preset-env`: Filter the configuration object passed to the PostCSS Preset Env plugin. +- `preset/stylesheet-loaders`: Filter the computed chain of stylesheet loaders for both dev and production presets. +- `preset/dev/stylesheet-loaders`: Filter the stylesheet loader chain for only the dev configuration. +- `preset/prod/stylesheet-loaders`: Filter the stylesheet loader chain for only the production configuration. diff --git a/index.js b/index.js index f8bf15c..5bc6e74 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,7 @@ module.exports = { config: require( './src/config' ), externals: require( './src/externals' ), helpers: { + addFilter: require( './src/helpers/filters' ).addFilter, choosePort: require( './src/helpers/choose-port' ), cleanOnExit: require( './src/helpers/clean-on-exit' ), filePath: require( './src/helpers/file-path' ), diff --git a/src/helpers/filters.js b/src/helpers/filters.js new file mode 100644 index 0000000..b43a220 --- /dev/null +++ b/src/helpers/filters.js @@ -0,0 +1,102 @@ +const registry = {}; + +/** + * Internal helper to return an ordered array of callbacks for a given priority. + * + * @private + * @param {string} hookName A hook name for which to return filters. + * @returns {Function[]} Array of filter functions. + */ +const getCallbacks = ( hookName ) => { + const priorities = Object.values( registry[ hookName ] || {} ) + // eslint-disable-next-line id-length + .sort( ( a, b ) => ( +a < +b ? -1 : 1 ) ); + return priorities.reduce( + ( callbacks, priority ) => { + return callbacks.concat( priority || [] ); + }, + [] + ); +}; + +/** + * Internal helper to set the registry to a specific state for testing. + * + * @private + * @param {Object} values New registry shape. + */ +const setupRegistry = ( values = {} ) => { + Object.keys( registry ).forEach( ( existingKey ) => { + Reflect.deleteProperty( registry, existingKey ); + } ); + + Object.keys( values ).forEach( ( newKey ) => { + registry[ newKey ] = values[ newKey ]; + } ); +}; + +/** + * Register a filter function for a given filter hook name. + * + * @param {string} hookName Name of hook to add a filter on. + * @param {Function} callback Callback to run on this hook. + * @param {Number} [priority] Optional numeric priority, default 10. + */ +const addFilter = ( hookName, callback, priority = 10 ) => { + if ( ! registry[ hookName ] ) { + registry[ hookName ] = {}; + } + if ( ! Array.isArray( registry[ hookName ][ priority ] ) ) { + registry[ hookName ][ priority ] = []; + } + registry[ hookName ][ priority ].push( callback ); +}; + +/** + * Remove a previously-added filter function for a given hook name. + * + * To remove a hook, the callback and priority arguments must match when the hook was added. + * + * @param {string} hookName Name of hook to add a filter on. + * @param {Function} callback Callback to run on this hook. + * @param {Number} [priority] Priority from which to remove the hook. + */ +const removeFilter = ( hookName, callback, priority = 10 ) => { + if ( ! registry[ hookName ] || ! registry[ hookName ][ priority ] ) { + // Not added, no action needed. + return; + } + registry[ hookName ][ priority ] = registry[ hookName ][ priority ] + .filter( ( hook ) => hook !== callback ); +}; + +/** + * Run input arguments through all registered callbacks for a specified filter, + * allowing each filter to transform the first argument. + * + * @param {string} hookName Name of hook to filter on. + * @param {...any} args Filter input arguments. + * @returns {any} Output of filter chain. + */ +const applyFilters = ( hookName, ...args ) => { + const callbacks = getCallbacks( hookName ); + + const [ firstArg, ...otherArgs ] = args; + + return callbacks.reduce( + ( val, callback ) => callback( val, ...otherArgs ), + firstArg, + ); +}; + +module.exports = { + addFilter, + removeFilter, + applyFilters, +}; + +if ( process.env.JEST_WORKER_ID ) { + // Exposed only for testing purposes. + module.exports.getCallbacks = getCallbacks; + module.exports.setupRegistry = setupRegistry; +} diff --git a/src/helpers/filters.test.js b/src/helpers/filters.test.js new file mode 100644 index 0000000..9f25e05 --- /dev/null +++ b/src/helpers/filters.test.js @@ -0,0 +1,142 @@ +const { + getCallbacks, + setupRegistry, + addFilter, + removeFilter, + applyFilters, +} = require( './filters' ); + +// Functions to use when testing filtering. +const toKebabCase = ( str ) => str.split( /[_ ]+/g ).join( '-' ); +const toLowerCase = ( str ) => str.toLowerCase(); +const toUpperCase = ( str ) => str.toUpperCase(); +const reverse = ( str ) => str.split( '' ).reverse().join( '' ); +const reverseWords = ( str ) => str.split( ' ' ).reverse().join( ' ' ); + +describe( 'filters', () => { + describe( '.getCallbacks() [internal]', () => { + + it( 'is a function', () => { + expect( getCallbacks ).toBeInstanceOf( Function ); + } ); + + it( 'returns callbacks for the requested hook', () => { + setupRegistry( { + 'loaders/js': { + 10: [ + toKebabCase, + toLowerCase, + ], + }, + 'loaders/css': { + 10: [ + toUpperCase, + ], + }, + } ); + expect( getCallbacks( 'loaders/js' ) ).toEqual( [ + toKebabCase, + toLowerCase, + ] ); + expect( getCallbacks( 'loaders/css' ) ).toEqual( [ + toUpperCase, + ] ); + } ); + + it( 'returns callbacks in priority order', () => { + setupRegistry( { + 'loaders/js': { + 10: [ 'a', 'b' ], + 3: [ 'c' ], + 22: [ 'd' ], + 7: [ 'e', 'f' ], + }, + } ); + const hooks = getCallbacks( 'loaders/js' ); + expect( hooks ).toEqual( [ 'c', 'e', 'f', 'a', 'b', 'd' ] ); + } ); + + it( 'returns empty array for uninitialized hook', () => { + setupRegistry(); + const hooks = getCallbacks( 'unused/hook' ); + expect( hooks ).toEqual( [] ); + } ); + } ); + + describe( 'addFilter', () => { + beforeEach( () => { + setupRegistry(); + } ); + + it( 'adds a callback for the specified filter', () => { + const hook = () => 'Woo'; + addFilter( 'loader/js', hook ); + expect( getCallbacks( 'loader/js' ) ).toEqual( [ hook ] ); + } ); + + it( 'adds callbacks with the specified priority', () => { + const hook = () => 'Woo'; + const hook2 = () => 'Woo-er'; + const hook3 = () => 'Woo-est'; + addFilter( 'loader/js', hook2, 9 ); + addFilter( 'loader/js', hook3, 11 ); + addFilter( 'loader/js', hook ); + expect( getCallbacks( 'loader/js' ) ).toEqual( [ hook2, hook, hook3 ] ); + } ); + } ); + + describe( 'removeFilter', () => { + beforeEach( () => { + setupRegistry(); + } ); + + it( 'removes a previously-added callback for the specified filter', () => { + const hook = () => 'Woo'; + addFilter( 'loader/js', hook ); + removeFilter( 'loader/js', hook ); + expect( getCallbacks( 'loader/js' ) ).toEqual( [] ); + } ); + + it( 'only removes the requested callback', () => { + const hook = () => 'Woo'; + const hook2 = () => 'Woo-er'; + addFilter( 'loader/js', hook ); + addFilter( 'loader/js', hook2 ); + removeFilter( 'loader/js', hook ); + expect( getCallbacks( 'loader/js' ) ).toEqual( [ hook2 ] ); + } ); + + it( 'does not remove a callback if the priority does not match', () => { + const hook = () => 'Woo'; + addFilter( 'loader/js', hook, 9 ); + removeFilter( 'loader/js', hook, 10 ); + expect( getCallbacks( 'loader/js' ) ).toEqual( [ hook ] ); + } ); + } ); + + describe( 'applyFilters', () => { + const input = 'Pasta Carbonara'; + + beforeEach( () => { + setupRegistry(); + } ); + + it( 'returns input unchanged if no filters', () => { + expect( applyFilters( 'do/stuff', input ) ).toBe( 'Pasta Carbonara' ); + } ); + + it( 'processes input with the registered filters', () => { + addFilter( 'words', toKebabCase ); + addFilter( 'words', toUpperCase ); + expect( applyFilters( 'words', input ) ).toBe( 'PASTA-CARBONARA' ); + } ); + + it( 'processes input through callbacks in the specified order', () => { + addFilter( 'words', toLowerCase, 9 ); + addFilter( 'words', toKebabCase, 11 ); + addFilter( 'words', reverseWords, 10 ); + addFilter( 'words', reverse, 10 ); + expect( applyFilters( 'words', input ) ).toBe( 'atsap-aranobrac' ); + } ); + } ); +} ); diff --git a/src/loaders.js b/src/loaders.js index c5ddd77..4f1afb5 100644 --- a/src/loaders.js +++ b/src/loaders.js @@ -5,26 +5,28 @@ const postcssFlexbugsFixes = require( 'postcss-flexbugs-fixes' ); const postcssPresetEnv = require( 'postcss-preset-env' ); const deepMerge = require( './helpers/deep-merge' ); +const { applyFilters } = require( './helpers/filters' ); /** * Export an object of named methods that generate corresponding loader config - * objects. To customize the default values of the loader, mutate the .defaults - * property exposed on each method (or pass a filterLoaders option to a preset). + * objects. To customize the default values of the loader, use addFilter() on + * either the hook `loader/loadername/defaults` (to adjust the default loader + * configuration) or `loader/loadername` (to alter the final, computed loader). */ const loaders = {}; const createLoaderFactory = loaderKey => { - const getFilteredLoader = ( options ) => { - // Handle missing options object. - if ( typeof options === 'function' ) { - return getFilteredLoader( {}, options ); - } - - // Generate the requested loader definition. - return deepMerge( loaders[ loaderKey ].defaults, options ); + return ( options ) => { + // Generate the requested loader definition. Expose filter seams both + // to customize the defaults, and to alter the final rendered output. + return applyFilters( + `loader/${ loaderKey }`, + deepMerge( + applyFilters( `loader/${ loaderKey }/defaults`, loaders[ loaderKey ].defaults ), + options + ) + ); }; - - return getFilteredLoader; }; // Define all supported loader factories within the loaders object. @@ -84,15 +86,15 @@ loaders.postcss.defaults = { options: { postcssOptions: { ident: 'postcss', - plugins: [ + plugins: applyFilters( 'loader/postcss/plugins', [ postcssFlexbugsFixes, - postcssPresetEnv( { + postcssPresetEnv( applyFilters( 'loader/postcss/preset-env', { autoprefixer: { flexbox: 'no-2009', }, stage: 3, - } ), - ], + } ) ), + ] ), }, }, }; diff --git a/src/presets.js b/src/presets.js index a4ac60c..9adc526 100644 --- a/src/presets.js +++ b/src/presets.js @@ -4,6 +4,7 @@ const filePath = require( './helpers/file-path' ); const findInObject = require( './helpers/find-in-object' ); const inferPublicPath = require( './helpers/infer-public-path' ); const isInstalled = require( './helpers/is-installed' ); +const { applyFilters } = require( './helpers/filters' ); const loaders = require( './loaders' ); const plugins = require( './plugins' ); const { ManifestPlugin, MiniCssExtractPlugin } = plugins.constructors; @@ -49,40 +50,6 @@ const ifInstalled = ( packageName, loader ) => { return [ loader ]; }; -/** - * Given a reference to a (possibly-undefined) filtering method, return a pair - * of helper functions to filter a loader definition object, or to get a loader - * definition object by its loaders dictionary key and then filter it. - * - * @param {Function} [filterLoaders] An optional filterLoaders function. Defaults - * to the identity function. - * @returns {Object} Object with `filterLoaders` and `getFilteredLoader` methods. - */ -const createFilteringHelpers = ( filterLoaders = ( input ) => input ) => ( { - /** - * Given a loader object and its key string, pass that object through the - * filterLoaders method (if one was provided) and return the filtered output. - * - * @param {Object} loader Webpack loader configuration object. - * @param {String} loaderKey String identifying which loader is being filtered. - * @returns {Object} Filtered loader object. - */ - filterLoaders: ( loader, loaderKey ) => { - return filterLoaders( loader, loaderKey ); - }, - - /** - * Helper method to reduce duplication when accessing and invoking loader factories. - * - * @param {String} loaderKey String key of a loader factory in the loaders object. - * @param {Object} [options] Options for this loader (optional). - * @returns {Object} Configured and filtered loader definition. - */ - getFilteredLoader: ( loaderKey, options ) => { - return filterLoaders( loaders[ loaderKey ]( options ), loaderKey ); - }, -} ); - /** * Promote a partial Webpack config into a full development-oriented configuration. * @@ -95,17 +62,10 @@ const createFilteringHelpers = ( filterLoaders = ( input ) => input ) => ( { * - an `.output.publicPath` string (unless a devServer.port is specified, * in which case publicPath defaults to `http://localhost:${ port }`) * - * @param {webpack.Configuration} config Configuration options to deeply merge into the defaults. - * @param {Object} [options] Optional options to modify configuration generation. - * @param {Function} [options.filterLoaders] An optional filter function that receives each - * computed loader definition and the name of that - * loader as it is generated, to permit per-config - * customization of loader options. + * @param {webpack.Configuration} config Configuration options to deeply merge into the defaults. * @returns {webpack.Configuration} A merged Webpack configuration object. */ -const development = ( config = {}, options = {} ) => { - const { filterLoaders, getFilteredLoader } = createFilteringHelpers( options.filterLoaders ); - +const development = ( config = {} ) => { /** * Default development environment-oriented Webpack options. This object is * defined at the time of function execution so that any changes to the @@ -139,9 +99,9 @@ const development = ( config = {}, options = {} ) => { strictExportPresence: true, rules: [ // Handle node_modules packages that contain sourcemaps. - getFilteredLoader( 'sourcemaps' ), + loaders.sourcemaps(), // Run all JS files through ESLint, if installed. - ...ifInstalled( 'eslint', getFilteredLoader( 'eslint', { + ...ifInstalled( 'eslint', loaders.eslint( { options: { emitWarning: true, }, @@ -152,36 +112,37 @@ const development = ( config = {}, options = {} ) => { // back to the resource loader at the end of the loader list. oneOf: [ // Enable processing TypeScript, if installed. - ...ifInstalled( 'typescript', getFilteredLoader( 'ts' ) ), + ...ifInstalled( 'typescript', loaders.ts() ), // Process JS with Babel. - getFilteredLoader( 'js' ), + loaders.js(), // Handle static asset files. - getFilteredLoader( 'assets' ), + loaders.assets(), // Parse styles using SASS, then PostCSS. - filterLoaders( { + // Permit filtering either on an env-specific or global basis. + applyFilters( 'preset/stylesheet-loaders', applyFilters( 'preset/dev/stylesheet-loaders', { test: /\.s?css$/, use: [ - getFilteredLoader( 'style' ), - getFilteredLoader( 'css', { + loaders.style(), + loaders.css( { options: { sourceMap: true, }, } ), - getFilteredLoader( 'postcss', { + loaders.postcss( { options: { sourceMap: true, }, } ), - getFilteredLoader( 'sass', { + loaders.sass( { options: { sourceMap: true, }, } ), ], - }, 'stylesheet' ), + } ) ), // Resource loader makes sure any non-matching assets still get served. // When you `import` an asset, you get its (virtual) filename. - getFilteredLoader( 'resource' ), + loaders.resource(), ], }, ], @@ -240,17 +201,10 @@ const development = ( config = {}, options = {} ) => { * merges specified options into an opinionated default production configuration * template. * - * @param {webpack.Configuration} config Configuration options to deeply merge into the defaults. - * @param {Object} [options] Optional options to modify configuration generation. - * @param {Function} [options.filterLoaders] An optional filter function that receives each - * computed loader definition and the name of that - * loader as it is generated, to permit per-config - * customization of loader options. + * @param {webpack.Configuration} config Configuration options to deeply merge into the defaults. * @returns {webpack.Configuration} A merged Webpack configuration object. */ -const production = ( config = {}, options = {} ) => { - const { filterLoaders, getFilteredLoader } = createFilteringHelpers( options.filterLoaders ); - +const production = ( config = {} ) => { // Determine whether source maps have been requested, and prepare an options // object to be passed to all CSS loaders to honor that request. const cssOptions = config.devtool ? @@ -291,33 +245,33 @@ const production = ( config = {}, options = {} ) => { strictExportPresence: true, rules: [ // Run all JS files through ESLint, if installed. - ...ifInstalled( 'eslint', getFilteredLoader( 'eslint' ) ), + ...ifInstalled( 'eslint', loaders.eslint() ), { // "oneOf" will traverse all following loaders until one will // match the requirements. If no loader matches, it will fall // back to the resource loader at the end of the loader list. oneOf: [ // Enable processing TypeScript, if installed. - ...ifInstalled( 'typescript', getFilteredLoader( 'ts' ) ), + ...ifInstalled( 'typescript', loaders.ts() ), // Process JS with Babel. - getFilteredLoader( 'js' ), + loaders.js(), // Handle static asset files. - getFilteredLoader( 'assets' ), + loaders.assets(), // Parse styles using SASS, then PostCSS. - filterLoaders( { + applyFilters( 'preset/stylesheet-loaders',applyFilters( 'preset/prod/stylesheet-loaders', { test: /\.s?css$/, use: [ // Extract CSS to its own file. MiniCssExtractPlugin.loader, // Process SASS into CSS. - getFilteredLoader( 'css', cssOptions ), - getFilteredLoader( 'postcss', cssOptions ), - getFilteredLoader( 'sass', cssOptions ), + loaders.css( cssOptions ), + loaders.postcss( cssOptions ), + loaders.sass( cssOptions ), ], - }, 'stylesheet' ), + } ) ), // Resource loader makes sure any non-matching assets still get served. // When you `import` an asset, you get its (virtual) filename. - getFilteredLoader( 'resource' ), + loaders.resource(), ], }, ], diff --git a/src/presets.test.js b/src/presets.test.js index 2392a71..81217f1 100644 --- a/src/presets.test.js +++ b/src/presets.test.js @@ -3,6 +3,7 @@ const { production, } = require( './presets' ); const plugins = require( './plugins' ); +const { addFilter } = require( './helpers/filters' ); jest.mock( 'process', () => ( { cwd: () => 'cwd', @@ -259,20 +260,20 @@ describe( 'presets', () => { } ); it( 'permits filtering the computed output of individual loaders', () => { + addFilter( 'loader/assets', ( loader ) => { + loader.test = /\.(png|jpg|jpeg|gif|svg)$/; + return loader; + } ); + addFilter( 'loader/resource', ( loader ) => { + loader.options = { + publicPath: '../../', + }; + return loader; + } ); const config = development( { entry: { main: 'some-file.js', }, - }, { - filterLoaders: ( loader, loaderType ) => { - if ( loaderType === 'file' ) { - loader.options.publicPath = '../../'; - } - if ( loaderType === 'url' ) { - loader.test = /\.(png|jpg|jpeg|gif|svg)$/; - } - return loader; - }, } ); const resourceLoader = getLoaderByName( config.module.rules, 'asset/resource' ); const assetsLoader = getLoaderByName( config.module.rules, 'asset' ); @@ -280,9 +281,12 @@ describe( 'presets', () => { expect( resourceLoader ).toEqual( expect.objectContaining( { exclude: [ /^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/ ], type: 'asset/resource', + options: { + publicPath: '../../', + }, } ) ); expect( assetsLoader ).toEqual( expect.objectContaining( { - test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|eot|ttf)$/, + test: /\.(png|jpg|jpeg|gif|svg)$/, type: 'asset', parser: { dataUrlCondition: { @@ -294,23 +298,18 @@ describe( 'presets', () => { } ); it( 'permits filtering the entire stylesheet loader chain', () => { + addFilter( 'preset/dev/stylesheet-loaders', ( loader ) => { + loader.test = /\.styl$/; + return loader; + } ); + addFilter( 'loader/sass', () => ( { + loader: 'stylus', + mode: 'development', + } ) ); const config = development( { entry: { main: 'some-file.js', }, - }, { - filterLoaders: ( loader, loaderType ) => { - if ( loaderType === 'stylesheet' ) { - loader.test = /\.styl$/; - } - if ( loaderType === 'sass' ) { - return { - loader: 'stylus', - mode: 'development', - }; - } - return loader; - }, } ); const styleChain = getLoaderByTest( config.module.rules, /\.styl$/ ); expect( styleChain ).toEqual( { @@ -469,20 +468,20 @@ describe( 'presets', () => { } ); it( 'permits filtering the computed output of individual loaders', () => { + addFilter( 'loader/assets', ( loader ) => { + loader.test = /\.(png|jpg|jpeg|gif|svg)$/; + return loader; + } ); + addFilter( 'loader/resource', ( loader ) => { + loader.options = { + publicPath: '../../', + }; + return loader; + } ); const config = production( { entry: { main: 'some-file.js', }, - }, { - filterLoaders: ( loader, loaderType ) => { - if ( loaderType === 'file' ) { - loader.options.publicPath = '../../'; - } - if ( loaderType === 'url' ) { - loader.test = /\.(png|jpg|jpeg|gif|svg)$/; - } - return loader; - }, } ); const resourceLoader = getLoaderByName( config.module.rules, 'asset/resource' ); const assetsLoader = getLoaderByName( config.module.rules, 'asset' ); @@ -490,9 +489,12 @@ describe( 'presets', () => { expect( resourceLoader ).toEqual( expect.objectContaining( { exclude: [ /^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/ ], type: 'asset/resource', + options: { + publicPath: '../../', + }, } ) ); expect( assetsLoader ).toEqual( expect.objectContaining( { - test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|eot|ttf)$/, + test: /\.(png|jpg|jpeg|gif|svg)$/, type: 'asset', parser: { dataUrlCondition: { @@ -504,22 +506,17 @@ describe( 'presets', () => { } ); it( 'permits filtering the entire stylesheet loader chain', () => { + addFilter( 'preset/prod/stylesheet-loaders', ( loader ) => { + loader.test = /\.styl$/; + return loader; + } ); + addFilter( 'loader/sass', () => ( { + loader: 'stylus', + } ) ); const config = production( { entry: { main: 'some-file.js', }, - }, { - filterLoaders: ( loader, loaderType ) => { - if ( loaderType === 'stylesheet' ) { - loader.test = /\.styl$/; - } - if ( loaderType === 'sass' ) { - return { - loader: 'stylus', - }; - } - return loader; - }, } ); const styleChain = getLoaderByTest( config.module.rules, /\.styl$/ ); expect( styleChain ).toEqual( { From 4a7a5a2fd796c2e8f273e1610a10e408148e8390 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 29 Apr 2022 09:38:11 -0400 Subject: [PATCH 14/83] Remove support for Node 10 --- .travis.yml | 5 +++-- docs/changelog.md | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 88bcb31..175810c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,12 @@ language: node_js script: "npm run lint && npm run test && npm run test-build" node_js: - - node - lts/* + - 16 + - 14 - 12 - - 10 branches: only: - main - beta + - v1 diff --git a/docs/changelog.md b/docs/changelog.md index 8a3010a..a3011f8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -8,6 +8,7 @@ nav_order: 10 ## v1.0 +- **Breaking**: End support for Node v10. Node v12.13 or later is now required. - **Breaking**: Switch to Webpack 5 and Webpack DevServer 4 - **Breaking**: Replace `filterLoaders` system with [individual hooks accesible via the new `addFilter` helper](https://humanmade.github.io/webpack-helpers/modules/presets.html#customizing-presets). - **Potentially Breaking**: Remove `loaders.url()` and `loaders.file()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` (for assets which can be inlined) and `loaders.resource()` (as a catch-all for other types) in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` or `loaders.file()` was used directly. From f1a4646f6487205b182dbfb8a817b646a8a6dacc Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 29 Apr 2022 09:41:24 -0400 Subject: [PATCH 15/83] Set publicPath to "" to avoid issues with "auto" See https://webpack.js.org/migrate/5/\#run-a-single-build-and-follow-advice --- src/presets.js | 2 ++ src/presets.test.js | 1 + 2 files changed, 3 insertions(+) diff --git a/src/presets.js b/src/presets.js index 9adc526..b83cf39 100644 --- a/src/presets.js +++ b/src/presets.js @@ -232,6 +232,8 @@ const production = ( config = {} ) => { // Inject a default entry point later on if none was specified. output: { + // Webpack 5 defaults "publicPath" to "auto," which we are not set up to handle. + publicPath: '', // Provide a default output path. path: filePath( 'build' ), pathinfo: false, diff --git a/src/presets.test.js b/src/presets.test.js index 81217f1..0c8b0f3 100644 --- a/src/presets.test.js +++ b/src/presets.test.js @@ -344,6 +344,7 @@ describe( 'presets', () => { filename: '[name].js', chunkFilename: '[name].[contenthash].chunk.js', path: 'build/', + publicPath: '', } ); } ); From 7755c07d40a652b3fa6744cbb06e5b68cd33d977 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 29 Apr 2022 09:41:48 -0400 Subject: [PATCH 16/83] Tag v1.0.0-beta.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1f9ec4a..d2d38e9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "1.0.0-beta.0", + "version": "1.0.0-beta.1", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made", From e702595d180be74a63b1890bbf5a68221d37c860 Mon Sep 17 00:00:00 2001 From: K Adam White Date: Fri, 29 Apr 2022 10:56:18 -0400 Subject: [PATCH 17/83] Update src/loaders.js Co-authored-by: Thorsten Frommen --- src/loaders.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/loaders.js b/src/loaders.js index 4f1afb5..53c8770 100644 --- a/src/loaders.js +++ b/src/loaders.js @@ -121,7 +121,7 @@ loaders.resource.defaults = { // its runtime that would otherwise be processed through "file" loader. // Also exclude `html` and `json` extensions so they get processed // by webpacks internal loaders. - exclude: [ /^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/ ], + exclude: [ /^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html?$/, /\.json$/ ], type: 'asset/resource', }; From 5e4d73b54427a90740cc20dfbfb781fbb95496f7 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 29 Apr 2022 11:32:19 -0400 Subject: [PATCH 18/83] Hash filenames by default --- docs/changelog.md | 2 +- src/presets.js | 8 ++++---- src/presets.test.js | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index a3011f8..f42effa 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,9 +10,9 @@ nav_order: 10 - **Breaking**: End support for Node v10. Node v12.13 or later is now required. - **Breaking**: Switch to Webpack 5 and Webpack DevServer 4 +- **Breaking**: Include a contenthash string in default bundle file names. Set `filename: '[name].js'` in your output configuration to restore the old behavior. - **Breaking**: Replace `filterLoaders` system with [individual hooks accesible via the new `addFilter` helper](https://humanmade.github.io/webpack-helpers/modules/presets.html#customizing-presets). - **Potentially Breaking**: Remove `loaders.url()` and `loaders.file()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` (for assets which can be inlined) and `loaders.resource()` (as a catch-all for other types) in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` or `loaders.file()` was used directly. -- **Potentially Breaking**: Remove `loaders.url()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` was used directly. - Remove `OptimizeCssAssetsPlugin` (`plugins.optimizeCssAssets()`) in favor of Webpack 5-compatible [CssMinimizerPlugin](https://github.com/webpack-contrib/css-minimizer-webpack-plugin) (`plugins.cssMinimizer()`) - Remove `plugins.hotModuleReplacement()`, which is now handled automatically by the DevServer in `hot` mode. - Include the `contenthash` in generated CSS filenames. [#204](https://github.com/humanmade/webpack-helpers/pull/204) diff --git a/src/presets.js b/src/presets.js index b83cf39..36b4e5b 100644 --- a/src/presets.js +++ b/src/presets.js @@ -88,9 +88,9 @@ const development = ( config = {} ) => { // Add /* filename */ comments to generated require()s in the output. pathinfo: true, // Provide a default output name. - filename: '[name].js', + filename: '[name].[contenthash:8].js', // Provide chunk filename. Requires content hash for cache busting. - chunkFilename: '[name].[contenthash].chunk.js', + chunkFilename: '[name].[contenthash:8].chunk.js', // `publicPath` will be inferred as a localhost URL based on output.path // when a devServer.port value is available. }, @@ -238,9 +238,9 @@ const production = ( config = {} ) => { path: filePath( 'build' ), pathinfo: false, // Provide a default output name. - filename: '[name].js', + filename: '[name].[contenthash:8].js', // Provide chunk filename. Requires content hash for cache busting. - chunkFilename: '[name].[contenthash].chunk.js', + chunkFilename: '[name].[contenthash:8].chunk.js', }, module: { diff --git a/src/presets.test.js b/src/presets.test.js index 0c8b0f3..6679c0e 100644 --- a/src/presets.test.js +++ b/src/presets.test.js @@ -84,8 +84,8 @@ describe( 'presets', () => { expect( config.entry ).toEqual( 'some-file.js' ); expect( config.output ).toEqual( { pathinfo: true, - filename: '[name].js', - chunkFilename: '[name].[contenthash].chunk.js', + filename: '[name].[contenthash:8].js', + chunkFilename: '[name].[contenthash:8].chunk.js', path: 'build/', } ); } ); @@ -341,8 +341,8 @@ describe( 'presets', () => { expect( config.entry ).toEqual( 'some-file.js' ); expect( config.output ).toEqual( { pathinfo: false, - filename: '[name].js', - chunkFilename: '[name].[contenthash].chunk.js', + filename: '[name].[contenthash:8].js', + chunkFilename: '[name].[contenthash:8].chunk.js', path: 'build/', publicPath: '', } ); From a08e8a03ba6b0c92ba894a60c8dc0d9a0d4e8b57 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 29 Apr 2022 11:33:05 -0400 Subject: [PATCH 19/83] Limit CSS filename hash to 8 characters --- src/plugins.js | 2 +- src/presets.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins.js b/src/plugins.js index 112ee19..73bdfdd 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -141,7 +141,7 @@ module.exports = { * @returns {MiniCssExtractPlugin} A configured MiniCssExtractPlugin instance. */ miniCssExtract: ( options = {} ) => new MiniCssExtractPlugin( { - filename: '[name].[contenthash].css', + filename: '[name].[contenthash:8].css', ...options, } ), diff --git a/src/presets.test.js b/src/presets.test.js index 6679c0e..47d5126 100644 --- a/src/presets.test.js +++ b/src/presets.test.js @@ -377,7 +377,7 @@ describe( 'presets', () => { ] ) ); const cssPlugins = config.plugins.filter( filterPlugins( MiniCssExtractPlugin ) ); expect( cssPlugins.length ).toBe( 1 ); - expect( cssPlugins[ 0 ].options.filename ).toEqual( '[name].[contenthash].css' ); + expect( cssPlugins[ 0 ].options.filename ).toEqual( '[name].[contenthash:8].css' ); } ); it( 'does not override or duplicate existing MiniCssExtractPlugin instances', () => { From f70f1240ba3df813243e75c848285a8b31c02986 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 29 Apr 2022 11:42:19 -0400 Subject: [PATCH 20/83] Fix test to expect more flexible html regex --- src/presets.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/presets.test.js b/src/presets.test.js index 47d5126..4481b6e 100644 --- a/src/presets.test.js +++ b/src/presets.test.js @@ -279,7 +279,7 @@ describe( 'presets', () => { const assetsLoader = getLoaderByName( config.module.rules, 'asset' ); const jsLoader = getLoaderByName( config.module.rules, 'babel-loader' ); expect( resourceLoader ).toEqual( expect.objectContaining( { - exclude: [ /^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/ ], + exclude: [ /^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html?$/, /\.json$/ ], type: 'asset/resource', options: { publicPath: '../../', @@ -488,7 +488,7 @@ describe( 'presets', () => { const assetsLoader = getLoaderByName( config.module.rules, 'asset' ); const jsLoader = getLoaderByName( config.module.rules, 'babel-loader' ); expect( resourceLoader ).toEqual( expect.objectContaining( { - exclude: [ /^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/ ], + exclude: [ /^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html?$/, /\.json$/ ], type: 'asset/resource', options: { publicPath: '../../', From b876ecb5a01dcfddeed56c04b23eeba39cecabca Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 29 Apr 2022 11:40:35 -0400 Subject: [PATCH 21/83] Consolidate filters used for presets, props @tfrommen --- docs/modules/presets.md | 4 +-- src/presets.js | 75 +++++++++++++++++++++++------------------ src/presets.test.js | 4 +-- 3 files changed, 45 insertions(+), 38 deletions(-) diff --git a/docs/modules/presets.md b/docs/modules/presets.md index ed4747a..aa9af16 100644 --- a/docs/modules/presets.md +++ b/docs/modules/presets.md @@ -139,6 +139,4 @@ Available hooks: - `loader/{loader name}/default`: Alter the default values before passing it to the loader configuration merge function. - `loader/postcss/plugins`: Filter the list of PostCSS plugins used by that specific loader. - `loader/postcss/preset-env`: Filter the configuration object passed to the PostCSS Preset Env plugin. -- `preset/stylesheet-loaders`: Filter the computed chain of stylesheet loaders for both dev and production presets. -- `preset/dev/stylesheet-loaders`: Filter the stylesheet loader chain for only the dev configuration. -- `preset/prod/stylesheet-loaders`: Filter the stylesheet loader chain for only the production configuration. +- `preset/stylesheet-loaders`: Filter the computed chain of stylesheet loaders output by the preset factories. This hook receives a second argument `environment` which will show either "production" or "development," to permit per-environment filtering. diff --git a/src/presets.js b/src/presets.js index 36b4e5b..7866142 100644 --- a/src/presets.js +++ b/src/presets.js @@ -118,28 +118,32 @@ const development = ( config = {} ) => { // Handle static asset files. loaders.assets(), // Parse styles using SASS, then PostCSS. - // Permit filtering either on an env-specific or global basis. - applyFilters( 'preset/stylesheet-loaders', applyFilters( 'preset/dev/stylesheet-loaders', { - test: /\.s?css$/, - use: [ - loaders.style(), - loaders.css( { - options: { - sourceMap: true, - }, - } ), - loaders.postcss( { - options: { - sourceMap: true, - }, - } ), - loaders.sass( { - options: { - sourceMap: true, - }, - } ), - ], - } ) ), + // Pass environment name as second parameter to give flexibility when filtering. + applyFilters( + 'preset/stylesheet-loaders', + { + test: /\.s?css$/, + use: [ + loaders.style(), + loaders.css( { + options: { + sourceMap: true, + }, + } ), + loaders.postcss( { + options: { + sourceMap: true, + }, + } ), + loaders.sass( { + options: { + sourceMap: true, + }, + } ), + ], + }, + 'development' + ), // Resource loader makes sure any non-matching assets still get served. // When you `import` an asset, you get its (virtual) filename. loaders.resource(), @@ -260,17 +264,22 @@ const production = ( config = {} ) => { // Handle static asset files. loaders.assets(), // Parse styles using SASS, then PostCSS. - applyFilters( 'preset/stylesheet-loaders',applyFilters( 'preset/prod/stylesheet-loaders', { - test: /\.s?css$/, - use: [ - // Extract CSS to its own file. - MiniCssExtractPlugin.loader, - // Process SASS into CSS. - loaders.css( cssOptions ), - loaders.postcss( cssOptions ), - loaders.sass( cssOptions ), - ], - } ) ), + // Pass environment name as second parameter to give flexibility when filtering. + applyFilters( + 'preset/stylesheet-loaders', + { + test: /\.s?css$/, + use: [ + // Extract CSS to its own file. + MiniCssExtractPlugin.loader, + // Process SASS into CSS. + loaders.css( cssOptions ), + loaders.postcss( cssOptions ), + loaders.sass( cssOptions ), + ], + }, + 'production' + ), // Resource loader makes sure any non-matching assets still get served. // When you `import` an asset, you get its (virtual) filename. loaders.resource(), diff --git a/src/presets.test.js b/src/presets.test.js index 4481b6e..fd00927 100644 --- a/src/presets.test.js +++ b/src/presets.test.js @@ -298,7 +298,7 @@ describe( 'presets', () => { } ); it( 'permits filtering the entire stylesheet loader chain', () => { - addFilter( 'preset/dev/stylesheet-loaders', ( loader ) => { + addFilter( 'preset/stylesheet-loaders', ( loader ) => { loader.test = /\.styl$/; return loader; } ); @@ -507,7 +507,7 @@ describe( 'presets', () => { } ); it( 'permits filtering the entire stylesheet loader chain', () => { - addFilter( 'preset/prod/stylesheet-loaders', ( loader ) => { + addFilter( 'preset/stylesheet-loaders', ( loader ) => { loader.test = /\.styl$/; return loader; } ); From 1bd56aa22024bb59494b94bfd75b002770ed1a96 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 29 Apr 2022 11:47:42 -0400 Subject: [PATCH 22/83] Tag v1.0.0-beta.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d2d38e9..3621613 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "1.0.0-beta.1", + "version": "1.0.0-beta.2", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made", From 6432ae96b036156f79f0628099c911a4916cfb46 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 29 Apr 2022 12:23:04 -0400 Subject: [PATCH 23/83] Use "development-asset-manifest.json" in dev mode for clarity --- src/presets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/presets.js b/src/presets.js index 7866142..029b24a 100644 --- a/src/presets.js +++ b/src/presets.js @@ -189,7 +189,7 @@ const development = ( config = {} ) => { if ( ! hasManifestPlugin ) { const outputPath = ( config.output && config.output.path ) || devDefaults.output.path; devDefaults.plugins.push( plugins.manifest( { - fileName: 'asset-manifest.json', + fileName: 'development-asset-manifest.json', seed: getSeedByDirectory( outputPath ), } ) ); } From 96cbc41b02bea96353eb2cf6dc5b3273a1411304 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 29 Apr 2022 12:23:19 -0400 Subject: [PATCH 24/83] Tag v1.0.0-beta.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3621613..c888fac 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "1.0.0-beta.2", + "version": "1.0.0-beta.3", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made", From cc71e96e03789da8f9fe0cbe47fd8bed58e159fd Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 29 Apr 2022 12:25:52 -0400 Subject: [PATCH 25/83] Document manifest name change --- docs/changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.md b/docs/changelog.md index f42effa..982afa5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -11,6 +11,7 @@ nav_order: 10 - **Breaking**: End support for Node v10. Node v12.13 or later is now required. - **Breaking**: Switch to Webpack 5 and Webpack DevServer 4 - **Breaking**: Include a contenthash string in default bundle file names. Set `filename: '[name].js'` in your output configuration to restore the old behavior. +- **Breaking**: Default DevServer manifest name is now `development-asset-manifest.json`, not `asset-manifest.json`. - **Breaking**: Replace `filterLoaders` system with [individual hooks accesible via the new `addFilter` helper](https://humanmade.github.io/webpack-helpers/modules/presets.html#customizing-presets). - **Potentially Breaking**: Remove `loaders.url()` and `loaders.file()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` (for assets which can be inlined) and `loaders.resource()` (as a catch-all for other types) in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` or `loaders.file()` was used directly. - Remove `OptimizeCssAssetsPlugin` (`plugins.optimizeCssAssets()`) in favor of Webpack 5-compatible [CssMinimizerPlugin](https://github.com/webpack-contrib/css-minimizer-webpack-plugin) (`plugins.cssMinimizer()`) From 02d7b13ea19f1dca1a1f27643bbc54b7c72449ab Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 29 Apr 2022 12:28:03 -0400 Subject: [PATCH 26/83] Fix tests after beta.3 change and improve manifest documentation --- docs/guides/getting-started.md | 6 +++--- docs/modules/helpers.md | 6 +++--- src/presets.test.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index 506d6c5..3aca816 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -264,7 +264,7 @@ To make use of this utility, we wrap our development config in a `choosePort` pr +] ); ``` -While these bundles are served from memory, now that we've specified a `publicPath`, a new file `asset-manifest.json` will be output into each project's `build/` folder. `themes/myproject/build/asset-manifest.json` will look like this: +While these bundles are served from memory, now that we've specified a `publicPath`, a new file `development-asset-manifest.json` will be output into each project's `build/` folder. `themes/myproject/build/development-asset-manifest.json` will look like this: ```json { "frontend.js": "http://localhost:8080/myproject-theme/frontend.js", @@ -286,8 +286,8 @@ We use the `cleanOnExit` helper to delete these files when the server shuts down + +// Clean up manifests on exit. +cleanOnExit( [ -+ filePath( 'mu-plugins/myproject-blocks/build/asset-manifest.json' ), -+ filePath( 'themes/myproject/build/asset-manifest.json' ), ++ filePath( 'mu-plugins/myproject-blocks/build/development-asset-manifest.json' ), ++ filePath( 'themes/myproject/build/development-asset-manifest.json' ), +] ); ``` diff --git a/docs/modules/helpers.md b/docs/modules/helpers.md index b0b0afc..73bfc73 100644 --- a/docs/modules/helpers.md +++ b/docs/modules/helpers.md @@ -33,15 +33,15 @@ module.exports = presets.production( { ## `cleanOnExit` -When using the `presets.development()` generator, an `asset-manifest.json` will automatically be generated so long as a `publicPath` URI can be determined. When working with an `asset-manifest.json` file, the `manifest` module provides a `cleanOnExit` method to easily remove manifests once the `webpack-dev-server` shuts down. +When using the `presets.development()` generator, a `development-asset-manifest.json` will automatically be generated so long as a `publicPath` URI can be determined. When working with a `development-asset-manifest.json` file, the `manifest` module provides a `cleanOnExit` method to easily remove manifests once the `webpack-dev-server` shuts down. ```js const { join } = require( 'path' ); const { helpers } = require( '@humanmade/webpack-helpers' ); helpers.cleanOnExit( [ - join( process.cwd(), 'content/mu-plugins/custom-blocks/build/asset-manifest.json' ), - join( process.cwd(), 'content/themes/my-theme/build/asset-manifest.json' ), + join( process.cwd(), 'content/mu-plugins/custom-blocks/build/development-asset-manifest.json' ), + join( process.cwd(), 'content/themes/my-theme/build/development-asset-manifest.json' ), ] ); ``` diff --git a/src/presets.test.js b/src/presets.test.js index fd00927..f99b9ed 100644 --- a/src/presets.test.js +++ b/src/presets.test.js @@ -173,7 +173,7 @@ describe( 'presets', () => { ] ) ); const manifestPlugins = config.plugins.filter( filterPlugins( ManifestPlugin ) ); expect( manifestPlugins.length ).toBe( 1 ); - expect( manifestPlugins[ 0 ].options.fileName ).toEqual( 'asset-manifest.json' ); + expect( manifestPlugins[ 0 ].options.fileName ).toEqual( 'development-asset-manifest.json' ); } ); it( 'does not inject a ManifestPlugin if publicPath cannot be inferred', () => { From 1173405a181438d03281c5655de1dda7a8fd47e8 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Tue, 17 May 2022 10:17:08 -0400 Subject: [PATCH 27/83] Ignore DS_Store files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 6a28462..77e8e70 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules *.log package-lock.json test/build + +.DS_Store From c30ef09cd8344fb34cd92ae472217d670f4dcbe9 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Tue, 17 May 2022 10:34:41 -0400 Subject: [PATCH 28/83] Allow loader to be skipped when rendering preset by filtering it to null --- docs/changelog.md | 1 + src/presets.js | 38 +++++++++++++++-- src/presets.test.js | 101 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 135 insertions(+), 5 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 982afa5..9abc27f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -14,6 +14,7 @@ nav_order: 10 - **Breaking**: Default DevServer manifest name is now `development-asset-manifest.json`, not `asset-manifest.json`. - **Breaking**: Replace `filterLoaders` system with [individual hooks accesible via the new `addFilter` helper](https://humanmade.github.io/webpack-helpers/modules/presets.html#customizing-presets). - **Potentially Breaking**: Remove `loaders.url()` and `loaders.file()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` (for assets which can be inlined) and `loaders.resource()` (as a catch-all for other types) in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` or `loaders.file()` was used directly. +- Allow `null` to be returned from an `addFilter` callback to skip a filter when using a configuration preset. - Remove `OptimizeCssAssetsPlugin` (`plugins.optimizeCssAssets()`) in favor of Webpack 5-compatible [CssMinimizerPlugin](https://github.com/webpack-contrib/css-minimizer-webpack-plugin) (`plugins.cssMinimizer()`) - Remove `plugins.hotModuleReplacement()`, which is now handled automatically by the DevServer in `hot` mode. - Include the `contenthash` in generated CSS filenames. [#204](https://github.com/humanmade/webpack-helpers/pull/204) diff --git a/src/presets.js b/src/presets.js index 029b24a..9d4568c 100644 --- a/src/presets.js +++ b/src/presets.js @@ -50,6 +50,36 @@ const ifInstalled = ( packageName, loader ) => { return [ loader ]; }; +/** + * Remove null entries from Webpack loaders array, in case a user returned null + * from a filter to opt out of a given loader in the preset. + * + * Detects and removes null entries from nested .oneOf or .use arrays. + * + * @param {Object[]} moduleRules Array of Webpack loader rules. + * + * @returns {Object[]} Filtered array with null items removed. + */ +const removeNullLoaders = ( moduleRules ) => { + return moduleRules + .map( ( rule ) => { + if ( rule && Array.isArray( rule.oneOf ) ) { + return { + ...rule, + oneOf: removeNullLoaders( rule.oneOf ), + }; + } + if ( rule && Array.isArray( rule.use ) ) { + return { + ...rule, + use: removeNullLoaders( rule.use ), + }; + } + return rule; + } ) + .filter( Boolean ); +}; + /** * Promote a partial Webpack config into a full development-oriented configuration. * @@ -97,7 +127,7 @@ const development = ( config = {} ) => { module: { strictExportPresence: true, - rules: [ + rules: removeNullLoaders( [ // Handle node_modules packages that contain sourcemaps. loaders.sourcemaps(), // Run all JS files through ESLint, if installed. @@ -149,7 +179,7 @@ const development = ( config = {} ) => { loaders.resource(), ], }, - ], + ] ), }, optimization: { @@ -249,7 +279,7 @@ const production = ( config = {} ) => { module: { strictExportPresence: true, - rules: [ + rules: removeNullLoaders( [ // Run all JS files through ESLint, if installed. ...ifInstalled( 'eslint', loaders.eslint() ), { @@ -285,7 +315,7 @@ const production = ( config = {} ) => { loaders.resource(), ], }, - ], + ] ), }, optimization: { diff --git a/src/presets.test.js b/src/presets.test.js index f99b9ed..b812538 100644 --- a/src/presets.test.js +++ b/src/presets.test.js @@ -3,13 +3,20 @@ const { production, } = require( './presets' ); const plugins = require( './plugins' ); -const { addFilter } = require( './helpers/filters' ); +const { addFilter, setupRegistry } = require( './helpers/filters' ); jest.mock( 'process', () => ( { cwd: () => 'cwd', versions: {}, } ) ); +/** + * Helper function to return null, used when testing filters. + * + * @returns {null} Null. + */ +const returnNull = () => null; + /** * Filter an array of plugins to only contain plugins of the provided type. * @@ -69,6 +76,10 @@ const getLoaderByTest = ( rules, loaderTest ) => { }; describe( 'presets', () => { + beforeEach( () => { + setupRegistry(); + } ); + describe( 'development()', () => { it( 'is a function', () => { expect( development ).toBeInstanceOf( Function ); @@ -324,6 +335,52 @@ describe( 'presets', () => { const sassLoader = getLoaderByTest( config.module.rules, /\.s?css$/ ); expect( sassLoader ).toBeNull(); } ); + + it( 'permits skipping a specific stylesheet loader by filtering it to null', () => { + addFilter( 'loader/postcss', returnNull ); + addFilter( 'loader/sass', returnNull ); + const config = development( { + entry: { + main: 'some-file.js', + }, + } ); + const styleChain = getLoaderByTest( config.module.rules, /\.s?css$/ ); + expect( styleChain ).toEqual( { + test: /\.s?css$/, + use: [ + { + loader: require.resolve( 'style-loader' ), + options: {}, + }, + { + loader: require.resolve( 'css-loader' ), + options: { + importLoaders: 1, + sourceMap: true, + }, + }, + ], + } ); + } ); + + it( 'does not include null loader entries if a loader was disabled with a filter', () => { + addFilter( 'preset/stylesheet-loaders', returnNull ); + addFilter( 'loader/ts', returnNull ); + addFilter( 'loader/eslint', returnNull ); + const config = development( { + entry: { + main: 'some-file.js', + }, + } ); + // Should have stripped out the null entries. + expect( config.module.rules[ 1 ].oneOf.length ).toBe( 3 ); + expect( config.module.rules[ 1 ].oneOf[ 0 ] ) + .toEqual( expect.objectContaining( { test: /\.jsx?$/ } ) ); + expect( config.module.rules[ 1 ].oneOf[ 1 ] ) + .toEqual( expect.objectContaining( { type: 'asset' } ) ); + expect( config.module.rules[ 1 ].oneOf[ 2 ] ) + .toEqual( expect.objectContaining( { type: 'asset/resource' } ) ); + } ); } ); describe( 'production()', () => { @@ -531,5 +588,47 @@ describe( 'presets', () => { const sassLoader = getLoaderByTest( config.module.rules, /\.s?css$/ ); expect( sassLoader ).toBeNull(); } ); + + it( 'permits skipping a specific stylesheet loader by filtering it to null', () => { + addFilter( 'loader/postcss', returnNull ); + addFilter( 'loader/sass', returnNull ); + const config = production( { + entry: { + main: 'some-file.js', + }, + } ); + const styleChain = getLoaderByTest( config.module.rules, /\.s?css$/ ); + expect( styleChain ).toEqual( { + test: /\.s?css$/, + use: [ + plugins.constructors.MiniCssExtractPlugin.loader, + { + loader: require.resolve( 'css-loader' ), + options: { + importLoaders: 1, + }, + }, + ], + } ); + } ); + + it( 'does not include null loader entries if a loader was disabled with a filter', () => { + addFilter( 'preset/stylesheet-loaders', returnNull ); + addFilter( 'loader/ts', returnNull ); + addFilter( 'loader/eslint', returnNull ); + const config = production( { + entry: { + main: 'some-file.js', + }, + } ); + // Should have stripped out the null entries. + expect( config.module.rules[ 0 ].oneOf.length ).toBe( 3 ); + expect( config.module.rules[ 0 ].oneOf[ 0 ] ) + .toEqual( expect.objectContaining( { test: /\.jsx?$/ } ) ); + expect( config.module.rules[ 0 ].oneOf[ 1 ] ) + .toEqual( expect.objectContaining( { type: 'asset' } ) ); + expect( config.module.rules[ 0 ].oneOf[ 2 ] ) + .toEqual( expect.objectContaining( { type: 'asset/resource' } ) ); + } ); } ); } ); From f5d7c102f4d1fc8240a5af4191e4dfde248a0077 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Tue, 17 May 2022 10:35:11 -0400 Subject: [PATCH 29/83] Tag v1.0.0-beta.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c888fac..ef8eed7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "1.0.0-beta.3", + "version": "1.0.0-beta.4", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made", From 1645bbc313ced2c05fd25e5166e03e43c4bbe6af Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Tue, 17 May 2022 17:32:17 -0400 Subject: [PATCH 30/83] Detect HTTPS when using the devServer.server flag (devServer.https is deprecated) --- src/helpers/infer-public-path.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/infer-public-path.js b/src/helpers/infer-public-path.js index e60f024..47cc282 100644 --- a/src/helpers/infer-public-path.js +++ b/src/helpers/infer-public-path.js @@ -10,7 +10,7 @@ const findInObject = require( './find-in-object' ); * @return {String} Public path. */ const inferPublicPath = ( config, port, defaults = {} ) => { - const protocol = findInObject( config, 'devServer.https' ) ? 'https' : 'http'; + const protocol = ( findInObject( config, 'devServer.https' ) || findInObject( config, 'devServer.server' ) === 'https' ) ? 'https' : 'http'; const outputPath = findInObject( config, 'output.path' ) || findInObject( defaults, 'output.path' ); From 9dbc21801279c3c3ed07d3c624f922e63e944167 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Tue, 17 May 2022 17:46:16 -0400 Subject: [PATCH 31/83] Simplify function body of removeNullLoaders helper --- src/presets.js | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/presets.js b/src/presets.js index 9d4568c..493cf32 100644 --- a/src/presets.js +++ b/src/presets.js @@ -60,25 +60,23 @@ const ifInstalled = ( packageName, loader ) => { * * @returns {Object[]} Filtered array with null items removed. */ -const removeNullLoaders = ( moduleRules ) => { - return moduleRules - .map( ( rule ) => { - if ( rule && Array.isArray( rule.oneOf ) ) { - return { - ...rule, - oneOf: removeNullLoaders( rule.oneOf ), - }; - } - if ( rule && Array.isArray( rule.use ) ) { - return { - ...rule, - use: removeNullLoaders( rule.use ), - }; - } - return rule; - } ) - .filter( Boolean ); -}; +const removeNullLoaders = ( moduleRules ) => moduleRules + .map( ( rule ) => { + if ( rule && Array.isArray( rule.oneOf ) ) { + return { + ...rule, + oneOf: removeNullLoaders( rule.oneOf ), + }; + } + if ( rule && Array.isArray( rule.use ) ) { + return { + ...rule, + use: removeNullLoaders( rule.use ), + }; + } + return rule; + } ) + .filter( Boolean ); /** * Promote a partial Webpack config into a full development-oriented configuration. From 0e48396b9926713a5d37404880cdf9e5931583b6 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Tue, 17 May 2022 17:50:08 -0400 Subject: [PATCH 32/83] Update tests to use devServer.server:"https" instead of devServer.https:true --- src/helpers/with-dynamic-port.test.js | 4 ++-- src/presets.test.js | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/helpers/with-dynamic-port.test.js b/src/helpers/with-dynamic-port.test.js index 7955c63..68033ba 100644 --- a/src/helpers/with-dynamic-port.test.js +++ b/src/helpers/with-dynamic-port.test.js @@ -151,7 +151,7 @@ describe( 'withDynamicPort', () => { it( 'does not overwrite existing config properties', async () => { const config = { devServer: { - https: true, + server: 'https', }, entry: { 'name': './bundle.js', @@ -167,7 +167,7 @@ describe( 'withDynamicPort', () => { choosePort.mockImplementationOnce( () => Promise.resolve( 8082 ) ); expect( await withDynamicPort( 9090, config ) ).toEqual( { devServer: { - https: true, + server: 'https', port: 8082, }, entry: { diff --git a/src/presets.test.js b/src/presets.test.js index b812538..8b80c84 100644 --- a/src/presets.test.js +++ b/src/presets.test.js @@ -128,7 +128,18 @@ describe( 'presets', () => { expect( config.output.publicPath ).toBe( 'http://localhost:9090/build/' ); } ); - it( 'accounts for the value of devServer.https when inferring publicPath URI', () => { + it( 'accounts for the value of devServer.server when inferring publicPath URI', () => { + const config = development( { + devServer: { + server: 'https', + port: 9090, + }, + entry: 'some-file.js', + } ); + expect( config.output.publicPath ).toBe( 'https://localhost:9090/build/' ); + } ); + + it( 'accounts for the value of devServer.https (deprecated in favor of .server) when inferring publicPath URI', () => { const config = development( { devServer: { https: true, From 1d59d74230b56da57638e86e7eb71ff29c5dfa50 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Tue, 17 May 2022 17:51:53 -0400 Subject: [PATCH 33/83] Tag v1.0.0-beta.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ef8eed7..21fc8e9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "1.0.0-beta.4", + "version": "1.0.0-beta.5", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made", From ef6449fe803b8fdbde15387d75cddaf333f4d860 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Wed, 18 May 2022 13:57:29 -0400 Subject: [PATCH 34/83] Expose removeFilter in addition to addFilter --- docs/changelog.md | 2 +- index.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 9abc27f..911ac3e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -12,7 +12,7 @@ nav_order: 10 - **Breaking**: Switch to Webpack 5 and Webpack DevServer 4 - **Breaking**: Include a contenthash string in default bundle file names. Set `filename: '[name].js'` in your output configuration to restore the old behavior. - **Breaking**: Default DevServer manifest name is now `development-asset-manifest.json`, not `asset-manifest.json`. -- **Breaking**: Replace `filterLoaders` system with [individual hooks accesible via the new `addFilter` helper](https://humanmade.github.io/webpack-helpers/modules/presets.html#customizing-presets). +- **Breaking**: Replace `filterLoaders` system with [individual hooks accesible via the new `addFilter` and `removeFilter` helpers](https://humanmade.github.io/webpack-helpers/modules/presets.html#customizing-presets). - **Potentially Breaking**: Remove `loaders.url()` and `loaders.file()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` (for assets which can be inlined) and `loaders.resource()` (as a catch-all for other types) in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` or `loaders.file()` was used directly. - Allow `null` to be returned from an `addFilter` callback to skip a filter when using a configuration preset. - Remove `OptimizeCssAssetsPlugin` (`plugins.optimizeCssAssets()`) in favor of Webpack 5-compatible [CssMinimizerPlugin](https://github.com/webpack-contrib/css-minimizer-webpack-plugin) (`plugins.cssMinimizer()`) diff --git a/index.js b/index.js index 5bc6e74..429f333 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,5 @@ +const filters = require( './src/helpers/filters' ); + /** * Expose public package API. */ @@ -6,7 +8,8 @@ module.exports = { config: require( './src/config' ), externals: require( './src/externals' ), helpers: { - addFilter: require( './src/helpers/filters' ).addFilter, + addFilter: filters.addFilter, + removeFilter: filters.removeFilter, choosePort: require( './src/helpers/choose-port' ), cleanOnExit: require( './src/helpers/clean-on-exit' ), filePath: require( './src/helpers/file-path' ), From df1eef85acfb88fb52bc8fb2d73bf43606ea8b1b Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Wed, 18 May 2022 13:57:44 -0400 Subject: [PATCH 35/83] Tag v1.0.0-beta.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 21fc8e9..f8af5c5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "1.0.0-beta.5", + "version": "1.0.0-beta.6", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made", From 05a8fc8bd04b6160ed3a4274a6065bc484ba480a Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Sat, 21 May 2022 13:03:53 -0400 Subject: [PATCH 36/83] Remove eslint-loader and add eslint-webpack-plugin --- docs/changelog.md | 1 + docs/modules/plugins.md | 1 + package.json | 2 +- src/loaders.js | 10 +--------- src/loaders.test.js | 14 +++----------- src/plugins.js | 14 ++++++++++++++ src/presets.js | 27 ++++++++++++++------------- src/presets.test.js | 2 -- 8 files changed, 35 insertions(+), 36 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 911ac3e..22ab31b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -13,6 +13,7 @@ nav_order: 10 - **Breaking**: Include a contenthash string in default bundle file names. Set `filename: '[name].js'` in your output configuration to restore the old behavior. - **Breaking**: Default DevServer manifest name is now `development-asset-manifest.json`, not `asset-manifest.json`. - **Breaking**: Replace `filterLoaders` system with [individual hooks accesible via the new `addFilter` and `removeFilter` helpers](https://humanmade.github.io/webpack-helpers/modules/presets.html#customizing-presets). +- **Breaking**: Remove deprecated `eslint-loader` and add `eslint-webpack-plugin` to presets as `plugins.eslint()`. - **Potentially Breaking**: Remove `loaders.url()` and `loaders.file()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` (for assets which can be inlined) and `loaders.resource()` (as a catch-all for other types) in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` or `loaders.file()` was used directly. - Allow `null` to be returned from an `addFilter` callback to skip a filter when using a configuration preset. - Remove `OptimizeCssAssetsPlugin` (`plugins.optimizeCssAssets()`) in favor of Webpack 5-compatible [CssMinimizerPlugin](https://github.com/webpack-contrib/css-minimizer-webpack-plugin) (`plugins.cssMinimizer()`) diff --git a/docs/modules/plugins.md b/docs/modules/plugins.md index d7b59ed..1da4e42 100644 --- a/docs/modules/plugins.md +++ b/docs/modules/plugins.md @@ -16,6 +16,7 @@ This module provides methods which create new instances of commonly-needed Webpa   | `plugins.clean()` | Create and return a new [`clean-webpack-plugin`](https://github.com/johnagan/clean-webpack-plugin) instance.   | `plugins.copy()` | Create and return a new [`copy-webpack-plugin`](https://github.com/webpack-contrib/copy-webpack-plugin) instance.   | `plugins.errorBell()` | Create and return a new [`bell-on-bundle-error-plugin`](https://www.npmjs.com/package/bell-on-bundler-error-plugin) instance. +  | `plugins.eslint()` | Create and return a [`eslint-webpack-plugin`](https://webpack.js.org/plugins/eslint-webpack-plugin/) instance.   | `plugins.fixStyleOnlyEntries()` | Create and return a [`webpack-fix-style-only-entries`](https://github.com/fqborges/webpack-fix-style-only-entries) instance to remove empty JS bundles for style-only entrypoints.   | `plugins.manifest()` | : Create and return a new [`webpack-manifest-plugin`](https://github.com/danethurber/webpack-manifest-plugin) instance, preconfigured to write the manifest file while running from a dev server. **P** | `plugins.miniCssExtract()` | Create and return a new [`mini-css-extract-plugin`](https://github.com/webpack-contrib/mini-css-extract-plugin) instance. diff --git a/package.json b/package.json index f8af5c5..8ce3c56 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "css-loader": "^6.7.1", "css-minimizer-webpack-plugin": "^3.4.1", "detect-port-alt": "^1.1.6", - "eslint-loader": "^4.0.2", + "eslint-webpack-plugin": "^3.1.1", "is-root": "^2.1.0", "mini-css-extract-plugin": "^2.6.0", "postcss": "^8.4.12", diff --git a/src/loaders.js b/src/loaders.js index 53c8770..c2ef27c 100644 --- a/src/loaders.js +++ b/src/loaders.js @@ -30,7 +30,7 @@ const createLoaderFactory = loaderKey => { }; // Define all supported loader factories within the loaders object. -[ 'assets', 'eslint', 'js', 'ts', 'style', 'css', 'postcss', 'sass', 'sourcemaps', 'resource' ].forEach( loaderKey => { +[ 'assets', 'js', 'ts', 'style', 'css', 'postcss', 'sass', 'sourcemaps', 'resource' ].forEach( loaderKey => { loaders[ loaderKey ] = createLoaderFactory( loaderKey ); } ); @@ -45,14 +45,6 @@ loaders.assets.defaults = { }, }; -loaders.eslint.defaults = { - test: /\.jsx?$/, - exclude: /(node_modules|bower_components)/, - enforce: 'pre', - loader: require.resolve( 'eslint-loader' ), - options: {}, -}; - loaders.js.defaults = { test: /\.jsx?$/, exclude: /(node_modules|bower_components)/, diff --git a/src/loaders.test.js b/src/loaders.test.js index a11d2e1..9cbc0d2 100644 --- a/src/loaders.test.js +++ b/src/loaders.test.js @@ -22,19 +22,11 @@ describe( 'loaders', () => { expect( result.value ).toBe( 42 ); } ); - describe( '.eslint()', () => { - it( 'tests for .js or .jsx files', () => { - expect( 'file.js'.match( loaders.eslint().test ) ).not.toBeNull(); - expect( 'file.jsx'.match( loaders.eslint().test ) ).not.toBeNull(); - expect( 'file.scss'.match( loaders.eslint().test ) ).toBeNull(); - } ); - } ); - describe( '.js()', () => { it( 'tests for .js or .jsx files', () => { - expect( 'file.js'.match( loaders.eslint().test ) ).not.toBeNull(); - expect( 'file.jsx'.match( loaders.eslint().test ) ).not.toBeNull(); - expect( 'file.scss'.match( loaders.eslint().test ) ).toBeNull(); + expect( 'file.js'.match( loaders.js().test ) ).not.toBeNull(); + expect( 'file.jsx'.match( loaders.js().test ) ).not.toBeNull(); + expect( 'file.scss'.match( loaders.js().test ) ).toBeNull(); } ); } ); diff --git a/src/plugins.js b/src/plugins.js index 73bdfdd..26b4d44 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -2,6 +2,7 @@ const { BundleAnalyzerPlugin } = require( 'webpack-bundle-analyzer' ); const { CleanWebpackPlugin } = require( 'clean-webpack-plugin' ); const BellOnBundleErrorPlugin = require( 'bell-on-bundler-error-plugin' ); const CopyPlugin = require( 'copy-webpack-plugin' ); +const ESLintPlugin = require( 'eslint-webpack-plugin' ); const FixStyleOnlyEntriesPlugin = require( 'webpack-fix-style-only-entries' ); const { WebpackManifestPlugin: ManifestPlugin } = require( 'webpack-manifest-plugin' ); const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' ); @@ -23,6 +24,7 @@ module.exports = { BundleAnalyzerPlugin, CleanWebpackPlugin, CopyPlugin, + ESLintPlugin, FixStyleOnlyEntriesPlugin, ManifestPlugin, MiniCssExtractPlugin, @@ -98,6 +100,18 @@ module.exports = { */ errorBell: () => new BellOnBundleErrorPlugin(), + /** + * Create a new ESLintPlugin instance to check your build for syntax or style issues. + * + * @param {Object} options Optional plugin options object. + * @returns {ESLintPlugin} A configured ESLintPlugin instance. + */ + eslint: ( options ) => new ESLintPlugin( { + extensions: [ 'js', 'jsx', 'ts', 'tsx' ], + exclude: [ 'node_modules', 'vendor', '*.min.js', '*.min.jsx' ], + ...options, + } ), + /** * Create a new FixStyleOnlyEntriesPlugin instance to remove unnecessary JS * files generated for style-only bundle entries. diff --git a/src/presets.js b/src/presets.js index 493cf32..eb9fd62 100644 --- a/src/presets.js +++ b/src/presets.js @@ -31,11 +31,11 @@ const getSeedByDirectory = ( path ) => { /** * Helper to detect whether a given package is installed, and return a spreadable - * array containing an appropriate loader if so. + * array containing an appropriate loader or plugin if so. * * @example - * rules: [ - * ...ifInstalled( 'eslint', loaders.eslint() ), + * plugins: [ + * ...ifInstalled( 'eslint', plugins.eslint() ), * ], * * @param {String} packageName The string name of the dependency for which to test. @@ -128,12 +128,6 @@ const development = ( config = {} ) => { rules: removeNullLoaders( [ // Handle node_modules packages that contain sourcemaps. loaders.sourcemaps(), - // Run all JS files through ESLint, if installed. - ...ifInstalled( 'eslint', loaders.eslint( { - options: { - emitWarning: true, - }, - } ) ), { // "oneOf" will traverse all following loaders until one will // match the requirements. If no loader matches, it will fall @@ -186,7 +180,13 @@ const development = ( config = {} ) => { devServer, - plugins: [], + plugins: [ + // Run all JS files through ESLint, if installed. + ...ifInstalled( 'eslint', plugins.eslint( { + // But don't let errors block the build. + failOnError: false, + } ) ), + ], }; // If no entry was provided, inject a default entry value. @@ -278,8 +278,6 @@ const production = ( config = {} ) => { module: { strictExportPresence: true, rules: removeNullLoaders( [ - // Run all JS files through ESLint, if installed. - ...ifInstalled( 'eslint', loaders.eslint() ), { // "oneOf" will traverse all following loaders until one will // match the requirements. If no loader matches, it will fall @@ -327,7 +325,10 @@ const production = ( config = {} ) => { stats, - plugins: [], + plugins: [ + // Run all JS files through ESLint, if installed. + ...ifInstalled( 'eslint', plugins.eslint() ), + ], }; // If no entry was provided, inject a default entry value. diff --git a/src/presets.test.js b/src/presets.test.js index 8b80c84..d532e9f 100644 --- a/src/presets.test.js +++ b/src/presets.test.js @@ -377,7 +377,6 @@ describe( 'presets', () => { it( 'does not include null loader entries if a loader was disabled with a filter', () => { addFilter( 'preset/stylesheet-loaders', returnNull ); addFilter( 'loader/ts', returnNull ); - addFilter( 'loader/eslint', returnNull ); const config = development( { entry: { main: 'some-file.js', @@ -626,7 +625,6 @@ describe( 'presets', () => { it( 'does not include null loader entries if a loader was disabled with a filter', () => { addFilter( 'preset/stylesheet-loaders', returnNull ); addFilter( 'loader/ts', returnNull ); - addFilter( 'loader/eslint', returnNull ); const config = production( { entry: { main: 'some-file.js', From a5d00518715937e37c9b99a8e8d4eda4bd9d9310 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Sat, 21 May 2022 13:21:05 -0400 Subject: [PATCH 37/83] Add test to test-build to validate linting behavior --- .eslintignore | 1 - .eslintrc.js | 5 ++++- package.json | 2 +- test/src/.eslintrc.js | 5 +++++ test/src/helpers.js | 4 ++++ test/src/linting-test.js | 1 + test/test-config.js | 4 ++++ 7 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 test/src/.eslintrc.js create mode 100644 test/src/linting-test.js diff --git a/.eslintignore b/.eslintignore index 537506f..5c7cc88 100644 --- a/.eslintignore +++ b/.eslintignore @@ -10,5 +10,4 @@ docs/ src/vendor # The test folder uses ES modules -test/src test/build diff --git a/.eslintrc.js b/.eslintrc.js index 77aa5f6..560d2f6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -251,7 +251,10 @@ module.exports = { 'error', 'last' ], - 'sort-imports': 'error', + 'sort-imports': [ + 'error', + { memberSyntaxSortOrder: [ 'all', 'multiple', 'single', 'none' ] }, + ], 'sort-keys': 'off', 'sort-vars': 'error', 'space-before-blocks': 'error', diff --git a/package.json b/package.json index 8ce3c56..620c6dd 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "webpack" ], "scripts": { - "lint": "eslint .", + "lint": "eslint . --ignore-pattern linting-test.js", "test": "jest", "test-build": "rm -rf test/build && webpack --config=test/test-config.js", "test-dev-server": "webpack-dev-server --config=test/test-config.js" diff --git a/test/src/.eslintrc.js b/test/src/.eslintrc.js new file mode 100644 index 0000000..de7ac3d --- /dev/null +++ b/test/src/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + parserOptions: { + sourceType: 'module', + }, +}; diff --git a/test/src/helpers.js b/test/src/helpers.js index 1c0daa0..393fd0b 100644 --- a/test/src/helpers.js +++ b/test/src/helpers.js @@ -4,3 +4,7 @@ export const getResults = async () => { await wait( 500 ); return 'Results'; }; + +export const lintingErrorsInProdButWarnsInDev = () => { + console.log( 'Very contrived example to hopefuly make test-build output clearer' ); +}; diff --git a/test/src/linting-test.js b/test/src/linting-test.js new file mode 100644 index 0000000..6495acc --- /dev/null +++ b/test/src/linting-test.js @@ -0,0 +1 @@ +import { lintingErrorsInProdButWarnsInDev } from './helpers'; diff --git a/test/test-config.js b/test/test-config.js index f196d43..c71f4f8 100644 --- a/test/test-config.js +++ b/test/test-config.js @@ -7,6 +7,8 @@ module.exports = [ name: 'production-test', entry: { prod: filePath( 'test/src/index.js' ), + // Expect this not to build. + 'linting-test': filePath( 'test/src/linting-test.js' ), }, output: { path: filePath( 'test/build/prod' ), @@ -42,6 +44,8 @@ module.exports = [ name: 'dev-test', entry: { prod: filePath( 'test/src/index.js' ), + // Expect this to warn. + 'linting-test': filePath( 'test/src/linting-test.js' ), }, output: { path: filePath( 'test/build/dev' ), From e8e30548982ed6b9e55615c038b318a0f21b3a16 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Sat, 21 May 2022 13:34:00 -0400 Subject: [PATCH 38/83] Swap out fix-style-only-entries plugin for "webpack-remove-empty-scripts" fork --- docs/changelog.md | 1 + docs/modules/plugins.md | 2 +- package.json | 4 ++-- src/plugins.js | 10 +++++----- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 22ab31b..6cdcef7 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -15,6 +15,7 @@ nav_order: 10 - **Breaking**: Replace `filterLoaders` system with [individual hooks accesible via the new `addFilter` and `removeFilter` helpers](https://humanmade.github.io/webpack-helpers/modules/presets.html#customizing-presets). - **Breaking**: Remove deprecated `eslint-loader` and add `eslint-webpack-plugin` to presets as `plugins.eslint()`. - **Potentially Breaking**: Remove `loaders.url()` and `loaders.file()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` (for assets which can be inlined) and `loaders.resource()` (as a catch-all for other types) in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` or `loaders.file()` was used directly. +- `plugins.fixStyleOnlyEntries()` now uses [`webpack-remove-empty-scripts`](https://github.com/webdiscus/webpack-remove-empty-scripts#webpack-remove-empty-scripts) instead of `webpack-fix-style-only-entries` due to Webpack 5 compatiblity issues with the original plugin. - Allow `null` to be returned from an `addFilter` callback to skip a filter when using a configuration preset. - Remove `OptimizeCssAssetsPlugin` (`plugins.optimizeCssAssets()`) in favor of Webpack 5-compatible [CssMinimizerPlugin](https://github.com/webpack-contrib/css-minimizer-webpack-plugin) (`plugins.cssMinimizer()`) - Remove `plugins.hotModuleReplacement()`, which is now handled automatically by the DevServer in `hot` mode. diff --git a/docs/modules/plugins.md b/docs/modules/plugins.md index 1da4e42..6a1c588 100644 --- a/docs/modules/plugins.md +++ b/docs/modules/plugins.md @@ -17,7 +17,7 @@ This module provides methods which create new instances of commonly-needed Webpa   | `plugins.copy()` | Create and return a new [`copy-webpack-plugin`](https://github.com/webpack-contrib/copy-webpack-plugin) instance.   | `plugins.errorBell()` | Create and return a new [`bell-on-bundle-error-plugin`](https://www.npmjs.com/package/bell-on-bundler-error-plugin) instance.   | `plugins.eslint()` | Create and return a [`eslint-webpack-plugin`](https://webpack.js.org/plugins/eslint-webpack-plugin/) instance. -  | `plugins.fixStyleOnlyEntries()` | Create and return a [`webpack-fix-style-only-entries`](https://github.com/fqborges/webpack-fix-style-only-entries) instance to remove empty JS bundles for style-only entrypoints. +  | `plugins.fixStyleOnlyEntries()` | Create and return a [`webpack-remove-empty-scripts`](https://github.com/webdiscus/webpack-remove-empty-scripts) instance (forked from the non-Webpack 5-compatible `webpack-fix-style-only-entries`) to remove empty JS bundles for style-only entrypoints.   | `plugins.manifest()` | : Create and return a new [`webpack-manifest-plugin`](https://github.com/danethurber/webpack-manifest-plugin) instance, preconfigured to write the manifest file while running from a dev server. **P** | `plugins.miniCssExtract()` | Create and return a new [`mini-css-extract-plugin`](https://github.com/webpack-contrib/mini-css-extract-plugin) instance. **P** | `plugins.terser()` | Create and return a new [`terser-webpack-plugin`](https://github.com/webpack-contrib/terser-webpack-plugin) instance, preconfigured with defaults based on `create-react-app`. diff --git a/package.json b/package.json index 620c6dd..26cafc0 100644 --- a/package.json +++ b/package.json @@ -66,8 +66,8 @@ "terser-webpack-plugin": "^5.3.1", "ts-loader": "^9.2.9", "webpack-bundle-analyzer": "^4.5.0", - "webpack-fix-style-only-entries": "^0.6.1", - "webpack-manifest-plugin": "^5.0.0" + "webpack-manifest-plugin": "^5.0.0", + "webpack-remove-empty-scripts": "^0.8.0" }, "peerDependencies": { "sass": "*", diff --git a/src/plugins.js b/src/plugins.js index 26b4d44..83f75e8 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -3,9 +3,9 @@ const { CleanWebpackPlugin } = require( 'clean-webpack-plugin' ); const BellOnBundleErrorPlugin = require( 'bell-on-bundler-error-plugin' ); const CopyPlugin = require( 'copy-webpack-plugin' ); const ESLintPlugin = require( 'eslint-webpack-plugin' ); -const FixStyleOnlyEntriesPlugin = require( 'webpack-fix-style-only-entries' ); const { WebpackManifestPlugin: ManifestPlugin } = require( 'webpack-manifest-plugin' ); const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' ); +const RemoveEmptyScriptsPlugin = require( 'webpack-remove-empty-scripts' ); const TerserPlugin = require( 'terser-webpack-plugin' ); const CssMinimizerPlugin = require( 'css-minimizer-webpack-plugin' ); @@ -25,10 +25,10 @@ module.exports = { CleanWebpackPlugin, CopyPlugin, ESLintPlugin, - FixStyleOnlyEntriesPlugin, ManifestPlugin, MiniCssExtractPlugin, CssMinimizerPlugin, + RemoveEmptyScriptsPlugin, TerserPlugin, }, @@ -113,14 +113,14 @@ module.exports = { } ), /** - * Create a new FixStyleOnlyEntriesPlugin instance to remove unnecessary JS + * Create a new RemoveEmptyScriptsPlugin instance to remove unnecessary JS * files generated for style-only bundle entries. * * @param {Object} [options] Optional plugin options object. * @param {RegExp} [options.exclude] Regular expression to filter what gets cleaned. - * @returns {FixStyleOnlyEntriesPlugin} A configured FixStyleOnlyEntriesPlugin instance. + * @returns {RemoveEmptyScriptsPlugin} A configured RemoveEmptyScriptsPlugin instance. */ - fixStyleOnlyEntries: ( options ) => new FixStyleOnlyEntriesPlugin( options ), + fixStyleOnlyEntries: ( options ) => new RemoveEmptyScriptsPlugin( options ), /** * Create a new ManifestPlugin instance to output an asset-manifest.json From bf9e4a4ed7e2359eedd0ac7c299fc46699e8238a Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Sat, 21 May 2022 13:35:52 -0400 Subject: [PATCH 39/83] Do not include lint validation in prod build test because it causes CI to return a failure --- test/test-config.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test-config.js b/test/test-config.js index c71f4f8..7abf56d 100644 --- a/test/test-config.js +++ b/test/test-config.js @@ -7,8 +7,9 @@ module.exports = [ name: 'production-test', entry: { prod: filePath( 'test/src/index.js' ), - // Expect this not to build. - 'linting-test': filePath( 'test/src/linting-test.js' ), + // Expect this not to build. Currently cannot do this because our CI script + // anticipates that all commands will return a 0 status. + // 'linting-test': filePath( 'test/src/linting-test.js' ), }, output: { path: filePath( 'test/build/prod' ), From 8410f9cd4d8c9d9ded238d919e60471b1fa96f91 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Sun, 22 May 2022 11:53:24 -0400 Subject: [PATCH 40/83] Remove leftover loaders.eslint() reference --- docs/modules/loaders.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/modules/loaders.md b/docs/modules/loaders.md index e211e41..8c65b45 100644 --- a/docs/modules/loaders.md +++ b/docs/modules/loaders.md @@ -11,7 +11,6 @@ nav_order: 3 This module provides functions that generate configurations for commonly-needed Webpack loaders. Use them within the `.module.rules` array, or use `presets.development()`/`presets.production()` to opt-in to some opinionated defaults. - `loaders.assets()`: Return a configured Webpack module loader rule for [`asset` modules](https://webpack.js.org/guides/asset-modules/#inlining-assets) which will be inlined if small enough. -- `loaders.eslint()`: Return a configured Webpack module loader rule for `eslint-loader`. - `loaders.js()`: Return a configured Webpack module loader rule for `js-loader`. - `loaders.ts()`: Return a configured Webpack module loader rule for `ts-loader`. - `loaders.style()`: Return a configured Webpack module loader rule for `style-loader`. From 62f38f88ca9451101bfaf1b4d05d5f777677b61e Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Sun, 22 May 2022 11:58:30 -0400 Subject: [PATCH 41/83] Preserve spacing in JSON --- .editorconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.editorconfig b/.editorconfig index 6e48422..93b895d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,7 +9,7 @@ insert_final_newline = true indent_style = tab trim_trailing_whitespace = true -# YAML files require spaces -[*.{yaml,yml}] +# YAML files require spaces. JSON doesn't, but our file uses them. +[*.{yaml,yml,json}] indent_style = space indent_size = 2 From 1420643a508c3769accfc222d33841ca5a533d2e Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Sun, 22 May 2022 17:06:53 -0400 Subject: [PATCH 42/83] Switch back to 16-character hashes (Asset_Loader expects this) Asset_Loader 0.5, which we are stuck with for some time, only detects that filenames are already hashed if they contain a 16-char hash. --- src/plugins.js | 2 +- src/presets.js | 8 ++++---- src/presets.test.js | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/plugins.js b/src/plugins.js index 83f75e8..516df6e 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -155,7 +155,7 @@ module.exports = { * @returns {MiniCssExtractPlugin} A configured MiniCssExtractPlugin instance. */ miniCssExtract: ( options = {} ) => new MiniCssExtractPlugin( { - filename: '[name].[contenthash:8].css', + filename: '[name].[contenthash].css', ...options, } ), diff --git a/src/presets.js b/src/presets.js index eb9fd62..9b18830 100644 --- a/src/presets.js +++ b/src/presets.js @@ -116,9 +116,9 @@ const development = ( config = {} ) => { // Add /* filename */ comments to generated require()s in the output. pathinfo: true, // Provide a default output name. - filename: '[name].[contenthash:8].js', + filename: '[name].[contenthash].js', // Provide chunk filename. Requires content hash for cache busting. - chunkFilename: '[name].[contenthash:8].chunk.js', + chunkFilename: '[name].[contenthash].chunk.js', // `publicPath` will be inferred as a localhost URL based on output.path // when a devServer.port value is available. }, @@ -270,9 +270,9 @@ const production = ( config = {} ) => { path: filePath( 'build' ), pathinfo: false, // Provide a default output name. - filename: '[name].[contenthash:8].js', + filename: '[name].[contenthash].js', // Provide chunk filename. Requires content hash for cache busting. - chunkFilename: '[name].[contenthash:8].chunk.js', + chunkFilename: '[name].[contenthash].chunk.js', }, module: { diff --git a/src/presets.test.js b/src/presets.test.js index d532e9f..ab62cf1 100644 --- a/src/presets.test.js +++ b/src/presets.test.js @@ -95,8 +95,8 @@ describe( 'presets', () => { expect( config.entry ).toEqual( 'some-file.js' ); expect( config.output ).toEqual( { pathinfo: true, - filename: '[name].[contenthash:8].js', - chunkFilename: '[name].[contenthash:8].chunk.js', + filename: '[name].[contenthash].js', + chunkFilename: '[name].[contenthash].chunk.js', path: 'build/', } ); } ); @@ -408,8 +408,8 @@ describe( 'presets', () => { expect( config.entry ).toEqual( 'some-file.js' ); expect( config.output ).toEqual( { pathinfo: false, - filename: '[name].[contenthash:8].js', - chunkFilename: '[name].[contenthash:8].chunk.js', + filename: '[name].[contenthash].js', + chunkFilename: '[name].[contenthash].chunk.js', path: 'build/', publicPath: '', } ); @@ -444,7 +444,7 @@ describe( 'presets', () => { ] ) ); const cssPlugins = config.plugins.filter( filterPlugins( MiniCssExtractPlugin ) ); expect( cssPlugins.length ).toBe( 1 ); - expect( cssPlugins[ 0 ].options.filename ).toEqual( '[name].[contenthash:8].css' ); + expect( cssPlugins[ 0 ].options.filename ).toEqual( '[name].[contenthash].css' ); } ); it( 'does not override or duplicate existing MiniCssExtractPlugin instances', () => { From 7d10a7305e4be44d976862399d79e2513d31db85 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Sun, 22 May 2022 17:07:25 -0400 Subject: [PATCH 43/83] Adjust build stats defaults and apply to dev builds as well --- src/config.js | 4 +--- src/presets.js | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config.js b/src/config.js index fa6c774..ede621e 100644 --- a/src/config.js +++ b/src/config.js @@ -23,12 +23,10 @@ module.exports = { * @type {Object} */ stats: { - all: false, + preset: 'summary', assets: true, colors: true, errors: true, - performance: true, - timings: true, warnings: true, }, }; diff --git a/src/presets.js b/src/presets.js index 9b18830..15746ba 100644 --- a/src/presets.js +++ b/src/presets.js @@ -180,6 +180,8 @@ const development = ( config = {} ) => { devServer, + stats, + plugins: [ // Run all JS files through ESLint, if installed. ...ifInstalled( 'eslint', plugins.eslint( { From ad40f3a2a7748751b9348aaf5175e2ed9d77d384 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Sun, 22 May 2022 17:17:51 -0400 Subject: [PATCH 44/83] Filter out null plugin instances Permits us to add filters later which would let a consumer null out a rendered plugin --- src/presets.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/presets.js b/src/presets.js index 15746ba..91a3fc0 100644 --- a/src/presets.js +++ b/src/presets.js @@ -188,7 +188,7 @@ const development = ( config = {} ) => { // But don't let errors block the build. failOnError: false, } ) ), - ], + ].filter( Boolean ), }; // If no entry was provided, inject a default entry value. @@ -330,7 +330,7 @@ const production = ( config = {} ) => { plugins: [ // Run all JS files through ESLint, if installed. ...ifInstalled( 'eslint', plugins.eslint() ), - ], + ].filter( Boolean ), }; // If no entry was provided, inject a default entry value. From a88e280a77df0642749583548af6e4dd95012a55 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Sun, 22 May 2022 17:39:03 -0400 Subject: [PATCH 45/83] Add SimpleBuildReportPlugin and expose via plugins.formatConsoleOutput --- package.json | 1 + src/plugins | 1 + src/plugins.js | 10 ++++++++++ 3 files changed, 12 insertions(+) create mode 160000 src/plugins diff --git a/package.json b/package.json index 26cafc0..97abf4b 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "eslint": "^7.32.0", "jest": "^26.6.3", "sass": "^1.51.0", + "simple-build-report-webpack-plugin": "^1.0.0", "typescript": "^4.6.3", "webpack": "^5.72.0", "webpack-cli": "^4.9.2", diff --git a/src/plugins b/src/plugins new file mode 160000 index 0000000..cfa997a --- /dev/null +++ b/src/plugins @@ -0,0 +1 @@ +Subproject commit cfa997a4225d999029601c6482b55f365d4daa7b diff --git a/src/plugins.js b/src/plugins.js index 516df6e..d1944b9 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -6,6 +6,7 @@ const ESLintPlugin = require( 'eslint-webpack-plugin' ); const { WebpackManifestPlugin: ManifestPlugin } = require( 'webpack-manifest-plugin' ); const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' ); const RemoveEmptyScriptsPlugin = require( 'webpack-remove-empty-scripts' ); +const SimpleBuildReportPlugin = require( 'simple-build-report-webpack-plugin' ); const TerserPlugin = require( 'terser-webpack-plugin' ); const CssMinimizerPlugin = require( 'css-minimizer-webpack-plugin' ); @@ -29,6 +30,7 @@ module.exports = { MiniCssExtractPlugin, CssMinimizerPlugin, RemoveEmptyScriptsPlugin, + SimpleBuildReportPlugin, TerserPlugin, }, @@ -122,6 +124,14 @@ module.exports = { */ fixStyleOnlyEntries: ( options ) => new RemoveEmptyScriptsPlugin( options ), + /** + * Instantiate a SimpleBuildReportPlugin to render build output using the + * webpack-format-messages package. + * + * @returns {SimpleBuildReportPlugin} Output formatter plugin instance. + */ + formatConsoleOutput: () => new SimpleBuildReportPlugin(), + /** * Create a new ManifestPlugin instance to output an asset-manifest.json * file, which can be consumed by the PHP server to auto-load generated From ca2005e6ab3db8c16d69a338c021f2c0a749a0a6 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Mon, 23 May 2022 11:32:55 -0400 Subject: [PATCH 46/83] Include a bundleAnalyzer plugin by default when --analyze is passed to webpack --- docs/changelog.md | 1 + docs/modules/plugins.md | 2 +- src/plugins.js | 6 +++--- src/presets.js | 11 +++++++++++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 6cdcef7..49b8aaf 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -15,6 +15,7 @@ nav_order: 10 - **Breaking**: Replace `filterLoaders` system with [individual hooks accesible via the new `addFilter` and `removeFilter` helpers](https://humanmade.github.io/webpack-helpers/modules/presets.html#customizing-presets). - **Breaking**: Remove deprecated `eslint-loader` and add `eslint-webpack-plugin` to presets as `plugins.eslint()`. - **Potentially Breaking**: Remove `loaders.url()` and `loaders.file()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` (for assets which can be inlined) and `loaders.resource()` (as a catch-all for other types) in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` or `loaders.file()` was used directly. +- A `webpack-bundle-analyzer` plugin is now automatically added to production builds when Webpack is invoked with the `--analyze` flag. - `plugins.fixStyleOnlyEntries()` now uses [`webpack-remove-empty-scripts`](https://github.com/webdiscus/webpack-remove-empty-scripts#webpack-remove-empty-scripts) instead of `webpack-fix-style-only-entries` due to Webpack 5 compatiblity issues with the original plugin. - Allow `null` to be returned from an `addFilter` callback to skip a filter when using a configuration preset. - Remove `OptimizeCssAssetsPlugin` (`plugins.optimizeCssAssets()`) in favor of Webpack 5-compatible [CssMinimizerPlugin](https://github.com/webpack-contrib/css-minimizer-webpack-plugin) (`plugins.cssMinimizer()`) diff --git a/docs/modules/plugins.md b/docs/modules/plugins.md index 6a1c588..7ef80f0 100644 --- a/docs/modules/plugins.md +++ b/docs/modules/plugins.md @@ -12,7 +12,7 @@ This module provides methods which create new instances of commonly-needed Webpa   | Plugin | Description ------ | ------ | ------------ -  | `plugins.bundleAnalyzer()` | Create and return a new [`webpack-bundle-analyzer`](https://github.com/webpack-contrib/webpack-bundle-analyzer) instance. When included this plugin is disabled by default unless the `--analyze` flag is passed on the command line. +  | `plugins.bundleAnalyzer()` | Create and return a new [`webpack-bundle-analyzer`](https://github.com/webpack-contrib/webpack-bundle-analyzer) instance. This plugin is included in production preset builds automatically when the `--analyze` flag is passed on the command line.   | `plugins.clean()` | Create and return a new [`clean-webpack-plugin`](https://github.com/johnagan/clean-webpack-plugin) instance.   | `plugins.copy()` | Create and return a new [`copy-webpack-plugin`](https://github.com/webpack-contrib/copy-webpack-plugin) instance.   | `plugins.errorBell()` | Create and return a new [`bell-on-bundle-error-plugin`](https://www.npmjs.com/package/bell-on-bundler-error-plugin) instance. diff --git a/src/plugins.js b/src/plugins.js index d1944b9..ee8cbdf 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -51,14 +51,14 @@ module.exports = { }, /** - * Create a new BundleAnalyzerPlugin instance. The analyzer is enabled by default - * only if `--analyze` is passed on the command line. + * Create a new BundleAnalyzerPlugin instance. * * @param {Object} [options] Optional plugin options object. * @returns {BundleAnalyzerPlugin} A configured BundleAnalyzerPlugin instance. */ bundleAnalyzer: ( options = {} ) => new BundleAnalyzerPlugin( { - analyzerMode: process.argv.indexOf( '--analyze' ) >= 0 ? 'static' : 'disabled', + analyzerMode: 'static', + generateStatsFile: true, openAnalyzer: false, reportFilename: 'bundle-analyzer-report.html', ...options, diff --git a/src/presets.js b/src/presets.js index 91a3fc0..26c345b 100644 --- a/src/presets.js +++ b/src/presets.js @@ -9,6 +9,8 @@ const loaders = require( './loaders' ); const plugins = require( './plugins' ); const { ManifestPlugin, MiniCssExtractPlugin } = plugins.constructors; +const isAnalyzeMode = process.argv.includes( '--analyze' ); + /** * Dictionary of shared seed objects by path. * @@ -330,6 +332,15 @@ const production = ( config = {} ) => { plugins: [ // Run all JS files through ESLint, if installed. ...ifInstalled( 'eslint', plugins.eslint() ), + // If webpack was invoked with the --analyze flag, include a bundleAnalyzer + // in production builds. Use the configuration name when present to separate + // output files for each webpack build in multi-configuration setups. + isAnalyzeMode ? + plugins.bundleAnalyzer( { + reportFilename: config.name ? `${ config.name }-analyzer-report.html` : 'bundle-analyzer-report.html', + statsFilename: config.name ? `${ config.name }-stats.json` : 'stats.json', + } ) : + null, ].filter( Boolean ), }; From 04f26151a89bc2ac2dbda23cf4dd920a8f474e26 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Mon, 23 May 2022 11:34:08 -0400 Subject: [PATCH 47/83] Include simpleBuildReport plugin in production presets --- docs/changelog.md | 2 ++ src/plugins | 1 - src/plugins.js | 16 ++++++++-------- src/presets.js | 2 ++ 4 files changed, 12 insertions(+), 9 deletions(-) delete mode 160000 src/plugins diff --git a/docs/changelog.md b/docs/changelog.md index 49b8aaf..e453bc3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -16,6 +16,8 @@ nav_order: 10 - **Breaking**: Remove deprecated `eslint-loader` and add `eslint-webpack-plugin` to presets as `plugins.eslint()`. - **Potentially Breaking**: Remove `loaders.url()` and `loaders.file()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` (for assets which can be inlined) and `loaders.resource()` (as a catch-all for other types) in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` or `loaders.file()` was used directly. - A `webpack-bundle-analyzer` plugin is now automatically added to production builds when Webpack is invoked with the `--analyze` flag. +- Add the [`simple-build-report-webpack-plugin`](https://github.com/kadamwhite/simple-build-report-webpack-plugin) as `plugins.simpleBuildReport()` +- Include a `plugins.simpleBuildReport()` instance in production preset builds to improve legibility of Webpack console output. - `plugins.fixStyleOnlyEntries()` now uses [`webpack-remove-empty-scripts`](https://github.com/webdiscus/webpack-remove-empty-scripts#webpack-remove-empty-scripts) instead of `webpack-fix-style-only-entries` due to Webpack 5 compatiblity issues with the original plugin. - Allow `null` to be returned from an `addFilter` callback to skip a filter when using a configuration preset. - Remove `OptimizeCssAssetsPlugin` (`plugins.optimizeCssAssets()`) in favor of Webpack 5-compatible [CssMinimizerPlugin](https://github.com/webpack-contrib/css-minimizer-webpack-plugin) (`plugins.cssMinimizer()`) diff --git a/src/plugins b/src/plugins deleted file mode 160000 index cfa997a..0000000 --- a/src/plugins +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cfa997a4225d999029601c6482b55f365d4daa7b diff --git a/src/plugins.js b/src/plugins.js index ee8cbdf..a8cdbf3 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -124,14 +124,6 @@ module.exports = { */ fixStyleOnlyEntries: ( options ) => new RemoveEmptyScriptsPlugin( options ), - /** - * Instantiate a SimpleBuildReportPlugin to render build output using the - * webpack-format-messages package. - * - * @returns {SimpleBuildReportPlugin} Output formatter plugin instance. - */ - formatConsoleOutput: () => new SimpleBuildReportPlugin(), - /** * Create a new ManifestPlugin instance to output an asset-manifest.json * file, which can be consumed by the PHP server to auto-load generated @@ -177,6 +169,14 @@ module.exports = { */ cssMinimizer: ( options = {} ) => new CssMinimizerPlugin( options ), + /** + * Instantiate a SimpleBuildReportPlugin to render build output in a human- + * oriented fashion. + * + * @returns {SimpleBuildReportPlugin} Output formatter plugin instance. + */ + simpleBuildReport: () => new SimpleBuildReportPlugin(), + /** * Create a new TerserPlugin instance, defaulting to a set of options * borrowed from create-react-app's configuration. diff --git a/src/presets.js b/src/presets.js index 26c345b..284fbed 100644 --- a/src/presets.js +++ b/src/presets.js @@ -332,6 +332,8 @@ const production = ( config = {} ) => { plugins: [ // Run all JS files through ESLint, if installed. ...ifInstalled( 'eslint', plugins.eslint() ), + // Use the simple build report plugin to clean up Webpack's terminal output. + plugins.simpleBuildReport(), // If webpack was invoked with the --analyze flag, include a bundleAnalyzer // in production builds. Use the configuration name when present to separate // output files for each webpack build in multi-configuration setups. From 82fb64cfd534a49ee5733e9c677745dca4e1ef1c Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Mon, 23 May 2022 11:43:55 -0400 Subject: [PATCH 48/83] Tag v1.0.0-beta.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97abf4b..564d94e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "1.0.0-beta.6", + "version": "1.0.0-beta.7", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made", From 8eaa3c94fad0ba6293eb0189546b5491bac6e3e9 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Mon, 23 May 2022 11:48:39 -0400 Subject: [PATCH 49/83] Correct language in changelog around loader filtering --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index e453bc3..d8f4796 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -19,7 +19,7 @@ nav_order: 10 - Add the [`simple-build-report-webpack-plugin`](https://github.com/kadamwhite/simple-build-report-webpack-plugin) as `plugins.simpleBuildReport()` - Include a `plugins.simpleBuildReport()` instance in production preset builds to improve legibility of Webpack console output. - `plugins.fixStyleOnlyEntries()` now uses [`webpack-remove-empty-scripts`](https://github.com/webdiscus/webpack-remove-empty-scripts#webpack-remove-empty-scripts) instead of `webpack-fix-style-only-entries` due to Webpack 5 compatiblity issues with the original plugin. -- Allow `null` to be returned from an `addFilter` callback to skip a filter when using a configuration preset. +- Allow `null` to be returned from an `addFilter` callback to skip a loader when using a configuration preset. - Remove `OptimizeCssAssetsPlugin` (`plugins.optimizeCssAssets()`) in favor of Webpack 5-compatible [CssMinimizerPlugin](https://github.com/webpack-contrib/css-minimizer-webpack-plugin) (`plugins.cssMinimizer()`) - Remove `plugins.hotModuleReplacement()`, which is now handled automatically by the DevServer in `hot` mode. - Include the `contenthash` in generated CSS filenames. [#204](https://github.com/humanmade/webpack-helpers/pull/204) From cdd57b242c4aa62cc84d9f41e6f67fd0e11822c1 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Mon, 23 May 2022 11:49:28 -0400 Subject: [PATCH 50/83] Do not deep-merge terserOptions, so that terser can be predictably configured --- docs/changelog.md | 1 + src/plugins.js | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index d8f4796..96d765e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -20,6 +20,7 @@ nav_order: 10 - Include a `plugins.simpleBuildReport()` instance in production preset builds to improve legibility of Webpack console output. - `plugins.fixStyleOnlyEntries()` now uses [`webpack-remove-empty-scripts`](https://github.com/webdiscus/webpack-remove-empty-scripts#webpack-remove-empty-scripts) instead of `webpack-fix-style-only-entries` due to Webpack 5 compatiblity issues with the original plugin. - Allow `null` to be returned from an `addFilter` callback to skip a loader when using a configuration preset. +- Do not deep merge options passed to `plugins.terserPlugin()`: options object can now fully overwrite `terserOptions` property if needed. - Remove `OptimizeCssAssetsPlugin` (`plugins.optimizeCssAssets()`) in favor of Webpack 5-compatible [CssMinimizerPlugin](https://github.com/webpack-contrib/css-minimizer-webpack-plugin) (`plugins.cssMinimizer()`) - Remove `plugins.hotModuleReplacement()`, which is now handled automatically by the DevServer in `hot` mode. - Include the `contenthash` in generated CSS filenames. [#204](https://github.com/humanmade/webpack-helpers/pull/204) diff --git a/src/plugins.js b/src/plugins.js index a8cdbf3..df060cf 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -186,7 +186,7 @@ module.exports = { * @param {Object} [options.terserOptions] Terser compressor options object. * @returns {TerserPlugin} A configured TerserPlugin instance. */ - terser: ( options = {} ) => new TerserPlugin( deepMerge( { + terser: ( options = {} ) => new TerserPlugin( { terserOptions: { parse: { // We want terser to parse ecma 8 code. However, we don't want it @@ -224,5 +224,6 @@ module.exports = { ascii_only: true, }, }, - }, options ) ), + ...options, + } ), }; From bcd10add2b9ac1ec45ac0d3cc757ac4918776140 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Mon, 23 May 2022 11:57:09 -0400 Subject: [PATCH 51/83] Permit filtering Terser defaults --- docs/changelog.md | 1 + src/plugins.js | 76 ++++++++++++++++++++++++----------------------- 2 files changed, 40 insertions(+), 37 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 96d765e..3a54f61 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -21,6 +21,7 @@ nav_order: 10 - `plugins.fixStyleOnlyEntries()` now uses [`webpack-remove-empty-scripts`](https://github.com/webdiscus/webpack-remove-empty-scripts#webpack-remove-empty-scripts) instead of `webpack-fix-style-only-entries` due to Webpack 5 compatiblity issues with the original plugin. - Allow `null` to be returned from an `addFilter` callback to skip a loader when using a configuration preset. - Do not deep merge options passed to `plugins.terserPlugin()`: options object can now fully overwrite `terserOptions` property if needed. +- Permit filtering the Terser default configuration using `addFilter( 'plugin/terser/defaults', cb )`. - Remove `OptimizeCssAssetsPlugin` (`plugins.optimizeCssAssets()`) in favor of Webpack 5-compatible [CssMinimizerPlugin](https://github.com/webpack-contrib/css-minimizer-webpack-plugin) (`plugins.cssMinimizer()`) - Remove `plugins.hotModuleReplacement()`, which is now handled automatically by the DevServer in `hot` mode. - Include the `contenthash` in generated CSS filenames. [#204](https://github.com/humanmade/webpack-helpers/pull/204) diff --git a/src/plugins.js b/src/plugins.js index df060cf..4920d20 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -10,7 +10,7 @@ const SimpleBuildReportPlugin = require( 'simple-build-report-webpack-plugin' ); const TerserPlugin = require( 'terser-webpack-plugin' ); const CssMinimizerPlugin = require( 'css-minimizer-webpack-plugin' ); -const deepMerge = require( './helpers/deep-merge' ); +const { applyFilters } = require( './helpers/filters' ); const isProfileMode = process.argv.includes( '--profile' ); @@ -187,43 +187,45 @@ module.exports = { * @returns {TerserPlugin} A configured TerserPlugin instance. */ terser: ( options = {} ) => new TerserPlugin( { - terserOptions: { - parse: { - // We want terser to parse ecma 8 code. However, we don't want it - // to apply any minification steps that turns valid ecma 5 code - // into invalid ecma 5 code. This is why the 'compress' and 'output' - // sections only apply transformations that are ecma 5 safe - // https://github.com/facebook/create-react-app/pull/4234 - ecma: 8, + ...applyFilters( 'plugin/terser/defaults', { + terserOptions: { + parse: { + // We want terser to parse ecma 8 code. However, we don't want it + // to apply any minification steps that turns valid ecma 5 code + // into invalid ecma 5 code. This is why the 'compress' and 'output' + // sections only apply transformations that are ecma 5 safe + // https://github.com/facebook/create-react-app/pull/4234 + ecma: 8, + }, + compress: { + ecma: 5, + warnings: false, + // Disabled because of an issue with Uglify breaking seemingly valid code: + // https://github.com/facebook/create-react-app/issues/2376 + // Pending further investigation: + // https://github.com/mishoo/UglifyJS2/issues/2011 + comparisons: false, + // Disabled because of an issue with Terser breaking valid code: + // https://github.com/facebook/create-react-app/issues/5250 + // Pending further investigation: + // https://github.com/terser-js/terser/issues/120 + inline: 2, + }, + mangle: { + safari10: true, + }, + // Added for profiling in devtools + keep_classnames: isProfileMode, + keep_fnames: isProfileMode, + output: { + ecma: 5, + comments: false, + // Turned on because emoji and regex is not minified properly using default + // https://github.com/facebook/create-react-app/issues/2488 + ascii_only: true, + }, }, - compress: { - ecma: 5, - warnings: false, - // Disabled because of an issue with Uglify breaking seemingly valid code: - // https://github.com/facebook/create-react-app/issues/2376 - // Pending further investigation: - // https://github.com/mishoo/UglifyJS2/issues/2011 - comparisons: false, - // Disabled because of an issue with Terser breaking valid code: - // https://github.com/facebook/create-react-app/issues/5250 - // Pending further investigation: - // https://github.com/terser-js/terser/issues/120 - inline: 2, - }, - mangle: { - safari10: true, - }, - // Added for profiling in devtools - keep_classnames: isProfileMode, - keep_fnames: isProfileMode, - output: { - ecma: 5, - comments: false, - // Turned on because emoji and regex is not minified properly using default - // https://github.com/facebook/create-react-app/issues/2488 - ascii_only: true, - }, - }, + } ), ...options, } ), }; From 1055650b954aa6b9cd5d7053b15d842b06ca0be6 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Mon, 23 May 2022 12:15:40 -0400 Subject: [PATCH 52/83] Change filter strings from "loader/...", "plugin/..." etc to "loaders/...", "plugins/..." etc, to mirror module names --- docs/changelog.md | 2 +- docs/modules/presets.md | 4 ++-- src/helpers/filters.test.js | 32 ++++++++++++++++---------------- src/loaders.js | 4 ++-- src/plugins.js | 2 +- src/presets.js | 4 ++-- src/presets.test.js | 32 ++++++++++++++++---------------- 7 files changed, 40 insertions(+), 40 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 3a54f61..34ca505 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -21,7 +21,7 @@ nav_order: 10 - `plugins.fixStyleOnlyEntries()` now uses [`webpack-remove-empty-scripts`](https://github.com/webdiscus/webpack-remove-empty-scripts#webpack-remove-empty-scripts) instead of `webpack-fix-style-only-entries` due to Webpack 5 compatiblity issues with the original plugin. - Allow `null` to be returned from an `addFilter` callback to skip a loader when using a configuration preset. - Do not deep merge options passed to `plugins.terserPlugin()`: options object can now fully overwrite `terserOptions` property if needed. -- Permit filtering the Terser default configuration using `addFilter( 'plugin/terser/defaults', cb )`. +- Permit filtering the Terser default configuration using `addFilter( 'plugins/terser/defaults', cb )`. - Remove `OptimizeCssAssetsPlugin` (`plugins.optimizeCssAssets()`) in favor of Webpack 5-compatible [CssMinimizerPlugin](https://github.com/webpack-contrib/css-minimizer-webpack-plugin) (`plugins.cssMinimizer()`) - Remove `plugins.hotModuleReplacement()`, which is now handled automatically by the DevServer in `hot` mode. - Include the `contenthash` in generated CSS filenames. [#204](https://github.com/humanmade/webpack-helpers/pull/204) diff --git a/docs/modules/presets.md b/docs/modules/presets.md index aa9af16..9a38dad 100644 --- a/docs/modules/presets.md +++ b/docs/modules/presets.md @@ -116,13 +116,13 @@ Array values are _merged_ when processing a preset, not overwritten. This allows const { addFilter } = require( 'helpers' ); // Intercept the "sass" loader and replace it with a Stylus loader. -addFilter( 'loader/sass', () => { +addFilter( 'loaders/sass', () => { return { loader: require.resolve( 'stylus-loader' ), }; } ); // Alter presets to use a different targeting regular expression. -addFilter( 'preset/stylesheet-loaders', ( loader ) => { +addFilter( 'presets/stylesheet-loaders', ( loader ) => { loader.test = /.\styl$/; return loader; } ); diff --git a/src/helpers/filters.test.js b/src/helpers/filters.test.js index 9f25e05..09fce2b 100644 --- a/src/helpers/filters.test.js +++ b/src/helpers/filters.test.js @@ -70,18 +70,18 @@ describe( 'filters', () => { it( 'adds a callback for the specified filter', () => { const hook = () => 'Woo'; - addFilter( 'loader/js', hook ); - expect( getCallbacks( 'loader/js' ) ).toEqual( [ hook ] ); + addFilter( 'loaders/js', hook ); + expect( getCallbacks( 'loaders/js' ) ).toEqual( [ hook ] ); } ); it( 'adds callbacks with the specified priority', () => { const hook = () => 'Woo'; const hook2 = () => 'Woo-er'; const hook3 = () => 'Woo-est'; - addFilter( 'loader/js', hook2, 9 ); - addFilter( 'loader/js', hook3, 11 ); - addFilter( 'loader/js', hook ); - expect( getCallbacks( 'loader/js' ) ).toEqual( [ hook2, hook, hook3 ] ); + addFilter( 'loaders/js', hook2, 9 ); + addFilter( 'loaders/js', hook3, 11 ); + addFilter( 'loaders/js', hook ); + expect( getCallbacks( 'loaders/js' ) ).toEqual( [ hook2, hook, hook3 ] ); } ); } ); @@ -92,25 +92,25 @@ describe( 'filters', () => { it( 'removes a previously-added callback for the specified filter', () => { const hook = () => 'Woo'; - addFilter( 'loader/js', hook ); - removeFilter( 'loader/js', hook ); - expect( getCallbacks( 'loader/js' ) ).toEqual( [] ); + addFilter( 'loaders/js', hook ); + removeFilter( 'loaders/js', hook ); + expect( getCallbacks( 'loaders/js' ) ).toEqual( [] ); } ); it( 'only removes the requested callback', () => { const hook = () => 'Woo'; const hook2 = () => 'Woo-er'; - addFilter( 'loader/js', hook ); - addFilter( 'loader/js', hook2 ); - removeFilter( 'loader/js', hook ); - expect( getCallbacks( 'loader/js' ) ).toEqual( [ hook2 ] ); + addFilter( 'loaders/js', hook ); + addFilter( 'loaders/js', hook2 ); + removeFilter( 'loaders/js', hook ); + expect( getCallbacks( 'loaders/js' ) ).toEqual( [ hook2 ] ); } ); it( 'does not remove a callback if the priority does not match', () => { const hook = () => 'Woo'; - addFilter( 'loader/js', hook, 9 ); - removeFilter( 'loader/js', hook, 10 ); - expect( getCallbacks( 'loader/js' ) ).toEqual( [ hook ] ); + addFilter( 'loaders/js', hook, 9 ); + removeFilter( 'loaders/js', hook, 10 ); + expect( getCallbacks( 'loaders/js' ) ).toEqual( [ hook ] ); } ); } ); diff --git a/src/loaders.js b/src/loaders.js index c2ef27c..afdc564 100644 --- a/src/loaders.js +++ b/src/loaders.js @@ -78,9 +78,9 @@ loaders.postcss.defaults = { options: { postcssOptions: { ident: 'postcss', - plugins: applyFilters( 'loader/postcss/plugins', [ + plugins: applyFilters( 'loaders/postcss/plugins', [ postcssFlexbugsFixes, - postcssPresetEnv( applyFilters( 'loader/postcss/preset-env', { + postcssPresetEnv( applyFilters( 'loaders/postcss/preset-env', { autoprefixer: { flexbox: 'no-2009', }, diff --git a/src/plugins.js b/src/plugins.js index 4920d20..094a0da 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -187,7 +187,7 @@ module.exports = { * @returns {TerserPlugin} A configured TerserPlugin instance. */ terser: ( options = {} ) => new TerserPlugin( { - ...applyFilters( 'plugin/terser/defaults', { + ...applyFilters( 'plugins/terser/defaults', { terserOptions: { parse: { // We want terser to parse ecma 8 code. However, we don't want it diff --git a/src/presets.js b/src/presets.js index 284fbed..f9cc8a5 100644 --- a/src/presets.js +++ b/src/presets.js @@ -144,7 +144,7 @@ const development = ( config = {} ) => { // Parse styles using SASS, then PostCSS. // Pass environment name as second parameter to give flexibility when filtering. applyFilters( - 'preset/stylesheet-loaders', + 'presets/stylesheet-loaders', { test: /\.s?css$/, use: [ @@ -296,7 +296,7 @@ const production = ( config = {} ) => { // Parse styles using SASS, then PostCSS. // Pass environment name as second parameter to give flexibility when filtering. applyFilters( - 'preset/stylesheet-loaders', + 'presets/stylesheet-loaders', { test: /\.s?css$/, use: [ diff --git a/src/presets.test.js b/src/presets.test.js index ab62cf1..228ce21 100644 --- a/src/presets.test.js +++ b/src/presets.test.js @@ -282,11 +282,11 @@ describe( 'presets', () => { } ); it( 'permits filtering the computed output of individual loaders', () => { - addFilter( 'loader/assets', ( loader ) => { + addFilter( 'loaders/assets', ( loader ) => { loader.test = /\.(png|jpg|jpeg|gif|svg)$/; return loader; } ); - addFilter( 'loader/resource', ( loader ) => { + addFilter( 'loaders/resource', ( loader ) => { loader.options = { publicPath: '../../', }; @@ -320,11 +320,11 @@ describe( 'presets', () => { } ); it( 'permits filtering the entire stylesheet loader chain', () => { - addFilter( 'preset/stylesheet-loaders', ( loader ) => { + addFilter( 'presets/stylesheet-loaders', ( loader ) => { loader.test = /\.styl$/; return loader; } ); - addFilter( 'loader/sass', () => ( { + addFilter( 'loaders/sass', () => ( { loader: 'stylus', mode: 'development', } ) ); @@ -348,8 +348,8 @@ describe( 'presets', () => { } ); it( 'permits skipping a specific stylesheet loader by filtering it to null', () => { - addFilter( 'loader/postcss', returnNull ); - addFilter( 'loader/sass', returnNull ); + addFilter( 'loaders/postcss', returnNull ); + addFilter( 'loaders/sass', returnNull ); const config = development( { entry: { main: 'some-file.js', @@ -375,8 +375,8 @@ describe( 'presets', () => { } ); it( 'does not include null loader entries if a loader was disabled with a filter', () => { - addFilter( 'preset/stylesheet-loaders', returnNull ); - addFilter( 'loader/ts', returnNull ); + addFilter( 'presets/stylesheet-loaders', returnNull ); + addFilter( 'loaders/ts', returnNull ); const config = development( { entry: { main: 'some-file.js', @@ -536,11 +536,11 @@ describe( 'presets', () => { } ); it( 'permits filtering the computed output of individual loaders', () => { - addFilter( 'loader/assets', ( loader ) => { + addFilter( 'loaders/assets', ( loader ) => { loader.test = /\.(png|jpg|jpeg|gif|svg)$/; return loader; } ); - addFilter( 'loader/resource', ( loader ) => { + addFilter( 'loaders/resource', ( loader ) => { loader.options = { publicPath: '../../', }; @@ -574,11 +574,11 @@ describe( 'presets', () => { } ); it( 'permits filtering the entire stylesheet loader chain', () => { - addFilter( 'preset/stylesheet-loaders', ( loader ) => { + addFilter( 'presets/stylesheet-loaders', ( loader ) => { loader.test = /\.styl$/; return loader; } ); - addFilter( 'loader/sass', () => ( { + addFilter( 'loaders/sass', () => ( { loader: 'stylus', } ) ); const config = production( { @@ -600,8 +600,8 @@ describe( 'presets', () => { } ); it( 'permits skipping a specific stylesheet loader by filtering it to null', () => { - addFilter( 'loader/postcss', returnNull ); - addFilter( 'loader/sass', returnNull ); + addFilter( 'loaders/postcss', returnNull ); + addFilter( 'loaders/sass', returnNull ); const config = production( { entry: { main: 'some-file.js', @@ -623,8 +623,8 @@ describe( 'presets', () => { } ); it( 'does not include null loader entries if a loader was disabled with a filter', () => { - addFilter( 'preset/stylesheet-loaders', returnNull ); - addFilter( 'loader/ts', returnNull ); + addFilter( 'presets/stylesheet-loaders', returnNull ); + addFilter( 'loaders/ts', returnNull ); const config = production( { entry: { main: 'some-file.js', From bc37992668fb9779eb3ed2852080680340e23d22 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Mon, 23 May 2022 12:30:22 -0400 Subject: [PATCH 53/83] Document the addFilter and removeFilter modules --- docs/modules/helpers.md | 64 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/docs/modules/helpers.md b/docs/modules/helpers.md index 73bfc73..0b71beb 100644 --- a/docs/modules/helpers.md +++ b/docs/modules/helpers.md @@ -98,3 +98,67 @@ module.exports = helpers.choosePort( 9090 ).then( port => [ } ), ] ); ``` + +## Filters + +Webpack Helpers v1.0 introduced a new system for altering the behavior of bundled loaders, plugins, and presets, also covered under [Customizing Presets](./presets.html#customizing-presets). This system uses a filtering approach similar to [the way PHP filters work within WordPress](https://developer.wordpress.org/reference/functions/add_filter/), with `addFilter` and `removeFilter` helpers you can use to modify various types of internal data at runtime. + +```js +// webpack.config.js +const { helpers } = require( '@humanmade/webpack-helpers' ); + +const { addFilter, removeFilter } = helpers; + +// Hook onto a specific filter by name, and provide a callback function to +// alter the value returned from that filter. +addFilter( 'filter/name', function( value ) { + return 'modified value'; +} ); + +// To filter only some values, you can remove a filter by passing the same +// function reference to removeFilter. +const filterFunction = ( value ) => 'modified value'; + +addFilter( 'filter/name', filterFunction ); + +// Do something where the filter gets applied. + +removeFilter( 'filter/name', filterFunction ); + +// Now if you do that same thing, the filter will no longer apply. +``` + +### Filter List + +Each [loader](./loaders.html) exposes at minimum two filters, `loaders/{name}` and `loaders/{name}/defaults`. For example, the defaults for `loaders.ts()` can be filtered using + +```js +addFilter( 'loaders/ts/defaults', ( loaderDefaultsObject ) => { + // return a filtered value +} ); +``` +or the computed final loader object can be modified after the fact with + +```js +addFilter( 'loaders/ts', ( loaderObject ) => { + // return a filtered value +} ); +``` + +Additional filters provided by Webpack Helpers: + +**`presets/stylesheet-loaders`** + +Filter the [stylesheet loader chain](https://webpack.js.org/concepts/loaders/#configuration) used in the `presets.production()` and `presets.development()` helpers. Functions added to this filter receive the stylesheet chain as their first argument, and the name of the compilation mode (`production` or `development`) as the second argument. + +**`plugins/terser/defaults`** + +Filter the [TerserPlugin default options object](https://webpack.js.org/plugins/terser-webpack-plugin/#options) if you wish to alter the minification settings used by the production preset. + +**`loaders/postcss/plugins`** + +Filter the list of [PostCSS plugins](https://github.com/postcss/postcss/blob/main/docs/plugins.md) used by the `loaders.postcss()` loader. + +**`loaders/postcss/preset-env`** + +Filter the [`postcss-preset-env` options](https://github.com/csstools/postcss-preset-env#options) used by the `loaders.postcss()` loader. From 0743793515167a6d2fead9502be64f68eadd8c73 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Mon, 23 May 2022 12:33:27 -0400 Subject: [PATCH 54/83] Finish converting loader names from "loader/" to "loaders/" --- docs/modules/presets.md | 2 +- src/loaders.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/modules/presets.md b/docs/modules/presets.md index 9a38dad..40502b4 100644 --- a/docs/modules/presets.md +++ b/docs/modules/presets.md @@ -139,4 +139,4 @@ Available hooks: - `loader/{loader name}/default`: Alter the default values before passing it to the loader configuration merge function. - `loader/postcss/plugins`: Filter the list of PostCSS plugins used by that specific loader. - `loader/postcss/preset-env`: Filter the configuration object passed to the PostCSS Preset Env plugin. -- `preset/stylesheet-loaders`: Filter the computed chain of stylesheet loaders output by the preset factories. This hook receives a second argument `environment` which will show either "production" or "development," to permit per-environment filtering. +- `presets/stylesheet-loaders`: Filter the computed chain of stylesheet loaders output by the preset factories. This hook receives a second argument `environment` which will show either "production" or "development," to permit per-environment filtering. diff --git a/src/loaders.js b/src/loaders.js index afdc564..d829956 100644 --- a/src/loaders.js +++ b/src/loaders.js @@ -20,9 +20,9 @@ const createLoaderFactory = loaderKey => { // Generate the requested loader definition. Expose filter seams both // to customize the defaults, and to alter the final rendered output. return applyFilters( - `loader/${ loaderKey }`, + `loaders/${ loaderKey }`, deepMerge( - applyFilters( `loader/${ loaderKey }/defaults`, loaders[ loaderKey ].defaults ), + applyFilters( `loaders/${ loaderKey }/defaults`, loaders[ loaderKey ].defaults ), options ) ); From cde3aeed6a923c940d6edbbcd1a4cfbcbf17d7b2 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Mon, 23 May 2022 12:31:20 -0400 Subject: [PATCH 55/83] Tag v1.0.0-beta.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 564d94e..ebde942 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "1.0.0-beta.7", + "version": "1.0.0-beta.8", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made", From 1591e844f0fd8f33c46dab189ec7dcd8c683ef9e Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Mon, 23 May 2022 16:06:13 -0400 Subject: [PATCH 56/83] Fix issue where simple build report plugin was listed as dev dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ebde942..a5b3403 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "eslint": "^7.32.0", "jest": "^26.6.3", "sass": "^1.51.0", - "simple-build-report-webpack-plugin": "^1.0.0", "typescript": "^4.6.3", "webpack": "^5.72.0", "webpack-cli": "^4.9.2", @@ -62,6 +61,7 @@ "run-parallel": "^1.2.0", "sass-loader": "^12.6.0", "signal-exit": "^3.0.7", + "simple-build-report-webpack-plugin": "^1.0.0", "source-map-loader": "^3.0.1", "style-loader": "^3.3.1", "terser-webpack-plugin": "^5.3.1", From ee6d54195973a26cb2bdb8bd1c8eafed54a8b434 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Mon, 23 May 2022 16:06:24 -0400 Subject: [PATCH 57/83] Tag v1.0.0-beta.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a5b3403..c194939 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "1.0.0-beta.8", + "version": "1.0.0-beta.9", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made", From e632c895ea2f2b72ccf1c54ff6b9b6a2e1e26537 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Tue, 24 May 2022 09:46:12 -0400 Subject: [PATCH 58/83] Add note about returning null from loader --- docs/modules/helpers.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/modules/helpers.md b/docs/modules/helpers.md index 0b71beb..f52aea3 100644 --- a/docs/modules/helpers.md +++ b/docs/modules/helpers.md @@ -145,6 +145,8 @@ addFilter( 'loaders/ts', ( loaderObject ) => { } ); ``` +If you return `null` from the `loaders/{name}` filter, it will remove that loader from the preset entirely. + Additional filters provided by Webpack Helpers: **`presets/stylesheet-loaders`** From bfbf0592e60b44757c989aa36ebac4b2b0378b69 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Tue, 24 May 2022 10:12:28 -0400 Subject: [PATCH 59/83] Fix bug where isObj in deep-merge would return true for "null" --- src/helpers/deep-merge.js | 2 +- src/helpers/deep-merge.test.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/helpers/deep-merge.js b/src/helpers/deep-merge.js index cf0f3a5..586bcbd 100644 --- a/src/helpers/deep-merge.js +++ b/src/helpers/deep-merge.js @@ -12,7 +12,7 @@ const isArr = val => Array.isArray( val ); * @param {*} val A value to test for object-ness. * @returns {Boolean} Whether the provided argument is an object. */ -const isObj = val => ( typeof val === 'object' ); +const isObj = val => ( typeof val === 'object' && val !== null ); /** * Given two objects, merge array and object properties, then allow scalar diff --git a/src/helpers/deep-merge.test.js b/src/helpers/deep-merge.test.js index 1ec25dc..2867239 100644 --- a/src/helpers/deep-merge.test.js +++ b/src/helpers/deep-merge.test.js @@ -35,4 +35,19 @@ describe( 'helpers/deep-merge', () => { }, } ); } ); + + it( 'can remove a property by specifying `undefined` in the second merged object', () => { + const obj1 = { + test: '.jsx?$', + devServer: { + port: 9191, + }, + }; + const obj2 = { + devServer: undefined, + }; + expect( deepMerge( obj1, obj2 ) ).toEqual( { + test: '.jsx?$', + } ); + } ); } ); From 015ad69c9f1cbf9c3fccbdf8156afeee23539f96 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Tue, 24 May 2022 12:26:00 -0400 Subject: [PATCH 60/83] Include modern image formats in image asset test --- src/loaders.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/loaders.js b/src/loaders.js index d829956..2c93e26 100644 --- a/src/loaders.js +++ b/src/loaders.js @@ -35,7 +35,7 @@ const createLoaderFactory = loaderKey => { } ); loaders.assets.defaults = { - test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|eot|ttf)$/, + test: /\.(png|jpg|jpeg|gif|avif|webp|svg|woff|woff2|eot|ttf)$/, type: 'asset', parser: { dataUrlCondition: { From 4ad3c3e1e8729260b4a1c47eee368fd613008e1c Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Tue, 24 May 2022 12:26:13 -0400 Subject: [PATCH 61/83] Upgrade bundled SimpleBuildReportPlugin --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c194939..c9bbcf9 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "run-parallel": "^1.2.0", "sass-loader": "^12.6.0", "signal-exit": "^3.0.7", - "simple-build-report-webpack-plugin": "^1.0.0", + "simple-build-report-webpack-plugin": "^1.0.2", "source-map-loader": "^3.0.1", "style-loader": "^3.3.1", "terser-webpack-plugin": "^5.3.1", From c319d171c92756f95526f08774f274f125847751 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Tue, 24 May 2022 12:26:44 -0400 Subject: [PATCH 62/83] Tag v1.0.0-beta.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c9bbcf9..e69d44c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "1.0.0-beta.9", + "version": "1.0.0-beta.10", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made", From e95a890e25cc1637b98c3f7cd99aaa32afd2b7ed Mon Sep 17 00:00:00 2001 From: K Adam White Date: Wed, 15 Jun 2022 10:06:51 -0400 Subject: [PATCH 63/83] Provide preset config as filter argument when appropriate (#207) This change permits filtering a loader based on the context of where it is used, which adds needed flexibility when using multi-configuration Webpack setups. The key specific changes: - Standardize filter names - When using presets.production() or presets.development(), pass the provided preset `config` object through to loader and preset filters so the filter can take contextual action - Improve documentation of filters in code - Add filter reference documentation to docs site - Remove or edit outdated documentation around customizing hooks Individual commits, squashed from #207: * Remove oneOf object wholesale in removeNullLoaders if its array is empty * Pass preset config argument to all filters when using a preset * Improve inline documentation for filters * Complete inline doc-blocks for filter hooks * Change loader name from "assets" to "asset" for consistency with other loaders * Change loader name from "sourcemaps" to "sourcemap" for consistency with other loaders * Add reference documentation for available hooks * Swap "mutate the defaults" docs in favor of recommending filtering * Add missing preset env hook argument docs --- docs/modules/loaders.md | 37 ++++++++---- docs/modules/presets.md | 38 +++++++++--- docs/reference.md | 10 ++++ docs/reference/hooks.md | 117 ++++++++++++++++++++++++++++++++++++ src/loaders.js | 52 +++++++++++++--- src/loaders.test.js | 6 +- src/presets.js | 68 +++++++++++++-------- src/presets.test.js | 128 ++++++++++++++++++++++++++++++++++++++-- 8 files changed, 396 insertions(+), 60 deletions(-) create mode 100644 docs/reference.md create mode 100644 docs/reference/hooks.md diff --git a/docs/modules/loaders.md b/docs/modules/loaders.md index 8c65b45..3dd58de 100644 --- a/docs/modules/loaders.md +++ b/docs/modules/loaders.md @@ -6,19 +6,23 @@ nav_order: 3 # Loaders Module -`const { loaders } = require( '@humanmade/webpack-helpers' );` +```js +const { loaders } = require( '@humanmade/webpack-helpers' ); +``` This module provides functions that generate configurations for commonly-needed Webpack loaders. Use them within the `.module.rules` array, or use `presets.development()`/`presets.production()` to opt-in to some opinionated defaults. -- `loaders.assets()`: Return a configured Webpack module loader rule for [`asset` modules](https://webpack.js.org/guides/asset-modules/#inlining-assets) which will be inlined if small enough. -- `loaders.js()`: Return a configured Webpack module loader rule for `js-loader`. -- `loaders.ts()`: Return a configured Webpack module loader rule for `ts-loader`. -- `loaders.style()`: Return a configured Webpack module loader rule for `style-loader`. +- `loaders.asset()`: Return a configured Webpack module loader rule for [`asset` modules](https://webpack.js.org/guides/asset-modules/#inlining-assets) which will be inlined when small enough. - `loaders.css()`: Return a configured Webpack module loader rule for `css-loader`. +- `loaders.js()`: Return a configured Webpack module loader rule for `js-loader`. - `loaders.postcss()`: Return a configured Webpack module loader rule for `postcss-loader`. -- `loaders.sass()`: Return a configured Webpack module loader rule for `sass-loader`. -- `loaders.sourcemaps()`: Return a configured Webpack module loader rule for `source-map-loader`. - `loaders.resource()`: Return a configured Webpack module loader rule for [`asset/resource` modules](https://webpack.js.org/guides/asset-modules/#resource-assets). +- `loaders.sass()`: Return a configured Webpack module loader rule for `sass-loader`. +- `loaders.sourcemap()`: Return a configured Webpack module loader rule for `source-map-loader`. +- `loaders.style()`: Return a configured Webpack module loader rule for `style-loader`. +- `loaders.ts()`: Return a configured Webpack module loader rule for `ts-loader`. + +The output from these loaders can optionally be [filtered](https://humanmade.github.io/webpack-helpers/reference/hooks.html). ## Customizing Loaders @@ -41,16 +45,25 @@ module.exports = { }; ``` -To alter the configuration for a loader prior to use within a preset, you may mutate the `.defaults` property on the loader method. +To alter the configuration for a loader prior to use within a preset, you may either [filter](https://humanmade.github.io/webpack-helpers/reference/hooks.html) the default loader options, or the final merged loader configuration. ```js const { helpers, loaders, presets } = require( '@humanmade/webpack-helpers' ); +const { addFilter } = helpers; + +// Adjust the loader defaults. +addFilter( 'loaders/js/defaults', ( defaults ) => ( { + ...defaults, + include: helpers.filePath( 'themes/my-theme/src' ), +} ) ); -// Mutate the loader defaults. -loaders.js.defaults.include = helpers.filePath( 'themes/my-theme/src' ); -loaders.css.defaults.options.url = false; +addFilter( 'loaders/css/defaults', ( defaults ) => { + defaults.options.url = false; + return defaults; +} ); +// The above customizations will now apply to all calls to loader or preset factories. module.exports = presets.development( { /* ... */ } ); ``` -These loaders are also used by the [presets](https://humanmade.github.io/webpack-helpers/modules/presets.html) methods described above. To adjust the behavior of a loader for a specific configuration generated using a preset, you may pass a second argument to the preset defining a filter function which can modify loader options as they are computed. See ["Customizing Presets"](https://humanmade.github.io/webpack-helpers/modules/presets.html#customizing-presets) for more information. +These loaders are also used by the [presets](https://humanmade.github.io/webpack-helpers/modules/presets.html) methods described above. To adjust the behavior of a loader for a specific configuration generated using a preset, see ["Customizing Presets"](https://humanmade.github.io/webpack-helpers/modules/presets.html#customizing-presets). diff --git a/docs/modules/presets.md b/docs/modules/presets.md index 40502b4..c7fb68f 100644 --- a/docs/modules/presets.md +++ b/docs/modules/presets.md @@ -113,7 +113,8 @@ module.exports.optimization.minimizer = [ Array values are _merged_ when processing a preset, not overwritten. This allows you to easily add plugins, but it can make it hard to _remove_ values from array properties like the module loader rules or plugins list. To change loader configuration options without completely removing a loader, or adjust loaders deep within a generated configuration tree, this library provides a `addFilter` helper function. `addFilter()` lets you register a callback which can transform the value of each computed loader, as well as some other useful configuration values. ```js -const { addFilter } = require( 'helpers' ); +const { helpers, presets } = require( '@humanmade/webpack-helpers' ); +const { addFilter } = helpers; // Intercept the "sass" loader and replace it with a Stylus loader. addFilter( 'loaders/sass', () => { @@ -128,15 +129,36 @@ addFilter( 'presets/stylesheet-loaders', ( loader ) => { } ); // Now, use the preset as normal and it will pick up Stylus files instead! -const config = production.preset( +const config = presets.production( { /* ...normal configuration options as described above ... */ } ); ``` -Available hooks: +If you are using a multi-configuration Webpack setup where the config file exports an array of individual configurations, you may wish to filter a loader, preset chain, or other value for only one of those configuration objects. To permit this, each filter callback will be passed the preset's configuration object as a final argument when the filter or loader is called in the context of a preset. For example, -- `loader/{loader name}`: Adjust the final output of [any of the methods on the `loaders` object](./loaders.html), for example `loader/sass` or `loader/js`. -- `loader/{loader name}/default`: Alter the default values before passing it to the loader configuration merge function. -- `loader/postcss/plugins`: Filter the list of PostCSS plugins used by that specific loader. -- `loader/postcss/preset-env`: Filter the configuration object passed to the PostCSS Preset Env plugin. -- `presets/stylesheet-loaders`: Filter the computed chain of stylesheet loaders output by the preset factories. This hook receives a second argument `environment` which will show either "production" or "development," to permit per-environment filtering. +```js +const { helpers, externals, presets } = require( '@humanmade/webpack-helpers' ); +const { addFilter } = helpers; + +// Do not use the JS loader in the frontend build. +addFilter( 'presets/js', ( loader, config ) => { + if ( config.name === 'frontend' ) { + return null; + } + return frontend; +} ); + +module.exports = [ + presets.production( { + name: 'frontend', + // ... + } ), + presets.production( { + name: 'editor', + externals, + // ... + } ), +]; +``` + +For more information, see [the full hooks reference.](https://humanmade.github.io/webpack-helpers/reference/hooks.html). diff --git a/docs/reference.md b/docs/reference.md new file mode 100644 index 0000000..deb8d0e --- /dev/null +++ b/docs/reference.md @@ -0,0 +1,10 @@ +--- +title: Reference +has_children: true +permalink: /reference +nav_order: 3 +--- + +# Reference Documentation + +Code-level documentation of specific components of `@humanmade/webpack-helpers`. diff --git a/docs/reference/hooks.md b/docs/reference/hooks.md new file mode 100644 index 0000000..0662a2b --- /dev/null +++ b/docs/reference/hooks.md @@ -0,0 +1,117 @@ +--- +title: Filter Hooks +parent: Reference +nav_order: 1 +--- + +# Filter Hooks + +These hooks are provided by Webpack Helpers to customize the behavior of a specific loader or preset within your project. + +`{loader name}` in the below filters may be any slug for an available [loader](https://humanmade.github.io/webpack-helpers/modules/loaders.html): `asset`, `css`, `js`, `postcss`, `resource`, `sass`, `sourcemap`, `style`, and `ts`. + +Remember to always return a value from a filter callback. + +## `loaders/{loader name}` + +Filter the full loader configuration object after merging any user-provided options with the (filtered) defaults. + +When called in the context of a preset, the callback will receive the preset factory's configuration object as a second argument. + +**Arguments:** + + name | type | description +----- | ---- | ------ +`loader` | `Object` | Loader configuration object. +`config` | `Object` or `null` | Preset configuration object if a preset is being rendered, else `null`. + +Return `null` from the callback to remove this loader from preset-generated configurations. + +```js +addFilter( 'loaders/{loader name}', ( loader, config = null ) => { + // Filter the loader definition globally or based on the specific + // configuration options provided to a preset factory. + return loader; +} ); +``` + +## `loaders/{loader name}/defaults` + +Adjust the default values used to define a given loader configuration. + +When called in the context of a preset, the callback will receive the preset factory's configuration object as a second argument. + +**Arguments:** + + name | type | description +----- | ---- | ------ +`defaults` | `Object` | Defaults for this specific loader. +`config` | `Object` or `null` | Preset configuration object if a preset is being rendered, else `null`. + +```js +addFilter( 'loaders/{loader name}/defaults', ( defaults, config = null ) => { + // Filter the loader's defaults globally or based on the specific + // configuration options provided to a preset factory. + return defaults; +} ); +``` + +## `presets/stylesheet-loaders` + +Filter the loaders used to process stylesheet imports when building your project. + +The callback will receive the environment type of the preset being rendered as its second argument, and the preset's configuration object argument as a third argument. + +**Arguments:** + + name | type | description +----- | ---- | ------ +`loader` | `Object` | Combined stylesheet loader rule. +`environment` | `string` | `'development'` or `'production'`. +`config` | `Object` | Preset configuration object. + +```js +addFilter( 'presets/stylesheet-loaders', ( loader, environment, config ) => { + // Filter the configured stylesheet loaders based on environment or the + // specific configuration options provided to the preset factory. + return loader; +} ); +``` + +## `loaders/postcss/plugins` + +Filter the default list of PostCSS plugins used by the PostCSS loader. + +Unlike the hooks above, callbacks added to this filter do not receive the preset configuration object. + +**Arguments:** + + name | type | description +----- | ---- | ------ +`pluginsArray` | `Array` | Array of [PostCSS plugin](https://github.com/postcss/postcss#plugins) definitions. + +```js +addFilter( 'loaders/postcss/plugins', ( pluginsArray ) => { + // Filter the plugins loaded by PostCSS. + return pluginsArray; +} ); +``` + +## `loaders/postcss/preset-env` + +Filter the [`postcss-preset-env` plugin's](https://github.com/csstools/postcss-plugins/tree/main/plugin-packs/postcss-preset-env#readme) configuration object. + +Unlike the hooks above, callbacks added to this filter do not receive the preset configuration object. + +**Arguments:** + + name | type | description +----- | ---- | ------ +`pluginOptions` | `Object` | `postcss-preset-env` plugin options object. + +```js +addFilter( 'loaders/postcss/preset-env', ( pluginOptions ) => { + // Filter the PostCSS Preset Env plugin options. + return pluginOptions; +} ); +``` diff --git a/src/loaders.js b/src/loaders.js index 2c93e26..e1064e4 100644 --- a/src/loaders.js +++ b/src/loaders.js @@ -15,26 +15,50 @@ const { applyFilters } = require( './helpers/filters' ); */ const loaders = {}; +/** + * Create a loader factory function for a given loader slug. + * + * @private + * @param {string} loaderKey Loader slug + * @returns {Function} Factory function to generate and filter a loader with the specified slug. + */ const createLoaderFactory = loaderKey => { - return ( options ) => { - // Generate the requested loader definition. Expose filter seams both - // to customize the defaults, and to alter the final rendered output. + return ( options = {}, config = null ) => { + /** + * Generate the requested loader definition. + * + * Allows customization of the final rendered loader via the loaders/{loaderslug} + * filter, which also receives the configuration passed to a preset factory if + * the loader is invoked in the context of a preset. + * + * @hook loaders/{$loader_slug} + * @param {Object} loader Complete loader definition object, after merging user-provided values with filtered defaults. + * @param {Object|null} [config] Configuration object for the preset being rendered, if loader is called while generating a preset. + */ return applyFilters( `loaders/${ loaderKey }`, deepMerge( - applyFilters( `loaders/${ loaderKey }/defaults`, loaders[ loaderKey ].defaults ), + /** + * Filter the loader's default configuration. + * + * @hook loaders/{$loader_slug}/defaults + * @param {Object} options Loader default options object. + * @param {Object|null} [config] Configuration object for the preset being rendered, if loader is called while generating a preset. + */ + applyFilters( `loaders/${ loaderKey }/defaults`, loaders[ loaderKey ].defaults, config ), options - ) + ), + config ); }; }; // Define all supported loader factories within the loaders object. -[ 'assets', 'js', 'ts', 'style', 'css', 'postcss', 'sass', 'sourcemaps', 'resource' ].forEach( loaderKey => { +[ 'asset', 'js', 'ts', 'style', 'css', 'postcss', 'sass', 'sourcemap', 'resource' ].forEach( loaderKey => { loaders[ loaderKey ] = createLoaderFactory( loaderKey ); } ); -loaders.assets.defaults = { +loaders.asset.defaults = { test: /\.(png|jpg|jpeg|gif|avif|webp|svg|woff|woff2|eot|ttf)$/, type: 'asset', parser: { @@ -78,8 +102,20 @@ loaders.postcss.defaults = { options: { postcssOptions: { ident: 'postcss', + /** + * Filter the default PostCSS Plugins array. + * + * @hook loaders/postcss/plugins + * @param {Array} plugins Array of PostCSS plugins. + */ plugins: applyFilters( 'loaders/postcss/plugins', [ postcssFlexbugsFixes, + /** + * Filter the default PostCSS Preset Env configuration. + * + * @hook loaders/postcss/preset-env + * @param {Object} presetEnvConfig PostCSS Preset Env plugin configuration. + */ postcssPresetEnv( applyFilters( 'loaders/postcss/preset-env', { autoprefixer: { flexbox: 'no-2009', @@ -100,7 +136,7 @@ loaders.sass.defaults = { }, }; -loaders.sourcemaps.defaults = { +loaders.sourcemap.defaults = { test: /\.(js|mjs|jsx|ts|tsx|css)$/, exclude: /@babel(?:\/|\\{1,2})runtime/, enforce: 'pre', diff --git a/src/loaders.test.js b/src/loaders.test.js index 9cbc0d2..8cce7a8 100644 --- a/src/loaders.test.js +++ b/src/loaders.test.js @@ -30,7 +30,7 @@ describe( 'loaders', () => { } ); } ); - describe( '.assets()', () => { + describe( '.asset()', () => { it( 'tests for static assets', () => { [ 'file.png', @@ -43,14 +43,14 @@ describe( 'loaders', () => { 'file.eot', 'file.ttf', ].forEach( acceptedFileType => { - expect( acceptedFileType.match( loaders.assets().test ) ).not.toBeNull(); + expect( acceptedFileType.match( loaders.asset().test ) ).not.toBeNull(); } ); [ 'file.js', 'file.css', 'file.html', ].forEach( unacceptedFileType => { - expect( unacceptedFileType.match( loaders.assets().test ) ).toBeNull(); + expect( unacceptedFileType.match( loaders.asset().test ) ).toBeNull(); } ); } ); } ); diff --git a/src/presets.js b/src/presets.js index f9cc8a5..601c501 100644 --- a/src/presets.js +++ b/src/presets.js @@ -65,9 +65,13 @@ const ifInstalled = ( packageName, loader ) => { const removeNullLoaders = ( moduleRules ) => moduleRules .map( ( rule ) => { if ( rule && Array.isArray( rule.oneOf ) ) { + const loaders = removeNullLoaders( rule.oneOf ); + if ( ! Array.isArray( loaders ) || ! loaders.length ) { + return null; + } return { ...rule, - oneOf: removeNullLoaders( rule.oneOf ), + oneOf: loaders, }; } if ( rule && Array.isArray( rule.use ) ) { @@ -129,48 +133,57 @@ const development = ( config = {} ) => { strictExportPresence: true, rules: removeNullLoaders( [ // Handle node_modules packages that contain sourcemaps. - loaders.sourcemaps(), + loaders.sourcemap( {}, config ), { // "oneOf" will traverse all following loaders until one will // match the requirements. If no loader matches, it will fall // back to the resource loader at the end of the loader list. oneOf: [ // Enable processing TypeScript, if installed. - ...ifInstalled( 'typescript', loaders.ts() ), + ...ifInstalled( 'typescript', loaders.ts( {}, config ) ), // Process JS with Babel. - loaders.js(), + loaders.js( {}, config ), // Handle static asset files. - loaders.assets(), - // Parse styles using SASS, then PostCSS. - // Pass environment name as second parameter to give flexibility when filtering. + loaders.asset( {}, config ), + /** + * Filter the full stylesheet loader definition for this preset. + * + * By default parses styles using Sass and then PostCSS. + * + * @hook presets/stylesheet-loaders + * @param {Object} loader Stylesheet loader rule. + * @param {string} environment "development" or "production". + * @param {Object} config Preset configuration object. + */ applyFilters( 'presets/stylesheet-loaders', { test: /\.s?css$/, use: [ - loaders.style(), + loaders.style( {}, config ), loaders.css( { options: { sourceMap: true, }, - } ), + }, config ), loaders.postcss( { options: { sourceMap: true, }, - } ), + }, config ), loaders.sass( { options: { sourceMap: true, }, - } ), + }, config ), ], }, - 'development' + 'development', + config ), // Resource loader makes sure any non-matching assets still get served. // When you `import` an asset, you get its (virtual) filename. - loaders.resource(), + loaders.resource( {}, config ), ], }, ] ), @@ -288,13 +301,21 @@ const production = ( config = {} ) => { // back to the resource loader at the end of the loader list. oneOf: [ // Enable processing TypeScript, if installed. - ...ifInstalled( 'typescript', loaders.ts() ), + ...ifInstalled( 'typescript', loaders.ts( {}, config ) ), // Process JS with Babel. - loaders.js(), + loaders.js( {}, config ), // Handle static asset files. - loaders.assets(), - // Parse styles using SASS, then PostCSS. - // Pass environment name as second parameter to give flexibility when filtering. + loaders.asset( {}, config ), + /** + * Filter the full stylesheet loader definition for this preset. + * + * By default parses styles using Sass and then PostCSS. + * + * @hook presets/stylesheet-loaders + * @param {Object} loader Stylesheet loader rule. + * @param {string} environment "development" or "production". + * @param {Object} config Preset configuration object. + */ applyFilters( 'presets/stylesheet-loaders', { @@ -303,16 +324,17 @@ const production = ( config = {} ) => { // Extract CSS to its own file. MiniCssExtractPlugin.loader, // Process SASS into CSS. - loaders.css( cssOptions ), - loaders.postcss( cssOptions ), - loaders.sass( cssOptions ), + loaders.css( cssOptions, config ), + loaders.postcss( cssOptions, config ), + loaders.sass( cssOptions, config ), ], }, - 'production' + 'production', + config ), // Resource loader makes sure any non-matching assets still get served. // When you `import` an asset, you get its (virtual) filename. - loaders.resource(), + loaders.resource( {}, config ), ], }, ] ), diff --git a/src/presets.test.js b/src/presets.test.js index 228ce21..7e1fab6 100644 --- a/src/presets.test.js +++ b/src/presets.test.js @@ -282,7 +282,7 @@ describe( 'presets', () => { } ); it( 'permits filtering the computed output of individual loaders', () => { - addFilter( 'loaders/assets', ( loader ) => { + addFilter( 'loaders/asset', ( loader ) => { loader.test = /\.(png|jpg|jpeg|gif|svg)$/; return loader; } ); @@ -298,7 +298,7 @@ describe( 'presets', () => { }, } ); const resourceLoader = getLoaderByName( config.module.rules, 'asset/resource' ); - const assetsLoader = getLoaderByName( config.module.rules, 'asset' ); + const assetLoader = getLoaderByName( config.module.rules, 'asset' ); const jsLoader = getLoaderByName( config.module.rules, 'babel-loader' ); expect( resourceLoader ).toEqual( expect.objectContaining( { exclude: [ /^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html?$/, /\.json$/ ], @@ -307,7 +307,7 @@ describe( 'presets', () => { publicPath: '../../', }, } ) ); - expect( assetsLoader ).toEqual( expect.objectContaining( { + expect( assetLoader ).toEqual( expect.objectContaining( { test: /\.(png|jpg|jpeg|gif|svg)$/, type: 'asset', parser: { @@ -347,6 +347,20 @@ describe( 'presets', () => { expect( sassLoader ).toBeNull(); } ); + it( 'passes the preset configuration object to the stylesheet chain filter', () => { + const config = { + entry: { + main: 'some-file.js', + }, + }; + addFilter( 'presets/stylesheet-loaders', ( loader, preset, presetConfig ) => { + expect( presetConfig ).toBe( config ); + expect( preset ).toBe( 'development' ); + return loader; + } ); + development( config ); + } ); + it( 'permits skipping a specific stylesheet loader by filtering it to null', () => { addFilter( 'loaders/postcss', returnNull ); addFilter( 'loaders/sass', returnNull ); @@ -391,6 +405,51 @@ describe( 'presets', () => { expect( config.module.rules[ 1 ].oneOf[ 2 ] ) .toEqual( expect.objectContaining( { type: 'asset/resource' } ) ); } ); + + it( 'passes the preset config as argument 2 to loader filters', () => { + addFilter( 'loaders/js', ( options, config ) => { + expect( config ).not.toBeNull(); + expect( config.name ).toBe( 'dev-build' ); + return options; + } ); + development( { + name: 'dev-build', + } ); + } ); + + it( 'permits filtering only a specific invocation of a preset', () => { + addFilter( 'presets/stylesheet-loaders', returnNull ); + addFilter( 'loaders/ts', returnNull ); + + const filterToNullInBuild1 = ( options, config ) => { + if ( config.name === 'build1' ) { + return null; + } + // Simplified version to make test easier. + return options.test ? + { test: options.test } : + { type: options.type }; + }; + addFilter( 'loaders/js', filterToNullInBuild1 ); + addFilter( 'loaders/asset', filterToNullInBuild1 ); + addFilter( 'loaders/resource', filterToNullInBuild1 ); + addFilter( 'loaders/sourcemap', filterToNullInBuild1 ); + + const config1 = development( { name: 'build1' } ); + const config2 = development( { name: 'build2' } ); + + expect( config1.module.rules ).toEqual( [] ); + expect( config2.module.rules ).toEqual( [ + { test: /\.(js|mjs|jsx|ts|tsx|css)$/ }, + { + oneOf: [ + { test: /\.jsx?$/ }, + { test: /\.(png|jpg|jpeg|gif|avif|webp|svg|woff|woff2|eot|ttf)$/ }, + { type: 'asset/resource' }, + ], + }, + ] ); + } ); } ); describe( 'production()', () => { @@ -536,7 +595,7 @@ describe( 'presets', () => { } ); it( 'permits filtering the computed output of individual loaders', () => { - addFilter( 'loaders/assets', ( loader ) => { + addFilter( 'loaders/asset', ( loader ) => { loader.test = /\.(png|jpg|jpeg|gif|svg)$/; return loader; } ); @@ -552,7 +611,7 @@ describe( 'presets', () => { }, } ); const resourceLoader = getLoaderByName( config.module.rules, 'asset/resource' ); - const assetsLoader = getLoaderByName( config.module.rules, 'asset' ); + const assetLoader = getLoaderByName( config.module.rules, 'asset' ); const jsLoader = getLoaderByName( config.module.rules, 'babel-loader' ); expect( resourceLoader ).toEqual( expect.objectContaining( { exclude: [ /^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html?$/, /\.json$/ ], @@ -561,7 +620,7 @@ describe( 'presets', () => { publicPath: '../../', }, } ) ); - expect( assetsLoader ).toEqual( expect.objectContaining( { + expect( assetLoader ).toEqual( expect.objectContaining( { test: /\.(png|jpg|jpeg|gif|svg)$/, type: 'asset', parser: { @@ -599,6 +658,20 @@ describe( 'presets', () => { expect( sassLoader ).toBeNull(); } ); + it( 'passes the preset configuration object to the stylesheet chain filter', () => { + const config = { + entry: { + main: 'some-file.js', + }, + }; + addFilter( 'presets/stylesheet-loaders', ( loader, preset, presetConfig ) => { + expect( presetConfig ).toBe( config ); + expect( preset ).toBe( 'production' ); + return loader; + } ); + production( config ); + } ); + it( 'permits skipping a specific stylesheet loader by filtering it to null', () => { addFilter( 'loaders/postcss', returnNull ); addFilter( 'loaders/sass', returnNull ); @@ -640,4 +713,47 @@ describe( 'presets', () => { .toEqual( expect.objectContaining( { type: 'asset/resource' } ) ); } ); } ); + + it( 'passes the preset config as argument 2 to loader filters', () => { + addFilter( 'loaders/js', ( options, config ) => { + expect( config ).not.toBeNull(); + expect( config.name ).toBe( 'dev-build' ); + return options; + } ); + production( { + name: 'dev-build', + } ); + } ); + + it( 'permits filtering only a specific invocation of a preset', () => { + addFilter( 'presets/stylesheet-loaders', returnNull ); + addFilter( 'loaders/ts', returnNull ); + + const filterToNullInBuild1 = ( options, config ) => { + if ( config.name === 'build1' ) { + return null; + } + // Simplified version to make test easier. + return options.test ? + { test: options.test } : + { type: options.type }; + }; + addFilter( 'loaders/js', filterToNullInBuild1 ); + addFilter( 'loaders/asset', filterToNullInBuild1 ); + addFilter( 'loaders/resource', filterToNullInBuild1 ); + + const config1 = production( { name: 'build1' } ); + const config2 = production( { name: 'build2' } ); + + expect( config1.module.rules ).toEqual( [] ); + expect( config2.module.rules ).toEqual( [ + { + oneOf: [ + { test: /\.jsx?$/ }, + { test: /\.(png|jpg|jpeg|gif|avif|webp|svg|woff|woff2|eot|ttf)$/ }, + { type: 'asset/resource' }, + ], + }, + ] ); + } ); } ); From dc67783d0a665a413163b1cd3e0d105e72ed3059 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Wed, 15 Jun 2022 16:38:53 +0200 Subject: [PATCH 64/83] Author initial upgrade guide --- docs/guides/upgrading-to-v1.md | 59 ++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 docs/guides/upgrading-to-v1.md diff --git a/docs/guides/upgrading-to-v1.md b/docs/guides/upgrading-to-v1.md new file mode 100644 index 0000000..85498c3 --- /dev/null +++ b/docs/guides/upgrading-to-v1.md @@ -0,0 +1,59 @@ +--- +title: Upgrading to v1 +parent: Guides +nav_order: 1 +--- + +# Upgrading to v1 + +## Installation + +Install `@humanmade/webpack-helpers@latest` with [npm](http://npmjs.com), along with more updated versions of `webpack` and its attendant dependencies: + +```bash +npm install --save-dev @humanmade/webpack-helpers@latest webpack@5 webpack-cli@4 webpack-dev-server@4 +``` + +## Breaking Changes + +### Handling multi-config arrays in DevServer + +In a large project you may have a Webpack configuration that [exports multiple separate Webpack configuration objects](https://webpack.js.org/configuration/configuration-types/#exporting-multiple-configurations) in a multi-config array. With Webpack 4 and the pre-1.0 version of this helper library, you may have code which maps over these arrays and adds a `devServer` property to each one: + +```js +``` + +Previously, if you exported an array of configurations from your Webpack development config, each one could have an identical `devServer` property and Webpack DevServer would handle all configs with the same server. This let us + +### Goodbye `filterLoaders`, welcome `addFilter` + +Previously, a second argument could be passed to a preset to define a function used to filter loaders used while generating that preset configuration. In v1.0, a [hooks system](https://humanmade.github.io/webpack-helpers/reference/hooks.html) has been introduced which provides an `addFilter` method. `addFilter` can be used to register a callback function that may adjust the value of a loader globally, or make changes specific to a given preset call, in a manner similar to the WordPress PHP hooks system. Read the [hooks reference](https://humanmade.github.io/webpack-helpers/reference/hooks.html), [loaders](https://humanmade.github.io/webpack-helpers/modules/loaders.html), and [preset](https://humanmade.github.io/webpack-helpers/modules/presets.html) documentation pages for more information. + +### ESLint v6 and earlier not supported + +The ESLint integration for Webpack 5 requires ESLint v7 or later. If your project uses ESLint 6 or before, you will need to upgrade ESLint before using Webpack Helpers 1.0. + +```bash +npm install --save-dev eslint@7 +``` + +On most projects ESLint should continue to work after upgrade with no further adjustments. + +### Importing named properties from JSON + +Due to ongoing changes to how JSON files are handled by Webpack, this code + +```js +import { name } from './block.json'; +``` + +may now trigger the error + +``` +Should not import the named export 'name' (imported as 'name') from default-exporting module (only default export is available soon) +``` +This error means that your code above needs to be changed to this: +```js +import blockData from './block.json'; +const { name } = blockData; +``` From 0a892f59b3a82a7e456455411d96c3dfdcea437c Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Wed, 15 Jun 2022 16:39:15 +0200 Subject: [PATCH 65/83] Tag v1.0.0-beta.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e69d44c..fbfda45 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "1.0.0-beta.10", + "version": "1.0.0-beta.11", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made", From b12898a0076e4529ca226da47a396861f9b90d8b Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Thu, 16 Jun 2022 11:55:20 +0200 Subject: [PATCH 66/83] Do not pass loader defaults to filters by reference to prevent mutation --- package.json | 1 + src/loaders.js | 3 ++- src/loaders.test.js | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index fbfda45..cec32cd 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "detect-port-alt": "^1.1.6", "eslint-webpack-plugin": "^3.1.1", "is-root": "^2.1.0", + "lodash.clonedeep": "^4.5.0", "mini-css-extract-plugin": "^2.6.0", "postcss": "^8.4.12", "postcss-flexbugs-fixes": "^5.0.2", diff --git a/src/loaders.js b/src/loaders.js index e1064e4..58345b9 100644 --- a/src/loaders.js +++ b/src/loaders.js @@ -1,6 +1,7 @@ /** * Export generator functions for common Webpack loader configurations. */ +const cloneDeep = require( 'lodash.clonedeep' ); const postcssFlexbugsFixes = require( 'postcss-flexbugs-fixes' ); const postcssPresetEnv = require( 'postcss-preset-env' ); @@ -45,7 +46,7 @@ const createLoaderFactory = loaderKey => { * @param {Object} options Loader default options object. * @param {Object|null} [config] Configuration object for the preset being rendered, if loader is called while generating a preset. */ - applyFilters( `loaders/${ loaderKey }/defaults`, loaders[ loaderKey ].defaults, config ), + applyFilters( `loaders/${ loaderKey }/defaults`, cloneDeep( loaders[ loaderKey ].defaults ), config ), options ), config diff --git a/src/loaders.test.js b/src/loaders.test.js index 8cce7a8..586731a 100644 --- a/src/loaders.test.js +++ b/src/loaders.test.js @@ -1,3 +1,4 @@ +const { addFilter, setupRegistry } = require( './helpers/filters' ); const loaders = require( './loaders' ); describe( 'loaders', () => { @@ -55,4 +56,22 @@ describe( 'loaders', () => { } ); } ); + describe( 'filtering', () => { + beforeEach( () => { + setupRegistry(); + } ); + + it( 'does not mutate defaults', () => { + addFilter( 'loaders/js/defaults', ( defaults ) => { + defaults.test = /\.js$/; + return defaults; + } ); + + const loader = loaders.js(); + + expect( loader.test ).toEqual( /\.js$/ ); + expect( loaders.js.defaults.test ).toEqual( /\.jsx?$/ ); + } ); + } ); + } ); From fa464e3d7a3c43f1d786f78282b047ad22392304 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Thu, 16 Jun 2022 11:56:13 +0200 Subject: [PATCH 67/83] Tag v1.0.0-beta.12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cec32cd..d17be9c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "1.0.0-beta.11", + "version": "1.0.0-beta.12", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made", From a20b4d8880662d2254102facb824c4236aa7e5ad Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 17 Jun 2022 12:18:05 +0200 Subject: [PATCH 68/83] Expand multi-config DevServer documentation --- docs/guides/upgrading-to-v1.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/guides/upgrading-to-v1.md b/docs/guides/upgrading-to-v1.md index 85498c3..60bdc3f 100644 --- a/docs/guides/upgrading-to-v1.md +++ b/docs/guides/upgrading-to-v1.md @@ -18,13 +18,24 @@ npm install --save-dev @humanmade/webpack-helpers@latest webpack@5 webpack-cli@4 ### Handling multi-config arrays in DevServer -In a large project you may have a Webpack configuration that [exports multiple separate Webpack configuration objects](https://webpack.js.org/configuration/configuration-types/#exporting-multiple-configurations) in a multi-config array. With Webpack 4 and the pre-1.0 version of this helper library, you may have code which maps over these arrays and adds a `devServer` property to each one: +In a large project you may have a Webpack configuration that [exports multiple separate Webpack configuration objects](https://webpack.js.org/configuration/configuration-types/#exporting-multiple-configurations) in an array. The `presets.development()` helper adds a `devServer` property to each generated configuration, and with Webpack 4 and the pre-1.0 version of this helper library this was not a problem: if each entry in an exported array of configurations had its own `devServer` property, DevServer used the configuration in the first item in the exported array and ignored the rest. + +In Webpack 5 and the latest DevServer, however, it is an error to define `devServer` properties on any configuration but the first item in the exported array. This means that you may need to map over the exported configurations and unset the `devServer` property on subsequent configurations. ```js +const configs = [ + presets.development( { /* ... */ } ), + presets.development( { /* ... */ } ), + presets.development( { /* ... */ } ), +]; +module.exports = configs.map( ( config, index ) => { + if ( index > 0 ) { + Reflect.deleteProperty( config, 'devServer' ); + } + return config; +} ); ``` -Previously, if you exported an array of configurations from your Webpack development config, each one could have an identical `devServer` property and Webpack DevServer would handle all configs with the same server. This let us - ### Goodbye `filterLoaders`, welcome `addFilter` Previously, a second argument could be passed to a preset to define a function used to filter loaders used while generating that preset configuration. In v1.0, a [hooks system](https://humanmade.github.io/webpack-helpers/reference/hooks.html) has been introduced which provides an `addFilter` method. `addFilter` can be used to register a callback function that may adjust the value of a loader globally, or make changes specific to a given preset call, in a manner similar to the WordPress PHP hooks system. Read the [hooks reference](https://humanmade.github.io/webpack-helpers/reference/hooks.html), [loaders](https://humanmade.github.io/webpack-helpers/modules/loaders.html), and [preset](https://humanmade.github.io/webpack-helpers/modules/presets.html) documentation pages for more information. From 046b247fa3bebbe9ff8c727d44cef756bd5332bc Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Wed, 22 Jun 2022 10:08:16 +0100 Subject: [PATCH 69/83] Standardize JSDoc on @returns instead of @return --- src/helpers/camel-case-dash.js | 2 +- src/helpers/infer-public-path.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helpers/camel-case-dash.js b/src/helpers/camel-case-dash.js index 11b3ffd..87a6b41 100644 --- a/src/helpers/camel-case-dash.js +++ b/src/helpers/camel-case-dash.js @@ -2,7 +2,7 @@ * Given a kebab-case string, returns a new camelCase string. * * @param {string} string Input kebab-case string. - * @return {string} Camel-cased string. + * @returns {string} Camel-cased string. */ module.exports = string => string.replace( /-([a-z])/g, diff --git a/src/helpers/infer-public-path.js b/src/helpers/infer-public-path.js index 47cc282..a83371d 100644 --- a/src/helpers/infer-public-path.js +++ b/src/helpers/infer-public-path.js @@ -7,7 +7,7 @@ const findInObject = require( './find-in-object' ); * @param {webpack.Configuration} config Webpack configuration object. * @param {Number} port Port to use for webpack-dev-server. * @param {Object} [defaults] Optional config default values. - * @return {String} Public path. + * @returns {String} Public path. */ const inferPublicPath = ( config, port, defaults = {} ) => { const protocol = ( findInObject( config, 'devServer.https' ) || findInObject( config, 'devServer.server' ) === 'https' ) ? 'https' : 'http'; From b42bf868fba93ae0ef906b59d94383304ce27de2 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 1 Jul 2022 09:06:18 -0400 Subject: [PATCH 70/83] Inject development-mode asset manifest even without publicPath This resolves an issue where multi-config entries would only output a manifest containing the entries from the first exported config. The origin of this issue is that we cannot define a devServer on every multi-config array, or DevServer v4 will error. But without defining a devServer we did not usually have the information needed to deduce a publicPath, and therefore did not insert the manifest plugin in subsequent configs. Remove test for not setting manifest plugin --- docs/changelog.md | 1 + docs/reference/hooks.md | 22 +++++++++++++ src/presets.js | 73 +++++++++++++++++++++++++++++++---------- src/presets.test.js | 14 +------- 4 files changed, 80 insertions(+), 30 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 34ca505..c1687ab 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -15,6 +15,7 @@ nav_order: 10 - **Breaking**: Replace `filterLoaders` system with [individual hooks accesible via the new `addFilter` and `removeFilter` helpers](https://humanmade.github.io/webpack-helpers/modules/presets.html#customizing-presets). - **Breaking**: Remove deprecated `eslint-loader` and add `eslint-webpack-plugin` to presets as `plugins.eslint()`. - **Potentially Breaking**: Remove `loaders.url()` and `loaders.file()` in favor of Webpack 5 [`asset` modules](https://webpack.js.org/guides/asset-modules/), now usable by including `loaders.asset()` (for assets which can be inlined) and `loaders.resource()` (as a catch-all for other types) in your module rules list. Asset modules are handled automatically in both presets, so this is only breaking if `loaders.url()` or `loaders.file()` was used directly. +- A `webpack-manifest-plugin` instance is now automatically injected in development mode even if `publicPath` is not specified. - A `webpack-bundle-analyzer` plugin is now automatically added to production builds when Webpack is invoked with the `--analyze` flag. - Add the [`simple-build-report-webpack-plugin`](https://github.com/kadamwhite/simple-build-report-webpack-plugin) as `plugins.simpleBuildReport()` - Include a `plugins.simpleBuildReport()` instance in production preset builds to improve legibility of Webpack console output. diff --git a/docs/reference/hooks.md b/docs/reference/hooks.md index 0662a2b..71cbf33 100644 --- a/docs/reference/hooks.md +++ b/docs/reference/hooks.md @@ -78,6 +78,28 @@ addFilter( 'presets/stylesheet-loaders', ( loader, environment, config ) => { } ); ``` +## `presets/manifest-options` + +Filter the [`webpack-manifest-plugin` options](https://github.com/shellscape/webpack-manifest-plugin#options) used when auto-injecting asset manifest plugins. + +The callback will receive the environment type of the preset being rendered as its second argument, and the preset's configuration object argument as a third argument. + +**Arguments:** + + name | type | description +----- | ---- | ------ +`options` | `Object` | Manifest plugin options object. +`environment` | `string` | `'development'` or `'production'`. +`config` | `Object` | Preset configuration object. + +```js +addFilter( 'presets/manifest-options', ( loader, environment, config ) => { + // Filter the configured stylesheet loaders based on environment or the + // specific configuration options provided to the preset factory. + return loader; +} ); +``` + ## `loaders/postcss/plugins` Filter the default list of PostCSS plugins used by the PostCSS loader. diff --git a/src/presets.js b/src/presets.js index 601c501..586f537 100644 --- a/src/presets.js +++ b/src/presets.js @@ -222,22 +222,42 @@ const development = ( config = {} ) => { } // If we had enough value to guess a publicPath, set that path as a default - // wherever appropriate and inject a ManifestPlugin instance to expose that - // public path to consuming applications. Any inferred values will still be - // overridden with their relevant values from `config`, when provided. + // wherever appropriate so that it will be used in any generated manifests. if ( publicPath ) { devDefaults.output.publicPath = publicPath; + } - // Check for an existing ManifestPlugin instance in config.plugins. - const hasManifestPlugin = plugins.findExistingInstance( config.plugins, ManifestPlugin ); - // Add a manifest with the inferred publicPath if none was present. - if ( ! hasManifestPlugin ) { - const outputPath = ( config.output && config.output.path ) || devDefaults.output.path; - devDefaults.plugins.push( plugins.manifest( { - fileName: 'development-asset-manifest.json', - seed: getSeedByDirectory( outputPath ), - } ) ); - } + // Check for an existing ManifestPlugin instance in config.plugins. + // Inject a ManifestPlugin instance if none is present to ensure generated + // files can be located by consuming aplications. Any inferred values will + // still be overridden with their relevant values from `config`, if provided. + const hasManifestPlugin = plugins.findExistingInstance( config.plugins, ManifestPlugin ); + // Add a manifest if none was present. + if ( ! hasManifestPlugin ) { + const outputPath = ( config.output && config.output.path ) || devDefaults.output.path; + /* eslint-disable function-paren-newline */ + devDefaults.plugins.push( plugins.manifest( + /** + * Filter the full stylesheet loader definition for this preset. + * + * By default parses styles using Sass and then PostCSS. + * + * @hook presets/manifest-options + * @param {Object} options Manifest plugin options object. + * @param {string} environment "development" or "production". + * @param {Object} config Preset configuration object. + */ + applyFilters( + 'presets/manifest-options', + { + fileName: 'development-asset-manifest.json', + seed: getSeedByDirectory( outputPath ), + }, + 'development', + config + ) + ) ); + /* eslint-enable */ } return deepMerge( devDefaults, config ); @@ -386,10 +406,29 @@ const production = ( config = {} ) => { // Add a manifest with the inferred publicPath if none was present. if ( ! hasManifestPlugin ) { const outputPath = ( config.output && config.output.path ) || prodDefaults.output.path; - prodDefaults.plugins.push( plugins.manifest( { - fileName: 'production-asset-manifest.json', - seed: getSeedByDirectory( outputPath ), - } ) ); + /* eslint-disable function-paren-newline */ + prodDefaults.plugins.push( plugins.manifest( + /** + * Filter the full stylesheet loader definition for this preset. + * + * By default parses styles using Sass and then PostCSS. + * + * @hook presets/manifest-options + * @param {Object} options Manifest plugin options object. + * @param {string} environment "development" or "production". + * @param {Object} config Preset configuration object. + */ + applyFilters( + 'presets/manifest-options', + { + fileName: 'production-asset-manifest.json', + seed: getSeedByDirectory( outputPath ), + }, + 'production', + config + ) + ) ); + /* eslint-enable */ } return deepMerge( prodDefaults, config ); diff --git a/src/presets.test.js b/src/presets.test.js index 7e1fab6..81ab9e0 100644 --- a/src/presets.test.js +++ b/src/presets.test.js @@ -180,7 +180,7 @@ describe( 'presets', () => { expect( config.output.publicPath ).toBe( 'https://my-custom-domain.local/' ); } ); - it( 'injects a ManifestPlugin if publicPath can be inferred and no manifest plugin is already present', () => { + it( 'injects a ManifestPlugin if no manifest plugin is already present', () => { const { ManifestPlugin } = plugins.constructors; const config = development( { devServer: { @@ -198,18 +198,6 @@ describe( 'presets', () => { expect( manifestPlugins[ 0 ].options.fileName ).toEqual( 'development-asset-manifest.json' ); } ); - it( 'does not inject a ManifestPlugin if publicPath cannot be inferred', () => { - const config = development( { - entry: { - main: 'some-file.css', - }, - } ); - expect( config.output ).not.toHaveProperty( 'publicPath' ); - expect( config.plugins ).toEqual( expect.not.arrayContaining( [ - expect.any( plugins.constructors.ManifestPlugin ), - ] ) ); - } ); - it( 'does not override or duplicate existing ManifestPlugin instances', () => { const { ManifestPlugin } = plugins.constructors; const config = development( { From 3201d61470650836246fb2299ddb2456953cdfb9 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 1 Jul 2022 09:21:17 -0400 Subject: [PATCH 71/83] Switch from [contenthash] back to [fullhash] (prev [hash]) in Dev config contenthash is not recommended in dev mode, and hash, the value we previously used, has been renamed fullhash for clarity. --- src/presets.js | 4 ++-- src/presets.test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/presets.js b/src/presets.js index 586f537..13a13d1 100644 --- a/src/presets.js +++ b/src/presets.js @@ -122,9 +122,9 @@ const development = ( config = {} ) => { // Add /* filename */ comments to generated require()s in the output. pathinfo: true, // Provide a default output name. - filename: '[name].[contenthash].js', + filename: '[name].[fullhash].js', // Provide chunk filename. Requires content hash for cache busting. - chunkFilename: '[name].[contenthash].chunk.js', + chunkFilename: '[name].[fullhash].chunk.js', // `publicPath` will be inferred as a localhost URL based on output.path // when a devServer.port value is available. }, diff --git a/src/presets.test.js b/src/presets.test.js index 81ab9e0..4c3ad28 100644 --- a/src/presets.test.js +++ b/src/presets.test.js @@ -95,8 +95,8 @@ describe( 'presets', () => { expect( config.entry ).toEqual( 'some-file.js' ); expect( config.output ).toEqual( { pathinfo: true, - filename: '[name].[contenthash].js', - chunkFilename: '[name].[contenthash].chunk.js', + filename: '[name].[fullhash].js', + chunkFilename: '[name].[fullhash].chunk.js', path: 'build/', } ); } ); From 870f4f09e2a26f1ce65e2173776db891ac30d087 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 1 Jul 2022 09:42:05 -0400 Subject: [PATCH 72/83] Set runtimeChunk: "single" to work around HMR issue with multiple entries See Webpack DevServer issue 2792. --- src/presets.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/presets.js b/src/presets.js index 13a13d1..ff6faf8 100644 --- a/src/presets.js +++ b/src/presets.js @@ -191,6 +191,7 @@ const development = ( config = {} ) => { optimization: { nodeEnv: 'development', + runtimeChunk: 'single', }, devServer, From 0e69dcdc28d4a515775cdb5f9e0b189c02b75270 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 1 Jul 2022 10:10:24 -0400 Subject: [PATCH 73/83] Tag v1.0.0-beta.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d17be9c..d03389d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "1.0.0-beta.12", + "version": "1.0.0-beta.13", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made", From a2637c5c73e3a278f6927c6e12be0b5705268a7f Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 1 Jul 2022 10:59:07 -0400 Subject: [PATCH 74/83] Automatically set devServer "host" property when we can --- src/presets.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/presets.js b/src/presets.js index ff6faf8..1b6e5b5 100644 --- a/src/presets.js +++ b/src/presets.js @@ -10,6 +10,7 @@ const plugins = require( './plugins' ); const { ManifestPlugin, MiniCssExtractPlugin } = plugins.constructors; const isAnalyzeMode = process.argv.includes( '--analyze' ); +const isDevServer = ! ! process.env.WEBPACK_DEV_SERVER; /** * Dictionary of shared seed objects by path. @@ -225,6 +226,9 @@ const development = ( config = {} ) => { // If we had enough value to guess a publicPath, set that path as a default // wherever appropriate so that it will be used in any generated manifests. if ( publicPath ) { + if ( isDevServer && publicPath.includes( 'http' ) ) { + devDefaults.devServer.host = new URL( publicPath ).hostname; + } devDefaults.output.publicPath = publicPath; } From 583b5ab167c078be77879188e6e35ad902cd5d83 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 1 Jul 2022 10:59:28 -0400 Subject: [PATCH 75/83] Tag v1.0.0-beta.14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d03389d..b66cb4f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "1.0.0-beta.13", + "version": "1.0.0-beta.14", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made", From 20fff25682467244b770e3b706a627da0de9c961 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 1 Jul 2022 13:50:13 -0400 Subject: [PATCH 76/83] Remove check for whether devServer is running, devServer is not relevant if it is not --- src/presets.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/presets.js b/src/presets.js index 1b6e5b5..21ecc23 100644 --- a/src/presets.js +++ b/src/presets.js @@ -226,7 +226,8 @@ const development = ( config = {} ) => { // If we had enough value to guess a publicPath, set that path as a default // wherever appropriate so that it will be used in any generated manifests. if ( publicPath ) { - if ( isDevServer && publicPath.includes( 'http' ) ) { + // If publicPath is a URL, default the devServer host to match. + if ( publicPath.includes( 'http' ) ) { devDefaults.devServer.host = new URL( publicPath ).hostname; } devDefaults.output.publicPath = publicPath; From bfa8c6a65aa6ea84d0eb5cd65e620579ff615ab7 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 1 Jul 2022 13:50:47 -0400 Subject: [PATCH 77/83] Revert switch to runtimeChunk: single, which requires unintuitive other changes You need to manually include the runtimeChunk if you do this. --- src/presets.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/presets.js b/src/presets.js index 21ecc23..b1c7b0e 100644 --- a/src/presets.js +++ b/src/presets.js @@ -191,8 +191,7 @@ const development = ( config = {} ) => { }, optimization: { - nodeEnv: 'development', - runtimeChunk: 'single', + nodeEnv: 'development' }, devServer, From 122790eccddcc218aa6b1e50c2b8a148776ef5eb Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 1 Jul 2022 13:51:05 -0400 Subject: [PATCH 78/83] Tag v1.0.0-beta.15 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b66cb4f..b7b871e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "1.0.0-beta.14", + "version": "1.0.0-beta.15", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made", From c7e9e6ab2c73f90bb93103ebf446e1b15fd499f7 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 1 Jul 2022 14:32:01 -0400 Subject: [PATCH 79/83] Only show DevServer overlay for errors, not warnings --- src/config.js | 7 +++++++ src/presets.js | 1 - 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/config.js b/src/config.js index ede621e..f117d72 100644 --- a/src/config.js +++ b/src/config.js @@ -16,6 +16,13 @@ module.exports = { // Enable gzip compression of generated files. compress: true, hot: 'only', + client: { + // Do not show disruptive overlay for warnings. + overlay: { + errors: true, + warnings: false, + }, + }, }, /** diff --git a/src/presets.js b/src/presets.js index b1c7b0e..0450b19 100644 --- a/src/presets.js +++ b/src/presets.js @@ -10,7 +10,6 @@ const plugins = require( './plugins' ); const { ManifestPlugin, MiniCssExtractPlugin } = plugins.constructors; const isAnalyzeMode = process.argv.includes( '--analyze' ); -const isDevServer = ! ! process.env.WEBPACK_DEV_SERVER; /** * Dictionary of shared seed objects by path. From 301a0f1d94ab96e8b3348aadc0d4f94f70b84793 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 1 Jul 2022 16:30:35 -0400 Subject: [PATCH 80/83] Store and re-use inferred publicPaths in subsequent builds to the same output folder This permits minimal-config multi-config dev builds which stack multiple config output into one manifest. Without a patch like this, the second dev config would get the publicPath "auto". --- src/helpers/infer-public-path.js | 38 +++++++++++++++++++++++++-- src/helpers/with-dynamic-port.js | 2 +- src/helpers/with-dynamic-port.test.js | 2 ++ src/presets.js | 15 +++++++---- src/presets.test.js | 20 ++++++++++++++ 5 files changed, 69 insertions(+), 8 deletions(-) diff --git a/src/helpers/infer-public-path.js b/src/helpers/infer-public-path.js index a83371d..e2a9b71 100644 --- a/src/helpers/infer-public-path.js +++ b/src/helpers/infer-public-path.js @@ -1,6 +1,25 @@ const filePath = require( './file-path' ); const findInObject = require( './find-in-object' ); +/** + * Dictionary of generated publicPath strings by file system path. + * + * @type {Object.} + */ +const publicPaths = {}; + +/** + * Return a consistent publicPath per output directory path. + * + * Only used when automatically generating public paths. + * + * @param {String} path Output directory path + * @returns {string} Shared publicPath string. + */ +const getPublicPathForDirectory = ( path ) => { + return publicPaths[ path ]; +}; + /** * Infer the public path based on the defined output path. * @@ -21,7 +40,22 @@ const inferPublicPath = ( config, port, defaults = {} ) => { .replace( /^\/*/, '' ) .replace( /\/*$/, '/' ); - return `${ protocol }://localhost:${ port }/${ relPath }`; + const publicPath = `${ protocol }://localhost:${ port }/${ relPath }`; + + publicPaths[ outputPath ] = publicPath; + return publicPath; +}; + +module.exports = { + inferPublicPath, + getPublicPathForDirectory, }; -module.exports = inferPublicPath; +if ( process.env.JEST_WORKER_ID ) { + // Exposed only for testing purposes. + module.exports.resetPublicPathsCache = () => { + Object.keys( publicPaths ).forEach( ( key ) => { + Reflect.deleteProperty( publicPaths, key ); + } ); + }; +} diff --git a/src/helpers/with-dynamic-port.js b/src/helpers/with-dynamic-port.js index a22567f..b09df26 100644 --- a/src/helpers/with-dynamic-port.js +++ b/src/helpers/with-dynamic-port.js @@ -6,7 +6,7 @@ const choosePort = require( './choose-port' ); const filePath = require( './file-path' ); const findInObject = require( './find-in-object' ); -const inferPublicPath = require( './infer-public-path' ); +const { inferPublicPath } = require( './infer-public-path' ); const DEFAULT_PORT = 9090; diff --git a/src/helpers/with-dynamic-port.test.js b/src/helpers/with-dynamic-port.test.js index 68033ba..5b2b7ff 100644 --- a/src/helpers/with-dynamic-port.test.js +++ b/src/helpers/with-dynamic-port.test.js @@ -1,6 +1,7 @@ const withDynamicPort = require( './with-dynamic-port' ); const choosePort = require( './choose-port' ); +const { resetPublicPathsCache } = require( './infer-public-path' ); jest.mock( './choose-port', () => jest.fn() ); @@ -9,6 +10,7 @@ describe( 'withDynamicPort', () => { beforeEach( () => { oldArgv = process.argv; + resetPublicPathsCache(); } ); afterEach( () => { diff --git a/src/presets.js b/src/presets.js index 0450b19..75386f9 100644 --- a/src/presets.js +++ b/src/presets.js @@ -2,7 +2,7 @@ const { devServer, stats } = require( './config' ); const deepMerge = require( './helpers/deep-merge' ); const filePath = require( './helpers/file-path' ); const findInObject = require( './helpers/find-in-object' ); -const inferPublicPath = require( './helpers/infer-public-path' ); +const { inferPublicPath, getPublicPathForDirectory } = require( './helpers/infer-public-path' ); const isInstalled = require( './helpers/is-installed' ); const { applyFilters } = require( './helpers/filters' ); const loaders = require( './loaders' ); @@ -221,11 +221,17 @@ const development = ( config = {} ) => { publicPath = inferPublicPath( config, port, devDefaults ); } - // If we had enough value to guess a publicPath, set that path as a default - // wherever appropriate so that it will be used in any generated manifests. + const outputPath = ( config.output && config.output.path ) || devDefaults.output.path; + // If we used a publicPath for this outputPath before, re-use it. + if ( ! publicPath && outputPath ) { + publicPath = getPublicPathForDirectory( outputPath ); + } + + // If we had enough config values to guess a publicPath, set that path in + // the default config so it can be used in generated manifests. if ( publicPath ) { // If publicPath is a URL, default the devServer host to match. - if ( publicPath.includes( 'http' ) ) { + if ( typeof publicPath === 'string' && publicPath.includes( 'http' ) ) { devDefaults.devServer.host = new URL( publicPath ).hostname; } devDefaults.output.publicPath = publicPath; @@ -238,7 +244,6 @@ const development = ( config = {} ) => { const hasManifestPlugin = plugins.findExistingInstance( config.plugins, ManifestPlugin ); // Add a manifest if none was present. if ( ! hasManifestPlugin ) { - const outputPath = ( config.output && config.output.path ) || devDefaults.output.path; /* eslint-disable function-paren-newline */ devDefaults.plugins.push( plugins.manifest( /** diff --git a/src/presets.test.js b/src/presets.test.js index 4c3ad28..e1821b4 100644 --- a/src/presets.test.js +++ b/src/presets.test.js @@ -4,6 +4,7 @@ const { } = require( './presets' ); const plugins = require( './plugins' ); const { addFilter, setupRegistry } = require( './helpers/filters' ); +const { resetPublicPathsCache } = require( './helpers/infer-public-path' ); jest.mock( 'process', () => ( { cwd: () => 'cwd', @@ -78,6 +79,7 @@ const getLoaderByTest = ( rules, loaderTest ) => { describe( 'presets', () => { beforeEach( () => { setupRegistry(); + resetPublicPathsCache(); } ); describe( 'development()', () => { @@ -180,6 +182,24 @@ describe( 'presets', () => { expect( config.output.publicPath ).toBe( 'https://my-custom-domain.local/' ); } ); + it( 're-uses previously inferred publicPath URIs in subsequent builds to the same directory', () => { + const config1 = development( { + devServer: { + port: 9090, + }, + output: { + path: 'some/folder', + } + } ); + const config2 = development( { + output: { + path: 'some/folder', + }, + } ); + expect( config1.output.publicPath ).toBe( 'http://localhost:9090/some/folder/' ); + expect( config1.output.publicPath ).toBe( config2.output.publicPath ); + } ); + it( 'injects a ManifestPlugin if no manifest plugin is already present', () => { const { ManifestPlugin } = plugins.constructors; const config = development( { From 77d09e9e6047d55ff92401d8507fa5ec3bac97de Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Fri, 1 Jul 2022 16:31:12 -0400 Subject: [PATCH 81/83] Tag v1.0.0-beta.16 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b7b871e..03b832c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "1.0.0-beta.15", + "version": "1.0.0-beta.16", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made", From 637e69514cd51e0db717143e212bc1837a1071a0 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Wed, 17 May 2023 16:23:07 -0400 Subject: [PATCH 82/83] Pin block-editor-hmr@0.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 03b832c..ac929dd 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@wordpress/babel-preset-default": "^6.9.0", "babel-loader": "^8.2.5", "bell-on-bundler-error-plugin": "^2.0.0", - "block-editor-hmr": "^0.6.3", + "block-editor-hmr": "^0.7.0", "chalk": "^4.1.2", "clean-webpack-plugin": "^4.0.0", "copy-webpack-plugin": "^10.2.4", From 4195d37480ba9e77d6fc5fe1097f9d4fc37d51b7 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Wed, 17 May 2023 16:24:13 -0400 Subject: [PATCH 83/83] Tag v1.0.0-beta.17 Pin block-editor-hmr to include a bugfix for a regression in files which do not export settings --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ac929dd..3ff1d3a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "1.0.0-beta.16", + "version": "1.0.0-beta.17", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made",