feat(rsbuild): add RSC support#7509
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis PR adds RSC support for rsbuild: it collects CSS and JS preload dependencies during Flight decode using AsyncLocalStorage, threads those assets through server serialization to the client, enables deferred decoding when assets are pre-provided, updates rsbuild asset paths to use an ChangesRSC Support for Rsbuild
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
View your CI Pipeline Execution ↗ for commit 5ae1b30
☁️ Nx Cloud last updated this comment at |
🚀 Changeset Version Preview6 package(s) bumped directly, 3 bumped as dependents. 🟩 Patch bumps
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Bundle Size Benchmarks
Current gzip tracks all emitted client JS chunks. Initial gzip tracks only the entry/import graph. Trend sparkline is historical current gzip ending with this PR measurement; lower is better. |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@e2e/react-start/rsc/tests/rsc-client-preload.spec.ts`:
- Around line 11-13: The current tag parsing only matches double-quoted
attributes so single-quoted rel/href are ignored; update the regexes used where
tag.match(...) is called (the expressions that populate relValue and href) to
accept either single or double quotes by capturing the quote character and using
a backreference (e.g., /\brel=(['"])(.*?)\1/ and /\bhref=(['"])(.*?)\1/) so
relValue and href get the attribute regardless of quote style; keep the existing
split(/\s+/).includes(rel) logic and the return shape ([href] or []) unchanged.
In `@e2e/react-start/rsc/tests/rsc-css-preload-complex.spec.ts`:
- Around line 105-107: The assertion loop runs immediately and can miss late CSS
requests; before checking that unusedCssHrefs were not requested, wait for
network activity to settle (e.g., call or implement a small helper like
waitForNetworkIdle(page, {idleTime: 200}) or await new Promise(resolve =>
setTimeout(resolve, 500))) so late async requests are observed; then run the
existing loop that checks requestedCssHrefs against unusedCssHrefs (use the
existing requestedCssHrefs and unusedCssHrefs variables).
- Around line 9-11: The current parsing for rel and href in the tag extraction
uses regexes that only match double-quoted attributes (the tag.match calls that
set rel and href), so change those regexes to accept either single or double
quotes (e.g. use a character class like ["'] for the quote delimiter and
backreference or non-capturing grouping) so rel =
tag.match(/\brel=(["'])(.*?)\1/)?[2] and href =
tag.match(/\bhref=(["'])(.*?)\1/)?[2] (or equivalent) — keep the subsequent
rel?.split(/\s+/).includes('stylesheet') && href ? [href] : [] logic unchanged.
In `@packages/react-start-rsc/src/serialization.server.ts`:
- Around line 195-204: The code currently nests cssHrefs under jsPreloads so
cssHrefs are dropped when jsPreloads is empty; change the construction of
serializedAssetDeps (around component[SERVER_COMPONENT_JS_PRELOADS],
component[SERVER_COMPONENT_CSS_HREFS], and the serializedAssetDeps variable) so
that cssHrefs is always serialized when present (e.g., include cssHrefs:
Array.from(cssHrefs) if cssHrefs?.size regardless of jsPreloads) and only
include jsPreloads: Array.from(jsPreloads) when jsPreloads?.size is truthy.
In `@packages/react-start/src/server.rsc.ts`:
- Line 1: The change replaced a broad re-export with only request/response
symbols, which narrows the RSC entrypoint and may break consumers; restore the
original public surface by either re-exporting the root module again (so
createStartHandler, HEADERS, RequestHandler, SessionConfig, etc. remain
available) or explicitly add those missing exports alongside the
request/response re-exports (referencing createStartHandler, HEADERS,
RequestHandler, SessionConfig and any other root/virtual-module symbols that
existed previously) to ensure the RSC entrypoint surface is not unintentionally
narrowed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3fa62cad-893e-46d3-87e2-c49c7e2808d3
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (37)
.changeset/mean-shirts-jog.mdbenchmarks/bundle-size/package.jsone2e/react-router/rspack-basic-file-based/package.jsone2e/react-router/rspack-basic-virtual-named-export-config-file-based/package.jsone2e/react-start/basic/package.jsone2e/react-start/css-inline/package.jsone2e/react-start/custom-server-rsbuild/package.jsone2e/react-start/hmr/package.jsone2e/react-start/import-protection/package.jsone2e/react-start/rsc/package.jsone2e/react-start/rsc/tests/rsc-client-preload.spec.tse2e/react-start/rsc/tests/rsc-css-preload-complex.spec.tse2e/react-start/server-functions/package.jsone2e/solid-router/rspack-basic-file-based/package.jsone2e/solid-router/rspack-basic-virtual-named-export-config-file-based/package.jsone2e/solid-start/basic/package.jsone2e/vue-router/rspack-basic-file-based/package.jsone2e/vue-router/rspack-basic-virtual-named-export-config-file-based/package.jsone2e/vue-start/basic/package.jsonexamples/react/quickstart-rspack-file-based/package.jsonexamples/solid/quickstart-rspack-file-based/package.jsonpackages/react-start-rsc/src/awaitLazyElements.tspackages/react-start-rsc/src/createServerComponentFromStream.tspackages/react-start-rsc/src/rsbuild/ssr-decode.tspackages/react-start-rsc/src/serialization.client.tspackages/react-start-rsc/src/serialization.server.tspackages/react-start-rsc/tests/ServerComponent.test.tsxpackages/react-start/package.jsonpackages/react-start/src/server.rsc.tspackages/solid-start/package.jsonpackages/start-plugin-core/package.jsonpackages/start-plugin-core/src/rsbuild/planning.tspackages/start-plugin-core/src/rsbuild/plugin.tspackages/start-plugin-core/src/rsbuild/virtual-modules.tspackages/start-server-core/package.jsonpackages/start-server-core/vite.config.tspackages/vue-start/package.json
| const relValue = tag.match(/\brel="([^"]*)"/)?.[1] | ||
| const href = tag.match(/\bhref="([^"]*)"/)?.[1] | ||
| return relValue?.split(/\s+/).includes(rel) && href ? [href] : [] |
There was a problem hiding this comment.
Harden rel/href parsing to handle single-quoted attributes.
Line 11 and Line 12 only match double-quoted attributes, so the helper can silently miss valid <link> tags when HTML serialization differs and make this test flaky.
Suggested fix
- const relValue = tag.match(/\brel="([^"]*)"/)?.[1]
- const href = tag.match(/\bhref="([^"]*)"/)?.[1]
+ const relMatch = tag.match(/\brel=(?:"([^"]*)"|'([^']*)')/i)
+ const hrefMatch = tag.match(/\bhref=(?:"([^"]*)"|'([^']*)')/i)
+ const relValue = relMatch?.[1] ?? relMatch?.[2]
+ const href = hrefMatch?.[1] ?? hrefMatch?.[2]📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const relValue = tag.match(/\brel="([^"]*)"/)?.[1] | |
| const href = tag.match(/\bhref="([^"]*)"/)?.[1] | |
| return relValue?.split(/\s+/).includes(rel) && href ? [href] : [] | |
| const relMatch = tag.match(/\brel=(?:"([^"]*)"|'([^']*)')/i) | |
| const hrefMatch = tag.match(/\bhref=(?:"([^"]*)"|'([^']*)')/i) | |
| const relValue = relMatch?.[1] ?? relMatch?.[2] | |
| const href = hrefMatch?.[1] ?? hrefMatch?.[2] | |
| return relValue?.split(/\s+/).includes(rel) && href ? [href] : [] |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@e2e/react-start/rsc/tests/rsc-client-preload.spec.ts` around lines 11 - 13,
The current tag parsing only matches double-quoted attributes so single-quoted
rel/href are ignored; update the regexes used where tag.match(...) is called
(the expressions that populate relValue and href) to accept either single or
double quotes by capturing the quote character and using a backreference (e.g.,
/\brel=(['"])(.*?)\1/ and /\bhref=(['"])(.*?)\1/) so relValue and href get the
attribute regardless of quote style; keep the existing
split(/\s+/).includes(rel) logic and the return shape ([href] or []) unchanged.
| for (const href of unusedCssHrefs) { | ||
| expect(requestedCssHrefs).not.toContain(href) | ||
| } |
There was a problem hiding this comment.
Wait for network to settle before asserting “unused CSS was not requested”.
As written, Line 105 checks immediately; late async requests after initial navigation can slip past and produce false positives.
Suggested fix
await expect(note).toHaveCSS('background-color', 'rgb(219, 234, 254)')
await expect(note).toHaveCSS('border-color', 'rgb(59, 130, 246)')
+ await page.waitForLoadState('networkidle')
for (const href of unusedCssHrefs) {
expect(requestedCssHrefs).not.toContain(href)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for (const href of unusedCssHrefs) { | |
| expect(requestedCssHrefs).not.toContain(href) | |
| } | |
| await page.waitForLoadState('networkidle') | |
| for (const href of unusedCssHrefs) { | |
| expect(requestedCssHrefs).not.toContain(href) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@e2e/react-start/rsc/tests/rsc-css-preload-complex.spec.ts` around lines 105 -
107, The assertion loop runs immediately and can miss late CSS requests; before
checking that unusedCssHrefs were not requested, wait for network activity to
settle (e.g., call or implement a small helper like waitForNetworkIdle(page,
{idleTime: 200}) or await new Promise(resolve => setTimeout(resolve, 500))) so
late async requests are observed; then run the existing loop that checks
requestedCssHrefs against unusedCssHrefs (use the existing requestedCssHrefs and
unusedCssHrefs variables).
| const jsPreloads = component[SERVER_COMPONENT_JS_PRELOADS] | ||
| const cssHrefs = component[SERVER_COMPONENT_CSS_HREFS] | ||
| // `cssHrefs` can also come from loadCss() marker links. Client-reference | ||
| // collection fills `jsPreloads`, so that gates per-stream serialization. | ||
| const serializedAssetDeps = jsPreloads?.size | ||
| ? { | ||
| ...(cssHrefs?.size ? { cssHrefs: Array.from(cssHrefs) } : {}), | ||
| jsPreloads: Array.from(jsPreloads), | ||
| } | ||
| : {} |
There was a problem hiding this comment.
Serialize cssHrefs independently of jsPreloads.
Line 199 currently drops cssHrefs whenever no JS preload was collected, but CSS marker links can be discovered without any client-reference JS deps. In that case the client never receives those stylesheet hrefs for deferred decode/preload.
Suggested fix
- const serializedAssetDeps = jsPreloads?.size
- ? {
- ...(cssHrefs?.size ? { cssHrefs: Array.from(cssHrefs) } : {}),
- jsPreloads: Array.from(jsPreloads),
- }
- : {}
+ const serializedAssetDeps =
+ cssHrefs?.size || jsPreloads?.size
+ ? {
+ ...(cssHrefs?.size ? { cssHrefs: Array.from(cssHrefs) } : {}),
+ ...(jsPreloads?.size
+ ? { jsPreloads: Array.from(jsPreloads) }
+ : {}),
+ }
+ : {}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const jsPreloads = component[SERVER_COMPONENT_JS_PRELOADS] | |
| const cssHrefs = component[SERVER_COMPONENT_CSS_HREFS] | |
| // `cssHrefs` can also come from loadCss() marker links. Client-reference | |
| // collection fills `jsPreloads`, so that gates per-stream serialization. | |
| const serializedAssetDeps = jsPreloads?.size | |
| ? { | |
| ...(cssHrefs?.size ? { cssHrefs: Array.from(cssHrefs) } : {}), | |
| jsPreloads: Array.from(jsPreloads), | |
| } | |
| : {} | |
| const jsPreloads = component[SERVER_COMPONENT_JS_PRELOADS] | |
| const cssHrefs = component[SERVER_COMPONENT_CSS_HREFS] | |
| // `cssHrefs` can also come from loadCss() marker links. Client-reference | |
| // collection fills `jsPreloads`, so that gates per-stream serialization. | |
| const serializedAssetDeps = | |
| cssHrefs?.size || jsPreloads?.size | |
| ? { | |
| ...(cssHrefs?.size ? { cssHrefs: Array.from(cssHrefs) } : {}), | |
| ...(jsPreloads?.size | |
| ? { jsPreloads: Array.from(jsPreloads) } | |
| : {}), | |
| } | |
| : {} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/react-start-rsc/src/serialization.server.ts` around lines 195 - 204,
The code currently nests cssHrefs under jsPreloads so cssHrefs are dropped when
jsPreloads is empty; change the construction of serializedAssetDeps (around
component[SERVER_COMPONENT_JS_PRELOADS], component[SERVER_COMPONENT_CSS_HREFS],
and the serializedAssetDeps variable) so that cssHrefs is always serialized when
present (e.g., include cssHrefs: Array.from(cssHrefs) if cssHrefs?.size
regardless of jsPreloads) and only include jsPreloads: Array.from(jsPreloads)
when jsPreloads?.size is truthy.
| @@ -1 +1 @@ | |||
| export * from '@tanstack/start-server-core' | |||
| export * from '@tanstack/start-server-core/request-response' | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Compare symbols exposed by the root index vs request-response subpath.
echo '=== root index exports ==='
fd -t f 'index.tsx' packages/start-server-core/src --exec rg -n '^export' {}
echo '=== request-response exports ==='
fd -t f 'request-response.ts' packages/start-server-core/src --exec rg -n '^export' {}
echo '=== usages of server.rsc entry ==='
rg -nP "react-start/.*server\.rsc|`@tanstack/react-start/server`" -g '!**/dist/**' -C2Repository: TanStack/router
Length of output: 50373
Re-exporting from @tanstack/start-server-core/request-response likely narrows the RSC entrypoint surface (breaking only if consumers relied on root-only symbols).
The change removes everything that @tanstack/start-server-core exports outside ./request-response (e.g. createStartHandler, HEADERS, RequestHandler/SessionConfig, and other root/virtual-module exports), since request-response.ts only exports the request/response helpers and related types. If server.rsc.ts is intended to be the RSC-only request/response API surface, this is a deliberate narrowing; otherwise it would be a breaking change.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/react-start/src/server.rsc.ts` at line 1, The change replaced a
broad re-export with only request/response symbols, which narrows the RSC
entrypoint and may break consumers; restore the original public surface by
either re-exporting the root module again (so createStartHandler, HEADERS,
RequestHandler, SessionConfig, etc. remain available) or explicitly add those
missing exports alongside the request/response re-exports (referencing
createStartHandler, HEADERS, RequestHandler, SessionConfig and any other
root/virtual-module symbols that existed previously) to ensure the RSC
entrypoint surface is not unintentionally narrowed.
Merging this PR will not alter performance
Comparing Footnotes
|
f40bb9e to
c232d41
Compare
There was a problem hiding this comment.
Nx Cloud is proposing a fix for your failed CI:
We added await page.waitForURL('/posts') in the "Should change title on client side navigation" test to fix the vite-prerender failure. In prerender mode the router reconciles against prerendered HTML during client-side navigation, introducing a timing gap between the URL change and the document-title update that caused the toHaveTitle assertion to time out. This one-line guard brings the test in line with the waitForURL pattern already used by other navigation tests in the same file.
Tip
✅ We verified this fix by re-running tanstack-react-start-e2e-basic:test:e2e--vite-prerender.
diff --git a/e2e/react-start/basic/tests/navigation.spec.ts b/e2e/react-start/basic/tests/navigation.spec.ts
index ce69a9d9..78488ce1 100644
--- a/e2e/react-start/basic/tests/navigation.spec.ts
+++ b/e2e/react-start/basic/tests/navigation.spec.ts
@@ -85,6 +85,7 @@ test('Should change title on client side navigation', async ({ page }) => {
await page.waitForURL('/')
await page.getByRole('link', { name: 'Posts' }).click()
+ await page.waitForURL('/posts')
await expect(page).toHaveTitle('Posts page')
})
Or Apply changes locally with:
npx nx-cloud apply-locally Je71-qazb
Apply fix locally with your editor ↗ View interactive diff ↗
🎓 Learn more about Self-Healing CI on nx.dev
Summary by CodeRabbit
New Features
Chores
@rsbuild/coredependency to ^2.0.8 across packages.Tests