Skip to content

Commit 4b00f6e

Browse files
feat: add universal target preset (browser + web worker + Node.js + Electron + NW.js) (#21214)
* feat: add universal target preset and comprehensive all-module-types test Add a dedicated `universal` target that merges `web` and `node` features, so users can write `target: "universal"` instead of `target: ["web", "node"]`. Add a comprehensive configCase exercising most module types webpack supports (esm, commonjs, json, deferred imports, source-phase imports, externals, dynamic import, top-level await, asset modules, css + css modules, html, wasm, workers) run under node, web and the universal target. Update the test runner to expand the `universal` preset to web + node when running universal bundles once per environment. * feat: universal target covers web workers and defaults to ESM output Expand the `universal` preset to merge web + webworker + node so it also covers Web Worker environments (previously `webworker` was `false`), and have it always declare ECMAScript module support. Default `experiments.outputModule` to true for universal targets: the target only works as ESM, so `target: "universal"` now builds out of the box instead of failing chunk-format selection. * perf: inline universal target properties instead of merging at runtime The universal preset resolved its properties by calling getTargetProperties for web/webworker/node and merging on every lookup. Replace that with the equivalent static literal (byte-identical result). * feat: universal target also covers Electron/NW.js and never exposes require Extend the universal preset to span Electron (all contexts) and NW.js in addition to browser, web worker and Node.js, so it externalizes the matching built-ins. Since universal output is always ESM, set `require` (synchronous CommonJS require) to false; node built-ins and `global` stay ambiguous (null) because they remain available under Node.js ESM. * test: cover all asset module types and verify universal runs per-env Add the remaining asset cases to the comprehensive universal test: asset/bytes, auto asset, and the `type: text` / `type: bytes` import attributes, plus css/global and import.meta.webpackContext. Add a per-config environment assertion; the universal config executes the same bundle in both a node-like and a web-like environment.
1 parent 03063b8 commit 4b00f6e

29 files changed

Lines changed: 536 additions & 6 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": minor
3+
---
4+
5+
Add a `universal` target preset (browser + web worker + Node.js + Electron + NW.js) that always outputs ECMAScript modules.

lib/config/defaults.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,12 @@ const applyExperimentsDefaults = (
576576
// TODO do we need sync web assembly in webpack@6?
577577
D(experiments, "syncWebAssembly", false);
578578
D(experiments, "asyncWebAssembly", experiments.futureDefaults);
579-
D(experiments, "outputModule", false);
579+
// the universal target (web + node, neither specific) only works as ESM
580+
const universal =
581+
Boolean(targetProperties) &&
582+
/** @type {TargetProperties} */ (targetProperties).node === null &&
583+
/** @type {TargetProperties} */ (targetProperties).web === null;
584+
D(experiments, "outputModule", universal);
580585
D(experiments, "lazyCompilation", undefined);
581586
D(experiments, "buildHttp", undefined);
582587
D(experiments, "cacheUnaffected", experiments.futureDefaults);

lib/config/target.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,37 @@ You can also more options via the 'target' option: 'browserslist' / 'browserslis
174174
global: false
175175
})
176176
],
177+
[
178+
"universal",
179+
"Universal target running in browser, web worker, Node.js, Electron and NW.js. Output is always ECMAScript modules.",
180+
/^universal$/,
181+
// merged web + webworker + node + electron + nwjs properties; output is
182+
// always ESM, so `require` (sync CommonJS) is never available
183+
() => ({
184+
node: null,
185+
web: null,
186+
webworker: null,
187+
browser: null,
188+
electron: null,
189+
nwjs: null,
190+
191+
electronMain: null,
192+
electronPreload: null,
193+
electronRenderer: null,
194+
195+
document: null,
196+
importScriptsInWorker: null,
197+
fetchWasm: null,
198+
nodeBuiltins: null,
199+
importScripts: null,
200+
require: false,
201+
global: null,
202+
203+
module: true,
204+
dynamicImport: true,
205+
dynamicImportInWorker: true
206+
})
207+
],
177208
[
178209
"[async-]node[X[.Y]]",
179210
"Node.js in version X.Y. The 'async-' prefix will load chunks asynchronously via 'fs' and 'vm' instead of 'require()'. Examples: node14.5, async-node10.",

test/Defaults.unittest.js

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2087,6 +2087,184 @@ describe("snapshots", () => {
20872087
`)
20882088
);
20892089

2090+
test(
2091+
"target universal (preset)",
2092+
{
2093+
// the universal preset enables ESM output (and web worker support) on its own
2094+
target: "universal"
2095+
},
2096+
(e) =>
2097+
e.toMatchInlineSnapshot(`
2098+
- Expected
2099+
+ Received
2100+
2101+
@@ ... @@
2102+
- "outputModule": false,
2103+
+ "outputModule": true,
2104+
@@ ... @@
2105+
- "electron": false,
2106+
- "electronMain": false,
2107+
- "electronPreload": false,
2108+
- "electronRenderer": false,
2109+
- "node": false,
2110+
- "nwjs": false,
2111+
+ "electron": true,
2112+
+ "electronMain": null,
2113+
+ "electronPreload": null,
2114+
+ "electronRenderer": null,
2115+
+ "node": true,
2116+
+ "nwjs": true,
2117+
@@ ... @@
2118+
- "externalsType": "var",
2119+
+ "externalsType": "module-import",
2120+
@@ ... @@
2121+
- "document": true,
2122+
- "dynamicImport": undefined,
2123+
- "dynamicImportInWorker": undefined,
2124+
+ "document": false,
2125+
+ "dynamicImport": true,
2126+
+ "dynamicImportInWorker": true,
2127+
@@ ... @@
2128+
- "module": undefined,
2129+
+ "module": true,
2130+
@@ ... @@
2131+
- "target": "web",
2132+
+ "target": undefined,
2133+
@@ ... @@
2134+
- "importMeta": true,
2135+
+ "importMeta": "preserve-unknown",
2136+
@@ ... @@
2137+
- "__dirname": "mock",
2138+
- "__filename": "mock",
2139+
+ "__dirname": "eval-only",
2140+
+ "__filename": "eval-only",
2141+
@@ ... @@
2142+
- "chunkFilename": "[name].js",
2143+
- "chunkFormat": "array-push",
2144+
+ "chunkFilename": "[name].mjs",
2145+
+ "chunkFormat": "module",
2146+
@@ ... @@
2147+
- "chunkLoading": "jsonp",
2148+
+ "chunkLoading": "import",
2149+
@@ ... @@
2150+
- "jsonp",
2151+
- "import-scripts",
2152+
+ "import",
2153+
@@ ... @@
2154+
- "fetch",
2155+
+ "universal",
2156+
@@ ... @@
2157+
- "document": true,
2158+
- "dynamicImport": undefined,
2159+
- "dynamicImportInWorker": undefined,
2160+
+ "document": false,
2161+
+ "dynamicImport": true,
2162+
+ "dynamicImportInWorker": true,
2163+
@@ ... @@
2164+
- "module": undefined,
2165+
+ "module": true,
2166+
@@ ... @@
2167+
- "filename": "[name].js",
2168+
- "globalObject": "self",
2169+
+ "filename": "[name].mjs",
2170+
+ "globalObject": "globalThis",
2171+
@@ ... @@
2172+
- "hotUpdateChunkFilename": "[id].[fullhash].hot-update.js",
2173+
+ "hotUpdateChunkFilename": "[id].[fullhash].hot-update.mjs",
2174+
@@ ... @@
2175+
- "hotUpdateMainFilename": "[runtime].[fullhash].hot-update.json",
2176+
+ "hotUpdateMainFilename": "[runtime].[fullhash].hot-update.json.mjs",
2177+
@@ ... @@
2178+
- "iife": true,
2179+
+ "iife": false,
2180+
@@ ... @@
2181+
- "module": false,
2182+
+ "module": true,
2183+
@@ ... @@
2184+
- "scriptType": false,
2185+
+ "scriptType": "module",
2186+
@@ ... @@
2187+
- "wasmLoading": "fetch",
2188+
+ "wasmLoading": "universal",
2189+
@@ ... @@
2190+
- "workerChunkFilename": "[name].js",
2191+
- "workerChunkLoading": "import-scripts",
2192+
+ "workerChunkFilename": "[name].mjs",
2193+
+ "workerChunkLoading": "import",
2194+
@@ ... @@
2195+
- "workerWasmLoading": "fetch",
2196+
+ "workerWasmLoading": "universal",
2197+
@@ ... @@
2198+
- "aliasFields": Array [
2199+
- "browser",
2200+
- ],
2201+
+ "aliasFields": Array [],
2202+
@@ ... @@
2203+
- "browser",
2204+
@@ ... @@
2205+
- "aliasFields": Array [
2206+
- "browser",
2207+
- ],
2208+
+ "aliasFields": Array [],
2209+
@@ ... @@
2210+
- "browser",
2211+
@@ ... @@
2212+
- "aliasFields": Array [
2213+
- "browser",
2214+
- ],
2215+
+ "aliasFields": Array [],
2216+
@@ ... @@
2217+
- "browser",
2218+
@@ ... @@
2219+
- "aliasFields": Array [
2220+
- "browser",
2221+
- ],
2222+
+ "aliasFields": Array [],
2223+
@@ ... @@
2224+
- "browser",
2225+
@@ ... @@
2226+
- "aliasFields": Array [
2227+
- "browser",
2228+
- ],
2229+
+ "aliasFields": Array [],
2230+
@@ ... @@
2231+
- "browser",
2232+
@@ ... @@
2233+
- "aliasFields": Array [
2234+
- "browser",
2235+
- ],
2236+
+ "aliasFields": Array [],
2237+
@@ ... @@
2238+
- "browser",
2239+
@@ ... @@
2240+
- "aliasFields": Array [
2241+
- "browser",
2242+
- ],
2243+
+ "aliasFields": Array [],
2244+
@@ ... @@
2245+
- "browser",
2246+
@@ ... @@
2247+
- "aliasFields": Array [
2248+
- "browser",
2249+
- ],
2250+
+ "aliasFields": Array [],
2251+
@@ ... @@
2252+
- "browser",
2253+
@@ ... @@
2254+
- "aliasFields": Array [
2255+
- "browser",
2256+
- ],
2257+
+ "aliasFields": Array [],
2258+
@@ ... @@
2259+
- "browser",
2260+
@@ ... @@
2261+
- "browser",
2262+
@@ ... @@
2263+
- "target": "web",
2264+
+ "target": "universal",
2265+
`)
2266+
);
2267+
20902268
test("records", { recordsPath: "some-path" }, (e) =>
20912269
e.toMatchInlineSnapshot(`
20922270
- Expected
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
(module
2+
(type $t0 (func (param i32 i32) (result i32)))
3+
(type $t1 (func (result i32)))
4+
(func $add (export "add") (type $t0) (param $p0 i32) (param $p1 i32) (result i32)
5+
(i32.add
6+
(get_local $p0)
7+
(get_local $p1)))
8+
(func $getNumber (export "getNumber") (type $t1) (result i32)
9+
(i32.const 40)))
14.6 KB
Loading
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// top-level await module, loaded via dynamic import so the entry stays sync
2+
const value = await Promise.resolve("tla");
3+
4+
export const tla = value;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = "cjs";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default "ctx-a";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default "ctx-b";

0 commit comments

Comments
 (0)