Skip to content

RFC: regroup the options — cache, mime, build, and folding hot.client into hot #2454

Description

@alexander-akait

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:

  1. accept both spellings, the new one winning when both are given
  2. warn on the legacy name, with the replacement in the message
  3. // TODO in the next major release on the alias
  4. 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

  1. 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.
  2. Does build earn its keep, or is writeToDisk + stats + outputFileSystem + serverSideRender fine where it is?
  3. serverSideRender in build, or left at the top?

🤖 Generated with Claude Code

https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions