feat: add linkInsert hook to CssLoadingRuntimeModule#20947
Conversation
Lets plugin developers control where the stylesheet <link> is inserted into the document by tapping CssLoadingRuntimeModule.getCompilationHooks(compilation).linkInsert. The waterfall hook receives the default insertion source (`document.head.appendChild(link);`) and the chunk, and returns the JS used to attach the link.
🦋 Changeset detectedLatest commit: c4d4e3d The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
This PR is packaged and the instant preview is available (0053c9f). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@0053c9f
yarn add -D webpack@https://pkg.pr.new/webpack@0053c9f
pnpm add -D webpack@https://pkg.pr.new/webpack@0053c9f |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #20947 +/- ##
==========================================
+ Coverage 91.33% 91.37% +0.04%
==========================================
Files 569 569
Lines 56940 56941 +1
Branches 15160 15161 +1
==========================================
+ Hits 52008 52032 +24
+ Misses 4932 4909 -23
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds a new linkInsert compilation hook to Webpack’s CSS loading runtime so plugin authors can customize how stylesheet <link> tags are attached to the document at runtime.
Changes:
- Introduces
linkInsertas a newSyncWaterfallHookonCssLoadingRuntimeModule.getCompilationHooks(compilation). - Updates the generated CSS loading runtime to call
linkInsert(defaulting todocument.head.appendChild(link);) when attaching newly created stylesheet links. - Adds a new config case to validate the hook can be tapped, plus a changeset describing the new capability.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/css/CssLoadingRuntimeModule.js | Defines the new hook and applies it to the link-attachment logic in the CSS loading runtime. |
| types.d.ts | Updates the generated type surface to include linkInsert in CssLoadingRuntimeModulePluginHooks. |
| .changeset/css-loading-link-insert-hook.md | Declares a minor release and documents the new hook’s intent and signature. |
| test/configCases/css/link-insert-hook/webpack.config.js | Adds a config case plugin that taps linkInsert to customize insertion behavior. |
| test/configCases/css/link-insert-hook/index.js | Adds an assertion that the hook’s custom code ran (via a custom attribute). |
| test/configCases/css/link-insert-hook/test.config.js | Ensures the correct bundles are executed for the new config case. |
| test/configCases/css/link-insert-hook/style.css | Adds a simple CSS file to trigger stylesheet chunk loading. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| withHmr | ||
| ? Template.asString([ | ||
| "if (hmr) {", | ||
| Template.indent("document.head.insertBefore(link, hmr);"), |
| await import("./style.css"); | ||
|
|
||
| const links = [...document.getElementsByTagName("link")]; | ||
| const styleLink = links.find((link) => /style_css/.test(link.href)); | ||
|
|
| "webpack": minor | ||
| --- | ||
|
|
||
| Add `linkInsert` hook to `CssLoadingRuntimeModule.getCompilationHooks(compilation)` so plugin developers can control where stylesheet `<link>` elements are inserted into the document. The hook receives the default insertion source (`document.head.appendChild(link);`) and the chunk, and returns the JS used to attach the link. |
…ource The hook now receives the entire default insertion snippet (including the needAttach guard and the HMR insertBefore branch when applicable), so plugins can replace the whole insertion strategy with a single tap.
Merging this PR will degrade performance by 50.45%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ⚡ | Simulation | benchmark "wasm-modules-sync", scenario '{"name":"mode-development","mode":"development"}' |
415.5 ms | 343.4 ms | +21.01% |
| ❌ | Memory | benchmark "asset-modules-resource", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
345.3 KB | 523.3 KB | -34.01% |
| ❌ | Memory | benchmark "many-modules-esm", scenario '{"name":"mode-development","mode":"development"}' |
834.4 KB | 1,102.4 KB | -24.31% |
| ⚡ | Memory | benchmark "many-modules-commonjs", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
254 KB | 197.5 KB | +28.62% |
| ❌ | Memory | benchmark "future-defaults", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
143.3 KB | 289.3 KB | -50.45% |
| ⚡ | Memory | benchmark "lodash", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
308.9 KB | 130.4 KB | ×2.4 |
Comparing claude/add-css-loading-hook-3flhD (c4d4e3d) with main (bd0e48f)
Footnotes
-
72 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
Plugin developers should not have to reproduce the HMR insertBefore
branch or the needAttach guard. The hook now receives just
`document.head.appendChild(link);` and the runtime keeps the surrounding
`if (hmr) { … } else if (needAttach) { <hook output> }` structure
intact.
If a plugin tapping linkInsert moves the stylesheet link out of document.head (e.g. into document.body or a shadow root), the HMR replacement path used document.head.insertBefore(link, hmr), which throws NotFoundError because hmr is no longer a child of document.head. Use hmr.parentNode.insertBefore so the replacement always lands in the same place the original link was inserted.
The hook receives the full default insertion source (including the HMR
`if (hmr) { hmr.parentNode.insertBefore(link, hmr); } else if (needAttach) { ... }`
branch when HMR is enabled), so plugins can control where the link is
placed on initial load and on hot updates with a single tap. Plugins
that only care about initial load can ignore the HMR branch in the
source.
Test asserts the link is actually relocated (parentNode is document.body
instead of document.head) rather than only checking that the hook ran.
Verifies the hook source runs during HMR replacement and that hmr.parentNode.insertBefore puts the new link in whatever parent the old link was relocated to (e.g. document.body), not in document.head.
Summary
position to control insert for link tags
What kind of change does this PR introduce?
feat
Did you add tests for your changes?
Yes
Does this PR introduce a breaking change?
No
If relevant, what needs to be documented once your changes are merged or what have you already documented?
Yes in future
Use of AI
Claude