Skip to content

feat: add linkInsert hook to CssLoadingRuntimeModule#20947

Merged
alexander-akait merged 6 commits into
mainfrom
claude/add-css-loading-hook-3flhD
May 12, 2026
Merged

feat: add linkInsert hook to CssLoadingRuntimeModule#20947
alexander-akait merged 6 commits into
mainfrom
claude/add-css-loading-hook-3flhD

Conversation

@alexander-akait
Copy link
Copy Markdown
Member

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

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.
Copilot AI review requested due to automatic review settings May 11, 2026 16:48
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 11, 2026

🦋 Changeset detected

Latest commit: c4d4e3d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack Minor

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 11, 2026

This PR is packaged and the instant preview is available (0053c9f).

Install it locally:

  • npm
npm i -D webpack@https://pkg.pr.new/webpack@0053c9f
  • yarn
yarn add -D webpack@https://pkg.pr.new/webpack@0053c9f
  • pnpm
pnpm add -D webpack@https://pkg.pr.new/webpack@0053c9f

@codecov
Copy link
Copy Markdown

codecov Bot commented May 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.37%. Comparing base (ff41f9c) to head (c4d4e3d).
⚠️ Report is 1 commits behind head on main.

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     
Flag Coverage Δ
integration 90.32% <100.00%> (+0.04%) ⬆️
test262 45.55% <ø> (ø)
unit 36.30% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 linkInsert as a new SyncWaterfallHook on CssLoadingRuntimeModule.getCompilationHooks(compilation).
  • Updates the generated CSS loading runtime to call linkInsert (defaulting to document.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.

Comment thread lib/css/CssLoadingRuntimeModule.js Outdated
withHmr
? Template.asString([
"if (hmr) {",
Template.indent("document.head.insertBefore(link, hmr);"),
Comment on lines +2 to +6
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.
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 11, 2026

Merging this PR will degrade performance by 50.45%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 3 improved benchmarks
❌ 3 regressed benchmarks
✅ 138 untouched benchmarks
⏩ 72 skipped benchmarks1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

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)

Open in CodSpeed

Footnotes

  1. 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.
@github-actions
Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging claude/add-css-loading-hook-3flhD into main will be
98.93%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
bin
   webpack.js98.77%100%100%98.77%91
examples
   build-common.js100%100%100%100%
   buildAll.js100%100%100%100%
   examples.js100%100%100%100%
   template-common.js98.21%100%100%98.21%72
examples/custom-javascript-parser
   test.filter.js100%100%100%100%
examples/custom-javascript-parser/internals
   acorn-parse.js100%100%100%100%
   meriyah-parse.js100%100%100%100%
   oxc-parse.js91.30%100%100%91.30%140, 142–143, 145, 147, 153–154, 161, 168, 90
examples/markdown
   webpack.config.mjs100%100%100%100%
examples/typescript
   test.filter.js50%100%100%50%5
examples/virtual-modules
   test.filter.js100%100%100%100%
examples/wasm-bindgen-esm
   test.filter.js100%100%100%100%
examples/wasm-complex
   test.filter.js100%100%100%100%
examples/wasm-simple
   test.filter.js100%100%100%100%
examples/wasm-simple-source-phase
   test.filter.js100%100%100%100%
lib
   APIPlugin.js100%100%100%100%
   AsyncDependenciesBlock.js100%100%100%100%
   AutomaticPrefetchPlugin.js100%100%100%100%
   BannerPlugin.js100%100%100%100%
   Cache.js98.21%100%100%98.21%101
   CacheFacade.js100%100%100%100%
   Chunk.js99.72%100%100%99.72%37
   ChunkGraph.js100%100%100%100%
   ChunkGroup.js100%100%100%100%
   ChunkTemplate.js100%100%100%100%
   CleanPlugin.js98.72%100%100%98.72%206, 226, 382
   CodeGenerationResults.js100%100%100%100%
   CompatibilityPlugin.js100%100%100%100%
   Compilation.js98.55%100%100%98.55%1554, 1850, 1857, 1865, 1887, 2783, 3208, 3870, 3899, 3952–3953, 3957, 3962, 3978–3979, 3993–3994, 3999–4000, 4477, 4503, 493, 498, 5211, 5292, 5307, 5332–5333, 5335, 5659, 5664, 5670, 5673, 5685, 5687, 5691, 5707, 5722, 5754, 5808, 5832, 5946, 712–713
   Compiler.js99.55%100%100%99.55%1116–1117, 1125
   ConcatenationScope.js98.59%100%100%98.59%189
   ConditionalInitFragment.js100%100%100%100%
   ConstPlugin.js100%100%100%100%
   ContextExclusionPlugin.js100%100%100%100%
   ContextModule.js100%100%100%100%
   ContextModuleFactory.js97.75%100%100%97.75%258, 393, 418, 443, 447, 458
   ContextReplacementPlugin.js100%100%100%100%
   DefinePlugin.js98.92%100%100%98.92%158–159, 175, 194, 268
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.20%100%100%98.20%379, 425
   DependencyTemplate.js100%100%100%100%
   DependencyTemplates.js100%100%100%100%
   DotenvPlugin.js97.88%100%100%97.88%237, 378, 391–392
   DynamicEntryPlugin.js100%100%100%100%
   EntryOptionPlugin.js100%100%100%100%
   EntryPlugin.js100%100%100%100%
   Entrypoint.js100%100%100%100%
   EnvironmentPlugin.js97.14%100%100%97.14%49
   ErrorHelpers.js100%100%100%100%
   EvalDevToolModulePlugin.js100%100%100%100%
   EvalSourceMapDevToolPlugin.js100%100%100%100%
   ExportsInfo.js100%100%100%100%
   ExportsInfoApiPlugin.js100%100%100%100%
   ExternalModule.js98.96%100%100%98.96%424–428, 576
   ExternalModuleFactoryPlugin.js100%100%100%100%
   ExternalsPlugin.js100%100%100%100%
   FileSystemInfo.js99.50%100%100%99.50%182, 2252–2253, 2256, 2267, 2278, 2289, 278, 3694, 3709, 3733
   FlagAllModulesAsUsedPlugin.js100%100%100%100%
   FlagDependencyExportsPlugin.js98.74%100%100%98.74%399, 401, 405
   FlagDependencyUsagePlugin.js100%100%100%100%
   FlagEntryExportAsUsedPlugin.js100%100%100%100%
   Generator.js100%100%100%100%
   HotModuleReplacementPlugin.js100%100%100%100%
   HotUpdateChunk.js100%100%100%100%
   IgnorePlugin.js100%100%100%100%
   IgnoreWarningsPlugin.js100%100%100%100%
   InitFragment.js100%100%100%100%
   JavascriptMetaInfoPlugin.js100%100%100%100%
   LibraryTemplatePlugin.js100%100%100%100%
   LoaderOptionsPlugin.js100%100%100%100%
   LoaderTargetPlugin.js100%100%100%100%
   MainTemplate.js100%100%100%100%
   ManifestPlugin.js100%100%100%100%
   Module.js98.50%100%100%98.50%1304, 1309, 1370, 1384, 1446, 1455
   ModuleFactory.js100%100%100%100%
   ModuleFilenameHelpers.js98.85%100%100%98.85%106, 108
   ModuleGraph.js99.73%100%100%99.73%1004
   ModuleGraphConnection.js100%100%100%100%
   ModuleInfoHeaderPlugin.js100%100%100%100%
   ModuleProfile.js100%100%100%100%
   ModuleSourceTypeConstants.js100%100%100%100%
   ModuleTemplate.js100%100%100%100%
   ModuleTypeConstants.js100%100%100%100%
   MultiCompiler.js99.69%100%100%99.69%645
   MultiStats.js100%100%100%100%
   MultiWatching.js100%100%100%100%
   NoEmitOnErrorsPlugin.js100%100%100%100%
   NodeStuffPlugin.js100%100%100%100%
   NormalModule.js97.78%100%100%97.78%1020, 1036, 1123, 1774, 1779–1789, 708, 711, 728, 745, 986
   NormalModuleFactory.js99.47%100%100%99.47%1074, 1383, 473, 485
   NormalModuleReplacementPlugin.js100%100%100%100%
   NullFactory.js100%100%100%100%
   OptimizationStages.js100%100%100%100%
   OptionsApply.js100%100%100%100%
   Parser.js100%100%100%100%
   PlatformPlugin.js100%100%100%100%
   PrefetchPlugin.js100%100%100%100%
   ProgressPlugin.js98.85%100%100%98.85%519–520, 525, 527, 591
   ProvidePlugin.js100%100%100%100%
   RawModule.js100%100%100%100%
   RecordIdsPlugin.js100%100%100%100%
   RequestShortener.js100%100%100%100%
   ResolverFactory.js100%100%100%100%
   RuntimeGlobals.js100%100%100%100%
   RuntimeModule.js100%100%100%100%
   RuntimePlugin.js100%100%100%100%
   RuntimeTemplate.js100%100%100%100%
   SelfModuleFactory.js100%100%100%100%
   SingleEntryPlugin.js100%100%100%100%
   SourceMapDevToolModuleOptionsPlugin.js100%100%100%100%
   SourceMapDevToolPlugin.js99.16%100%100%99.16%267–268, 610
   Stats.js100%100%100%100%
   Template.js100%100%100%100%
   TemplatedPathPlugin.js98.86%100%100%98.86%134–135
   UseStrictPlugin.js100%100%100%100%
   WarnCaseSensitiveModulesPlugin.js100%100%100%100%
   WarnDeprecatedOptionPlugin.js100%100%100%100%
   WarnNoModeSetPlugin.js100%100%100%100%
   WatchIgnorePlugin.js100%100%100%100%
   Watching.js100%100%100%100%
   WebpackError.js100%100%100%100%
   WebpackIsIncludedPlugin.js100%100%100%100%
   WebpackOptionsApply.js100%100%100%100%
   WebpackOptionsDefaulter.js100%100%100%100%
   buildChunkGraph.js99.87%100%100%99.87%325
   cli.js98.71%100%100%98.71%117, 469, 501, 543, 813
   index.js100%100%100%100%
   validateSchema.js94.67%100%100%94.67%100, 87, 89, 98
   webpack.js97.22%100%100%97.22%196, 218, 220
lib/asset
   AssetBytesGenerator.js100%100%100%100%
   AssetBytesParser.js100%100%100%100%
   AssetGenerator.js100%100%100%100%
   AssetModulesPlugin.js97.77%100%100%97.77%285, 309, 312, 364, 40
   AssetParser.js100%100%100%100%
   AssetSourceGenerator.js100%100%100%100%
   AssetSourceParser.js100%100%100%100%
   RawDataUrlModule.js100%100%100%100%
lib/async-modules
   AsyncModuleHelpers.js100%100%100%100%
   AwaitDependenciesInitFragment.js100%100%100%100%
   InferAsyncModulesPlugin.js100%100%100%100%
lib/cache
   AddBuildDependenciesPlugin.js100%100%100%100%
   AddManagedPathsPlugin.js100%100%100%100%
   IdleFileCachePlugin.js97.92%100%100%97.92%71, 83, 91
   MemoryCachePlugin.js95.83%100%100%95.83%33
   MemoryWithGcCachePlugin.js93.15%100%100%93.15%106, 113–114, 122, 89
   PackFileCacheStrategy.js96.40%100%100%96.40%1250, 1350, 1354, 1416, 628, 647, 657–659, 661, 677–678, 683, 686, 688, 693, 698, 722, 728, 762, 768, 774, 779, 790, 799, 804–805, 807, 824, 830–831, 833
   ResolverCachePlugin.js100%100%100%100%
   getLazyHashedEtag.js100%100%100%100%
   mergeEtags.js100%100%100%100%
lib/config
   browserslistTargetHandler.js100%100%100%100%
   defaults.js99.15%100%100%99.15%1327–1329, 1337, 271, 274, 279, 283, 472
   normalization.js99%100%100%99%191–192, 258, 273
   target.js100%100%100%100%
lib/container
   ContainerEntryDependency.js100%100%100%100%
   ContainerEntryModule.js100%100%100%100%
   ContainerEntryModuleFactory.js100%100%100%100%
   ContainerExposedDependency.js100%100%100%100%
   ContainerPlugin.js100%100%100%100%
   ContainerReferencePlugin.js100%100%100%100%
   FallbackDependency.js100%100%100%100%
   FallbackItemDependency.js100%100%100%100%
   FallbackModule.js100%100%100%100%
   FallbackModuleFactory.js100%100%100%100%
   HoistContainerReferencesPlugin.js100%100%100%100%
   ModuleFederationPlugin.js100%100%100%100%
   RemoteModule.js100%100%100%100%
   RemoteRuntimeModule.js100%100%100%100%
   

@alexander-akait alexander-akait merged commit 0053c9f into main May 12, 2026
61 checks passed
@alexander-akait alexander-akait deleted the claude/add-css-loading-hook-3flhD branch May 12, 2026 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants