Repository navigation
Commit f4d235e
authored
feat: add the SSR manifest and collect server styles without a DOM (#21913)
* feat: add the SSR manifest and collect server styles without a DOM
A server render puts a lazy route's markup in the document long before the
browser asks for the chunk carrying that route's stylesheet, so the page
arrives unstyled. Two additive, opt-in primitives close that:
- `SSRManifestPlugin` emits `ssr-manifest.json`, mapping each source module to
the client assets it needs — its chunk, the splits loaded with it and the
async ancestors it is reached through — so a server injects `modulepreload`
and stylesheet tags for exactly what it rendered. Stylesheets are listed in
the order they cascade, the rule the extracted HTML already writes them by.
- The CSS runtimes collect styles on a document-less target (`target: "node"`),
not only behind a runtime check on a universal bundle, so
`__webpack_css_server_styles__` answers in a dedicated server build. A
worker keeps the DOM path: it has no `fs` to read a stylesheet with.
* fix: address review findings on the SSR manifest
- annotate the plugin's schema options with the release that adds them
- assert the shared chunk is listed before asserting nothing else is, so the
filter cannot pass on an empty list
- resolve the example's manifest from the server module rather than the
working directory
- shorten the changeset and one comment to the repository's limits
* refactor: order the extracted page's stylesheets through the shared helper
The cascade order the SSR manifest needs is the one the extracted HTML already
computes, so the manifest's copy left two identical implementations in the
tree. The html path now calls the shared helper and its own copy goes, which
also covers the comparator from both sides.
Adds a case for a route reached through two async ancestors: the manifest has
to name their stylesheets too, and their order has to be stable when the
cascade cannot place them.
* fix: warn on an unresolvable publicPath and wait for a pending server stylesheet
`SSRManifestPlugin` cannot resolve `output.publicPath: "auto"` — the browser
computes it from the script url — so it falls back to `/`. Served from anywhere
else, the urls the server prints are not the ones the runtime builds: nothing is
adopted and every stylesheet is fetched again. Warn instead of failing silently.
The css loading runtime treated any adopted `<link>` as already applied, which
holds for one the parser saw in `<head>` but not for one a streamed render
inserts later. Wait on a same-origin link that has no `sheet` yet; a cross-origin
sheet never exposes one, so it keeps the render-blocking assumption.
* docs: make the SSR example a real React app with a runnable server
The example rendered a hardcoded string and never ran. It is now a React 19 app
hydrated in the browser, with the route code-split so its stylesheet genuinely
travels with a chunk the browser has not asked for — the case the SSR manifest
exists to close. `node dist/server/main.mjs` serves it: `/` names the route's
stylesheet from the manifest, `/inline` inlines the collected server styles, and
the two are there to be compared.
Locating the client build through `import.meta.url` did not work: in module code
that is the source module's url, not the emitted bundle's, so the path resolved
outside `dist` and the manifest read would have failed. Use the runtime public
path instead. The manifest is re-read per render in development, so a client
rebuild needs no restart, and the collected-styles registry now says that it is
process-wide and carries no initial stylesheet.
* docs: refresh the SSR example output after the rebase
The css loading runtime grew by the `.catch` main added to the no-DOM read and
the adopted-stylesheet check, so the example's stats output moves with it.
* fix: reject directories before streaming the SSR example's client assets
`/dist/client/` resolved to the client output directory itself, which
`existsSync` accepted and `createReadStream` then failed on with an unhandled
`EISDIR` — one unauthenticated request ended the process. Stat once, require a
regular file, and destroy the response on a stream error instead.
* test: filter the SSR example where ESM-only @babel/core cannot load
The example gained a babel-loader rule for JSX, and `Examples.test.js` builds it
inside Jest's vm, where @babel/core@8 cannot be required as CommonJS — the Deno
job and every integration shard below Node 24.9 failed on it. Carry the same
filter the other two babel-using examples already ship.
* refactor: sort the css cascade through the shared comparators
`compareCssChunkOrder` re-implemented what `lib/util/comparators.js` already
exports, down to the tie-break `Array#sort` needs on the legacy node versions.
Compose `compareNumbers` and `compareStrings` instead — `compareNumbers` also
orders the `Infinity` a group's walk never reaches, which is why the hand-rolled
version could not subtract.
Also states in the SSR example that stylesheet order is per manifest entry, and
that `precedence` is what orders stylesheets across entries.
* build: follow output.module after experiments.outputModule was removed
#22011 dropped `experiments.outputModule` from the schema, so the five configs
setting it now fail validation. Read `output.module` instead — which is also
what both harnesses read to decide whether to emit and run the bundle as `.mjs`.
Also drops the `rsbuild` word from `cspell.json`, unused since the example was
rewritten, and trims a comment in the example server back to two lines.
* docs: refresh the SSR example output for the current runtime
The css and chunk-loading runtime modules grew on main, so the example's stats
output moves with them; module counts are unchanged.
* fix: complete the SSR manifest for every entry shape
Four gaps, each reproduced with a failing case first.
`dependOn` and a per-entry `runtime` chunk were both dropped: the walk skipped
every initial parent on the grounds that the document already carries it, which
holds for independent entries but not for the one an entry is built against,
and it never looked at the entrypoint's own runtime chunk. A server preloading
a rendered module got a set the module cannot actually run from.
A stylesheet was recognised by its `.css` name, so `output.cssFilename` with
any other extension sent it to the sorted bucket and silently dropped the
cascade order the manifest exists to state. Ask what webpack recorded when it
emitted the asset instead; source maps come from `related.sourceMap` for the
same reason.
One file built in two layers is two modules emitting into different chunks, so
the layer now qualifies the key rather than the two collapsing into one entry
listing both chunks' files.
* docs: refresh the SSR example output for the current runtime
* refactor: follow the lib reorganization in the SSR manifest plugin
`Chunk`, `ChunkGraph`, `ChunkGroup`, `ModuleGraph`, `Entrypoint` and
`HotUpdateChunk` moved under `graph/` and `Module`/`NormalModule` under
`module/`. `HotUpdateChunk` has no compatibility shim at its old path, so the
require broke outright; the rest resolved through shims but typed as the
module rather than the instance.
* refactor: move SSRManifestPlugin out of the lib root
The root of `lib/` is the core a build is made of plus the shims holding old
`webpack/lib/<Name>` paths open; every plugin lives with what it acts on.
`SSRManifestPlugin` describes the emitted asset set, so it belongs beside
`ManifestPlugin` in `lib/output/`. It has not shipped under the old path, so it
owes no shim — `yarn find-deep-imports --check` agrees.
States the rule in AGENTS.md, since the map is what a reader consults before
adding a file.
Also repoints the `Validation` case at the package root: that file moved into
`test/unitCases/`, which left its `require("..")` resolving to `test/`.
* fix: brace both branches of the source-map lookup
`curly` wants the `if` braced once its `else if` is, which is what reddened
`lint`.
* build: regenerate the SSRManifestPlugin validator
The shared `absolutePath` schema learned to accept a `file:` URL, so the
precompiled validator this plugin ships had to be regenerated against it.
* test: cover the source-map filtering in the SSR manifest
The manifest drops a chunk's source maps from its auxiliary files, and
nothing exercised it: no ssr case set a devtool. Two SourceMapDevToolPlugin
instances give one chunk a single map and another a pair, which is how
webpack records `related.sourceMap` as a string and as an array.
* fix(css): do not fail a chunk on an adopted stylesheet that never reports
A server-rendered <link> whose request already failed replays no event, so
the wait for its `sheet` ran to `chunkLoadTimeout`, removed the link from
the document and rejected with a ChunkLoadError. Only a link webpack itself
created now times out as an error; an adopted one resolves and stays put.
Also hold the SSR example to Babel 8's real floor, Node 24.11.
* perf(css): emit the server-rendered stylesheet wait only where SSR needs it
Adopting a <link> a server printed costs 125 bytes in the css loading
runtime, which every css build carried. SSRManifestPlugin now declares
`hasServerRenderedStylesheets` as a tree runtime requirement and the runtime
reads it, so a build without the plugin emits the bytes it emitted before.1 parent 09235a9 commit f4d235e
114 files changed
Lines changed: 2740 additions & 162 deletions
File tree
- .changeset
- declarations/plugins
- examples/ssr
- lib
- css
- dependencies
- errors
- output
- runtime
- schemas/plugins
- test
- configCases
- css
- css-server-styles-node-inject
- css-server-styles-node
- css-webworker-no-fs
- server-rendered-stylesheet-failed
- server-rendered-stylesheet-pending
- server-rendered-stylesheet
- ssr
- manifest-auto-public-path
- manifest-css-ancestors
- manifest-css-entry
- manifest-css-filename
- manifest-css-order
- manifest-entry-options
- manifest-layers
- manifest-nested-async
- manifest-source-maps
- manifest
- render
- unitCases
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
59 | 69 | | |
60 | 70 | | |
61 | 71 | | |
| |||
155 | 165 | | |
156 | 166 | | |
157 | 167 | | |
158 | | - | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
159 | 172 | | |
160 | 173 | | |
161 | 174 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
0 commit comments