Skip to content

Commit 601fdf1

Browse files
committed
docs: add Modern Web Platform guide (Web Components, Import Maps, PWA)
1 parent 046a2b1 commit 601fdf1

1 file changed

Lines changed: 187 additions & 0 deletions

File tree

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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: 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+
```json
106+
{
107+
"imports": {
108+
"lodash-es": "/assets/lodash-es.js"
109+
}
110+
}
111+
```
112+
113+
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; webpack does not validate that file.
114+
115+
**index.html** (order matters: import map before your bundle)
116+
117+
```html
118+
<script type="importmap" src="/importmap.json"></script>
119+
<script type="module" src="/dist/main.mjs"></script>
120+
```
121+
122+
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.
123+
124+
### Limitations and future work
125+
126+
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.
127+
128+
## Progressive Web Apps (PWA) and Service Workers
129+
130+
### Problem
131+
132+
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.
133+
134+
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.
135+
136+
### Approach
137+
138+
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).
139+
140+
Install:
141+
142+
```bash
143+
npm install workbox-webpack-plugin workbox-precaching --save-dev
144+
```
145+
146+
**webpack.config.js**
147+
148+
```js
149+
import path from "node:path";
150+
import { fileURLToPath } from "node:url";
151+
import HtmlWebpackPlugin from "html-webpack-plugin";
152+
import { InjectManifest } from "workbox-webpack-plugin";
153+
154+
const __filename = fileURLToPath(import.meta.url);
155+
const __dirname = path.dirname(__filename);
156+
157+
export default {
158+
entry: "./src/index.js",
159+
output: {
160+
filename: "[name].[contenthash].js",
161+
path: path.resolve(__dirname, "dist"),
162+
clean: true,
163+
},
164+
plugins: [
165+
new HtmlWebpackPlugin({ title: "PWA + content hashes" }),
166+
new InjectManifest({
167+
swSrc: path.resolve(__dirname, "src/service-worker.js"),
168+
swDest: "service-worker.js",
169+
}),
170+
],
171+
};
172+
```
173+
174+
**src/service-worker.js** (precache template)
175+
176+
```js
177+
import { precacheAndRoute } from "workbox-precaching";
178+
179+
// Replaced at build time with webpack's precache manifest (hashed asset URLs).
180+
precacheAndRoute(globalThis.__WB_MANIFEST);
181+
```
182+
183+
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.
184+
185+
### Limitations and future work
186+
187+
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)