diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 537506f..0000000 --- a/.eslintignore +++ /dev/null @@ -1,14 +0,0 @@ -# /node_modules/* and /bower_components/* in the project root are ignored by default - -# Ignore generated files -**/__snapshots__/ - -# Ignore documentation site -docs/ - -# Ignore third-party code -src/vendor - -# The test folder uses ES modules -test/src -test/build diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 5837717..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,293 +0,0 @@ -module.exports = { - 'env': { - 'node': true, - 'commonjs': true, - 'jest': true, - 'es6': true - }, - 'extends': 'eslint:recommended', - 'parserOptions': { - 'ecmaVersion': 2018 - }, - 'rules': { - 'accessor-pairs': 'error', - 'array-bracket-newline': 'off', - 'array-bracket-spacing': [ - 'error', - 'always' - ], - 'array-callback-return': 'error', - 'array-element-newline': 'off', - 'arrow-body-style': 'off', - 'arrow-parens': 'off', - 'arrow-spacing': [ - 'error', - { - 'after': true, - 'before': true - } - ], - 'block-scoped-var': 'error', - 'block-spacing': 'error', - 'brace-style': 'error', - 'callback-return': 'error', - 'capitalized-comments': 'off', - 'class-methods-use-this': 'error', - 'comma-dangle': 'off', - 'comma-spacing': 'off', - 'comma-style': [ - 'error', - 'last' - ], - 'complexity': 'error', - 'computed-property-spacing': [ - 'error', - 'always' - ], - // 'consistent-return': 'error', - 'consistent-this': 'error', - 'curly': 'error', - 'default-case': 'error', - 'dot-location': [ 'error', 'property' ], - 'dot-notation': 'error', - 'eol-last': 'error', - 'eqeqeq': 'error', - 'func-call-spacing': 'error', - 'func-name-matching': 'error', - 'func-names': 'error', - 'func-style': [ - 'error', - 'expression' - ], - 'function-paren-newline': 'error', - 'generator-star-spacing': 'error', - 'global-require': 'error', - 'guard-for-in': 'error', - 'handle-callback-err': 'error', - 'id-blacklist': 'error', - 'id-length': 'error', - 'id-match': 'error', - 'implicit-arrow-linebreak': [ - 'error', - 'beside' - ], - 'indent': 'off', - 'indent-legacy': 'off', - 'init-declarations': 'off', - 'jsx-quotes': 'error', - 'key-spacing': 'off', - 'keyword-spacing': 'error', - 'line-comment-position': 'error', - 'linebreak-style': [ - 'error', - 'unix' - ], - 'lines-around-comment': 'off', - 'lines-around-directive': 'error', - 'lines-between-class-members': 'error', - 'max-classes-per-file': 'error', - 'max-depth': 'error', - 'max-len': 'off', - 'max-lines': 'off', - 'max-lines-per-function': 'off', - 'max-nested-callbacks': 'error', - 'max-params': 'error', - 'max-statements-per-line': 'error', - 'multiline-comment-style': [ - 'error', - 'separate-lines' - ], - 'multiline-ternary': [ - 'error', - 'always-multiline' - ], - 'new-cap': 'error', - 'new-parens': 'error', - 'newline-after-var': 'off', - 'newline-before-return': 'off', - 'newline-per-chained-call': 'error', - 'no-alert': 'error', - 'no-array-constructor': 'error', - 'no-async-promise-executor': 'error', - 'no-await-in-loop': 'error', - 'no-bitwise': 'error', - 'no-buffer-constructor': 'error', - 'no-caller': 'error', - 'no-catch-shadow': 'error', - 'no-confusing-arrow': [ 'error', { 'allowParens': true } ], - 'no-continue': 'error', - 'no-div-regex': 'error', - 'no-duplicate-imports': 'error', - 'no-else-return': 'error', - 'no-empty-function': 'error', - 'no-eq-null': 'error', - 'no-eval': 'error', - 'no-extend-native': 'error', - 'no-extra-bind': 'error', - 'no-extra-label': 'error', - 'no-extra-parens': 'off', - 'no-floating-decimal': 'error', - 'no-implicit-coercion': 'error', - 'no-implicit-globals': 'error', - 'no-implied-eval': 'error', - 'no-inline-comments': 'error', - 'no-invalid-this': 'error', - 'no-iterator': 'error', - 'no-label-var': 'error', - 'no-labels': 'error', - 'no-lone-blocks': 'error', - 'no-lonely-if': 'error', - 'no-loop-func': 'error', - 'no-magic-numbers': 'off', - 'no-misleading-character-class': 'error', - 'no-mixed-operators': 'error', - 'no-mixed-requires': 'error', - 'no-multi-assign': 'error', - 'no-multi-spaces': 'error', - 'no-multi-str': 'error', - 'no-multiple-empty-lines': 'error', - 'no-native-reassign': 'error', - 'no-negated-condition': 'error', - 'no-negated-in-lhs': 'error', - 'no-nested-ternary': 'error', - 'no-new': 'error', - 'no-new-func': 'error', - 'no-new-object': 'error', - 'no-new-require': 'error', - 'no-new-wrappers': 'error', - 'no-octal-escape': 'error', - 'no-param-reassign': 'error', - 'no-path-concat': 'error', - 'no-plusplus': 'error', - 'no-process-exit': 'error', - 'no-proto': 'error', - 'no-prototype-builtins': 'error', - 'no-restricted-globals': 'error', - 'no-restricted-imports': 'error', - 'no-restricted-modules': 'error', - 'no-restricted-properties': 'error', - 'no-restricted-syntax': 'error', - 'no-return-assign': 'error', - 'no-return-await': 'error', - 'no-script-url': 'error', - 'no-self-compare': 'error', - 'no-sequences': 'error', - 'no-shadow-restricted-names': 'error', - 'no-spaced-func': 'error', - 'no-sync': 'error', - 'no-tabs': [ - 'error', - { - 'allowIndentationTabs': true - } - ], - 'no-template-curly-in-string': 'error', - 'no-ternary': 'off', - 'no-throw-literal': 'error', - 'no-trailing-spaces': 'error', - 'no-undef-init': 'error', - 'no-undefined': 'off', - 'no-underscore-dangle': 'error', - 'no-unmodified-loop-condition': 'error', - 'no-unneeded-ternary': 'error', - 'no-unused-expressions': 'error', - 'no-use-before-define': 'error', - 'no-useless-call': 'error', - 'no-useless-catch': 'error', - 'no-useless-computed-key': 'error', - 'no-useless-concat': 'error', - 'no-useless-constructor': 'error', - 'no-useless-rename': 'error', - 'no-useless-return': 'error', - 'no-var': 'error', - 'no-void': 'error', - 'no-warning-comments': 'warn', - 'no-whitespace-before-property': 'error', - 'no-with': 'error', - 'nonblock-statement-body-position': 'error', - 'object-curly-newline': 'error', - 'object-curly-spacing': [ - 'error', - 'always' - ], - 'object-property-newline': 'error', - 'object-shorthand': 'error', - 'one-var': 'off', - 'one-var-declaration-per-line': 'error', - 'operator-assignment': 'error', - 'operator-linebreak': [ - 'error', - 'after' - ], - 'padded-blocks': 'off', - 'padding-line-between-statements': 'error', - 'prefer-arrow-callback': 'error', - 'prefer-const': 'off', - 'prefer-destructuring': 'error', - 'prefer-numeric-literals': 'error', - 'prefer-object-spread': 'error', - 'prefer-promise-reject-errors': 'error', - 'prefer-reflect': 'error', - 'prefer-rest-params': 'error', - 'prefer-spread': 'error', - 'prefer-template': 'error', - 'quote-props': 'off', - 'quotes': [ - 'error', - 'single' - ], - 'radix': 'error', - 'require-atomic-updates': 'error', - 'require-await': 'error', - 'require-jsdoc': 'error', - 'require-unicode-regexp': 'off', - 'rest-spread-spacing': [ - 'error', - 'never' - ], - 'semi': 'error', - 'semi-spacing': 'error', - 'semi-style': [ - 'error', - 'last' - ], - 'sort-imports': 'error', - 'sort-keys': 'off', - 'sort-vars': 'error', - 'space-before-blocks': 'error', - 'space-before-function-paren': 'error', - 'space-in-parens': [ - 'error', - 'always' - ], - 'space-infix-ops': 'error', - 'spaced-comment': [ - 'error', - 'always' - ], - 'strict': [ - 'error', - 'never' - ], - 'switch-colon-spacing': 'error', - 'symbol-description': 'error', - 'template-curly-spacing': [ - 'error', - 'always' - ], - 'template-tag-spacing': 'error', - 'unicode-bom': [ - 'error', - 'never' - ], - 'valid-jsdoc': 'off', - 'vars-on-top': 'error', - 'wrap-iife': 'error', - 'wrap-regex': 'error', - 'yield-star-spacing': 'error', - 'yoda': [ - 'error', - 'never' - ] - } -}; diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/.travis.yml b/.travis.yml index 88bcb31..5248909 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,6 @@ script: "npm run lint && npm run test && npm run test-build" node_js: - node - lts/* - - 12 - - 10 branches: only: - main diff --git a/README.md b/README.md index c504b98..ac50d2b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Webpack Helpers -A [Human Made](https://humanmade.com) project. [![Build Status](https://travis-ci.com/humanmade/webpack-helpers.svg?branch=main)](https://travis-ci.com/humanmade/webpack-helpers) +A [Human Made](https://humanmade.com) project. ![Build Status](https://app.travis-ci.com/humanmade/webpack-helpers.svg?token=xNUfWUZqcGkpUy3iiQyu&branch=main) ## Background @@ -12,6 +12,16 @@ Visit [humanmade.github.io/webpack-helpers](https://humanmade.github.io/webpack- Visit the [`docs/`](./docs) folder to view or modify the content used to generate this documentation site. +## Requirements + +- Node.js >= 22.0.0 + +This project includes an `.nvmrc` file specifying Node.js 22. If you use nvm, you can run: + +```bash +nvm use +``` + ## What's In The Box ### [Opinionated Configuration Presets](https://humanmade.github.io/webpack-helpers/modules/presets) diff --git a/docs/changelog.md b/docs/changelog.md index 5b4df9b..50fb217 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,9 +6,50 @@ nav_order: 10 # Changelog -## Next - -- Include the `contenthash` in generated CSS filenames. [#204](https://github.com/humanmade/webpack-helpers/pull/204) +## v1.0.0-alpha + +### Added + +- Full Webpack 5 support with modern optimizations and performance improvements +- ESLint 9+ support with flat configuration format (`eslint.config.js`) +- Modern `eslint-webpack-plugin` replacing deprecated `eslint-loader` +- Enhanced TypeScript support for `.ts` and `.tsx` files +- Updated peer dependencies to latest stable versions +- Improved build performance with Webpack 5's enhanced tree shaking +- Module federation capabilities support + +### Changed + +- **BREAKING**: Upgraded from Webpack 4 to Webpack 5 +- **BREAKING**: Updated ESLint support to use ESLint 9+ flat configuration format +- **BREAKING**: Replaced deprecated `eslint-loader` with `eslint-webpack-plugin` +- **BREAKING**: Updated minimum Node.js requirement to align with Webpack 5 +- Updated all bundled dependencies to latest stable versions +- Improved error handling and debugging capabilities +- Enhanced development server performance + +### Removed + +- Support for legacy `.eslintrc.*` configuration files (use `eslint.config.js` instead) +- Webpack 4 compatibility and related legacy code +- `eslint-loader` dependency (replaced with `eslint-webpack-plugin`) +- Outdated peer dependency constraints + +### Migration Guide + +- Update your `package.json` to use `webpack@5`, `webpack-cli@5`, and `webpack-dev-server@5` +- If using ESLint, migrate from `.eslintrc.*` files to `eslint.config.js` using the flat configuration format +- Review and update any custom webpack configurations to ensure Webpack 5 compatibility +- Update Node.js to a supported version if needed + +## v0.12.0 + +* Include contenthash in generated CSS filenames by @kadamwhite in https://github.com/humanmade/webpack-helpers/pull/204 +* Use Just the HM Docs theme by @joeleenk in https://github.com/humanmade/webpack-helpers/pull/210 +* Add workflow to deploy GH pages by @joeleenk in https://github.com/humanmade/webpack-helpers/pull/212 +* Update to Jekyll 4, inherit from theme by @joeleenk in https://github.com/humanmade/webpack-helpers/pull/213 +* Update getting-started.md by @pamprn09 in https://github.com/humanmade/webpack-helpers/pull/216 +* Update externals for WP 6.2 and add snapshot update command by @Sephsekla in https://github.com/humanmade/webpack-helpers/pull/217 ## v0.11.1 diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index d184e87..08ce490 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -18,17 +18,10 @@ While this package depends in turn on a number of loaders and plugins, it delibe ** *Check notice before proceeding:** ```bash -npm install --save-dev @humanmade/webpack-helpers webpack@4 webpack-cli@3 webpack-dev-server sass +npm install --save-dev @humanmade/webpack-helpers webpack@5 webpack-cli@6 webpack-dev-server@5 sass ``` -Note that we specify Webpack version 4. Support for Webpack 5 is anticipated in the v1.0 release of these helpers, but at present using Webpack 4 provides the most predictable and stable experience across our projects. - -***❗ *Notice*** -This verson is outdated and might leave you with outdated versions of this library and associated Webpack tooling. -There's a [pending release](https://github.com/humanmade/webpack-helpers/pull/205) that will fix this problem. But for now you should install humanmade/webpack-helpers@beta, webpack @5, webpack-cli@4, and webpack-dev-server@4. To do so run the following command: -```bash -npm install --save-dev @humanmade/webpack-helpers@beta webpack@5 webpack-cli@4 webpack-dev-server@4 -``` +**Webpack 5 Support:** This package now fully supports Webpack 5 with all the latest features and optimizations. We recommend using Webpack 5 for all new projects as it provides better performance, improved tree shaking, and enhanced module federation capabilities. ## Configuring Webpack @@ -54,7 +47,41 @@ By the end of this guide Webpack will take our source JavaScript files from thes **ESLint** -If [ESLint](https://eslint.org/) is installed, `eslint-loader` will be used to validate that your code compiles and passes required style rules before the bundle is generated. While ESLint will be used if present, these helpers do not assume any specific configuration or rules. If you aren't using ESLint you may install and configure it with basic syntax and style rules by following the [official getting started guide](https://eslint.org/docs/user-guide/getting-started), or by installing Human Made's [`@humanmade/eslint-config`](https://www.npmjs.com/package/@humanmade/eslint-config) preset. +If [ESLint](https://eslint.org/) is installed, the webpack plugin `eslint-webpack-plugin` will be used to validate that your code compiles and passes required style rules before the bundle is generated. This package now supports ESLint 9+ with the modern flat configuration format. + +To configure ESLint for your project, create an `eslint.config.js` file in your project root: + +```js +// eslint.config.js +import js from '@eslint/js'; + +export default [ + js.configs.recommended, + { + files: ['**/*.js', '**/*.jsx'], + languageOptions: { + ecmaVersion: 2024, + sourceType: 'module', + globals: { + // Browser globals + window: 'readonly', + document: 'readonly', + // WordPress globals + wp: 'readonly', + jQuery: 'readonly', + $: 'readonly' + } + }, + rules: { + // Add your custom rules here + 'no-console': 'warn', + 'no-unused-vars': 'warn' + } + } +]; +``` + +**Note:** ESLint 9 uses the new flat configuration format. The old `.eslintrc.*` files are no longer supported. If you need to migrate from an older ESLint configuration, refer to the [ESLint migration guide](https://eslint.org/docs/latest/use/configure/migration-guide). **Babel** diff --git a/docs/index.md b/docs/index.md index 8e39b97..af7af5a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -54,7 +54,8 @@ The included presets provide the following out of the box: - Automatic inlining of small image and font assets as Data URI strings. - PostCSS [Autoprefixer](https://github.com/postcss/autoprefixer#readme) support and Flexbox bug fixes. - [TypeScript](https://www.typescriptlang.org/) compilation for `.ts` and `.tsx` files, if the `typescript` npm package is installed. -- Automatic [ESLint](https://eslint.org/) linting on build, if the `eslint` npm package is installed. +- Automatic [ESLint](https://eslint.org/) linting on build using ESLint 9+ with flat configuration format, if the `eslint` npm package is installed. +- Full Webpack 5 support with modern optimizations and module federation capabilities. ### WordPress Core Externals diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..bafe1d9 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,233 @@ +import globals from 'globals'; +import js from '@eslint/js'; + +export default [ + { + ignores: [ + '**/__snapshots__/**', + 'docs/**', + 'src/vendor/**', + 'test/build/**', + 'node_modules/**', + 'bower_components/**' + ] + }, + js.configs.recommended, + { + languageOptions: { + ecmaVersion: 2024, + sourceType: 'module', + globals: { + ...globals.node, + ...globals.commonjs, + ...globals.jest, + ...globals.es6, + ...globals.browser + } + }, + rules: { + 'accessor-pairs': 'error', + 'array-bracket-spacing': [ 'error', 'always' ], + 'array-callback-return': 'error', + 'arrow-body-style': 'off', + 'arrow-parens': 'off', + 'arrow-spacing': [ + 'error', + { + 'after': true, + 'before': true + } + ], + 'block-scoped-var': 'error', + 'block-spacing': 'error', + 'brace-style': 'error', + 'callback-return': 'error', + 'capitalized-comments': 'off', + 'class-methods-use-this': 'error', + 'comma-dangle': 'off', + 'comma-spacing': 'off', + 'comma-style': [ 'error', 'last' ], + 'complexity': 'error', + 'computed-property-spacing': [ 'error', 'always' ], + 'consistent-this': 'error', + 'curly': 'error', + 'default-case': 'error', + 'dot-location': [ 'error', 'property' ], + 'dot-notation': 'error', + 'eol-last': 'error', + 'eqeqeq': 'error', + 'func-call-spacing': 'error', + 'func-name-matching': 'error', + 'func-names': 'error', + 'func-style': [ 'error', 'expression' ], + 'function-paren-newline': 'error', + 'generator-star-spacing': 'error', + 'global-require': 'error', + 'guard-for-in': 'error', + 'handle-callback-err': 'error', + 'id-length': 'error', + 'id-match': 'error', + 'implicit-arrow-linebreak': [ 'error', 'beside' ], + 'indent': 'off', + 'init-declarations': 'off', + 'jsx-quotes': 'error', + 'key-spacing': 'off', + 'keyword-spacing': 'error', + 'line-comment-position': 'error', + 'linebreak-style': [ 'error', 'unix' ], + 'lines-around-comment': 'off', + 'lines-between-class-members': 'error', + 'max-classes-per-file': 'error', + 'max-depth': 'error', + 'max-len': 'off', + 'max-lines': 'off', + 'max-lines-per-function': 'off', + 'max-nested-callbacks': 'error', + 'max-params': 'error', + 'max-statements-per-line': 'error', + 'multiline-comment-style': [ 'error', 'separate-lines' ], + 'multiline-ternary': [ 'error', 'always-multiline' ], + 'new-cap': 'error', + 'new-parens': 'error', + 'newline-after-var': 'off', + 'newline-before-return': 'off', + 'newline-per-chained-call': 'error', + 'no-alert': 'error', + 'no-array-constructor': 'error', + 'no-async-promise-executor': 'error', + 'no-await-in-loop': 'error', + 'no-bitwise': 'error', + 'no-caller': 'error', + 'no-confusing-arrow': [ 'error', { 'allowParens': true } ], + 'no-continue': 'error', + 'no-div-regex': 'error', + 'no-duplicate-imports': 'error', + 'no-else-return': 'error', + 'no-empty-function': 'error', + 'no-eq-null': 'error', + 'no-eval': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'error', + 'no-extra-label': 'error', + 'no-extra-parens': 'off', + 'no-floating-decimal': 'error', + 'no-implicit-coercion': 'error', + 'no-implicit-globals': 'error', + 'no-implied-eval': 'error', + 'no-inline-comments': 'error', + 'no-invalid-this': 'error', + 'no-iterator': 'error', + 'no-label-var': 'error', + 'no-labels': 'error', + 'no-lone-blocks': 'error', + 'no-lonely-if': 'error', + 'no-loop-func': 'error', + 'no-magic-numbers': 'off', + 'no-misleading-character-class': 'error', + 'no-mixed-operators': 'error', + 'no-mixed-requires': 'error', + 'no-multi-assign': 'error', + 'no-multi-spaces': 'error', + 'no-multi-str': 'error', + 'no-multiple-empty-lines': 'error', + 'no-negated-condition': 'error', + 'no-nested-ternary': 'error', + 'no-new': 'error', + 'no-new-func': 'error', + 'no-new-object': 'error', + 'no-new-require': 'error', + 'no-new-wrappers': 'error', + 'no-octal-escape': 'error', + 'no-param-reassign': 'error', + 'no-path-concat': 'error', + 'no-plusplus': 'error', + 'no-process-exit': 'error', + 'no-proto': 'error', + 'no-prototype-builtins': 'error', + 'no-restricted-globals': 'error', + 'no-restricted-imports': 'error', + 'no-restricted-modules': 'error', + 'no-restricted-properties': 'error', + 'no-restricted-syntax': 'error', + 'no-return-assign': 'error', + 'no-return-await': 'error', + 'no-script-url': 'error', + 'no-self-compare': 'error', + 'no-sequences': 'error', + 'no-shadow-restricted-names': 'error', + 'no-tabs': [ 'error', { 'allowIndentationTabs': true } ], + 'no-template-curly-in-string': 'error', + 'no-ternary': 'off', + 'no-throw-literal': 'error', + 'no-trailing-spaces': 'error', + 'no-undef-init': 'error', + 'no-undefined': 'off', + 'no-underscore-dangle': 'error', + 'no-unmodified-loop-condition': 'error', + 'no-unneeded-ternary': 'error', + 'no-unused-expressions': 'error', + 'no-use-before-define': 'error', + 'no-useless-call': 'error', + 'no-useless-catch': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-concat': 'error', + 'no-useless-constructor': 'error', + 'no-useless-rename': 'error', + 'no-useless-return': 'error', + 'no-var': 'error', + 'no-void': 'error', + 'no-warning-comments': 'warn', + 'no-whitespace-before-property': 'error', + 'no-with': 'error', + 'nonblock-statement-body-position': 'error', + 'object-curly-newline': 'error', + 'object-curly-spacing': [ 'error', 'always' ], + 'object-property-newline': 'error', + 'object-shorthand': 'error', + 'one-var': 'off', + 'one-var-declaration-per-line': 'error', + 'operator-assignment': 'error', + 'operator-linebreak': [ 'error', 'after' ], + 'padded-blocks': 'off', + 'padding-line-between-statements': 'error', + 'prefer-arrow-callback': 'error', + 'prefer-const': 'off', + 'prefer-destructuring': 'error', + 'prefer-numeric-literals': 'error', + 'prefer-object-spread': 'error', + 'prefer-promise-reject-errors': 'error', + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'prefer-template': 'error', + 'quote-props': 'off', + 'quotes': [ 'error', 'single' ], + 'radix': 'error', + 'require-atomic-updates': 'error', + 'require-await': 'error', + 'require-unicode-regexp': 'off', + 'rest-spread-spacing': [ 'error', 'never' ], + 'semi': 'error', + 'semi-spacing': 'error', + 'semi-style': [ 'error', 'last' ], + 'sort-imports': 'error', + 'sort-keys': 'off', + 'sort-vars': 'error', + 'space-before-blocks': 'error', + 'space-before-function-paren': 'error', + 'space-in-parens': [ 'error', 'always' ], + 'space-infix-ops': 'error', + 'spaced-comment': [ 'error', 'always' ], + 'strict': [ 'error', 'never' ], + 'switch-colon-spacing': 'error', + 'symbol-description': 'error', + 'template-curly-spacing': [ 'error', 'always' ], + 'template-tag-spacing': 'error', + 'unicode-bom': [ 'error', 'never' ], + 'vars-on-top': 'error', + 'wrap-iife': 'error', + 'wrap-regex': 'error', + 'yield-star-spacing': 'error', + 'yoda': [ 'error', 'never' ] + } + } +]; diff --git a/index.js b/index.js index f8bf15c..9f5b227 100644 --- a/index.js +++ b/index.js @@ -1,18 +1,28 @@ /** * Expose public package API. */ +const config = require( './src/config' ); +const externals = require( './src/externals' ); +const choosePort = require( './src/helpers/choose-port' ); +const cleanOnExit = require( './src/helpers/clean-on-exit' ); +const filePath = require( './src/helpers/file-path' ); +const findInObject = require( './src/helpers/find-in-object' ); +const withDynamicPort = require( './src/helpers/with-dynamic-port' ); +const loaders = require( './src/loaders' ); +const plugins = require( './src/plugins' ); +const presets = require( './src/presets' ); + module.exports = { - /* eslint-disable global-require */ - config: require( './src/config' ), - externals: require( './src/externals' ), + config, + externals, helpers: { - choosePort: require( './src/helpers/choose-port' ), - cleanOnExit: require( './src/helpers/clean-on-exit' ), - filePath: require( './src/helpers/file-path' ), - findInObject: require( './src/helpers/find-in-object' ), - withDynamicPort: require( './src/helpers/with-dynamic-port' ), + choosePort, + cleanOnExit, + filePath, + findInObject, + withDynamicPort, }, - loaders: require( './src/loaders' ), - plugins: require( './src/plugins' ), - presets: require( './src/presets' ), + loaders, + plugins, + presets, }; diff --git a/package.json b/package.json index 7b3d552..24e7d49 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { "name": "@humanmade/webpack-helpers", "public": true, - "version": "0.12.0", + "version": "1.0.0-alpha", "description": "Reusable Webpack configuration components & related helper utilities.", "main": "index.js", "author": "Human Made", "license": "MIT", + "engines": { + "node": ">=22.0.0" + }, "homepage": "https://github.com/humanmade/webpack-helpers#readme", "repository": { "type": "git", @@ -22,7 +25,7 @@ "test": "jest", "update-snapshot": "jest --updateSnapshot", "test-build": "rm -rf test/build && webpack --config=test/test-config.js", - "test-dev-server": "webpack-dev-server --config=test/test-config.js" + "test-dev-server": "webpack serve --config=test/test-config.js" }, "jest": { "setupFilesAfterEnv": [ @@ -30,53 +33,53 @@ ] }, "devDependencies": { - "eslint": "^7.17.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" + "@eslint/js": "^9.34.0", + "eslint": "^9.34.0", + "eslint-webpack-plugin": "^5.0.2", + "globals": "^16.3.0", + "jest": "^30.1.3", + "sass": "^1.92.0", + "typescript": "^5.9.2", + "webpack": "^5.101.3", + "webpack-cli": "^6.0.1", + "webpack-dev-server": "^5.2.2" }, "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/core": "^7.28.3", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@wordpress/babel-preset-default": "^8.30.0", + "babel-loader": "^10.0.0", "babel-plugin-transform-react-jsx": "^6.24.1", - "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", + "bell-on-bundler-error-plugin": "^3.0.0", + "block-editor-hmr": "^0.7.0", + "chalk": "^5.6.0", + "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^13.0.1", + "css-loader": "^7.1.2", + "css-minimizer-webpack-plugin": "^7.0.2", "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", + "inquirer": "^12.9.4", + "is-root": "^3.0.0", + "mini-css-extract-plugin": "^2.9.4", + "postcss": "^8.5.6", "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": "^8.2.0", + "postcss-preset-env": "^10.3.1", + "run-parallel": "^1.2.0", + "sass-loader": "^16.0.5", + "signal-exit": "^4.1.0", + "style-loader": "^4.0.0", + "terser-webpack-plugin": "^5.3.14", + "ts-loader": "^9.5.4", + "webpack-bundle-analyzer": "^4.10.2", + "webpack-fix-style-only-entries": "^0.6.1", + "webpack-manifest-plugin": "^5.0.1" }, "peerDependencies": { "sass": "*", "typescript": "*", - "webpack": "^4.0.0", - "webpack-cli": "^3.0.0", - "webpack-dev-server": "^3.0.0" + "webpack": "^5.0.0", + "webpack-cli": "^5.0.0", + "webpack-dev-server": "^5.0.0" } } diff --git a/src/__snapshots__/externals.test.js.snap b/src/__snapshots__/externals.test.js.snap index 65c37ad..d91e3eb 100644 --- a/src/__snapshots__/externals.test.js.snap +++ b/src/__snapshots__/externals.test.js.snap @@ -1,7 +1,7 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`externals contains entries for all core WordPress JS global members 1`] = ` -Object { +{ "@wordpress/a11y": "wp.a11y", "@wordpress/annotations": "wp.annotations", "@wordpress/api-fetch": "wp.apiFetch", diff --git a/src/config.js b/src/config.js index 144df73..437c7ec 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': '*', }, - hotOnly: true, - watchOptions: { - aggregateTimeout: 300, - }, + hot: true, + }, + + watchOptions: { + aggregateTimeout: 300, }, /** diff --git a/src/helpers/clean-on-exit.js b/src/helpers/clean-on-exit.js index 944aaab..72b51c0 100644 --- a/src/helpers/clean-on-exit.js +++ b/src/helpers/clean-on-exit.js @@ -18,7 +18,7 @@ const cleanOnExit = ( paths = [] ) => { paths.forEach( path => { try { unlinkSync( path ); - } catch ( err ) { + } catch ( err ) { // eslint-disable-line no-unused-vars // Silently ignore unlinking errors: so long as the file is gone, that is ok. } } ); diff --git a/src/helpers/is-installed.js b/src/helpers/is-installed.js index 228d9ac..4509af5 100644 --- a/src/helpers/is-installed.js +++ b/src/helpers/is-installed.js @@ -5,12 +5,11 @@ * @returns {Boolean} Whether the package is available via `require()`. */ module.exports = ( packageName ) => { - /* eslint-disable global-require */ + const checkRequire = require; try { - require( packageName ); + checkRequire( packageName ); return true; - } catch ( err ) { + } catch ( err ) { // eslint-disable-line no-unused-vars return false; } - /* eslint-enable */ }; diff --git a/src/loaders.js b/src/loaders.js index 0a23115..0cfe3c4 100644 --- a/src/loaders.js +++ b/src/loaders.js @@ -28,18 +28,10 @@ const createLoaderFactory = loaderKey => { }; // Define all supported loader factories within the loaders object. -[ 'eslint', 'js', 'ts', 'url', 'style', 'css', 'postcss', 'sass', 'file' ].forEach( loaderKey => { +[ 'js', 'ts', 'style', 'css', 'postcss', 'sass', 'asset', 'assetResource', 'assetInline' ].forEach( loaderKey => { loaders[ loaderKey ] = createLoaderFactory( loaderKey ); } ); -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)/, @@ -56,14 +48,27 @@ loaders.ts.defaults = { loader: require.resolve( 'ts-loader' ), }; -loaders.url.defaults = { +// Asset modules replace url-loader and file-loader in Webpack 5 +loaders.asset.defaults = { test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|eot|ttf)$/, - loader: require.resolve( 'url-loader' ), - options: { - limit: 10000, + type: 'asset', + parser: { + dataUrlCondition: { + maxSize: 10000, + }, }, }; +loaders.assetResource.defaults = { + test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|eot|ttf)$/, + type: 'asset/resource', +}; + +loaders.assetInline.defaults = { + test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|eot|ttf)$/, + type: 'asset/inline', +}; + loaders.style.defaults = { loader: require.resolve( 'style-loader' ), options: {}, @@ -102,11 +107,4 @@ 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: {}, -}; - module.exports = loaders; diff --git a/src/loaders.test.js b/src/loaders.test.js index deb445d..1a380fd 100644 --- a/src/loaders.test.js +++ b/src/loaders.test.js @@ -22,23 +22,16 @@ 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(); } ); } ); - describe( '.url()', () => { + describe( '.asset()', () => { it( 'tests for static assets', () => { [ 'file.png', @@ -51,16 +44,19 @@ describe( 'loaders', () => { 'file.eot', 'file.ttf', ].forEach( acceptedFileType => { - expect( acceptedFileType.match( loaders.url().test ) ).not.toBeNull(); + expect( acceptedFileType.match( loaders.asset().test ) ).not.toBeNull(); } ); [ 'file.js', 'file.css', 'file.html', ].forEach( unacceptedFileType => { - expect( unacceptedFileType.match( loaders.url().test ) ).toBeNull(); + expect( unacceptedFileType.match( loaders.asset().test ) ).toBeNull(); } ); } ); + it( 'uses asset module type', () => { + expect( loaders.asset().type ).toBe( 'asset' ); + } ); } ); } ); diff --git a/src/plugins.js b/src/plugins.js index 3de1ad3..1c4b8b8 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -1,16 +1,19 @@ const { BundleAnalyzerPlugin } = require( 'webpack-bundle-analyzer' ); const { CleanWebpackPlugin } = require( 'clean-webpack-plugin' ); const { HotModuleReplacementPlugin } = require( 'webpack' ); +const { WebpackManifestPlugin: ManifestPlugin } = require( 'webpack-manifest-plugin' ); + const BellOnBundleErrorPlugin = require( 'bell-on-bundler-error-plugin' ); const CopyPlugin = require( 'copy-webpack-plugin' ); +const CssMinimizerPlugin = require( 'css-minimizer-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 TerserPlugin = require( 'terser-webpack-plugin' ); -const OptimizeCssAssetsPlugin = require( 'optimize-css-assets-webpack-plugin' ); const deepMerge = require( './helpers/deep-merge' ); + module.exports = { /** * Expose plugin constructor functions for use in consuming applications. @@ -22,11 +25,12 @@ module.exports = { BundleAnalyzerPlugin, CleanWebpackPlugin, CopyPlugin, + ESLintPlugin, FixStyleOnlyEntriesPlugin, HotModuleReplacementPlugin, ManifestPlugin, MiniCssExtractPlugin, - OptimizeCssAssetsPlugin, + CssMinimizerPlugin, TerserPlugin, }, @@ -154,12 +158,28 @@ module.exports = { } ), /** - * Create a new OptimizeCssAssetsPlugin instance. + * Create a new CssMinimizerPlugin instance (replaces OptimizeCssAssetsPlugin in Webpack 5). + * + * @param {Object} [options] Optional plugin configuration options. + * @returns {CssMinimizerPlugin} A configured CssMinimizerPlugin instance. + */ + cssMinimizer: ( options = {} ) => new CssMinimizerPlugin( options ), + + + /** + * Create a new ESLintPlugin instance. * * @param {Object} [options] Optional plugin configuration options. - * @returns {OptimizeCssAssetsPlugin} A configured OptimizeCssAssetsPlugin instance. + * @returns {ESLintPlugin} A configured ESLintPlugin instance. */ - optimizeCssAssets: ( options = {} ) => new OptimizeCssAssetsPlugin( options ), + eslint: ( options = {} ) => new ESLintPlugin( { + extensions: [ 'js', 'jsx', 'ts', 'tsx' ], + files: [ 'src/**/*' ], + eslintPath: 'eslint/use-at-your-own-risk', + overrideConfigFile: 'eslint.config.mjs', + failOnError: true, + ...options, + } ), /** * Create a new TerserPlugin instance, defaulting to a set of options diff --git a/src/presets.js b/src/presets.js index 9f3a28a..95e9e43 100644 --- a/src/presets.js +++ b/src/presets.js @@ -1,4 +1,4 @@ -const { devServer, stats } = require( './config' ); +const { devServer, stats, watchOptions } = require( './config' ); const deepMerge = require( './helpers/deep-merge' ); const filePath = require( './helpers/file-path' ); const findInObject = require( './helpers/find-in-object' ); @@ -8,6 +8,7 @@ const loaders = require( './loaders' ); const plugins = require( './plugins' ); const { ManifestPlugin, MiniCssExtractPlugin } = plugins.constructors; + /** * Dictionary of shared seed objects by path. * @@ -138,23 +139,15 @@ const development = ( config = {}, options = {} ) => { module: { strictExportPresence: true, rules: [ - // Run all JS files through ESLint, if installed. - ...ifInstalled( 'eslint', getFilteredLoader( 'eslint', { - options: { - emitWarning: true, - }, - } ) ), { // "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 asset modules 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' ), // Parse styles using SASS, then PostCSS. filterLoaders( { test: /\.s?css$/, @@ -177,9 +170,8 @@ 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' ), + // Asset modules handle images, fonts, etc. (replaces url-loader and file-loader) + getFilteredLoader( 'asset' ), ], }, ], @@ -191,9 +183,12 @@ const development = ( config = {}, options = {} ) => { devServer: { ...devServer, - stats, }, + watchOptions, + + stats, + plugins: [ plugins.hotModuleReplacement(), ], @@ -293,19 +288,15 @@ const production = ( config = {}, options = {} ) => { module: { strictExportPresence: true, rules: [ - // Run all JS files through ESLint, if installed. - ...ifInstalled( 'eslint', getFilteredLoader( 'eslint' ) ), { // "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 asset modules 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' ), // Parse styles using SASS, then PostCSS. filterLoaders( { test: /\.s?css$/, @@ -318,9 +309,8 @@ 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' ), + // Asset modules handle images, fonts, etc. (replaces url-loader and file-loader) + getFilteredLoader( 'asset' ), ], }, ], @@ -329,29 +319,36 @@ const production = ( config = {}, options = {} ) => { optimization: { minimizer: [ plugins.terser(), - plugins.optimizeCssAssets( ( + plugins.cssMinimizer( ( // Set option to output source maps if devtool is set. config.devtool && ! ( /inline-/ ).test( config.devtool ) ? { - cssProcessorOptions: { - map: { - inline: false, - }, + minimizerOptions: { + preset: [ + 'default', + { + map: { + inline: false, + }, + }, + ], }, } : undefined ) ), ], nodeEnv: 'production', - noEmitOnErrors: true, + emitOnErrors: false, }, - stats, + stats, - plugins: [], - }; + plugins: [ + // Plugins will be added conditionally below + ], +}; - // If no entry was provided, inject a default entry value. +// If no entry was provided, inject a default entry value. if ( ! config.entry ) { prodDefaults.entry = { index: filePath( 'src/index.js' ), diff --git a/src/presets.test.js b/src/presets.test.js index b623b2a..d92be7b 100644 --- a/src/presets.test.js +++ b/src/presets.test.js @@ -258,29 +258,29 @@ describe( 'presets', () => { }, }, { filterLoaders: ( loader, loaderType ) => { - if ( loaderType === 'file' ) { - loader.options.publicPath = '../../'; - } - if ( loaderType === 'url' ) { + if ( loaderType === 'asset' ) { + // Test filtering asset modules (Webpack 5 replacement for file/url loaders) loader.test = /\.(png|jpg|jpeg|gif|svg)$/; + loader.parser = { + dataUrlCondition: { + maxSize: 8000, + }, + }; } return loader; }, } ); - const fileLoader = getLoaderByName( config.module.rules, 'file-loader' ); - const urlLoader = getLoaderByName( config.module.rules, 'url-loader' ); + // Look for asset module rule instead of file-loader/url-loader + const assetRule = getLoaderByTest( config.module.rules, /\.(png|jpg|jpeg|gif|svg)$/ ); const jsLoader = getLoaderByName( config.module.rules, 'babel-loader' ); - expect( fileLoader ).toEqual( expect.objectContaining( { - exclude: /\.(js|html|json)$/, - options: { - publicPath: '../../', - }, - } ) ); - expect( urlLoader ).toEqual( expect.objectContaining( { + expect( assetRule ).toEqual( expect.objectContaining( { test: /\.(png|jpg|jpeg|gif|svg)$/, - options: { - limit: 10000, - }, + type: 'asset', + parser: expect.objectContaining( { + dataUrlCondition: { + maxSize: 8000, + }, + } ), } ) ); expect( jsLoader ).not.toBeNull(); } ); @@ -467,29 +467,29 @@ describe( 'presets', () => { }, }, { filterLoaders: ( loader, loaderType ) => { - if ( loaderType === 'file' ) { - loader.options.publicPath = '../../'; - } - if ( loaderType === 'url' ) { + if ( loaderType === 'asset' ) { + // Test filtering asset modules (Webpack 5 replacement for file/url loaders) loader.test = /\.(png|jpg|jpeg|gif|svg)$/; + loader.parser = { + dataUrlCondition: { + maxSize: 8000, + }, + }; } return loader; }, } ); - const fileLoader = getLoaderByName( config.module.rules, 'file-loader' ); - const urlLoader = getLoaderByName( config.module.rules, 'url-loader' ); + // Look for asset module rule instead of file-loader/url-loader + const assetRule = getLoaderByTest( config.module.rules, /\.(png|jpg|jpeg|gif|svg)$/ ); const jsLoader = getLoaderByName( config.module.rules, 'babel-loader' ); - expect( fileLoader ).toEqual( expect.objectContaining( { - exclude: /\.(js|html|json)$/, - options: { - publicPath: '../../', - }, - } ) ); - expect( urlLoader ).toEqual( expect.objectContaining( { + expect( assetRule ).toEqual( expect.objectContaining( { test: /\.(png|jpg|jpeg|gif|svg)$/, - options: { - limit: 10000, - }, + type: 'asset', + parser: expect.objectContaining( { + dataUrlCondition: { + maxSize: 8000, + }, + } ), } ) ); expect( jsLoader ).not.toBeNull(); } ); diff --git a/test/src/index.js b/test/src/index.js index 032f5a8..e5adcd2 100644 --- a/test/src/index.js +++ b/test/src/index.js @@ -1,7 +1,7 @@ -import { getResults } from './helpers'; - import './style.scss'; +import { getResults } from './helpers'; + ( async () => { const results = await getResults(); console.log( results ); diff --git a/test/test-config.js b/test/test-config.js index f196d43..ca6d38e 100644 --- a/test/test-config.js +++ b/test/test-config.js @@ -10,7 +10,7 @@ module.exports = [ }, output: { path: filePath( 'test/build/prod' ), - } + }, } ), // This second build targets the same output directory as 'production-test' // to demonstrate whether the asset manifest is correctly shared between @@ -23,7 +23,7 @@ module.exports = [ }, output: { path: filePath( 'test/build/prod' ), - } + }, } ), // This third build targets a different output directory than the others // to make sure that manifests are not shared _between_ directories. @@ -35,7 +35,7 @@ module.exports = [ }, output: { path: filePath( 'test/build/prod-alternate' ), - } + }, } ), // This build uses a development configuration instead of a prod one. presets.development( {