diff --git a/src/content/guides/modern-web-platform.mdx b/src/content/guides/modern-web-platform.mdx new file mode 100644 index 000000000000..ba31c73fab32 --- /dev/null +++ b/src/content/guides/modern-web-platform.mdx @@ -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 ` + +``` + +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.