-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
docs: add Modern Web Platform guide (Web Components, Import Maps, PWA) #8140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
601fdf1
b5cbcc0
b9a7679
75da3df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| --- | ||
| 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`](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 | ||
|
|
||
| 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 `<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. | ||
|
|
||
| ### Approach | ||
|
|
||
| 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. | ||
|
|
||
| **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 { | ||
| mode: "production", | ||
| experiments: { | ||
| outputModule: true, | ||
| }, | ||
| entry: "./src/index.js", | ||
| externalsType: "module", | ||
| externals: { | ||
| "lodash-es": "lodash-es", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lets add an example using cdns too
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @evenstensberg for the feedback! I've added a link to the MDN article on |
||
| }, | ||
| output: { | ||
| module: true, | ||
| filename: "[name].mjs", | ||
| path: path.resolve(__dirname, "dist"), | ||
| clean: true, | ||
| }, | ||
| }; | ||
| ``` | ||
|
|
||
| **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": "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 — either a local path or a CDN URL; webpack does not validate that file. | ||
|
|
||
| **index.html** (order matters: import map before your bundle) | ||
|
|
||
| ```html | ||
| <script type="importmap" src="/importmap.json"></script> | ||
| <script type="module" src="/dist/main.mjs"></script> | ||
| ``` | ||
|
|
||
| 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. | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is good