From 6fe5ec99b9a382c30f29de59c4a37663c2b5e83d Mon Sep 17 00:00:00 2001 From: Ryan Leeson Date: Tue, 24 Feb 2026 09:36:37 -0500 Subject: [PATCH 1/8] Issue #428: Handle webpack-dev-server HMR related proxy errors --- packages/toolkit/README.md | 18 ++++++++++++++- packages/toolkit/config/webpack/devServer.js | 24 ++++++++++++++------ packages/toolkit/package.json | 4 ++-- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/packages/toolkit/README.md b/packages/toolkit/README.md index 4818224c..6341b11e 100644 --- a/packages/toolkit/README.md +++ b/packages/toolkit/README.md @@ -43,9 +43,25 @@ npm install --save-dev 10up-toolkit If you're using a version of NPM lower than 7 and `10up-toolkit` from version `4.0.0` you'll also need to install the following dependencies manually: ```bash{showPrompt} -npm install --save-dev stylelint @10up/stylelint-config @10up/eslint-config @10up/babel-preset-default +npm install --save-dev stylelint @10up/stylelint-config @10up/eslint-config @10up/babel-preset-default webpack-dev-server ``` +#### Upgrading the toolkit + +When you upgrade `10up-toolkit`, the lock file (`package-lock.json` or `yarn.lock`) may keep an older version of transitive dependencies such as `webpack-dev-server`. If you use the dev server or hot reload and see issues after upgrading, ensure a matching version is installed: + +```bash{showPrompt} +npm update webpack-dev-server +``` + +Or reinstall from a clean state: + +```bash{showPrompt} +rm -rf node_modules package-lock.json && npm install +``` + +The toolkit declares `webpack-dev-server` as a peer dependency so that your project’s dependency tree controls the version and upgrades are predictable when you run `npm install` after bumping the toolkit (npm 7+ installs peer dependencies automatically). + ### Setting it up In order to get `10up-toolkit` up and running simply define the `source` and `main` properties in your `package.json` file. diff --git a/packages/toolkit/config/webpack/devServer.js b/packages/toolkit/config/webpack/devServer.js index 4dd6365e..3b090116 100644 --- a/packages/toolkit/config/webpack/devServer.js +++ b/packages/toolkit/config/webpack/devServer.js @@ -1,3 +1,6 @@ +// Check for WebPack Dev Server version with fallback +const { version: webpackDevServerVersion = '0' } = require('webpack-dev-server'); + module.exports = ({ isPackage, isModule, @@ -23,6 +26,19 @@ module.exports = ({ // do nothing } + // If WebPack Dev Service version is v5.x.x, use the new proxy configuration + // Supports Toolkit upgrades where WDS was not updated to v5.x.x + const distProxyConfiguration = { + '/dist': { + pathRewrite: { + '^/dist': '', + }, + }, + }; + const proxy = webpackDevServerVersion.startsWith('5.') + ? [distProxyConfiguration] + : distProxyConfiguration; + return { devMiddleware: { writeToDisk: true, @@ -37,13 +53,7 @@ module.exports = ({ }, }, port: Number(devServerPort), - proxy: { - '/dist': { - pathRewrite: { - '^/dist': '', - }, - }, - }, + proxy, }; } diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index a0757265..83fad8d4 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -69,7 +69,6 @@ "url-loader": "^4.1.1", "webpack": "^5.89.0", "webpack-bundle-analyzer": "^4.10.1", - "webpack-dev-server": "^5.2.2", "webpack-sources": "^3.2.3", "webpackbar": "^6.0.0", "yaml": "^2.4.1" @@ -85,7 +84,8 @@ "@10up/stylelint-config": ">=3.0.0", "@linaria/babel-preset": ">=4.3.3", "@linaria/webpack-loader": ">=4.1.11", - "typescript": ">=5.0.0" + "typescript": ">=5.0.0", + "webpack-dev-server": "^5.2.2" }, "peerDependenciesMeta": { "@linaria/webpack-loader": { From f9f07efd0bf5b22be6bac06e8f5503573abc7277 Mon Sep 17 00:00:00 2001 From: Ryan Leeson Date: Tue, 24 Feb 2026 19:30:08 -0500 Subject: [PATCH 2/8] Issue #428: Handle minimum package requirements and messaging --- packages/toolkit/README.md | 2 ++ packages/toolkit/config/webpack/devServer.js | 11 ++++++++ packages/toolkit/utils/index.js | 9 ++++++- packages/toolkit/utils/package.js | 27 ++++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/packages/toolkit/README.md b/packages/toolkit/README.md index 6341b11e..00061cb3 100644 --- a/packages/toolkit/README.md +++ b/packages/toolkit/README.md @@ -62,6 +62,8 @@ rm -rf node_modules package-lock.json && npm install The toolkit declares `webpack-dev-server` as a peer dependency so that your project’s dependency tree controls the version and upgrades are predictable when you run `npm install` after bumping the toolkit (npm 7+ installs peer dependencies automatically). +**Scenarios where an older version can still be used:** Installing with `npm install --legacy-peer-deps` skips peer dependency installation and conflict checks, so an older or transitive copy of `webpack-dev-server` may be used. Pinning an old version in your own `package.json` or having another dependency that depends on an older `webpack-dev-server` can also leave an incompatible version in the tree. In those cases the toolkit will print a warning at runtime; install a matching version (e.g. `webpack-dev-server@^5.2.2`) in your project to clear it. + ### Setting it up In order to get `10up-toolkit` up and running simply define the `source` and `main` properties in your `package.json` file. diff --git a/packages/toolkit/config/webpack/devServer.js b/packages/toolkit/config/webpack/devServer.js index 3b090116..3a6249b2 100644 --- a/packages/toolkit/config/webpack/devServer.js +++ b/packages/toolkit/config/webpack/devServer.js @@ -1,5 +1,16 @@ // Check for WebPack Dev Server version with fallback const { version: webpackDevServerVersion = '0' } = require('webpack-dev-server'); +const { isMinimumPackageVersion } = require('../../utils'); + +const MIN_WDS_VERSION = '5.2.2'; +if (!isMinimumPackageVersion(webpackDevServerVersion, MIN_WDS_VERSION)) { + // eslint-disable-next-line no-console -- runtime warning for incompatible peer + console.warn( + `[10up-toolkit] webpack-dev-server ${webpackDevServerVersion} was resolved; ${MIN_WDS_VERSION} or newer is recommended.\n` + + ' If you used --legacy-peer-deps or have an older version in your tree, install a matching version:\n' + + ' npm install --save-dev webpack-dev-server@^5.2.2', + ); +} module.exports = ({ isPackage, diff --git a/packages/toolkit/utils/index.js b/packages/toolkit/utils/index.js index 2e985415..b280dc6e 100644 --- a/packages/toolkit/utils/index.js +++ b/packages/toolkit/utils/index.js @@ -28,7 +28,13 @@ const { } = require('./config'); const { fromProjectRoot, fromConfigRoot, hasProjectFile } = require('./file'); -const { hasPackageProp, getPackagePath, getPackage, getPackageVersion } = require('./package'); +const { + hasPackageProp, + getPackagePath, + getPackage, + getPackageVersion, + isMinimumPackageVersion, +} = require('./package'); const { displayWebpackStats } = require('./webpack'); @@ -79,6 +85,7 @@ module.exports = { getTenUpScriptsPackageBuildConfig, hasWebpackConfig, displayWebpackStats, + isMinimumPackageVersion, transformBlockJson, getGitBranch, }; diff --git a/packages/toolkit/utils/package.js b/packages/toolkit/utils/package.js index 1117aa54..bf16c5c3 100644 --- a/packages/toolkit/utils/package.js +++ b/packages/toolkit/utils/package.js @@ -55,7 +55,34 @@ const isPackageInstalled = (packageName) => { return false; }; +/** + * Compares two semver-like version strings (e.g. "5.2.2", "1.0.0-beta.1"). + * Only the numeric segments are compared; non-numeric segments are treated as 0. + * + * @param {string} actual - The resolved version (e.g. from a package). + * @param {string} min - The minimum required version. + * @returns {boolean} True if actual >= min, false otherwise. + */ +const isMinimumPackageVersion = (actual, min) => { + const actualVersions = actual.split('.').map(Number); + const minimumVersions = min.split('.').map(Number); + + for (let i = 0; i < Math.max(actualVersions.length, minimumVersions.length); i++) { + const actualVersionLevel = actualVersions[i] || 0; + const minimumVersionLevel = minimumVersions[i] || 0; + if (actualVersionLevel > minimumVersionLevel) { + return true; + } + if (actualVersionLevel < minimumVersionLevel) { + return false; + } + } + + return true; +}; + module.exports = { + isMinimumPackageVersion, isPackageInstalled, getPackagePath, hasPackageProp, From eb51b02a4f3cef65d4844afec84f46dc4a953c4b Mon Sep 17 00:00:00 2001 From: Ryan Leeson Date: Tue, 24 Feb 2026 19:45:21 -0500 Subject: [PATCH 3/8] Issue #428: Move the warning and version check outside of the module --- packages/toolkit/config/webpack.config.js | 17 ++++++++++++++++ packages/toolkit/config/webpack/devServer.js | 21 ++------------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/toolkit/config/webpack.config.js b/packages/toolkit/config/webpack.config.js index ffb66ee5..a5fbddea 100644 --- a/packages/toolkit/config/webpack.config.js +++ b/packages/toolkit/config/webpack.config.js @@ -1,10 +1,12 @@ /** * Internal dependencies */ +const { version: webpackDevServerVersion = '0' } = require('webpack-dev-server'); const { getBuildFiles, getTenUpScriptsConfig, getTenUpScriptsPackageBuildConfig, + isMinimumPackageVersion, } = require('../utils'); const { getModuleBuildFiles } = require('../utils/config'); @@ -42,6 +44,20 @@ const defaultTargets = [ 'not ie_mob <=11', ]; +const MIN_WDS_VERSION = '5.2.2'; +const useLegacyProxy = !isMinimumPackageVersion(webpackDevServerVersion, MIN_WDS_VERSION); +const isTestEnv = typeof process.env.JEST_WORKER_ID !== 'undefined'; + +// Skip warning in tests so snapshot/output isn't noisy; version check and useLegacyProxy still run. +if (useLegacyProxy && !isTestEnv) { + // eslint-disable-next-line no-console -- runtime warning for incompatible peer + console.warn( + `[10up-toolkit] webpack-dev-server ${webpackDevServerVersion} was resolved; ${MIN_WDS_VERSION} or newer is recommended.\n` + + ' If you used --legacy-peer-deps or have an older version in your tree, install a matching version:\n' + + ' npm install --save-dev webpack-dev-server@^5.2.2', + ); +} + const config = { projectConfig, packageConfig, @@ -51,6 +67,7 @@ const config = { mode, isProduction, defaultTargets, + useLegacyProxy, }; const baseConfig = { diff --git a/packages/toolkit/config/webpack/devServer.js b/packages/toolkit/config/webpack/devServer.js index 3a6249b2..1c285e4f 100644 --- a/packages/toolkit/config/webpack/devServer.js +++ b/packages/toolkit/config/webpack/devServer.js @@ -1,21 +1,8 @@ -// Check for WebPack Dev Server version with fallback -const { version: webpackDevServerVersion = '0' } = require('webpack-dev-server'); -const { isMinimumPackageVersion } = require('../../utils'); - -const MIN_WDS_VERSION = '5.2.2'; -if (!isMinimumPackageVersion(webpackDevServerVersion, MIN_WDS_VERSION)) { - // eslint-disable-next-line no-console -- runtime warning for incompatible peer - console.warn( - `[10up-toolkit] webpack-dev-server ${webpackDevServerVersion} was resolved; ${MIN_WDS_VERSION} or newer is recommended.\n` + - ' If you used --legacy-peer-deps or have an older version in your tree, install a matching version:\n' + - ' npm install --save-dev webpack-dev-server@^5.2.2', - ); -} - module.exports = ({ isPackage, isModule, projectConfig: { devServer, devURL, hot, devServerPort }, + useLegacyProxy = false, }) => { if (!devServer && !hot) { return undefined; @@ -37,8 +24,6 @@ module.exports = ({ // do nothing } - // If WebPack Dev Service version is v5.x.x, use the new proxy configuration - // Supports Toolkit upgrades where WDS was not updated to v5.x.x const distProxyConfiguration = { '/dist': { pathRewrite: { @@ -46,9 +31,7 @@ module.exports = ({ }, }, }; - const proxy = webpackDevServerVersion.startsWith('5.') - ? [distProxyConfiguration] - : distProxyConfiguration; + const proxy = useLegacyProxy ? distProxyConfiguration : [distProxyConfiguration]; return { devMiddleware: { From 49f92fcdac46fe136b324039c97fdf648c2319af Mon Sep 17 00:00:00 2001 From: Ryan Leeson Date: Tue, 24 Feb 2026 20:39:02 -0500 Subject: [PATCH 4/8] Issue #428: Correct the WDS5 proxy configuration --- packages/toolkit/config/webpack/devServer.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/toolkit/config/webpack/devServer.js b/packages/toolkit/config/webpack/devServer.js index 1c285e4f..03134965 100644 --- a/packages/toolkit/config/webpack/devServer.js +++ b/packages/toolkit/config/webpack/devServer.js @@ -24,14 +24,22 @@ module.exports = ({ // do nothing } - const distProxyConfiguration = { + const distProxyConfiguration = [ + { + context: '/dist', + pathRewrite: { + '^/dist': '', + }, + }, + ]; + const legacyProxyConfiguration = { '/dist': { pathRewrite: { '^/dist': '', }, }, }; - const proxy = useLegacyProxy ? distProxyConfiguration : [distProxyConfiguration]; + const proxy = useLegacyProxy ? legacyProxyConfiguration : distProxyConfiguration; return { devMiddleware: { From 5fc6234699c7670cc1c5747b2bb606dcdcbddf8b Mon Sep 17 00:00:00 2001 From: Ryan Leeson Date: Wed, 25 Feb 2026 07:58:08 -0500 Subject: [PATCH 5/8] Issue #428: Tests for the proxy configuration option --- .../config/webpack/__tests__/devServer.js | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/packages/toolkit/config/webpack/__tests__/devServer.js b/packages/toolkit/config/webpack/__tests__/devServer.js index 0d4dd613..107ce166 100644 --- a/packages/toolkit/config/webpack/__tests__/devServer.js +++ b/packages/toolkit/config/webpack/__tests__/devServer.js @@ -1,4 +1,5 @@ const externals = require('../externals'); +const getDevServer = require('../devServer'); describe('externals module function', () => { it('returns default externals for project config', () => { @@ -57,3 +58,70 @@ describe('externals module function', () => { }); }); }); + +describe('devServer', () => { + const baseHotOptions = { + isPackage: false, + isModule: false, + projectConfig: { + devServer: false, + devURL: 'http://example.test', + hot: true, + devServerPort: 3000, + }, + }; + + it('returns undefined when neither devServer nor hot is enabled', () => { + expect( + getDevServer({ + ...baseHotOptions, + projectConfig: { ...baseHotOptions.projectConfig, hot: false }, + }), + ).toBeUndefined(); + expect( + getDevServer({ + isPackage: false, + isModule: false, + projectConfig: { + devServer: false, + devURL: 'http://example.test', + hot: false, + devServerPort: 3000, + }, + }), + ).toBeUndefined(); + }); + + it('uses array-based proxy configuration (v5 style) when useLegacyProxy is false', () => { + const result = getDevServer({ ...baseHotOptions, useLegacyProxy: false }); + expect(result).toBeDefined(); + expect(result.proxy).toEqual([ + { + context: '/dist', + pathRewrite: { + '^/dist': '', + }, + }, + ]); + }); + + it('uses array-based proxy configuration (v5 style) when useLegacyProxy is omitted', () => { + const result = getDevServer(baseHotOptions); + expect(result).toBeDefined(); + expect(Array.isArray(result.proxy)).toBe(true); + expect(result.proxy[0]).toHaveProperty('context', '/dist'); + expect(result.proxy[0].pathRewrite).toEqual({ '^/dist': '' }); + }); + + it('uses object-based proxy configuration (legacy style) when useLegacyProxy is true', () => { + const result = getDevServer({ ...baseHotOptions, useLegacyProxy: true }); + expect(result).toBeDefined(); + expect(result.proxy).toEqual({ + '/dist': { + pathRewrite: { + '^/dist': '', + }, + }, + }); + }); +}); From 23a7856d4cb380e4fe043e1c85a54496f92e86cf Mon Sep 17 00:00:00 2001 From: Ryan Leeson Date: Wed, 25 Feb 2026 08:41:52 -0500 Subject: [PATCH 6/8] Issue #428: Add changeset and fix changesets configuration --- .changeset/angry-rockets-smash.md | 5 +++++ .changeset/config.json | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/angry-rockets-smash.md diff --git a/.changeset/angry-rockets-smash.md b/.changeset/angry-rockets-smash.md new file mode 100644 index 00000000..d56254c5 --- /dev/null +++ b/.changeset/angry-rockets-smash.md @@ -0,0 +1,5 @@ +--- +"10up-toolkit": minor +--- + +Fix HMR for non-script module based installations and support better peer dependency management diff --git a/.changeset/config.json b/.changeset/config.json index acdaa5ee..00c73266 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -5,10 +5,10 @@ "fixed": [], "linked": [], "access": "restricted", - "baseBranch": "trunk", + "baseBranch": "develop", "updateInternalDependencies": "patch", "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { "onlyUpdatePeerDependentsWhenOutOfRange": true }, "ignore": ["tenup-theme", "@10up/component-accordion", "@10up/library-ts-test"] -} \ No newline at end of file +} From 7df699fbcc588d1411d7a08c337c2924c10c15c7 Mon Sep 17 00:00:00 2001 From: Ryan Leeson Date: Wed, 25 Feb 2026 09:00:52 -0500 Subject: [PATCH 7/8] Issue #428: Update warning message dynamic version text --- packages/toolkit/config/webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolkit/config/webpack.config.js b/packages/toolkit/config/webpack.config.js index a5fbddea..3ea9042c 100644 --- a/packages/toolkit/config/webpack.config.js +++ b/packages/toolkit/config/webpack.config.js @@ -54,7 +54,7 @@ if (useLegacyProxy && !isTestEnv) { console.warn( `[10up-toolkit] webpack-dev-server ${webpackDevServerVersion} was resolved; ${MIN_WDS_VERSION} or newer is recommended.\n` + ' If you used --legacy-peer-deps or have an older version in your tree, install a matching version:\n' + - ' npm install --save-dev webpack-dev-server@^5.2.2', + ` npm install --save-dev webpack-dev-server@${MIN_WDS_VERSION}`, ); } From f7ca85dd2c9f58cbb75d8c9227a3f3896189484e Mon Sep 17 00:00:00 2001 From: Ryan Leeson Date: Wed, 25 Feb 2026 12:23:17 -0500 Subject: [PATCH 8/8] Issue #428: Revise package versioning check to use webpack-dev-server package.json --- .../webpack-fast-refresh.js.snap | 7 +++-- packages/toolkit/config/webpack.config.js | 9 +++--- packages/toolkit/utils/index.js | 6 ++-- packages/toolkit/utils/package.js | 28 ++++++++++++++++--- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/packages/toolkit/config/__tests__/__snapshots__/webpack-fast-refresh.js.snap b/packages/toolkit/config/__tests__/__snapshots__/webpack-fast-refresh.js.snap index 8b65cd6d..3ea0ec1b 100644 --- a/packages/toolkit/config/__tests__/__snapshots__/webpack-fast-refresh.js.snap +++ b/packages/toolkit/config/__tests__/__snapshots__/webpack-fast-refresh.js.snap @@ -18,13 +18,14 @@ exports[`webpack.config.js includes react-webpack-fast-refresh with the --hot op }, "hot": true, "port": 8000, - "proxy": { - "/dist": { + "proxy": [ + { + "context": "/dist", "pathRewrite": { "^/dist": "", }, }, - }, + ], }, "devtool": "source-map", "entry": "() => getEntryPoints({ diff --git a/packages/toolkit/config/webpack.config.js b/packages/toolkit/config/webpack.config.js index 3ea9042c..fb1a496c 100644 --- a/packages/toolkit/config/webpack.config.js +++ b/packages/toolkit/config/webpack.config.js @@ -1,9 +1,9 @@ /** * Internal dependencies */ -const { version: webpackDevServerVersion = '0' } = require('webpack-dev-server'); const { getBuildFiles, + getInstalledPackageVersion, getTenUpScriptsConfig, getTenUpScriptsPackageBuildConfig, isMinimumPackageVersion, @@ -45,11 +45,12 @@ const defaultTargets = [ ]; const MIN_WDS_VERSION = '5.2.2'; -const useLegacyProxy = !isMinimumPackageVersion(webpackDevServerVersion, MIN_WDS_VERSION); +const isHotReloadEnabled = projectConfig.devServer || projectConfig.hot; const isTestEnv = typeof process.env.JEST_WORKER_ID !== 'undefined'; +const webpackDevServerVersion = getInstalledPackageVersion('webpack-dev-server', '0'); +const useLegacyProxy = !isMinimumPackageVersion(webpackDevServerVersion, MIN_WDS_VERSION); -// Skip warning in tests so snapshot/output isn't noisy; version check and useLegacyProxy still run. -if (useLegacyProxy && !isTestEnv) { +if (isHotReloadEnabled && useLegacyProxy && !isTestEnv) { // eslint-disable-next-line no-console -- runtime warning for incompatible peer console.warn( `[10up-toolkit] webpack-dev-server ${webpackDevServerVersion} was resolved; ${MIN_WDS_VERSION} or newer is recommended.\n` + diff --git a/packages/toolkit/utils/index.js b/packages/toolkit/utils/index.js index b280dc6e..2a08cea9 100644 --- a/packages/toolkit/utils/index.js +++ b/packages/toolkit/utils/index.js @@ -29,10 +29,11 @@ const { const { fromProjectRoot, fromConfigRoot, hasProjectFile } = require('./file'); const { - hasPackageProp, - getPackagePath, + getInstalledPackageVersion, getPackage, + getPackagePath, getPackageVersion, + hasPackageProp, isMinimumPackageVersion, } = require('./package'); @@ -69,6 +70,7 @@ module.exports = { getJestOverrideConfigFile, hasJestConfig, getEnvironmentFromBranch, + getInstalledPackageVersion, hasPackageProp, hasPrettierConfig, hasEslintConfig, diff --git a/packages/toolkit/utils/package.js b/packages/toolkit/utils/package.js index bf16c5c3..57a7b584 100644 --- a/packages/toolkit/utils/package.js +++ b/packages/toolkit/utils/package.js @@ -38,6 +38,25 @@ const getPackageVersion = async () => { return pkg.version; }; +/** + * Returns the version of an installed npm package by name. + * Resolves the package's package.json so it works without importing the package. + * Uses read-pkg to read and parse the package.json. + * + * @param {string} packageName - The name of the npm package (e.g. 'webpack-dev-server'). + * @param {string} [fallback='0'] - Value to return if the package is not installed or version is unreadable. + * @returns {string} The package version string or the fallback. + */ +const getInstalledPackageVersion = (packageName, fallback = '0') => { + try { + const pkgPath = require.resolve(`${packageName}/package.json`); + const pkg = readPkg.sync({ cwd: path.dirname(pkgPath) }); + return typeof pkg.version === 'string' ? pkg.version : fallback; + } catch { + return fallback; + } +}; + /** * Checks whether the passed package name is installed in the project. * @@ -82,10 +101,11 @@ const isMinimumPackageVersion = (actual, min) => { }; module.exports = { - isMinimumPackageVersion, - isPackageInstalled, - getPackagePath, - hasPackageProp, + getInstalledPackageVersion, getPackage, + getPackagePath, getPackageVersion, + hasPackageProp, + isMinimumPackageVersion, + isPackageInstalled, };