Have you used AI?
Yes
Feature Proposal
Make webpack handle static assets the way Vite does, out of the box:
import logo from "./logo.svg"; // → URL (auto data-URI under webpack's threshold), zero config
import raw from "./doc.md?raw"; // → file source as a string
import url from "./img.png?url"; // → emitted file URL (explicit)
import data from "./img.png?inline"; // → data-URI, regardless of size
import file from "./img.png?no-inline"; // → forced emitted file (never data-URI)
Two layers, each independently acceptable, listed cheapest-first so the
discussion can stop at either:
- Query suffixes —
?raw / ?url / ?inline / ?no-inline.
- Built-in asset extension recognition —
import x from "./logo.svg" works
with no module.rules (images, media, fonts, …), using type: "asset" and
webpack's existing inline threshold (no change to it).
Both map onto asset module types webpack already has — no new module type, and
no change to the auto-inline threshold. Layer 2 is the opinionated one: it bakes
an extension list into core, which webpack has deliberately avoided so far. That
trade-off is the heart of this discussion.
Feature Use Case
Today the decision "import this file as a string / URL / data-URI" lives in
webpack.config.js, keyed on the file's name, and importing a .svg/.png
at all requires a rule. Every project re-writes the same boilerplate, and it is
easy to get subtly wrong.
Concrete example — examples/markdown/webpack.config.mjs imports one markdown
file as a string today. It costs two filename-coupled edits:
// 1) a dedicated rule keyed on the filename
{ test: /raw-to-string\.md$/, type: "asset/source" },
// 2) AND that same filename must be excluded from the html-loader rule
{
test: /\.(md|markdown|…)$/i,
exclude: /(raw-to-string|raw-to-uint8-array)\.md/,
loader: "html-loader",
/* … */
}
With ?raw both the rule and the exclude disappear and the call site says what
it wants: import md from "./whatever.md?raw".
What the manual equivalent looks like today
To replicate Vite's query suffixes with current webpack you write:
module.exports = {
module: {
rules: [
{
oneOf: [
// order matters: `no-inline` must precede `inline`, because a naive
// /inline/ also matches "no-inline".
{ resourceQuery: /(\?|&)raw(&|$)/, type: "asset/source" },
{ resourceQuery: /(\?|&)url(&|$)/, type: "asset/resource" },
{ resourceQuery: /(\?|&)no-inline(&|$)/, type: "asset/resource" },
{ resourceQuery: /(\?|&)inline(&|$)/, type: "asset/inline" }
]
},
// plus a rule per asset extension you want to import without a query…
{ test: /\.(png|jpe?g|gif|svg|webp|avif|woff2?|…)$/i, type: "asset" }
]
}
};
This is boilerplate every project repeats, and the inline/no-inline regex
overlap plus oneOf ordering are real footguns.
Use cases
Because HTML <img src> and CSS url() resolve through the same module pipeline
(the query is preserved end to end, verified by test/configCases/html/sources
and test/configCases/css/url), all of this applies there too, not only in JS.
-
SVG / images in HTML (directly relevant to experiments.html): inline a
small icon as a data-URI on one element, keep another as an emitted file — per
element, not globally:
<img src="./logo.svg?inline"> <!-- → src="data:image/svg+xml,…" -->
<img src="./hero.jpg?url"> <!-- → src="hero.<hash>.jpg" (emitted) -->
This is the data-URI form. Inlining an SVG as raw <svg> DOM markup (so it is
styleable) is a separate, magic-comment-based feature — see Out of scope.
-
CSS url(): inline vs emitted per declaration
(url(./bg.png?inline) / url(./photo.jpg?no-inline)).
-
JS / TS source as text via ?raw: shaders, SQL, templates, markdown.
-
?url for Web APIs: a stable asset URL for new Audio(), <video>,
fetch(), or a worker, never inlined.
Prior art (Vite, verified against its docs)
- No query → default export is the resolved URL; assets under
assetsInlineLimit
(Vite's default: 4096 bytes) are auto-inlined as base64 data URIs, the rest
emitted.
- Query suffixes:
?url, ?raw, ?inline, ?no-inline (plus ?worker etc.,
out of scope).
- Common image / media / font filetypes are detected as assets automatically
(KNOWN_ASSET_TYPES) — zero config.
Intentional divergence: this proposal keeps webpack's existing auto-inline
threshold (module.parser.asset.dataUrlCondition.maxSize, default 8096 bytes,
size <= maxSize ⇒ inline) rather than adopting Vite's 4096 — following webpack's
own convention instead of changing a default everyone already relies on.
Proposed behavior
Layer 1 — query suffixes
Append a resourceQuery oneOf to module.defaultRules:
| Query |
Maps to |
Result |
?raw |
asset/source |
file source as a string |
?url |
asset/resource |
emitted file URL |
?no-inline |
asset/resource |
force emit (never data-URI) |
?inline |
asset/inline |
data-URI regardless of size |
Appended last, so a query wins over the extension-based type. defaultRules
compile before user module.rules, so a user's own ?raw/etc. rule still
overrides. Modules with no query, or a non-matching query (?foo=bar), are
unaffected — the rules are query-gated and purely additive.
Layer 2 — built-in asset extension recognition
Add default rules mirroring Vite's KNOWN_ASSET_TYPES, each type: "asset"
(automatic inline-or-emit using webpack's existing 8096-byte threshold — no
per-rule dataUrlCondition, no threshold change):
- images:
png jpg jpeg jfif pjpeg pjp gif svg ico webp avif
- media:
mp4 webm ogg mp3 wav flac aac opus mov m4a vtt
- fonts:
woff woff2 eot ttf otf
- other:
webmanifest pdf txt
(Exact list pinned to Vite's at implementation time.) Because defaultRules run
before user rules, anyone with an existing file-loader / type: "asset/*" rule
for these extensions keeps it.
Additional Context
Compatibility / risks
-
Baked-in extension list (Layer 2) is the philosophical shift, webpack
intentionally requires opt-in asset rules. Maintaining the list in core is an
ongoing cost. → gate behind futureDefaults.
-
No threshold change: the auto-inline limit stays at webpack's 8096, scoped
by reusing the global asset default; existing type: "asset" users see no
change.
-
Query-word collisions with existing user query conventions → mitigated by
precedence (user rules win) and futureDefaults gating.
-
The default assetModuleFilename is [hash][ext][query], so emitted names
include ?url / ?no-inline. Cosmetic; may want to strip recognized flags.
-
Interaction with the dependency: "url" rule (new URL(...)): a query on
a new URL() target would also honor ?inline / ?url.
- I think we should honor
?inline, ?url, and ?no-inline in new URL() for consistency, but ?raw in that context should either be ignored or emit a warning, since it doesn't really fit the semantics of new URL().
Open Questions
- Mirror Vite's
KNOWN_ASSET_TYPES exactly, or a trimmed list? https://github.com/vitejs/vite/blob/dae9bb10cfc7acabaebadfcda82d16530ade748d/packages/vite/src/node/constants.ts#L146
- Keeping webpack's 8096-byte threshold (vs Vite's 4096), agreed?
- Strip recognized flags (
?url, …) from [query] in emitted filenames?
- Should
?raw apply to any module (incl. .js), matching Vite?
Have you used AI?
Yes
Feature Proposal
Make webpack handle static assets the way Vite does, out of the box:
Two layers, each independently acceptable, listed cheapest-first so the
discussion can stop at either:
?raw/?url/?inline/?no-inline.import x from "./logo.svg"workswith no
module.rules(images, media, fonts, …), usingtype: "asset"andwebpack's existing inline threshold (no change to it).
Both map onto asset module
types webpack already has — no new module type, andno change to the auto-inline threshold. Layer 2 is the opinionated one: it bakes
an extension list into core, which webpack has deliberately avoided so far. That
trade-off is the heart of this discussion.
Feature Use Case
Today the decision "import this file as a string / URL / data-URI" lives in
webpack.config.js, keyed on the file's name, and importing a.svg/.pngat all requires a rule. Every project re-writes the same boilerplate, and it is
easy to get subtly wrong.
Concrete example —
examples/markdown/webpack.config.mjsimports one markdownfile as a string today. It costs two filename-coupled edits:
With
?rawboth the rule and theexcludedisappear and the call site says whatit wants:
import md from "./whatever.md?raw".What the manual equivalent looks like today
To replicate Vite's query suffixes with current webpack you write:
This is boilerplate every project repeats, and the
inline/no-inlineregexoverlap plus
oneOfordering are real footguns.Use cases
Because HTML
<img src>and CSSurl()resolve through the same module pipeline(the query is preserved end to end, verified by
test/configCases/html/sourcesand
test/configCases/css/url), all of this applies there too, not only in JS.SVG / images in HTML (directly relevant to
experiments.html): inline asmall icon as a data-URI on one element, keep another as an emitted file — per
element, not globally:
This is the data-URI form. Inlining an SVG as raw
<svg>DOM markup (so it isstyleable) is a separate, magic-comment-based feature — see Out of scope.
CSS
url(): inline vs emitted per declaration(
url(./bg.png?inline)/url(./photo.jpg?no-inline)).JS / TS source as text via
?raw: shaders, SQL, templates, markdown.?urlfor Web APIs: a stable asset URL fornew Audio(),<video>,fetch(), or a worker, never inlined.Prior art (Vite, verified against its docs)
assetsInlineLimit(Vite's default: 4096 bytes) are auto-inlined as base64 data URIs, the rest
emitted.
?url,?raw,?inline,?no-inline(plus?workeretc.,out of scope).
(
KNOWN_ASSET_TYPES) — zero config.Intentional divergence: this proposal keeps webpack's existing auto-inline
threshold (
module.parser.asset.dataUrlCondition.maxSize, default 8096 bytes,size <= maxSize⇒ inline) rather than adopting Vite's 4096 — following webpack'sown convention instead of changing a default everyone already relies on.
Proposed behavior
Layer 1 — query suffixes
Append a
resourceQueryoneOftomodule.defaultRules:?rawasset/source?urlasset/resource?no-inlineasset/resource?inlineasset/inlineAppended last, so a query wins over the extension-based type.
defaultRulescompile before user
module.rules, so a user's own?raw/etc. rule stilloverrides. Modules with no query, or a non-matching query (
?foo=bar), areunaffected — the rules are query-gated and purely additive.
Layer 2 — built-in asset extension recognition
Add default rules mirroring Vite's
KNOWN_ASSET_TYPES, eachtype: "asset"(automatic inline-or-emit using webpack's existing 8096-byte threshold — no
per-rule
dataUrlCondition, no threshold change):png jpg jpeg jfif pjpeg pjp gif svg ico webp avifmp4 webm ogg mp3 wav flac aac opus mov m4a vttwoff woff2 eot ttf otfwebmanifest pdf txt(Exact list pinned to Vite's at implementation time.) Because
defaultRulesrunbefore user rules, anyone with an existing
file-loader/type: "asset/*"rulefor these extensions keeps it.
Additional Context
Compatibility / risks
Baked-in extension list (Layer 2) is the philosophical shift, webpack
intentionally requires opt-in asset rules. Maintaining the list in core is an
ongoing cost. → gate behind
futureDefaults.No threshold change: the auto-inline limit stays at webpack's 8096, scoped
by reusing the global
assetdefault; existingtype: "asset"users see nochange.
Query-word collisions with existing user query conventions → mitigated by
precedence (user rules win) and
futureDefaultsgating.The default
assetModuleFilenameis[hash][ext][query], so emitted namesinclude
?url/?no-inline. Cosmetic; may want to strip recognized flags.Interaction with the
dependency: "url"rule (new URL(...)): a query ona
new URL()target would also honor?inline/?url.?inline,?url, and?no-inlineinnew URL()for consistency, but?rawin that context should either be ignored or emit a warning, since it doesn't really fit the semantics ofnew URL().Open Questions
KNOWN_ASSET_TYPESexactly, or a trimmed list? https://github.com/vitejs/vite/blob/dae9bb10cfc7acabaebadfcda82d16530ade748d/packages/vite/src/node/constants.ts#L146?url, …) from[query]in emitted filenames??rawapply to any module (incl..js), matching Vite?