From 601fdf1b1c31e320bbfea4721171adbc83d761df Mon Sep 17 00:00:00 2001
From: phoekerson
Date: Sun, 29 Mar 2026 14:04:04 +0000
Subject: [PATCH 01/12] docs: add Modern Web Platform guide (Web Components,
Import Maps, PWA)
---
src/content/guides/modern-web-platform.mdx | 187 +++++++++++++++++++++
1 file changed, 187 insertions(+)
create mode 100644 src/content/guides/modern-web-platform.mdx
diff --git a/src/content/guides/modern-web-platform.mdx b/src/content/guides/modern-web-platform.mdx
new file mode 100644
index 000000000000..fabf686e8275
--- /dev/null
+++ b/src/content/guides/modern-web-platform.mdx
@@ -0,0 +1,187 @@
+---
+title: Modern Web Platform
+description: Web Components, import maps, and PWAs with webpack 5.
+sort: 26
+contributors:
+ - phoekerson
+---
+
+This guide describes practical webpack patterns for **Web Components**, **Import Maps**, and **Progressive Web Apps** (PWAs) with **Service Workers**. Each section states the problem, shows a minimal configuration you can copy, and notes current limits relative to future webpack improvements.
+
+T> Familiarity with [code splitting](/guides/code-splitting/), [caching](/guides/caching/) (`[contenthash]`), and the [`SplitChunksPlugin`](/plugins/split-chunks-plugin/) helps.
+
+## Web Components with webpack
+
+### Problem
+
+If more than one JavaScript bundle executes `customElements.define()` for the same tag name, the browser throws **DOMException: Failed to execute 'define' on 'CustomElementRegistry'**. That often happens when the module that registers an element is duplicated: separate entry points or async chunks each contain a copy of the registration code, so two bundles both run `define` for the same tag.
+
+### Approach
+
+Use [`optimization.splitChunks`](/configuration/optimization/#optimizationsplitchunks) so the module that defines the element lives in a **single shared chunk** loaded once. Adjust `cacheGroups` so your element definitions (or a dedicated folder such as `src/elements/`) are forced into one chunk. See [Prevent Duplication](/guides/code-splitting/#prevent-duplication) for the general idea.
+
+**webpack.config.js**
+
+```js
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+export default {
+ entry: {
+ main: "./src/main.js",
+ admin: "./src/admin.js",
+ },
+ output: {
+ filename: "[name].js",
+ path: path.resolve(__dirname, "dist"),
+ clean: true,
+ },
+ optimization: {
+ splitChunks: {
+ chunks: "all",
+ cacheGroups: {
+ // Put shared custom element modules in one async chunk.
+ customElements: {
+ test: /[\\/]src[\\/]elements[\\/]/,
+ name: "custom-elements",
+ chunks: "all",
+ enforce: true,
+ },
+ },
+ },
+ },
+};
+```
+
+Ensure both entries import the same registration module (for example `./elements/my-element.js`) so webpack can emit one `custom-elements.js` chunk instead of inlining duplicate registration in `main` and `admin`.
+
+### Limitations and future work
+
+Splitting alone does not change **browser** rules: the tag name must still be registered exactly once per document. Webpack does not yet provide a first-class “register this custom element once” primitive beyond chunk graph control. Native support for deduplicating custom element registration across the build is **planned**; until then, rely on shared chunks and a single registration module.
+
+## Import Maps with webpack
+
+### Problem
+
+[Import maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap) let the browser resolve **bare specifiers** (`import "lodash-es"` from `importmap.json` or an inline `
+
+```
+
+W> [`experiments.outputModule`](/configuration/experiments/#experimentsoutputmodule) and [`output.module`](/configuration/output/#outputmodule) are still experimental. Check the latest [webpack release notes](https://github.com/webpack/webpack/releases) before relying on them in production.
+
+### Limitations and future work
+
+Webpack **does not** emit or update `importmap.json` for you. You must maintain the map so specifiers and URLs stay aligned with `externals` and your server layout. Automatic import-map generation is **not** available in webpack 5 today; future tooling may reduce this manual step.
+
+## Progressive Web Apps (PWA) and Service Workers
+
+### Problem
+
+Long-lived caching requires **stable URLs** for HTML but **versioned URLs** for scripts and styles. Using [`[contenthash]`](/guides/caching/) in `output.filename` changes those URLs every build. A **service worker** precache list must list the **exact** URLs after each build, or offline shells will point at missing files.
+
+The [`workbox-webpack-plugin`](/guides/progressive-web-application/) **`GenerateSW`** plugin generates an entire service worker for you. That is convenient, but when you need **full control** over service worker code (custom routing, `skipWaiting` behavior, or coordination with `[contenthash]` and other plugins), **`InjectManifest`** is appropriate: you write the worker, and Workbox injects the precache manifest at build time from webpack’s asset list.
+
+### Approach
+
+Use `[contenthash]` for emitted assets and add **`InjectManifest`** from `workbox-webpack-plugin`. Your source template imports `workbox-precaching` and calls `precacheAndRoute(self.__WB_MANIFEST)`; the plugin replaces `self.__WB_MANIFEST` with the list of webpack assets (including hashed filenames).
+
+Install:
+
+```bash
+npm install workbox-webpack-plugin workbox-precaching --save-dev
+```
+
+**webpack.config.js**
+
+```js
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import HtmlWebpackPlugin from "html-webpack-plugin";
+import { InjectManifest } from "workbox-webpack-plugin";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+export default {
+ entry: "./src/index.js",
+ output: {
+ filename: "[name].[contenthash].js",
+ path: path.resolve(__dirname, "dist"),
+ clean: true,
+ },
+ plugins: [
+ new HtmlWebpackPlugin({ title: "PWA + content hashes" }),
+ new InjectManifest({
+ swSrc: path.resolve(__dirname, "src/service-worker.js"),
+ swDest: "service-worker.js",
+ }),
+ ],
+};
+```
+
+**src/service-worker.js** (precache template)
+
+```js
+import { precacheAndRoute } from "workbox-precaching";
+
+// Replaced at build time with webpack's precache manifest (hashed asset URLs).
+precacheAndRoute(globalThis.__WB_MANIFEST);
+```
+
+Register the emitted `service-worker.js` from your app (for example in `src/index.js`) with `navigator.serviceWorker.register("/service-worker.js")`, served from `dist/` with the correct scope.
+
+### Limitations and future work
+
+You must keep **`InjectManifest`** in sync with your output filenames and plugins; `GenerateSW` remains the simpler path when you do not need a custom worker. Webpack does not ship a built-in service worker precache generator; tighter integration with hashed assets may arrive in future releases. Until then, Workbox’s **`InjectManifest`** is a well-supported way to align `[contenthash]` output with precaching.
From b5cbcc010c0e9732ab9808f5f7e4883c84119000 Mon Sep 17 00:00:00 2001
From: Caleb MINTOUMBA
Date: Sun, 5 Apr 2026 20:17:07 +0000
Subject: [PATCH 02/12] Update src/content/guides/modern-web-platform.mdx
Co-authored-by: Aviv Keller
---
src/content/guides/modern-web-platform.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/content/guides/modern-web-platform.mdx b/src/content/guides/modern-web-platform.mdx
index fabf686e8275..326f5fb2b654 100644
--- a/src/content/guides/modern-web-platform.mdx
+++ b/src/content/guides/modern-web-platform.mdx
@@ -14,7 +14,7 @@ T> Familiarity with [code splitting](/guides/code-splitting/), [caching](/guides
### Problem
-If more than one JavaScript bundle executes `customElements.define()` for the same tag name, the browser throws **DOMException: Failed to execute 'define' on 'CustomElementRegistry'**. That often happens when the module that registers an element is duplicated: separate entry points or async chunks each contain a copy of the registration code, so two bundles both run `define` for the same tag.
+If more than one JavaScript bundle executes `customElements.define()` for the same tag name, the browser throws `DOMException: Failed to execute 'define' on 'CustomElementRegistry'`. That often happens when the module that registers an element is duplicated: separate entry points or async chunks each contain a copy of the registration code, so two bundles both run `define` for the same tag.
### Approach
From 75da3df5d5563b2461a232f7848f7077d4ae419e Mon Sep 17 00:00:00 2001
From: phoekerson
Date: Mon, 6 Apr 2026 01:38:28 +0000
Subject: [PATCH 03/12] docs(modern-web-platform): link DOMException to MDN and
add CDN import map example
---
src/content/guides/modern-web-platform.mdx | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/src/content/guides/modern-web-platform.mdx b/src/content/guides/modern-web-platform.mdx
index 326f5fb2b654..ba31c73fab32 100644
--- a/src/content/guides/modern-web-platform.mdx
+++ b/src/content/guides/modern-web-platform.mdx
@@ -14,7 +14,7 @@ T> Familiarity with [code splitting](/guides/code-splitting/), [caching](/guides
### Problem
-If more than one JavaScript bundle executes `customElements.define()` for the same tag name, the browser throws `DOMException: Failed to execute 'define' on 'CustomElementRegistry'`. That often happens when the module that registers an element is duplicated: separate entry points or async chunks each contain a copy of the registration code, so two bundles both run `define` for the same tag.
+If more than one JavaScript bundle executes `customElements.define()` for the same tag name, the browser throws [`DOMException`](https://developer.mozilla.org/en-US/docs/Web/API/DOMException): `Failed to execute 'define' on 'CustomElementRegistry'`. That often happens when the module that registers an element is duplicated: separate entry points or async chunks each contain a copy of the registration code, so two bundles both run `define` for the same tag.
### Approach
@@ -102,15 +102,27 @@ export default {
**importmap.json** (served alongside your HTML; URLs must match your deployment)
+Local vendor file:
+
+```json
+{
+ "imports": {
+ "lodash-es": "/vendor/lodash-es.js"
+ }
+}
+```
+
+CDN (no self-hosting required):
+
```json
{
"imports": {
- "lodash-es": "/assets/lodash-es.js"
+ "lodash-es": "https://cdn.jsdelivr.net/npm/lodash-es@4/+esm"
}
}
```
-The key `"lodash-es"` must match both the **`externals` key** and the **specifier** in your source (`import … from "lodash-es"`). The value is the URL the browser loads; webpack does not validate that file.
+The key `"lodash-es"` must match both the **`externals` key** and the **specifier** in your source (`import … from "lodash-es"`). The value is the URL the browser loads — either a local path or a CDN URL; webpack does not validate that file.
**index.html** (order matters: import map before your bundle)
From 40a434d0df15faafac34f23a02df7f82185b6fde Mon Sep 17 00:00:00 2001
From: phoekerson
Date: Mon, 13 Apr 2026 23:28:50 +0000
Subject: [PATCH 04/12] fix: add AI Coding Assistants Policy link to footer
---
src/components/Footer/Footer.jsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/components/Footer/Footer.jsx b/src/components/Footer/Footer.jsx
index d0550f6579d1..42be41195b3c 100644
--- a/src/components/Footer/Footer.jsx
+++ b/src/components/Footer/Footer.jsx
@@ -38,6 +38,7 @@ const Footer = () => (
The OpenJS Foundation |{" "}
+ AI Coding Assistants Policy |{" "}
Terms of Use |{" "}
Privacy Policy |{" "}
Bylaws |{" "}
From cfb7f05004abb1ae7bf89bf801671acc554e1e2d Mon Sep 17 00:00:00 2001
From: phoekerson
Date: Mon, 13 Apr 2026 23:44:23 +0000
Subject: [PATCH 05/12] fix: add OpenJS Foundation AI Coding Assistants Policy
to footer
---
src/components/Footer/Footer.jsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/components/Footer/Footer.jsx b/src/components/Footer/Footer.jsx
index 42be41195b3c..67b7413059a7 100644
--- a/src/components/Footer/Footer.jsx
+++ b/src/components/Footer/Footer.jsx
@@ -38,7 +38,9 @@ const Footer = () => (
The OpenJS Foundation |{" "}
- AI Coding Assistants Policy |{" "}
+
+ AI Coding Assistants Policy
+ |{" "}
Terms of Use |{" "}
Privacy Policy |{" "}
Bylaws |{" "}
From 26d0bcbe293303adfd2e98ac321ee705306a1bf8 Mon Sep 17 00:00:00 2001
From: phoekerson
Date: Tue, 14 Apr 2026 03:55:38 +0000
Subject: [PATCH 06/12] fix(footer): format OpenJS AI policy link to satisfy
Prettier
---
src/components/Footer/Footer.jsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/components/Footer/Footer.jsx b/src/components/Footer/Footer.jsx
index 67b7413059a7..3a29e683f526 100644
--- a/src/components/Footer/Footer.jsx
+++ b/src/components/Footer/Footer.jsx
@@ -40,8 +40,8 @@ const Footer = () => (
The OpenJS Foundation |{" "}
AI Coding Assistants Policy
- |{" "}
- Terms of Use |{" "}
+ {" "}
+ | Terms of Use |{" "}
Privacy Policy |{" "}
Bylaws |{" "}
Code of Conduct |{" "}
From 0ebba45224661acbc16bc78930cf8c47740eae61 Mon Sep 17 00:00:00 2001
From: phoekerson
Date: Tue, 14 Apr 2026 17:13:56 +0000
Subject: [PATCH 07/12] docs(guides): add native css guide for experiments.css
---
src/content/guides/native-css.mdx | 153 ++++++++++++++++++++++++++++++
1 file changed, 153 insertions(+)
create mode 100644 src/content/guides/native-css.mdx
diff --git a/src/content/guides/native-css.mdx b/src/content/guides/native-css.mdx
new file mode 100644
index 000000000000..d20dc13dbf27
--- /dev/null
+++ b/src/content/guides/native-css.mdx
@@ -0,0 +1,153 @@
+---
+title: Native CSS
+sort: 27
+contributors:
+ - alexander-akait
+---
+
+This guide shows how to use webpack native CSS handling with `experiments.css`.
+
+T> `experiments.css` is still experimental. It is expected to become the default in webpack v6, but behavior can still change while development continues.
+
+## Getting Started
+
+Enable native CSS support in your webpack configuration:
+
+**webpack.config.js**
+
+```js
+export default {
+ experiments: {
+ css: true,
+ },
+};
+```
+
+With this option enabled, webpack can process CSS without adding `css-loader` and `mini-css-extract-plugin` for the basic flow.
+
+## Importing CSS
+
+After enabling the experiment, import `.css` files directly from JavaScript:
+
+**src/index.js**
+
+```js
+import "./styles.css";
+
+const element = document.createElement("h1");
+element.textContent = "Hello native CSS";
+document.body.appendChild(element);
+```
+
+**src/styles.css**
+
+```css
+h1 {
+ color: #1f6feb;
+}
+```
+
+Webpack will process the CSS and include it in the build output.
+
+## CSS Modules
+
+Native CSS support also includes CSS Modules. Use the `.module.css` extension:
+
+**src/button.module.css**
+
+```css
+.button {
+ background: #0d6efd;
+ color: white;
+ border: 0;
+ border-radius: 4px;
+ padding: 8px 12px;
+}
+```
+
+**src/index.js**
+
+```js
+import * as styles from "./button.module.css";
+
+const button = document.createElement("button");
+button.className = styles.button;
+button.textContent = "Click me";
+document.body.appendChild(button);
+```
+
+T> CSS Modules class names are exported. By default, named exports are enabled for CSS modules.
+
+## Production Build
+
+With `experiments.css: true`, webpack provides native CSS extraction and content hashing for CSS assets in production builds.
+
+Compared to the classic setup:
+
+- Traditional approach: `css-loader` + `mini-css-extract-plugin`
+- Native approach: `experiments.css` with built-in extraction behavior
+
+This reduces configuration and keeps the CSS pipeline closer to webpack core features.
+
+## Experimental Status & Known Limitations
+
+`experiments.css` is explicitly experimental, so treat it as opt-in and test carefully before broad rollout.
+
+Known points to keep in mind:
+
+- APIs and behavior may still evolve before webpack v6 defaults.
+- Some loader-specific options are not part of native CSS behavior (for example, loader-specific filters).
+- If your project relies on advanced loader chains, validate each part before migrating fully.
+
+## Migration Guide
+
+If you currently use `css-loader` and `mini-css-extract-plugin`, migrate in small steps.
+
+### 1) Start from a classic setup
+
+**webpack.config.js**
+
+```js
+import MiniCssExtractPlugin from "mini-css-extract-plugin";
+
+export default {
+ module: {
+ rules: [
+ {
+ test: /\.css$/i,
+ use: [MiniCssExtractPlugin.loader, "css-loader"],
+ },
+ ],
+ },
+ plugins: [new MiniCssExtractPlugin()],
+};
+```
+
+### 2) Switch to native CSS
+
+**webpack.config.js**
+
+```js
+export default {
+ experiments: {
+ css: true,
+ },
+};
+```
+
+### 3) Keep imports unchanged
+
+Your JS imports can stay the same:
+
+```js
+import "./styles.css";
+import * as styles from "./button.module.css";
+```
+
+### 4) Validate output in development and production
+
+Check that:
+
+- styles are applied correctly in development,
+- generated CSS files are emitted for production,
+- CSS Modules exports match your existing usage.
From c8122cb4bc68ab62c283f24706ce972160acda62 Mon Sep 17 00:00:00 2001
From: phoekerson
Date: Tue, 14 Apr 2026 17:28:00 +0000
Subject: [PATCH 08/12] docs(guides): fix contributors field in native css
guide frontmatter
---
src/content/guides/native-css.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/content/guides/native-css.mdx b/src/content/guides/native-css.mdx
index d20dc13dbf27..ae43d0a43553 100644
--- a/src/content/guides/native-css.mdx
+++ b/src/content/guides/native-css.mdx
@@ -2,7 +2,7 @@
title: Native CSS
sort: 27
contributors:
- - alexander-akait
+ - phoekerson
---
This guide shows how to use webpack native CSS handling with `experiments.css`.
From 2213920b101da14f6f3ef440c6e0a2f8d93792f3 Mon Sep 17 00:00:00 2001
From: phoekerson
Date: Tue, 14 Apr 2026 17:33:31 +0000
Subject: [PATCH 09/12] docs(guides): expand native css migration with loader
option mapping
---
src/content/guides/native-css.mdx | 105 +++++++++++++++++++++++++++++-
1 file changed, 102 insertions(+), 3 deletions(-)
diff --git a/src/content/guides/native-css.mdx b/src/content/guides/native-css.mdx
index ae43d0a43553..bd2173e88e14 100644
--- a/src/content/guides/native-css.mdx
+++ b/src/content/guides/native-css.mdx
@@ -78,6 +78,31 @@ document.body.appendChild(button);
T> CSS Modules class names are exported. By default, named exports are enabled for CSS modules.
+You can customize CSS Modules behavior using parser and generator options:
+
+**webpack.config.js**
+
+```js
+export default {
+ experiments: {
+ css: true,
+ },
+ module: {
+ parser: {
+ "css/module": {
+ namedExports: true,
+ },
+ },
+ generator: {
+ "css/module": {
+ exportsConvention: "camel-case-only",
+ localIdentName: "[uniqueName]-[id]-[local]",
+ },
+ },
+ },
+};
+```
+
## Production Build
With `experiments.css: true`, webpack provides native CSS extraction and content hashing for CSS assets in production builds.
@@ -101,7 +126,7 @@ Known points to keep in mind:
## Migration Guide
-If you currently use `css-loader` and `mini-css-extract-plugin`, migrate in small steps.
+If you currently use `css-loader`, `mini-css-extract-plugin`, and `style-loader`, migrate in small steps.
### 1) Start from a classic setup
@@ -135,7 +160,81 @@ export default {
};
```
-### 3) Keep imports unchanged
+### 3) Migrate `css-loader` options first
+
+Most CSS Modules-related options should move to native parser/generator config.
+
+**webpack.config.js**
+
+```js
+export default {
+ experiments: {
+ css: true,
+ },
+ module: {
+ parser: {
+ css: {
+ import: true,
+ url: true,
+ },
+ "css/module": {
+ namedExports: true,
+ },
+ },
+ generator: {
+ "css/module": {
+ exportsConvention: "camel-case-only",
+ localIdentName: "[uniqueName]-[id]-[local]",
+ },
+ },
+ },
+};
+```
+
+Notes:
+
+- `import` and `url` are native parser switches for CSS handling.
+- `namedExports` controls CSS Modules exports behavior.
+- `exportsConvention` and `localIdentName` provide class export/name shaping.
+- `css-loader` filter-style options are not available as direct equivalents; use webpack mechanisms such as `IgnorePlugin` when needed.
+
+### 4) Replace `mini-css-extract-plugin`
+
+When `experiments.css` is enabled, webpack provides native CSS extraction and content hash handling for CSS output files.
+
+That means you can remove:
+
+- `MiniCssExtractPlugin.loader` from `module.rules`,
+- the `new MiniCssExtractPlugin()` plugin instance.
+
+### 5) Replace `style-loader` with `exportType: "style"`
+
+If you used `style-loader` for runtime style injection, use CSS module parser `exportType: "style"`:
+
+**webpack.config.js**
+
+```js
+export default {
+ experiments: {
+ css: true,
+ },
+ module: {
+ rules: [
+ {
+ test: /\.css$/i,
+ type: "css/module",
+ parser: {
+ exportType: "style",
+ },
+ },
+ ],
+ },
+};
+```
+
+This mode injects a `