Skip to content

Commit 99b73ea

Browse files
authored
docs: add Modern Web Platform guide (Web Components, Import Maps, PWA) (#8140)
1 parent e998196 commit 99b73ea

File tree

1 file changed

+199
-0
lines changed

1 file changed

+199
-0
lines changed
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
---
2+
title: Modern Web Platform
3+
description: Web Components, import maps, and PWAs with webpack 5.
4+
sort: 26
5+
contributors:
6+
- phoekerson
7+
---
8+
9+
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.
10+
11+
T> Familiarity with [code splitting](/guides/code-splitting/), [caching](/guides/caching/) (`[contenthash]`), and the [`SplitChunksPlugin`](/plugins/split-chunks-plugin/) helps.
12+
13+
## Web Components with webpack
14+
15+
### Problem
16+
17+
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.
18+
19+
### Approach
20+
21+
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.
22+
23+
**webpack.config.js**
24+
25+
```js
26+
import path from "node:path";
27+
import { fileURLToPath } from "node:url";
28+
29+
const __filename = fileURLToPath(import.meta.url);
30+
const __dirname = path.dirname(__filename);
31+
32+
export default {
33+
entry: {
34+
main: "./src/main.js",
35+
admin: "./src/admin.js",
36+
},
37+
output: {
38+
filename: "[name].js",
39+
path: path.resolve(__dirname, "dist"),
40+
clean: true,
41+
},
42+
optimization: {
43+
splitChunks: {
44+
chunks: "all",
45+
cacheGroups: {
46+
// Put shared custom element modules in one async chunk.
47+
customElements: {
48+
test: /[\\/]src[\\/]elements[\\/]/,
49+
name: "custom-elements",
50+
chunks: "all",
51+
enforce: true,
52+
},
53+
},
54+
},
55+
},
56+
};
57+
```
58+
59+
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`.
60+
61+
### Limitations and future work
62+
63+
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.
64+
65+
## Import Maps with webpack
66+
67+
### Problem
68+
69+
[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 `<script type="importmap">`). If webpack **bundles** those dependencies, you do not need an import map for them. If you want the **browser** to load a dependency from a URL (CDN or `/vendor/`) while your application code keeps bare imports, mark those modules as [`externals`](/configuration/externals/) so webpack emits `import` statements that match your map.
70+
71+
### Approach
72+
73+
Enable [ES module output](/configuration/output/#outputmodule) (`experiments.outputModule` and `output.module`), set [`externalsType: "module"`](/configuration/externals/#externalstypemodule) for static imports, and list each bare specifier in `externals` with the same string the browser will resolve via the import map.
74+
75+
**webpack.config.js**
76+
77+
```js
78+
import path from "node:path";
79+
import { fileURLToPath } from "node:url";
80+
81+
const __filename = fileURLToPath(import.meta.url);
82+
const __dirname = path.dirname(__filename);
83+
84+
export default {
85+
mode: "production",
86+
experiments: {
87+
outputModule: true,
88+
},
89+
entry: "./src/index.js",
90+
externalsType: "module",
91+
externals: {
92+
"lodash-es": "lodash-es",
93+
},
94+
output: {
95+
module: true,
96+
filename: "[name].mjs",
97+
path: path.resolve(__dirname, "dist"),
98+
clean: true,
99+
},
100+
};
101+
```
102+
103+
**importmap.json** (served alongside your HTML; URLs must match your deployment)
104+
105+
Local vendor file:
106+
107+
```json
108+
{
109+
"imports": {
110+
"lodash-es": "/vendor/lodash-es.js"
111+
}
112+
}
113+
```
114+
115+
CDN (no self-hosting required):
116+
117+
```json
118+
{
119+
"imports": {
120+
"lodash-es": "https://cdn.jsdelivr.net/npm/lodash-es@4/+esm"
121+
}
122+
}
123+
```
124+
125+
The key `"lodash-es"` must match both the **`externals` key** and the **specifier** in your source (`importfrom "lodash-es"`). The value is the URL the browser loads — either a local path or a CDN URL; webpack does not validate that file.
126+
127+
**index.html** (order matters: import map before your bundle)
128+
129+
```html
130+
<script type="importmap" src="/importmap.json"></script>
131+
<script type="module" src="/dist/main.mjs"></script>
132+
```
133+
134+
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.
135+
136+
### Limitations and future work
137+
138+
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.
139+
140+
## Progressive Web Apps (PWA) and Service Workers
141+
142+
### Problem
143+
144+
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.
145+
146+
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.
147+
148+
### Approach
149+
150+
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).
151+
152+
Install:
153+
154+
```bash
155+
npm install workbox-webpack-plugin workbox-precaching --save-dev
156+
```
157+
158+
**webpack.config.js**
159+
160+
```js
161+
import path from "node:path";
162+
import { fileURLToPath } from "node:url";
163+
import HtmlWebpackPlugin from "html-webpack-plugin";
164+
import { InjectManifest } from "workbox-webpack-plugin";
165+
166+
const __filename = fileURLToPath(import.meta.url);
167+
const __dirname = path.dirname(__filename);
168+
169+
export default {
170+
entry: "./src/index.js",
171+
output: {
172+
filename: "[name].[contenthash].js",
173+
path: path.resolve(__dirname, "dist"),
174+
clean: true,
175+
},
176+
plugins: [
177+
new HtmlWebpackPlugin({ title: "PWA + content hashes" }),
178+
new InjectManifest({
179+
swSrc: path.resolve(__dirname, "src/service-worker.js"),
180+
swDest: "service-worker.js",
181+
}),
182+
],
183+
};
184+
```
185+
186+
**src/service-worker.js** (precache template)
187+
188+
```js
189+
import { precacheAndRoute } from "workbox-precaching";
190+
191+
// Replaced at build time with webpack's precache manifest (hashed asset URLs).
192+
precacheAndRoute(globalThis.__WB_MANIFEST);
193+
```
194+
195+
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.
196+
197+
### Limitations and future work
198+
199+
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.

0 commit comments

Comments
 (0)