Skip to content

feat: harden universal (node + web) target support — CSS SSR, coverage, runtime helper#21208

Merged
alexander-akait merged 9 commits into
mainfrom
feat/universal-target-support
Jun 17, 2026
Merged

feat: harden universal (node + web) target support — CSS SSR, coverage, runtime helper#21208
alexander-akait merged 9 commits into
mainfrom
feat/universal-target-support

Conversation

@alexander-akait

Copy link
Copy Markdown
Member

Summary

Hardens webpack's universal target (target: ["web", "node"] + output.module, i.e. the neutral platform where platform.web === null && platform.node === null) so universal bundles run in Node for SSR without crashing and expose their styles server-side, and broadens cross-platform test coverage.

  • CSS in Node: the css-style-sheet export returns a text-carrying fallback object when CSSStyleSheet is absent; style injection and link-chunk loading collect CSS into a globalThis registry an SSR host can read, reading the emitted .css asynchronously via the shared getBuiltinModule helper.
  • Adds RuntimeTemplate#isWebLikePlatformExpression() as the single node-or-web runtime probe, reused by the universal async wasm loader.
  • Trims dead browser-only loadScript runtime from ESM hot-update bundles.

Single-platform (web / node) builds emit no extra runtime — every node/web branch is gated to neutral targets. No related tracking issue.

What kind of change does this PR introduce?

feat (also includes test and perf changes).

Did you add tests for your changes?

Yes — test/configCases/css/{import-css-stylesheet-universal, export-type-style-universal, universal-async-css-chunk}, test/configCases/target/universal-public-path, and test/configCases/externals/{universal-node-vs-browser, universal-phase-imports-source}; each runs under both the web and node sub-platforms of a universal target.

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?

The server-side CSS registry exposed at globalThis["__webpack_css__" + uniqueName] (the SSR retrieval surface for style/link CSS under a universal target) should be documented.

Use of AI

Yes. Implemented with Claude Code (Anthropic): AI assisted in auditing the runtime/generators, writing the implementation and tests, and verifying behavior. All changes were reviewed and validated against the test suite, per the webpack AI policy.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DU6PsnqWro8626o19PVM6h


Generated by Claude Code

…urce-phase imports

Cross-platform (web + node) checks for a universal ESM target:
- asset/resource public path resolves to publicPath + hashed name, incl. an
  asset in a dynamically imported chunk
- a node builtin and a browser-only `global` external each resolve on the
  platform that provides them
- `import source` of externals runs under both web and node

https://claude.ai/code/session_01DU6PsnqWro8626o19PVM6h
…r SSR

Make universal CSS apps run in Node end-to-end and expose their styles, with
no extra runtime code on single-platform builds:
- Add `RuntimeTemplate#cssServerStyleRegistry()` — one `globalThis` registry
  (namespaced by uniqueName) an SSR host reads; and `isWebLikePlatformExpression()`.
- `css-style-sheet` export: web emits `new CSSStyleSheet()`, node a text-carrying
  fallback object, universal a `typeof CSSStyleSheet` branch between them.
- `style` injection collects into the registry when there is no DOM.
- `link` chunk loading reads the emitted `.css` via the shared `getBuiltinModule`
  helper and collects it, falling back to the previous no-op on read error.
- Reuse the new web-like probe in the universal async wasm loader.

Only neutral/universal targets emit these branches; web and node-only are unchanged.

https://claude.ai/code/session_01DU6PsnqWro8626o19PVM6h
ESM/module-format HMR downloads hot-update chunks via a local import()-based
loader, so the global `__webpack_require__.l` (LoadScriptRuntimeModule, which
references `document`) was emitted but never called. Stop requiring it, removing
dead browser-only code from every module-format HMR bundle (web, node, universal).

https://claude.ai/code/session_01DU6PsnqWro8626o19PVM6h
Use fs.readFile instead of readFileSync in the universal CSS chunk-loading node
branch so an SSR server's event loop is not blocked while reading the emitted
.css; the chunk promise already resolves via loadingEnded in the callback.

https://claude.ai/code/session_01DU6PsnqWro8626o19PVM6h
@changeset-bot

changeset-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 25fc336

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

@codecov

codecov Bot commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.73%. Comparing base (80f5ed8) to head (25fc336).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
lib/css/CssGenerator.js 80.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #21208      +/-   ##
==========================================
+ Coverage   92.71%   92.73%   +0.01%     
==========================================
  Files         589      591       +2     
  Lines       64190    64400     +210     
  Branches    17806    17863      +57     
==========================================
+ Hits        59516    59720     +204     
- Misses       4674     4680       +6     
Flag Coverage Δ
css-parsing 28.65% <0.00%> (-0.06%) ⬇️
html5lib 31.11% <0.00%> (-0.07%) ⬇️
integration 88.71% <91.66%> (+0.02%) ⬆️
test262 45.40% <0.00%> (-0.16%) ⬇️
unit 40.94% <0.00%> (-0.09%) ⬇️

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

☔ View full report in Codecov by Harness.
📢 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.

…n older node

process.getBuiltinModule is node 22.3+, so it was falsy on older node, leaving
the SSR CSS registry unpopulated. Read the emitted .css via dynamic import('fs')
(the universal wasm loader's pattern) so it works on every node version.

The universal-node-vs-browser external test relies on node-commonjs (createRequire
via getBuiltinModule), so gate it with supportsProcessGetBuiltinModule like the
other node-commonjs universal tests.
@codspeed-hq

codspeed-hq Bot commented Jun 17, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ 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

⚡ 2 improved benchmarks
❌ 2 regressed benchmarks
✅ 140 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "side-effects-reexport", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 128.7 KB 859.3 KB -85.03%
Memory benchmark "asset-modules-bytes", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 246.9 KB 325.8 KB -24.19%
Memory benchmark "lodash", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 859.1 KB 126.8 KB ×6.8
Memory benchmark "wasm-modules-async", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 330.1 KB 190.5 KB +73.3%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing feat/universal-target-support (25fc336) with main (66a67df)

Open in CodSpeed

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

This PR is packaged and the instant preview is available (6e43ab8).

Install it locally:

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

The server-side style registry is exposed on globalThis, which only exists on
node 12+, so skip these cases on older node like the existing universal-css test
(universal targets already default output.globalObject to globalThis).
It uses a `global globalThis` external and asserts against globalThis, which
only exists on node 12+, so skip it on older node like the other universal cases.
The universal ESM bundles use globalThis/modern output unsupported on node 10,
so gate the last two universal cases on supportsGlobalThis (node 12+) like the
existing universal-css test, keeping the node 10 CI shard green.
…port

Its node SSR style collection writes to globalThis (node 12+); gate the hot case
like the universal config cases so the node 10 CI shard stays green.
@github-actions

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging feat/universal-target-support into main will be
99.34%
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.js100%100%100%100%
examples/typescript-non-erasable
   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%39
   ChunkGraph.js100%100%100%100%
   ChunkGroup.js100%100%100%100%
   ChunkTemplate.js100%100%100%100%
   CleanPlugin.js99.15%100%100%99.15%206, 226
   CodeGenerationResults.js100%100%100%100%
   CompatibilityPlugin.js100%100%100%100%
   Compilation.js98.43%100%100%98.43%1618, 1937, 1944, 1952, 1974, 1977, 2917, 3396–3397, 3429, 4093, 4123, 4176–4177, 4181, 4186, 4202–4203, 4217–4218, 4223–4224, 4701, 4727, 514, 519, 5535, 5567, 5584, 5600, 5616, 5631, 5656–5657, 5659, 5987, 5992, 5998, 6001, 6013, 6015, 6019, 6035, 6050, 6082, 6136, 6160, 6274, 764–765
   Compiler.js99.56%100%100%99.56%1135–1136, 1144
   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.40%100%100%97.40%258, 395, 418, 420, 424, 433–434
   ContextReplacementPlugin.js100%100%100%100%
   DefinePlugin.js99%100%100%99%171–172, 188, 207, 281
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.28%100%100%98.28%425, 471
   DependencyTemplate.js100%100%100%100%
   DependencyTemplates.js100%100%100%100%
   DotenvPlugin.js98.41%100%100%98.41%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.66%100%100%98.66%1057, 1060, 445–449, 597
   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, 3693, 3708, 3732
   FlagAllModulesAsUsedPlugin.js100%100%100%100%
   FlagDependencyExportsPlugin.js98.46%100%100%98.46%425, 434, 436, 440
   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%
   LazyBarrel.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%1285, 1290, 1350, 1364, 1426, 1435
   ModuleFactory.js100%100%100%100%
   ModuleFilenameHelpers.js98.85%100%100%98.85%106, 108
   ModuleGraph.js99.73%100%100%99.73%1005
   ModuleGraphConnection.js100%100%100%100%
   ModuleInfoHeaderPlugin.js100%100%100%100%
   ModuleNotFoundError.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%659
   MultiStats.js100%100%100%100%
   MultiWatching.js100%100%100%100%
   NoEmitOnErrorsPlugin.js100%100%100%100%
   NodeStuffPlugin.js100%100%100%100%
   NormalModule.js97.90%100%100%97.90%1240, 1243, 1260, 1277, 1524, 1558, 1574, 1661, 2017, 2316, 2321–2331, 419, 423, 577
   NormalModuleFactory.js99.47%100%100%99.47%1083, 1392, 486, 498
   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.js98.62%100%100%98.62%220, 224, 226, 419, 430, 891
   Stats.js100%100%100%100%
   Template.js100%100%100%100%
   TemplatedPathPlugin.js99.17%100%100%99.17%176–177
   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%371
   cli.js98.62%100%100%98.62%10, 119, 545, 577, 627, 897
   index.js99.72%100%100%99.72%184
   validateSchema.js94.67%100%100%94.67%100, 87, 89, 98
   webpack.js96.33%100%100%96.33%10, 198, 220, 222
lib/asset
   AssetBytesGenerator.js100%100%100%100%
   AssetBytesParser.js100%100%100%100%
   AssetGenerator.js100%100%100%100%
   AssetModule.js100%100%100%100%
   AssetModulesPlugin.js97.32%100%100%97.32%281, 305, 308, 36, 360, 41
   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.31%100%100%99.31%1439–1441, 1449, 274, 277, 282, 286
   defineConfig.js100%100%100%100%
   normalization.js99.01%100%100%99.01%191–192, 258, 273
   target.js100%100%100%100%
lib/container
   ContainerEntryDependency.js

@alexander-akait alexander-akait merged commit 6e43ab8 into main Jun 17, 2026
66 checks passed
@alexander-akait alexander-akait deleted the feat/universal-target-support branch June 17, 2026 21:08
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.

1 participant