There are 32 options across three levels: 16 at the top, 10 under hot, 15 under hot.client. Individually each is defensible; together they read as an inbox rather than a design. This is the plan for fixing that, landing non-breaking first and completing at the next major.
What is actually wrong
The top level isn't "too many options" — it's that 16 scalars sit next to one 25-key object, so hot looks like the design and everything else looks like sediment. Within that list, four options are the same topic (caching), two are the same topic (mime), and four have nothing to do with serving a request at all.
hot vs hot.client is a mechanism boundary, not a concept boundary. It splits on which side of the wire applies the setting, which is not how anyone thinks about configuration — and it is why four names appear on both sides:
|
hot.* |
hot.client.* |
what it really is |
transport |
the server speaks |
the browser speaks |
one setting; the client defaults to the server's |
token |
endpoint requires |
browser sends |
one setting; the client defaults to the server's |
progress |
publish the events |
draw the badge |
unrelated, sharing a word |
path |
served at |
connected to |
genuinely different |
Step 1 — cache and mime (doing now, non-breaking)
// before
{ etag: "strong", lastModified: true, cacheControl: "max-age=0", cacheImmutable: true,
mimeTypes: { "text/x-y": ["z"] }, mimeTypeDefault: "text/plain" }
// after
{ cache: { etag: "strong", lastModified: true, control: "max-age=0", immutable: true },
mime: { types: { "text/x-y": ["z"] }, default: "text/plain" } }
6 → 2. cacheControl/cacheImmutable lose their stutter, and mimeTypeDefault — the least pleasant name in the API — stops existing.
Step 2 — build (proposed)
These four are not about answering a request, which is why writeToDisk reads like a serving option and isn't:
{ build: { fileSystem, writeToDisk, stats, serverSideRender } }
serverSideRender is the weak member — it exposes stats on res.locals, so it is arguably HTTP-side. Either leave it at the top or accept the slight impurity.
Step 3 — fold hot.client into hot (proposed, next major)
hot: {
// the wire
transport: "sse", path: "/__webpack_hmr", heartbeat: 10000,
server, cors, token, inject,
// the browser, no longer in a sub-object
url, // was client.path — an absolute url when it differs from `path`
name, autoConnect, reconnect, timeout, dynamicPublicPath,
overlay, progress, logging,
hot, liveReload, reload,
}
Two options stop existing: client.transport and client.token only ever mattered for a browser pointed at a different endpoint, and hot.url already carries a scheme and a query — so the token travels in it. client.path → hot.url removes the path/path collision, and progress resolves itself once the deprecated server-side option is gone (#2451).
One flat namespace, no name appearing twice, and nothing to learn about which side applies what.
Rejected: grouping the serving options
publicPath, index, methods, headers stay flat. They are the four people actually set, they match serve-static naming, and response: {} or serve: {} buys nothing but migration pain.
Where that lands
middleware(compiler, {
publicPath, index, methods, headers, // serving, flat
modifyResponseData, forwardError, // escape hatches
cache: {}, // 4 options
mime: {}, // 2
build: {}, // 4
hot: {}, // ~17, flat
});
Six flat keys and four namespaces. hot stops being the odd one out.
Names worth fixing while we are here
reload vs liveReload is the worst pair in the API: one reloads on a build, the other reloads when an update cannot be applied. reload → reloadOnFailedUpdate.
urlPrefix does not say it names url parameters → urlParamPrefix.
Compatibility
Every step lands the same way, which is the path hot.statsOptions is already on:
- accept both spellings, the new one winning when both are given
- warn on the legacy name, with the replacement in the message
// TODO in the next major release on the alias
- delete at the major
Translation happens in one normalizeOptions at the entry point, so the rest of the code only ever sees the new shape, and the legacy keys are left on the options object untouched for anything reading instance.context.options.
One cost worth stating: webpack-dev-server forwards options.devMiddleware verbatim (lib/Server.js:2526), so every rename here is also a rename in devServer.devMiddleware.*. It does not block anything, but the migration note has to cover both packages.
Open questions
- Warn immediately, or stay silent until the release before the major? Warning is what moves people, but
etag and lastModified are common, so a minor that starts warning is noisy for configurations that are otherwise fine.
- Does
build earn its keep, or is writeToDisk + stats + outputFileSystem + serverSideRender fine where it is?
serverSideRender in build, or left at the top?
🤖 Generated with Claude Code
https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
There are 32 options across three levels: 16 at the top, 10 under
hot, 15 underhot.client. Individually each is defensible; together they read as an inbox rather than a design. This is the plan for fixing that, landing non-breaking first and completing at the next major.What is actually wrong
The top level isn't "too many options" — it's that 16 scalars sit next to one 25-key object, so
hotlooks like the design and everything else looks like sediment. Within that list, four options are the same topic (caching), two are the same topic (mime), and four have nothing to do with serving a request at all.hotvshot.clientis a mechanism boundary, not a concept boundary. It splits on which side of the wire applies the setting, which is not how anyone thinks about configuration — and it is why four names appear on both sides:hot.*hot.client.*transporttokenprogresspathStep 1 —
cacheandmime(doing now, non-breaking)6 → 2.
cacheControl/cacheImmutablelose their stutter, andmimeTypeDefault— the least pleasant name in the API — stops existing.Step 2 —
build(proposed)These four are not about answering a request, which is why
writeToDiskreads like a serving option and isn't:serverSideRenderis the weak member — it exposes stats onres.locals, so it is arguably HTTP-side. Either leave it at the top or accept the slight impurity.Step 3 — fold
hot.clientintohot(proposed, next major)Two options stop existing:
client.transportandclient.tokenonly ever mattered for a browser pointed at a different endpoint, andhot.urlalready carries a scheme and a query — so the token travels in it.client.path→hot.urlremoves thepath/pathcollision, andprogressresolves itself once the deprecated server-side option is gone (#2451).One flat namespace, no name appearing twice, and nothing to learn about which side applies what.
Rejected: grouping the serving options
publicPath,index,methods,headersstay flat. They are the four people actually set, they matchserve-staticnaming, andresponse: {}orserve: {}buys nothing but migration pain.Where that lands
Six flat keys and four namespaces.
hotstops being the odd one out.Names worth fixing while we are here
reloadvsliveReloadis the worst pair in the API: one reloads on a build, the other reloads when an update cannot be applied.reload→reloadOnFailedUpdate.urlPrefixdoes not say it names url parameters →urlParamPrefix.Compatibility
Every step lands the same way, which is the path
hot.statsOptionsis already on:// TODO in the next major releaseon the aliasTranslation happens in one
normalizeOptionsat the entry point, so the rest of the code only ever sees the new shape, and the legacy keys are left on the options object untouched for anything readinginstance.context.options.One cost worth stating: webpack-dev-server forwards
options.devMiddlewareverbatim (lib/Server.js:2526), so every rename here is also a rename indevServer.devMiddleware.*. It does not block anything, but the migration note has to cover both packages.Open questions
etagandlastModifiedare common, so a minor that starts warning is noisy for configurations that are otherwise fine.buildearn its keep, or iswriteToDisk+stats+outputFileSystem+serverSideRenderfine where it is?serverSideRenderinbuild, or left at the top?🤖 Generated with Claude Code
https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA