From 601fdf1b1c31e320bbfea4721171adbc83d761df Mon Sep 17 00:00:00 2001 From: phoekerson Date: Sun, 29 Mar 2026 14:04:04 +0000 Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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)