feat(html): add module.parser.html.sources option#21022
Conversation
🦋 Changeset detectedLatest commit: d18dca0 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 (224f3ee). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@224f3ee
yarn add -D webpack@https://pkg.pr.new/webpack@224f3ee
pnpm add -D webpack@https://pkg.pr.new/webpack@224f3ee |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #21022 +/- ##
==========================================
+ Coverage 91.61% 91.66% +0.05%
==========================================
Files 573 573
Lines 59766 59874 +108
Branches 16144 16159 +15
==========================================
+ Hits 54755 54885 +130
+ Misses 5011 4989 -22
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 HTML-module parser option (module.parser.html.sources) to control whether/which URL-like HTML attributes are extracted as webpack dependencies/entries, enabling full opt-out or customization.
Changes:
- Implement
module.parser.html.sourcesinHtmlParser(supportstrue/false/custom array; adds newstylesheet-inlinehandling). - Wire defaults, schema validation, and public typings for the new parser option.
- Add configCases coverage for
sources: false, opt-out-by-array, and custom source types; update defaults snapshots accordingly.
Reviewed changes
Copilot reviewed 32 out of 37 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/html/HtmlParser.js | Core implementation of the sources option and new source-type handling. |
| lib/html/HtmlModulesPlugin.js | Validates module.parser.html options against the new schema; passes options into HtmlParser. |
| lib/config/defaults.js | Sets default module.parser.html.sources to true when experiments.html is enabled. |
| schemas/WebpackOptions.json | Adds HtmlParserOptions schema and exposes it under module.parser.html. |
| schemas/plugins/HtmlParserOptions.json | Plugin schema ref for HtmlParserOptions. |
| schemas/plugins/HtmlParserOptions.check.js | Generated schema validator for HtmlParserOptions. |
| schemas/plugins/HtmlParserOptions.check.d.ts | Generated TS typings for the validator module. |
| declarations/WebpackOptions.d.ts | Public TypeScript typings for module.parser.html.sources. |
| types.d.ts | Generated bundled typings updated for the new option/types. |
| lib/dependencies/HtmlScriptSrcDependency.js | Aligns elementKind naming (script vs script-classic). |
| test/configCases/html/parser-sources-disabled/* | Tests that sources: false leaves URL attributes untouched but still processes inline <script>. |
| test/configCases/html/parser-sources-list-only/* | Tests that an array without "..." opts out of defaults (only listed sources are processed). |
| test/configCases/html/parser-sources-types/* | Tests custom type handling (script, script-module, stylesheet, stylesheet-inline). |
| test/configCases/html/parser-sources-custom/* | Tests custom URL attribute rewrites (src/srcset/tagless matching) without creating entries. |
| test/Defaults.unittest.js | Updates default snapshots to include module.parser.html.sources: true. |
| cspell.json | Adds srcset to dictionary. |
| .changeset/html-parser-sources-option.md | Changeset entry for the new option. |
Files not reviewed (1)
- schemas/plugins/HtmlParserOptions.check.js: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Single property read — any-tag entries were folded into | ||
| // `tagSources` at build time. | ||
| const sourceItem = tagSources[attributeName]; | ||
|
|
||
| if (!sourceItem) continue; |
There was a problem hiding this comment.
Fixed in 38dd497. The lookup tables (DEFAULT_SOURCES_BY_TAG, the per-tag buckets, the any-tag table, and the empty sources: false table) are now built via a dict() helper that returns Object.create(null), so tag/attribute names like constructor/__proto__/toString no longer resolve to inherited values. Added a regression test in parser-sources-disabled with <constructor name="./proto-bypass.js"> that would have created a bogus entry (and failed the build) under the old plain-object tables.
Generated by Claude Code
| // value with the processed CSS at render time. | ||
| if (type === "stylesheet-inline") { | ||
| if (attributeValue.trim() === "") continue; | ||
| const request = `data:text/css,${encodeURIComponent(attributeValue)}`; |
There was a problem hiding this comment.
Fixed in 38dd497. Both inline-CSS paths (the <style> body and the stylesheet-inline attribute) now build data:text/css;base64,<utf8-base64> instead of data:text/css,${encodeURIComponent(...)}, mirroring the inline-<script> handler. This avoids the Buffer.from(decoded, "ascii") truncation in decodeDataURI for non-ASCII CSS. The style-tag test now asserts a non-ASCII content: "café →" rule round-trips intact.
Generated by Claude Code
| /** @typedef {import("../Compiler")} Compiler */ | ||
| /** @typedef {{ request: string, entryName: string, kind: "classic" | "esm-script" | "modulepreload" | "stylesheet" }} EntryScriptInfo */ | ||
| /** @typedef {{ request: string, entryName: string, type: "script" | "script-module" | "modulepreload" | "stylesheet" }} EntryScriptInfo */ | ||
|
|
There was a problem hiding this comment.
Fixed in 38dd497 — updated the classic / esm-script references in HtmlModulesPlugin.js (the apply() header comment and the in-loop comment) to the current script / script-module group keys.
Generated by Claude Code
| const dep = new HtmlScriptSrcDependency( | ||
| value, | ||
| [sourceStart, sourceEnd], | ||
| entryName, | ||
| entryCategory, | ||
| elementKind, | ||
| start, | ||
| end |
There was a problem hiding this comment.
Implemented option (a) in c63d2a4. HtmlScriptSrcDependency.Template now detects whether the originating tag is the native element for its kind (<script> for script/script-module, <link> for stylesheet/modulepreload) via isNativeTagForKind. Native tags are still cloned verbatim (preserving defer/async/media/etc.); for a custom element mapped to a script/script-module/stylesheet type, the template now synthesizes a real <script>/<link rel="stylesheet"> sibling (new buildScriptTag, reusing the existing buildStylesheetLink), copying the CSP/fetch attributes (nonce/crossorigin/referrerpolicy). The custom element's own tag is still rewritten in place to its entry chunk, which is the intended behavior for a user-declared source.
Added test/configCases/html/parser-sources-custom-tag-siblings — a custom <my-script> (script) and <my-module> (script-module) with optimization.runtimeChunk, asserting the split-out runtime chunk is loaded by a real classic / type="module" <script> sibling and that no <my-script …></script> / <my-module …></script> clone is emitted. Existing native-tag sibling tests (extract-runtime-chunk, extract-split-chunks, css-runtime-and-split-chunks) still pass unchanged.
Generated by Claude Code
c63d2a4 to
62ef555
Compare
62ef555 to
d206a9c
Compare
| // With `output.module` enabled, a classic `<script src>` is | ||
| // upgraded in place to `<script type="module" src>` (see the | ||
| // ConstDependency insertion below). Account for that in the | ||
| // dependency's `elementKind` so sibling tags emitted by the | ||
| // template for additional entry chunks (runtime / split chunks) | ||
| // also use `type="module"`. Custom-element tags don't get this | ||
| // auto-upgrade — the user owns their tag's attributes. | ||
| const willBeModuleScript = | ||
| type === "script-module" || | ||
| (outputModule && type === "script" && elementName === "script"); |
There was a problem hiding this comment.
Fixed in efcd2b9. willBeModuleScript no longer requires elementName === "script", so under output.module any type: "script" source — native <script> or a custom element — resolves to elementKind: "script-module". The template therefore synthesizes <script type="module"> siblings for the ESM runtime/split chunks instead of classic scripts that would fail to load an ES module. The in-place type-attribute rewrite (reconcileScriptTypeAttr) stays guarded by elementName === "script", so a custom element's own attributes are left untouched.
Added test/configCases/html/parser-sources-custom-tag-module-siblings — a custom <my-script> (type: script) with experiments.outputModule + runtimeChunk, asserting the runtime sibling is emitted as <script type="module" src="…"> and that no classic sibling or cloned <my-script …></script> appears.
Generated by Claude Code
When set to `false`, the HTML parser leaves URL-like attribute values (`<img src>`, `<link href>`, `<script src>`, …) untouched and does not turn `<script src>` / `<link rel="modulepreload">` / `<link rel="stylesheet">` into compilation entries. Inline `<script>` and `<style>` bodies are still processed. Use `webpackIgnore` comments or `IgnorePlugin` to skip individual URLs.
`sources` now accepts `{ list: [...] }` in addition to a boolean. List
entries may be `"..."` (inlines the built-in sources) or
`{ tag, attribute, type: "src" | "srcset" }` to extract custom URL
attributes (e.g. `data-src` for lazy-loaded images). `tag: "*"`
matches any element. User-supplied sources are always plain URL
rewrites — only built-in defaults promote `<script src>` /
`<link rel="modulepreload" | "stylesheet">` into chunk entries.
…ent matching Removes the `tag: "*"` wildcard in favor of simply omitting `tag` to match any element. Also simplifies `resolveSources` and the parser walk: the disabled state now returns empty maps (single code path) and the per-element walk drops the redundant null-check on the resolved sources.
…/stylesheet-inline types
`sources` is now `boolean | Array<...>` instead of `boolean | { list:
Array<...> }` — the wrapping object only ever held one key.
New `type` values let custom elements opt into the same handling as
the built-in tags:
- `script` — URL becomes a classic chunk entry (like `<script src>`)
- `script-module` — URL becomes an ES-module chunk entry (like `<script type="module" src>`)
- `stylesheet` — URL becomes a CSS chunk entry (like `<link rel="stylesheet">`)
- `stylesheet-inline` — attribute value IS inline CSS text; routed through the CSS pipeline and the value is replaced with the processed CSS at render time
`<script>`'s `type="module"` is no longer auto-detected when the user
opts in via `type: "script"` / `"script-module"` — the user's choice
wins.
`SourceItem` now carries a `kind` field (string or attribute-driven function) that directly names the dependency to create — `src`, `script`, `script-module`, `modulepreload`, `stylesheet`, `stylesheet-inline`. The polymorphic defaults (`<link href>`, `<script src>`) move their attribute-driven decisions into a `kind` function alongside `filter`, so the parser walk has one code path regardless of whether a source came from the defaults or from the user's `sources` array. Internal chunk-kind renames to match the user-facing types: `classic` → `script`, `esm-script` → `script-module`, and `HtmlScriptElementKind`'s `script-classic` → `script`.
`SourceItem.type` now matches the user-facing `type` key in `sources:
[{ tag, attribute, type }]`. Same rename for the local walk variable
and `EntryScriptInfo.type`. `elementKind` stays — it's a separate
concept (HTML element rendering shape, not the dispatch type).
The standalone `resolveSources` helper is gone; the constructor builds
the per-tag and any-tag maps directly. `sources: true`/`undefined`
shares the `DEFAULT_SOURCES` reference (no allocation); `false` and
arrays build fresh maps. `"..."` still expands defaults in place.
The walk's per-attribute lookup is now a single property read
(`tagSources[attr]`) instead of a `(tag && tag[attr]) || (any &&
any[attr])` chain. Any-tag entries are folded into every per-tag
bucket once at construction (tag-specific still wins via spread
order), so the fallback is already baked in.
`buildSourcesByTag` returns `{ byTag, anyTag }`; the parser stores
both fields (the bare `anyTag` is still used as the per-element
fallback for tags missing from `byTag`, e.g. `<custom-elem>` against
a user any-tag rule). The default and `false` configs share
precomputed `ResolvedSources` constants, so the constructor is two
field assignments off a literal pick.
The `ANY_TAG` sentinel and the inline `sourcesByTag[ANY_TAG]` lookup
are gone — the data layout itself encodes the fallback.
Microbenchmark of the matching-element path: ~10% faster (137 ms →
126 ms over 1M iters). Non-matching elements still hit the per-element
short-circuit and are unchanged.
Drops `buildSourcesByTag`, `ResolvedSources` typedef, `DEFAULT_RESOLVED`, and `EMPTY_RESOLVED`. The default and `false` cases are now two-line early returns in the constructor that just reference the precomputed `DEFAULT_SOURCES_BY_TAG` / `EMPTY_SOURCES_BY_TAG` tables — no helper call, no wrapper struct, no per-parser allocation. Only the user-array path runs build logic, and it sits in the constructor where it's used. Same spread-based build, same any-tag fold, same walk lookup — purely a structural simplification.
Drops the `options && options.sources` guard — `createParser` always passes a parser-options object, so `options.sources` is safe to read directly. The default-source state is now set unconditionally at the top of the constructor (`this.sourcesByTag = DEFAULT_SOURCES_BY_TAG; this.anyTagSources = undefined`), and the no-user-option path is just a single `return` — no loop, no branch, no helper. The user-array path still runs the build, but the `"..."` spread order is flipped so user entries always override defaults regardless of where `"..."` sits in the array (the position-aware semantic wasn't exercised by any test and produced counter-intuitive behavior when `"..."` came after user entries).
Set the parser default explicitly in `applyModuleDefaults` so the resolved config carries `sources: true` instead of leaving the key undefined and relying on HtmlParser to interpret undefined as "use the built-in sources". Updates `Defaults.unittest.js` snapshots (and the regenerated `types.d.ts`) to reflect the new key. Behavior unchanged: HtmlParser still treats `true` (and `undefined`) the same way — both reference the precomputed `DEFAULT_SOURCES_BY_TAG` table.
`SourceItem` is now `{ type, filter }` — no separate `parse` field.
`type` carries `"srcset"` as a real value (it's part of `SourceType`
now), matching the user-facing schema. The walk derives the parser
from the resolved type (`parseSrcset` for `"srcset"`, `parseSrc`
otherwise) and dispatches `"src"` and `"srcset"` to the same
`HtmlSourceDependency` branch.
Drops the `isSrcset` mapping from the user-array build — user
entries are stored as-is. Default entries that were `{ parse:
parseSrcset, type: "src" }` are now just `{ type: "srcset" }`,
including the shared `PLAIN_SRCSET` singleton.
`SourceEntry` and `SourceItem` are now structurally the same modulo
`type` being a string in `SourceEntry` and `SourceType | resolver`
in `SourceItem`. types.d.ts regenerated.
`"..."` no longer triggers an inner `Object.keys(DEFAULT_SOURCES_BY_TAG)`
loop. The constructor checks once whether `"..."` is in the array and,
if so, seeds `byTag` with a single shallow spread of
`DEFAULT_SOURCES_BY_TAG`. Per-tag writes during the user loop use
spread (`{ ...byTag[tag], [attr]: item }`), so default buckets are
safely aliased until a user entry forces a new object.
Same semantics as before (`"..."` opts the defaults in; user entries
override), just one fewer loop and one fewer object allocation per
expanded default tag.
HtmlParser's constructor takes only `options` now. `hashFunction`, `context`, `outputModule`, and `css` are pulled from `state.compilation` (outputOptions / compiler.context / options.experiments.css) at the top of `parse()` and used as locals through the rest of the method — they're not stored on the parser instance. HtmlModulesPlugin's `createParser` tap drops the four extra arguments and the local `cssEnabled` computation; the call site is now just `new HtmlParser(parserOptions)`. types.d.ts regenerated to drop the corresponding fields from the declared shape.
- Build the tag/attribute lookup tables with null prototypes so HTML names that collide with Object.prototype keys (`__proto__`, `constructor`, `toString`, …) can't resolve to inherited values, create bogus dependencies, or bypass `sources: false`. - Base64-encode (UTF-8) inline CSS data URIs — both `<style>` bodies and `stylesheet-inline` attributes — so non-ASCII CSS round-trips instead of being corrupted by `decodeDataURI`'s ASCII path. - Correct stale `classic`/`esm-script` comments to `script`/`script-module`. - Add proto-bypass and non-ASCII-CSS regression tests.
- Add the missing `description` to `parser.html.sources` array `items` (schemas-lint requires every items schema to be documented). - Cast `module.parser[HTML_MODULE_TYPE]` to `NonNullable<ParserOptionsByModuleTypeKnown[HTML_MODULE_TYPE]>` in defaults, matching the asset/json/css parser-default pattern, so the `"sources"` key is assignable instead of `never`.
A user `sources` entry can map a custom element (e.g. `<my-script src>`) onto `type: script`/`script-module`/`stylesheet`. When such an entry was split across multiple chunks (runtimeChunk/splitChunks), the template cloned the custom element verbatim and appended `</script>`, producing invalid, non-executing markup like `<my-script src="runtime.js"></script>`. Detect whether the originating tag is the native element for its kind (`<script>` / `<link>`); clone only native tags (to preserve attributes like `defer`/`media`), and synthesize a real `<script>`/`<link>` sibling for custom elements, copying the CSP/fetch attributes. Adds a parser-sources-custom-tag-siblings configCase exercising custom `script` and `script-module` sources with runtimeChunk.
…dule When `output.module` is enabled the emitted chunks are ESM, so a custom element mapped to `type: "script"` (e.g. `<my-script src>`) must get `<script type="module">` sibling tags for its runtime/split chunks — a classic `<script>` sibling can't load an ES module. Derive `willBeModuleScript`/`elementKind` from `outputModule` for any `type: "script"` source, not only a native `<script>`; the in-place `type`-attribute rewrite stays native-only (a custom element's attributes are the user's to own). Adds parser-sources-custom-tag-module-siblings exercising a custom `script` source with output.module + runtimeChunk.
The base64 inline-CSS change updated ConfigTest.snap but not the parallel ConfigCacheTest.snap, breaking ConfigCacheTestCases. Regenerate it so both carry the `data:text/css;base64,` headers and the non-ASCII `café →` rule.
The `sources` schema option generates `--module-parser-html-sources` (and its `-attribute`/`-tag`/`-type`/`-reset` companions), but the Cli `getArguments` snapshot never captured them, failing the basic test job. Regenerate it so the snapshot matches the schema-derived CLI flags.
The rebase hit a conflict in the generated `schemas/WebpackOptions.check.js` (main added `output.environment.let`); regenerate it from the merged schema so the validator carries both that option and `module.parser.html.sources`.
09772be to
4f741b1
Compare
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Types CoverageCoverage after merging claude/html-modules-sources-option-gDmbB into main will be
Coverage Report
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("should not let Object.prototype-named tags bypass sources:false", () => { | ||
| // `<constructor name="./proto-bypass.js">` must stay untouched — the | ||
| // lookup tables are null-prototype, so `constructor` doesn't resolve | ||
| // to an inherited value and never becomes a chunk entry (the file | ||
| // doesn't exist; a bogus entry would fail the build). | ||
| expect(page).toContain('name="./proto-bypass.js"'); | ||
| expect(page).not.toMatch(/__html_[a-f0-9]+_\d+/); | ||
| }); |
main's new module.parser.html.sources (#21022) encodes inline <style> as data:text/css;base64,… instead of data:text/css,…. Match the new prefix; the old style module is uniquely identified by prefix in the changeset, so no base64 decoding is needed.
main's new module.parser.html.sources (#21022) encodes inline <style> as data:text/css;base64,… instead of data:text/css,…. Match the new prefix; the old style module is uniquely identified by prefix in the changeset, so no base64 decoding is needed.
* feat(html): add module.parser.html.sources option
When set to `false`, the HTML parser leaves URL-like attribute values
(`<img src>`, `<link href>`, `<script src>`, …) untouched and does not
turn `<script src>` / `<link rel="modulepreload">` / `<link
rel="stylesheet">` into compilation entries. Inline `<script>` and
`<style>` bodies are still processed. Use `webpackIgnore` comments or
`IgnorePlugin` to skip individual URLs.
* feat(html): support custom sources list in module.parser.html.sources
`sources` now accepts `{ list: [...] }` in addition to a boolean. List
entries may be `"..."` (inlines the built-in sources) or
`{ tag, attribute, type: "src" | "srcset" }` to extract custom URL
attributes (e.g. `data-src` for lazy-loaded images). `tag: "*"`
matches any element. User-supplied sources are always plain URL
rewrites — only built-in defaults promote `<script src>` /
`<link rel="modulepreload" | "stylesheet">` into chunk entries.
* refactor(html): drop tag:'*' from sources list; omit tag for any-element matching
Removes the `tag: "*"` wildcard in favor of simply omitting `tag` to
match any element. Also simplifies `resolveSources` and the parser
walk: the disabled state now returns empty maps (single code path) and
the per-element walk drops the redundant null-check on the resolved
sources.
* feat(html): inline sources array, add script/script-module/stylesheet/stylesheet-inline types
`sources` is now `boolean | Array<...>` instead of `boolean | { list:
Array<...> }` — the wrapping object only ever held one key.
New `type` values let custom elements opt into the same handling as
the built-in tags:
- `script` — URL becomes a classic chunk entry (like `<script src>`)
- `script-module` — URL becomes an ES-module chunk entry (like `<script type="module" src>`)
- `stylesheet` — URL becomes a CSS chunk entry (like `<link rel="stylesheet">`)
- `stylesheet-inline` — attribute value IS inline CSS text; routed through the CSS pipeline and the value is replaced with the processed CSS at render time
`<script>`'s `type="module"` is no longer auto-detected when the user
opts in via `type: "script"` / `"script-module"` — the user's choice
wins.
* refactor(html): unify user and default sources under a single kind field
`SourceItem` now carries a `kind` field (string or attribute-driven
function) that directly names the dependency to create — `src`,
`script`, `script-module`, `modulepreload`, `stylesheet`,
`stylesheet-inline`. The polymorphic defaults (`<link href>`,
`<script src>`) move their attribute-driven decisions into a `kind`
function alongside `filter`, so the parser walk has one code path
regardless of whether a source came from the defaults or from the
user's `sources` array.
Internal chunk-kind renames to match the user-facing types:
`classic` → `script`, `esm-script` → `script-module`, and
`HtmlScriptElementKind`'s `script-classic` → `script`.
* refactor(html): rename SourceItem.kind → type; inline resolveSources
`SourceItem.type` now matches the user-facing `type` key in `sources:
[{ tag, attribute, type }]`. Same rename for the local walk variable
and `EntryScriptInfo.type`. `elementKind` stays — it's a separate
concept (HTML element rendering shape, not the dispatch type).
The standalone `resolveSources` helper is gone; the constructor builds
the per-tag and any-tag maps directly. `sources: true`/`undefined`
shares the `DEFAULT_SOURCES` reference (no allocation); `false` and
arrays build fresh maps. `"..."` still expands defaults in place.
* refactor(html): flatten DEFAULT_SOURCES into a single merged lookup
`DEFAULT_SOURCES` is now a flat array of `SourceEntry` objects that
shares its shape with user-supplied `sources` entries (`{ tag?,
attribute, type, filter? }`). User arrays expand `"..."` via
`flatMap(e => e === "..." ? DEFAULT_SOURCES : [e])`, so user entries
can prepend, append, or override the built-in list with no special
casing for defaults vs user input.
Per-tag and any-tag maps collapsed into one `Map<string, SourceItem>`
keyed by `${tag}\0${attribute}`; any-tag entries land under
`\0${attribute}` and are queried only when `hasAnyTagSource` is set
(empty for the defaults). The walk's lookup is now two `Map.get`s
with no per-element gating.
* perf(html): use Object.create(null) dicts and per-tag short-circuit
Replaces the single composite-keyed Map with two null-prototype object
dictionaries: a nested `sourcesByTag[tag][attribute]` table for the
common per-tag case and a flat `anyTagSources[attribute]` table for
tag-less user entries. Plain-object property access is significantly
faster than `Map.get` once shapes stabilize, and avoids allocating a
`${tag}\0${attribute}` string on every attribute lookup. Null
prototypes also keep tag/attribute names like `__proto__` or
`toString` from colliding with `Object.prototype`.
Restores the per-element short-circuit: if a tag has no entry and no
any-tag entries exist, the walk returns immediately without touching
`pendingAttributes`. Most HTML elements (`<div>`, `<p>`, `<span>`, …)
hit this fast path.
Defaults are precomputed once at module load (`DEFAULT_RESOLVED_SOURCES`)
and shared across every parser created with the default `sources`
config — no per-parser allocation, and V8's inline caches stay hot
across instances since the object shapes are identical.
Microbenchmark (1M iterations): ~14x faster for typical HTML pages
(mostly non-source elements) and ~4x faster when every element has a
match. Behavior is unchanged.
* refactor(html): collapse sources lookup to a single table + helper
Drops `ResolvedSources` typedef, `EMPTY_RESOLVED_SOURCES`, and the
`anyTagSources` / `hasAnyTagSource` fields on the parser. Everything
lives in one `sourcesByTag` field — any-tag entries are stored under
the sentinel key `ANY_TAG` (`"\0"`, which can't appear in an HTML
tag name).
Constructor is now a single ternary that either reuses the
precomputed default table (the hot path — zero work per parser, zero
allocation), reuses the shared empty table for `sources: false`, or
calls `buildSourcesByTag` for a user array. `buildSourcesByTag` is
the only function that actually runs per parser, and only in the
user-array case.
Walk still short-circuits on the per-tag dict access and falls back
to the any-tag dict — both are null-prototype object property reads,
same speed as before.
* refactor(html): hand-write DEFAULT_SOURCES_BY_TAG in resolved shape
Drops the separate `DEFAULT_SOURCES` array and the module-load loop
that built `DEFAULT_SOURCES_BY_TAG` from it. The lookup table is now
written directly with shared `PLAIN_SRC` / `PLAIN_SRCSET` singletons
and a small `dict()` helper that null-prototypes object literals.
`buildSourcesByTag` shrinks to user-array handling only: a `"..."`
literal copies tag buckets straight out of `DEFAULT_SOURCES_BY_TAG`
via `Object.assign`, no intermediate flat array. The constructor
passes `userSources` directly — no `flatMap` allocation either.
The walk hot path is unchanged.
* refactor(html): drop dict()/Object.create(null) for sources tables
HTML tag and attribute names come from a known vocabulary, and these
tables are never mutated after construction, so the prototype-pollution
guard isn't pulling its weight. Plain object literals are simpler and
just as fast for the static lookup pattern used here.
`DEFAULT_SOURCES_BY_TAG`, `EMPTY_SOURCES_BY_TAG`, and the per-tag
buckets created by `buildSourcesByTag` are all plain `{}` now. No
helper, no `Object.create(null)`, no `Object.assign` ceremony for the
defaults.
* refactor(html): build tag buckets via spread; cache anyTagSources
`buildSourcesByTag` no longer keeps a mutable per-tag `bucket` local
or guards `if (!bucket) byTag[tag] = bucket = {}`. Each write is a
single expression — `byTag[tag] = { ...byTag[tag], [attr]: item }` —
that works whether the tag has been seen or not (spreading `undefined`
is a no-op). `"..."` expansion uses the same idiom to merge default
buckets in place.
The any-tag bucket (`sourcesByTag[ANY_TAG]`) is now cached once on the
parser as `this.anyTagSources`. The walk's per-element short-circuit
and per-attribute fallback both read this cached value instead of
re-indexing into `sourcesByTag` with the sentinel each time.
* perf(html): fold any-tag entries into per-tag buckets at build time
The walk's per-attribute lookup is now a single property read
(`tagSources[attr]`) instead of a `(tag && tag[attr]) || (any &&
any[attr])` chain. Any-tag entries are folded into every per-tag
bucket once at construction (tag-specific still wins via spread
order), so the fallback is already baked in.
`buildSourcesByTag` returns `{ byTag, anyTag }`; the parser stores
both fields (the bare `anyTag` is still used as the per-element
fallback for tags missing from `byTag`, e.g. `<custom-elem>` against
a user any-tag rule). The default and `false` configs share
precomputed `ResolvedSources` constants, so the constructor is two
field assignments off a literal pick.
The `ANY_TAG` sentinel and the inline `sourcesByTag[ANY_TAG]` lookup
are gone — the data layout itself encodes the fallback.
Microbenchmark of the matching-element path: ~10% faster (137 ms →
126 ms over 1M iters). Non-matching elements still hit the per-element
short-circuit and are unchanged.
* refactor(html): inline source resolution into the constructor
Drops `buildSourcesByTag`, `ResolvedSources` typedef, `DEFAULT_RESOLVED`,
and `EMPTY_RESOLVED`. The default and `false` cases are now two-line
early returns in the constructor that just reference the precomputed
`DEFAULT_SOURCES_BY_TAG` / `EMPTY_SOURCES_BY_TAG` tables — no helper
call, no wrapper struct, no per-parser allocation.
Only the user-array path runs build logic, and it sits in the
constructor where it's used. Same spread-based build, same any-tag
fold, same walk lookup — purely a structural simplification.
* refactor(html): make default source path do nothing extra
Drops the `options && options.sources` guard — `createParser` always
passes a parser-options object, so `options.sources` is safe to read
directly. The default-source state is now set unconditionally at the
top of the constructor (`this.sourcesByTag = DEFAULT_SOURCES_BY_TAG;
this.anyTagSources = undefined`), and the no-user-option path is just
a single `return` — no loop, no branch, no helper.
The user-array path still runs the build, but the `"..."` spread
order is flipped so user entries always override defaults regardless
of where `"..."` sits in the array (the position-aware semantic
wasn't exercised by any test and produced counter-intuitive
behavior when `"..."` came after user entries).
* feat(html): default module.parser.html.sources to true
Set the parser default explicitly in `applyModuleDefaults` so the
resolved config carries `sources: true` instead of leaving the key
undefined and relying on HtmlParser to interpret undefined as "use
the built-in sources". Updates `Defaults.unittest.js` snapshots
(and the regenerated `types.d.ts`) to reflect the new key.
Behavior unchanged: HtmlParser still treats `true` (and `undefined`)
the same way — both reference the precomputed `DEFAULT_SOURCES_BY_TAG`
table.
* refactor(html): make srcset a first-class SourceItem type
`SourceItem` is now `{ type, filter }` — no separate `parse` field.
`type` carries `"srcset"` as a real value (it's part of `SourceType`
now), matching the user-facing schema. The walk derives the parser
from the resolved type (`parseSrcset` for `"srcset"`, `parseSrc`
otherwise) and dispatches `"src"` and `"srcset"` to the same
`HtmlSourceDependency` branch.
Drops the `isSrcset` mapping from the user-array build — user
entries are stored as-is. Default entries that were `{ parse:
parseSrcset, type: "src" }` are now just `{ type: "srcset" }`,
including the shared `PLAIN_SRCSET` singleton.
`SourceEntry` and `SourceItem` are now structurally the same modulo
`type` being a string in `SourceEntry` and `SourceType | resolver`
in `SourceItem`. types.d.ts regenerated.
* refactor(html): apply defaults with a single shallow spread
`"..."` no longer triggers an inner `Object.keys(DEFAULT_SOURCES_BY_TAG)`
loop. The constructor checks once whether `"..."` is in the array and,
if so, seeds `byTag` with a single shallow spread of
`DEFAULT_SOURCES_BY_TAG`. Per-tag writes during the user loop use
spread (`{ ...byTag[tag], [attr]: item }`), so default buckets are
safely aliased until a user entry forces a new object.
Same semantics as before (`"..."` opts the defaults in; user entries
override), just one fewer loop and one fewer object allocation per
expanded default tag.
* refactor(html): derive parser env from compilation, not constructor args
HtmlParser's constructor takes only `options` now. `hashFunction`,
`context`, `outputModule`, and `css` are pulled from
`state.compilation` (outputOptions / compiler.context /
options.experiments.css) at the top of `parse()` and used as locals
through the rest of the method — they're not stored on the parser
instance.
HtmlModulesPlugin's `createParser` tap drops the four extra arguments
and the local `cssEnabled` computation; the call site is now just
`new HtmlParser(parserOptions)`.
types.d.ts regenerated to drop the corresponding fields from the
declared shape.
* fix(html): harden sources lookup (null-proto tables, UTF-8 CSS)
- Build the tag/attribute lookup tables with null prototypes so HTML
names that collide with Object.prototype keys (`__proto__`,
`constructor`, `toString`, …) can't resolve to inherited values,
create bogus dependencies, or bypass `sources: false`.
- Base64-encode (UTF-8) inline CSS data URIs — both `<style>` bodies and
`stylesheet-inline` attributes — so non-ASCII CSS round-trips instead
of being corrupted by `decodeDataURI`'s ASCII path.
- Correct stale `classic`/`esm-script` comments to `script`/`script-module`.
- Add proto-bypass and non-ASCII-CSS regression tests.
* fix(html): resolve pre-existing tsc and schema-lint errors
- Add the missing `description` to `parser.html.sources` array `items`
(schemas-lint requires every items schema to be documented).
- Cast `module.parser[HTML_MODULE_TYPE]` to
`NonNullable<ParserOptionsByModuleTypeKnown[HTML_MODULE_TYPE]>` in
defaults, matching the asset/json/css parser-default pattern, so the
`"sources"` key is assignable instead of `never`.
* fix(html): emit real script/link siblings for custom-element sources
A user `sources` entry can map a custom element (e.g. `<my-script src>`)
onto `type: script`/`script-module`/`stylesheet`. When such an entry was
split across multiple chunks (runtimeChunk/splitChunks), the template
cloned the custom element verbatim and appended `</script>`, producing
invalid, non-executing markup like `<my-script src="runtime.js"></script>`.
Detect whether the originating tag is the native element for its kind
(`<script>` / `<link>`); clone only native tags (to preserve attributes
like `defer`/`media`), and synthesize a real `<script>`/`<link>` sibling
for custom elements, copying the CSP/fetch attributes.
Adds a parser-sources-custom-tag-siblings configCase exercising custom
`script` and `script-module` sources with runtimeChunk.
* fix(html): module-script siblings for custom elements under output.module
When `output.module` is enabled the emitted chunks are ESM, so a custom
element mapped to `type: "script"` (e.g. `<my-script src>`) must get
`<script type="module">` sibling tags for its runtime/split chunks — a
classic `<script>` sibling can't load an ES module. Derive
`willBeModuleScript`/`elementKind` from `outputModule` for any
`type: "script"` source, not only a native `<script>`; the in-place
`type`-attribute rewrite stays native-only (a custom element's attributes
are the user's to own).
Adds parser-sources-custom-tag-module-siblings exercising a custom
`script` source with output.module + runtimeChunk.
* test(html): refresh style-tag persistent-cache snapshot
The base64 inline-CSS change updated ConfigTest.snap but not the parallel
ConfigCacheTest.snap, breaking ConfigCacheTestCases. Regenerate it so both
carry the `data:text/css;base64,` headers and the non-ASCII `café →` rule.
* test(cli): add module.parser.html.sources flags to getArguments snapshot
The `sources` schema option generates `--module-parser-html-sources` (and
its `-attribute`/`-tag`/`-type`/`-reset` companions), but the Cli
`getArguments` snapshot never captured them, failing the basic test job.
Regenerate it so the snapshot matches the schema-derived CLI flags.
* chore: regenerate WebpackOptions validator after rebase onto main
The rebase hit a conflict in the generated `schemas/WebpackOptions.check.js`
(main added `output.environment.let`); regenerate it from the merged schema
so the validator carries both that option and `module.parser.html.sources`.
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
---------
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
When set to
false, the HTML parser leaves URL-like attribute values(
<img src>,<link href>,<script src>, …) untouched and does notturn
<script src>/<link rel="modulepreload">/<link rel="stylesheet">into compilation entries. Inline<script>and<style>bodies are still processed. UsewebpackIgnorecomments orIgnorePluginto skip individual URLs.