Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/angry-rockets-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"10up-toolkit": minor
---

Fix HMR for non-script module based installations and support better peer dependency management
4 changes: 2 additions & 2 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "trunk",
"baseBranch": "develop",
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing Changesets baseBranch from trunk to develop is likely to break the existing release automation that runs on both develop and trunk (see .github/workflows/release.yml and CONTRIBUTING, which describe stable releases from trunk). If the intent is to target develop only for @next, consider handling that in the workflow/action inputs instead of globally changing baseBranch, or confirm Changesets can still open the correct PRs/publish when running on trunk.

Suggested change
"baseBranch": "develop",
"baseBranch": "trunk",

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure on this one, I have not used changeset that much before. It did fail when I tried to run it with trunk set as the baseBranch.

"updateInternalDependencies": "patch",
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
},
"ignore": ["tenup-theme", "@10up/component-accordion", "@10up/library-ts-test"]
}
}
20 changes: 19 additions & 1 deletion packages/toolkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,27 @@ 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).

**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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
18 changes: 18 additions & 0 deletions packages/toolkit/config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
*/
const {
getBuildFiles,
getInstalledPackageVersion,
getTenUpScriptsConfig,
getTenUpScriptsPackageBuildConfig,
isMinimumPackageVersion,
} = require('../utils');
const { getModuleBuildFiles } = require('../utils/config');

Expand Down Expand Up @@ -42,6 +44,21 @@ const defaultTargets = [
'not ie_mob <=11',
];

const MIN_WDS_VERSION = '5.2.2';
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);

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` +
' 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@${MIN_WDS_VERSION}`,
);
}

const config = {
projectConfig,
packageConfig,
Expand All @@ -51,6 +68,7 @@ const config = {
mode,
isProduction,
defaultTargets,
useLegacyProxy,
};

const baseConfig = {
Expand Down
68 changes: 68 additions & 0 deletions packages/toolkit/config/webpack/__tests__/devServer.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const externals = require('../externals');
const getDevServer = require('../devServer');

describe('externals module function', () => {
it('returns default externals for project config', () => {
Expand Down Expand Up @@ -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': '',
},
},
});
});
});
26 changes: 19 additions & 7 deletions packages/toolkit/config/webpack/devServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module.exports = ({
isPackage,
isModule,
projectConfig: { devServer, devURL, hot, devServerPort },
useLegacyProxy = false,
}) => {
if (!devServer && !hot) {
return undefined;
Expand All @@ -23,6 +24,23 @@ module.exports = ({
// do nothing
}

const distProxyConfiguration = [
{
context: '/dist',
pathRewrite: {
'^/dist': '',
},
},
];
const legacyProxyConfiguration = {
'/dist': {
pathRewrite: {
'^/dist': '',
},
},
};
const proxy = useLegacyProxy ? legacyProxyConfiguration : distProxyConfiguration;

return {
devMiddleware: {
writeToDisk: true,
Expand All @@ -37,13 +55,7 @@ module.exports = ({
},
},
port: Number(devServerPort),
proxy: {
'/dist': {
pathRewrite: {
'^/dist': '',
},
},
},
proxy,
};
}

Expand Down
4 changes: 2 additions & 2 deletions packages/toolkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webpack-dev-server@^5.2.2 requires Node >=18.12.0 (per its published engines), which can conflict with consumers running Node 16 (and with this package’s own engines.node if it’s still set lower). Consider either documenting/enforcing the Node >=18 requirement more explicitly here (e.g. update engines) or widening the peer range to allow a Node-16-compatible WDS major (and rely on useLegacyProxy for v4).

Suggested change
"webpack-dev-server": "^5.2.2"
"webpack-dev-server": "^4.0.0 || ^5.2.2"

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can update the engines declaration for this package, perhaps that update was missed when the overall .nvmrc was updated to use v18?

},
"peerDependenciesMeta": {
"@linaria/webpack-loader": {
Expand Down
11 changes: 10 additions & 1 deletion packages/toolkit/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@ const {
} = require('./config');
const { fromProjectRoot, fromConfigRoot, hasProjectFile } = require('./file');

const { hasPackageProp, getPackagePath, getPackage, getPackageVersion } = require('./package');
const {
getInstalledPackageVersion,
getPackage,
getPackagePath,
getPackageVersion,
hasPackageProp,
isMinimumPackageVersion,
} = require('./package');

const { displayWebpackStats } = require('./webpack');

Expand Down Expand Up @@ -63,6 +70,7 @@ module.exports = {
getJestOverrideConfigFile,
hasJestConfig,
getEnvironmentFromBranch,
getInstalledPackageVersion,
hasPackageProp,
hasPrettierConfig,
hasEslintConfig,
Expand All @@ -79,6 +87,7 @@ module.exports = {
getTenUpScriptsPackageBuildConfig,
hasWebpackConfig,
displayWebpackStats,
isMinimumPackageVersion,
transformBlockJson,
getGitBranch,
};
53 changes: 50 additions & 3 deletions packages/toolkit/utils/package.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -55,10 +74,38 @@ 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;
Comment on lines +77 to +100
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isMinimumPackageVersion doesn’t perform real semver comparison despite the JSDoc examples (e.g. 1.0.0-beta.1 will be treated as >= 1.0.0 because the trailing .1 is compared as an extra numeric segment). Either switch to a proper semver comparator (preferred, since this is a shared util) or tighten the docstring/implementation to clearly define the supported version formats (e.g. x.y.z only).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can switch to use node-semver or something similar if wanted. This was a simple comparator, I don't think it warrants a package for this single use case of testing major versions.

};

module.exports = {
isPackageInstalled,
getPackagePath,
hasPackageProp,
getInstalledPackageVersion,
getPackage,
getPackagePath,
getPackageVersion,
hasPackageProp,
isMinimumPackageVersion,
isPackageInstalled,
};
Loading