feat: resolve url() inside HTML style attributes via CSS parser as option#21157
Conversation
…option Add a CSS parser `as` option (default `"stylesheet"`) that selects the parse entry point. With `as: "declaration-list"` the source is parsed as a block's contents (CSS Syntax §5.4.5) instead of a full stylesheet, which is the correct model for an element's `style="..."` attribute. HTML modules now route URL-bearing `style` attributes through the CSS pipeline (via the new `html-style-attribute` dependency category, parsed with `as: "declaration-list"`), so `url()` / `image-set()` references in inline styles resolve relative to the HTML file. https://claude.ai/code/session_01RtuVwAXk4wSAPE85d8qadX
Instead of switching on an `as` string inside `grammar`, `SourceProcessor` now accepts a `consume` driver (a `TopLevelConsumer`) and defaults to the stylesheet consumer. `consumeADeclarationList` is exported as a reusable driver, and `CssParser` maps its `as` option to it — so the walk no longer hardcodes how the source is parsed. https://claude.ai/code/session_01RtuVwAXk4wSAPE85d8qadX
The injectable top-level driver now routes through the §5.3 `parse*`
functions instead of the §5.4 `consume*` internals: the default uses
`parseAStylesheetsContents` and a `style` attribute uses the new
`parseADeclarationList` ("parse a block's contents", §5.3.6). Both receive
the already-built `TokenStream`, which `normalizeIntoTokenStream` returns
as-is, so there is no re-tokenization. `parseAStylesheetsContents` gains the
same optional streaming `onRule` sink `consumeAStylesheetsContents` already
had, so the default path keeps its rule-by-rule memory profile.
https://claude.ai/code/session_01RtuVwAXk4wSAPE85d8qadX
Drop the `parseADeclarationList` wrapper — its name reflects the old spec term "parse a list of declarations", which CSS Syntax renamed to "parse a block's contents". CssParser now builds the `style`-attribute top-level parser straight from `parseABlocksContents` (§5.3.6), reusing the existing token stream as before. https://claude.ai/code/session_01RtuVwAXk4wSAPE85d8qadX
…lk driver Drop the "declaration-list" terminology and the grammar's internal default. `SourceProcessor#process` now requires the caller to pass the top-level parse entry point, and CssParser passes one of the spec §5.3 functions directly: `parseAStylesheet` for a stylesheet, `parseABlocksContents` for a block's contents (the `as: "block"` mode, e.g. an HTML `style` attribute). The walk reuses the existing token stream and walks the returned nodes. Revert the now-unused streaming `onRule` extension on `parseAStylesheetsContents`. https://claude.ai/code/session_01RtuVwAXk4wSAPE85d8qadX
Rename the `style` attribute parse mode from `as: "block"` to
`as: "block-contents"` so the option values match the CSS Syntax §5.3 entry
points ("parse a block's contents" / "parse a stylesheet"), leaving room for
future spec-named modes. Also remove the now-unused `onRule` streaming param
from `consumeAStylesheetsContents`.
https://claude.ai/code/session_01RtuVwAXk4wSAPE85d8qadX
…purely additive Restore the original "consume a stylesheet's contents" streaming driver (`consumeAStylesheetsContents(ts, onRule)`) for the default `as: "stylesheet"` mode — the spec parse logic is unchanged. The `as: "block-contents"` mode is now a small additive branch in the walk's grammar that parses a block's contents instead. Drops the `TopLevelParser` indirection. https://claude.ai/code/session_01RtuVwAXk4wSAPE85d8qadX
Give `consumeABlocksContents` the same streaming `onNode` callback that `consumeAStylesheetsContents` already exposes, so both share a `(ts, onNode)` shape. The walk's `grammar` now dispatches `as` through a `TOP_LEVEL_CONSUMERS` map instead of a branch — a future `as` mode is one map entry, no new walk code, and every mode streams (no full AST retained). https://claude.ai/code/session_01RtuVwAXk4wSAPE85d8qadX
…heet-inline Let developers route a custom HTML attribute's value through the CSS pipeline as a block's contents (a declaration list, like a `style` attribute) via the new `stylesheet-style-attribute` source type, alongside `stylesheet-style` for a full inline stylesheet. Renames the experimental `stylesheet-inline` type to `stylesheet-style` so the inline pair mirrors `<style>` / `style=`. https://claude.ai/code/session_01RtuVwAXk4wSAPE85d8qadX
🦋 Changeset detectedLatest commit: 5486f05 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 (bbb8c3f). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@bbb8c3f
yarn add -D webpack@https://pkg.pr.new/webpack@bbb8c3f
pnpm add -D webpack@https://pkg.pr.new/webpack@bbb8c3f |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #21157 +/- ##
==========================================
+ Coverage 92.34% 92.35% +0.01%
==========================================
Files 581 581
Lines 63372 63402 +30
Branches 17522 17539 +17
==========================================
+ Hits 58520 58558 +38
+ Misses 4852 4844 -8
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Annotate the visitor arrays and cast the `VisitorMap` / visitor params, matching the strict test-suite typing introduced on main (#21147), so the two added `as: "block-contents"` tests pass `tsc -p tsconfig.types.test.json`. https://claude.ai/code/session_01RtuVwAXk4wSAPE85d8qadX
Merging this PR will improve performance by 30.47%
Performance Changes
Tip Curious why this is faster? Comment Comparing |
Summary
webpack's experimental HTML support didn't resolve
url()(norimage-set()/@import) references inside inlinestyle="..."attributes — the value was emitted verbatim, so relative asset URLs broke. This routes astyleattribute's value through webpack's CSS pipeline (like a<style>block) so those references resolve relative to the HTML file.Because an attribute value is a CSS block's contents (a declaration list), not a full stylesheet, a new CSS parser option
module.parser.css.as("stylesheet"|"block-contents") selects the top-level production to parse as; the default stays"stylesheet". Developers can opt their own custom attributes into the CSS pipeline via the HTML source typesstylesheet-style(a full inline stylesheet, like a<style>body) andstylesheet-style-attribute(a block's contents, likestyle=).What kind of change does this PR introduce?
feat
Did you add tests for your changes?
Yes —
test/configCases/html/style-attribute/(url()/image-set() resolution insidestyleattributes, basic + cache),test/configCases/html/parser-sources-types/(the newstylesheet-style-attributesource type), and unit coverage intest/walkCssTokensParser.unittest.js(theas: "block-contents"walk).Does this PR introduce a breaking change?
No for stable APIs. Within experimental
experiments.html, the recently-added source typestylesheet-inlineis renamed tostylesheet-style— migration: rename that string inmodule.parser.html.sources.If relevant, what needs to be documented once your changes are merged or what have you already documented?
The new
module.parser.css.asoption and thestylesheet-style/stylesheet-style-attributeHTML source types should be added to the parser-options docs.Use of AI
Yes — implemented with the help of Claude Code (Anthropic). It drafted the CSS parser
asoption, the walk's top-level-consumer dispatch, the HTML source-type wiring, and the tests; every change was reviewed and verified locally by runningyarn tscand the targeted CSS/HTML Jest suites. Per the webpack AI policy, the output was checked for correctness rather than committed blindly.Generated by Claude Code