diff --git a/.changeset/use-compiler-platform-target.md b/.changeset/use-compiler-platform-target.md index 55c89a2187..59bb7b74f4 100644 --- a/.changeset/use-compiler-platform-target.md +++ b/.changeset/use-compiler-platform-target.md @@ -2,4 +2,4 @@ "webpack-dev-server": minor --- -Use `compiler.platform` to determine the target environment instead of inspecting the resolved `target` string. +Use `compiler.platform` to determine the target environment instead of inspecting the resolved `target` string. Universal targets (`"universal"` or `["web", "node"]`, where `compiler.platform.universal` is `true` since webpack `5.108.0`) are treated as web targets so the client runtime is injected. diff --git a/lib/Server.js b/lib/Server.js index 8c3335858a..21fb16c6b5 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -574,10 +574,20 @@ class Server { /** * @private * @param {Compiler} compiler compiler - * @returns {boolean} true when target is `web`, otherwise false + * @returns {boolean} true when target is `web` or `universal`, otherwise false */ static isWebTarget(compiler) { - return compiler.platform.web || false; + const { platform } = compiler; + + // A `web` or universal target (`web` and `node` both `null`) injects the + // client. `target: false` is `null` everywhere, so it is excluded. + return Boolean( + platform.web || + platform?.universal || + (compiler.options.target !== false && + platform.web === null && + platform.node === null), + ); } /** @@ -1683,6 +1693,38 @@ class Server { __webpack_dev_server_client__: this.getClientTransport(), }).apply(compiler); + // For universal targets `webpack/hot/emitter` uses Node's `events`, + // which breaks in the browser. Swap it for webpack's `EventTarget` + // emitter when available. + if ( + compiler.options.output.module && + compiler.platform.web === null && + compiler.platform.node === null + ) { + let emitter; + + try { + emitter = cjsRequire.resolve("webpack/hot/emitter-event-target.js"); + } catch { + // older webpack versions do not ship the `EventTarget` emitter + } + + if (emitter) { + new webpack.NormalModuleReplacementPlugin( + /emitter(\.js)?$/, + (result) => { + if ( + /webpack[/\\]hot|webpack-dev-server[/\\]client/.test( + result.context, + ) + ) { + result.request = emitter; + } + }, + ).apply(compiler); + } + } + if (this.options.hot) { const HMRPluginExists = compiler.options.plugins.find( (plugin) => diff --git a/package-lock.json b/package-lock.json index 56317674d0..da33a774c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,7 +99,7 @@ "typescript": "^5.7.2", "typescript-eslint": "^8.36.0", "wait-for-expect": "^3.0.2", - "webpack": "^5.105.4", + "webpack": "^5.108.0", "webpack-cli": "^7.0.2", "webpack-merge": "^6.0.1" }, @@ -4748,28 +4748,6 @@ "@types/ms": "*" } }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -8155,14 +8133,14 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "version": "5.24.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.1.tgz", + "integrity": "sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw==", "devOptional": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" @@ -8381,9 +8359,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "devOptional": true, "license": "MIT" }, @@ -10761,13 +10739,6 @@ "tslib": "2" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "devOptional": true, - "license": "BSD-2-Clause" - }, "node_modules/global-directory": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", @@ -12314,6 +12285,37 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/jiti": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", @@ -12991,9 +12993,9 @@ } }, "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", "devOptional": true, "license": "MIT", "engines": { @@ -14394,6 +14396,67 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -17918,9 +17981,9 @@ } }, "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "devOptional": true, "license": "MIT", "engines": { @@ -18026,71 +18089,6 @@ "node": ">=10" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", - "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/terser-webpack-plugin/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -18852,13 +18850,12 @@ "license": "MIT" }, "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", "devOptional": true, "license": "MIT", "dependencies": { - "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" }, "engines": { @@ -18883,13 +18880,12 @@ } }, "node_modules/webpack": { - "version": "5.106.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", - "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", + "version": "5.108.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.108.0.tgz", + "integrity": "sha512-Ln1JuYGPRTXcHECapSFSvACtHmWEN5sQqFJeLLGQ0057S7qzT2eXUz0MZUedtmIrNy3nJgnITSubIYKGED9jSQ==", "devOptional": true, "license": "MIT", "dependencies": { - "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", @@ -18899,20 +18895,19 @@ "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.20.0", - "es-module-lexer": "^2.0.0", + "enhanced-resolve": "^5.22.2", + "es-module-lexer": "^2.1.0", "eslint-scope": "5.1.1", "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", - "loader-runner": "^4.3.1", + "loader-runner": "^4.3.2", "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.6.1", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.17", - "watchpack": "^2.5.1", - "webpack-sources": "^3.3.4" + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.0" }, "bin": { "webpack": "bin/webpack.js" @@ -19041,9 +19036,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", - "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", + "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", "devOptional": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index ae9ab5ef55..14ee3edef1 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "typescript": "^5.7.2", "typescript-eslint": "^8.36.0", "wait-for-expect": "^3.0.2", - "webpack": "^5.105.4", + "webpack": "^5.108.0", "webpack-cli": "^7.0.2", "webpack-merge": "^6.0.1" }, diff --git a/test/e2e/__snapshots__/target.test.js.snap.webpack5 b/test/e2e/__snapshots__/target.test.js.snap.webpack5 index 62f2319d3d..b48bddf035 100644 --- a/test/e2e/__snapshots__/target.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/target.test.js.snap.webpack5 @@ -60,6 +60,18 @@ exports[`target > should work using "nwjs" target 1`] = ` [] `; +exports[`target > should work using "universal" target 1`] = ` +[ + "[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.", + "[HMR] Waiting for update signal from WDS...", + "Hey.", +] +`; + +exports[`target > should work using "universal" target 2`] = ` +[] +`; + exports[`target > should work using "web" target 1`] = ` [ "[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.", @@ -84,6 +96,18 @@ exports[`target > should work using "web,es5" target 2`] = ` [] `; +exports[`target > should work using "web,node" target 1`] = ` +[ + "[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.", + "[HMR] Waiting for update signal from WDS...", + "Hey.", +] +`; + +exports[`target > should work using "web,node" target 2`] = ` +[] +`; + exports[`target > should work using "webworker" target 1`] = ` [ "[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.", diff --git a/test/e2e/target.test.js b/test/e2e/target.test.js index f182fed7fb..0976b11f44 100644 --- a/test/e2e/target.test.js +++ b/test/e2e/target.test.js @@ -20,6 +20,11 @@ const port = portsMap.target; const sortByTerm = (data, term) => data.toSorted((a, b) => (a.indexOf(term) < b.indexOf(term) ? -1 : 1)); +// `"universal"` and combined `["web", "node"]` targets emit ESM and are only +// available since webpack `5.108.0`, but `peerDependencies` allows `^5.101.0`. +const [major, minor] = webpack.version.split(".").map(Number); +const supportsUniversalTarget = major > 5 || (major === 5 && minor >= 108); + describe("target", () => { const targets = [ false, @@ -35,6 +40,7 @@ describe("target", () => { "node-webkit", "es5", ["web", "es5"], + ...(supportsUniversalTarget ? ["universal", ["web", "node"]] : []), ]; for (const target of targets) { @@ -70,6 +76,20 @@ describe("target", () => { waitUntil: "networkidle0", }); + // ESM bundles load deferred, so wait for the entry to run + if (compiler.options.output.module) { + const deadline = Date.now() + 10000; + + while ( + !consoleMessages.some((message) => message.text() === "Hey.") && + Date.now() < deadline + ) { + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + } + } + t.assert.snapshot(consoleMessages.map((message) => message.text())); if ( diff --git a/test/helpers/html-generator-plugin.js b/test/helpers/html-generator-plugin.js index 6b469ea12c..5f0b26541b 100644 --- a/test/helpers/html-generator-plugin.js +++ b/test/helpers/html-generator-plugin.js @@ -1,4 +1,4 @@ -const HTMLContentForIndex = ` +const HTMLContentForIndex = (scriptType, mainScript) => `
@@ -7,12 +7,12 @@ const HTMLContentForIndex = `