Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/mean-shirts-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@tanstack/start-plugin-core': patch
'@tanstack/start-server-core': patch
'@tanstack/react-start-rsc': patch
'@tanstack/react-start': patch
'@tanstack/solid-start': patch
'@tanstack/vue-start': patch
---

feat(rsbuild): add RSC support
2 changes: 1 addition & 1 deletion benchmarks/bundle-size/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"vue": "^3.5.16"
},
"devDependencies": {
"@rsbuild/core": "^2.0.1",
"@rsbuild/core": "^2.0.8",
"@rsbuild/plugin-react": "^2.0.0",
"@tanstack/router-plugin": "workspace:^",
"@types/react": "^19.0.8",
Expand Down
2 changes: 1 addition & 1 deletion e2e/react-router/rspack-basic-file-based/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@rsbuild/core": "^2.0.1",
"@rsbuild/core": "^2.0.8",
"@rsbuild/plugin-react": "^2.0.0",
"@tailwindcss/postcss": "^4.2.2",
"@tanstack/router-e2e-utils": "workspace:^",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@rsbuild/core": "^2.0.1",
"@rsbuild/core": "^2.0.8",
"@rsbuild/plugin-react": "^2.0.0",
"@tailwindcss/postcss": "^4.2.2",
"@tanstack/router-e2e-utils": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion e2e/react-start/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@rsbuild/core": "^2.0.1",
"@rsbuild/core": "^2.0.8",
"@rsbuild/plugin-react": "^2.0.0",
"@tailwindcss/postcss": "^4.2.2",
"@tailwindcss/vite": "^4.2.2",
Expand Down
2 changes: 1 addition & 1 deletion e2e/react-start/basic/tests/client-output.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ test('SSR HTML emits IIFE client scripts and classic script preloads', async ({
expect(html).toMatch(/<link[^>]+rel="preload"[^>]+as="script"/)

const clientEntry = html.match(
/<script\b[^>]+src="([^"]*\/static\/js\/index[^"]*)"[^>]*>/,
/<script\b[^>]+src="([^"]*\/assets\/js\/index[^"]*)"[^>]*>/,
)
expect(clientEntry).toBeTruthy()
expect(clientEntry![0]).toContain('async')
Expand Down
2 changes: 1 addition & 1 deletion e2e/react-start/css-inline/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@rsbuild/core": "^2.0.0",
"@rsbuild/core": "^2.0.8",
"@rsbuild/plugin-react": "^2.0.0",
"@tanstack/router-e2e-utils": "workspace:^",
"@types/node": "^22.10.2",
Expand Down
2 changes: 1 addition & 1 deletion e2e/react-start/custom-server-rsbuild/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@rsbuild/core": "^2.0.1",
"@rsbuild/core": "^2.0.8",
"@rsbuild/plugin-react": "^2.0.0",
"@tailwindcss/postcss": "^4.2.2",
"@tanstack/router-e2e-utils": "workspace:^",
Expand Down
4 changes: 3 additions & 1 deletion e2e/react-start/deferred-hydration/tests/hydration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,9 @@ async function documentModulePreloadHrefs(page: Page) {

function isHydrateBoundaryResource(url: string) {
return (
url.includes('/assets/components-') || url.includes('/static/js/async/')
url.includes('/assets/components-') ||
url.includes('/assets/js/async/') ||
url.includes('/static/js/async/')
)
}

Expand Down
2 changes: 1 addition & 1 deletion e2e/react-start/hmr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@rsbuild/core": "^2.0.1",
"@rsbuild/core": "^2.0.8",
"@rsbuild/plugin-react": "^2.0.0",
"@tailwindcss/postcss": "^4.2.2",
"@tailwindcss/vite": "^4.2.2",
Expand Down
2 changes: 1 addition & 1 deletion e2e/react-start/import-protection/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@rsbuild/core": "^2.0.1",
"@rsbuild/core": "^2.0.8",
"@rsbuild/plugin-react": "^2.0.0",
"@tanstack/router-e2e-utils": "workspace:^",
"@types/node": "^22.10.2",
Expand Down
7 changes: 6 additions & 1 deletion e2e/react-start/rsc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
"toolchain": "vite",
"mode": "ssr",
"shards": 6
},
{
"toolchain": "rsbuild",
"mode": "ssr",
"shards": 6
}
]
}
Expand All @@ -35,7 +40,7 @@
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@rsbuild/core": "^2.0.1",
"@rsbuild/core": "^2.0.8",
"@rsbuild/plugin-react": "^2.0.0",
"@tanstack/eslint-plugin-start": "workspace:^",
"@tanstack/router-e2e-utils": "workspace:^",
Expand Down
39 changes: 25 additions & 14 deletions e2e/react-start/rsc/tests/rsc-client-preload.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,23 @@ import { test } from '@tanstack/router-e2e-utils'
import { waitForHydration } from './hydration'

test.describe('RSC Client Component Preload Tests', () => {
function getModulePreloads(html: string): Array<string> {
function getLinkHrefs(html: string, rel: string): Array<string> {
return Array.from(
html.matchAll(/<link rel="modulepreload" href="([^"]*)"/g),
(match) => match[1]!,
)
html.matchAll(/<link\b[^>]*>/g),
(match) => match[0],
).flatMap((tag) => {
const relValue = tag.match(/\brel="([^"]*)"/)?.[1]
const href = tag.match(/\bhref="([^"]*)"/)?.[1]
return relValue?.split(/\s+/).includes(rel) && href ? [href] : []
Comment on lines +11 to +13
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
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.

})
}

function getModulePreloads(html: string): Array<string> {
return getLinkHrefs(html, 'modulepreload')
}

function getStylesheets(html: string): Array<string> {
return getLinkHrefs(html, 'stylesheet')
}

test('client component JS is modulepreloaded in SSR HTML', async ({
Expand All @@ -30,24 +42,23 @@ test.describe('RSC Client Component Preload Tests', () => {
)

expect(extraPreloads.length).toBeGreaterThan(0)
expect(extraPreloads.some((href) => href.includes('/assets/'))).toBe(true)
})

test('client component CSS is preloaded in head', async ({ page }) => {
await page.goto('/rsc-client-preload')
test('client component CSS is included in SSR HTML', async ({ page }) => {
const response = await page.goto('/rsc-client-preload')
const html = await response?.text()
await page.waitForURL('/rsc-client-preload')

expect(html).toBeDefined()

// Verify client component is visible
await expect(page.getByTestId('client-widget')).toBeVisible()

// Check for CSS preload link in <head>
// In dev mode, CSS is loaded differently than prod, so we check for stylesheet
const cssPreload = page.locator(
'head link[rel="preload"][as="style"], head link[rel="stylesheet"][href*="ClientWidget"]',
)
const cssPreloadCount = await cssPreload.count()
const stylesheetHrefs = getStylesheets(html!)

// Should have at least one CSS preload or stylesheet for the client component
expect(cssPreloadCount).toBeGreaterThanOrEqual(0) // Relaxed for dev mode
expect(stylesheetHrefs.length).toBeGreaterThan(0)
expect(stylesheetHrefs.some((href) => href.includes('/assets/'))).toBe(true)
})

test('client component renders with CSS module styles applied', async ({
Expand Down
28 changes: 28 additions & 0 deletions e2e/react-start/rsc/tests/rsc-no-js.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { expect } from '@playwright/test'
import { test } from '@tanstack/router-e2e-utils'

function getStylesheetHrefs(html: string): Array<string> {
return Array.from(
html.matchAll(/<link\b[^>]*>/g),
(match) => match[0],
).flatMap((tag) => {
const rel = tag.match(/\brel="([^"]*)"/)?.[1]
const href = tag.match(/\bhref="([^"]*)"/)?.[1]
return rel?.split(/\s+/).includes('stylesheet') && href ? [href] : []
})
}

/**
* RSC No-JavaScript Tests
*
Expand Down Expand Up @@ -329,5 +340,22 @@ test.describe('RSC No-JavaScript Rendering', () => {
(el) => getComputedStyle(el).backgroundColor,
)
expect(bgColorB).toBe('rgb(204, 251, 241)') // #ccfbf1

const widgetC = page.getByTestId('client-widget-c')
await expect(widgetC).toHaveCount(0)

const stylesheetHrefs = getStylesheetHrefs(html!)
const linkedCss = await Promise.all(
stylesheetHrefs.map(async (href) => {
const response = await page.request.get(new URL(href, page.url()).href)
return response.text()
}),
)
expect(linkedCss.join('\n')).not.toContain('serverb-note')

const note = page.getByTestId('serverb-note')
await expect(note).toBeVisible()
await expect(note).toHaveCSS('background-color', 'rgb(219, 234, 254)')
await expect(note).toHaveCSS('border-color', 'rgb(59, 130, 246)')
})
})
2 changes: 1 addition & 1 deletion e2e/react-start/server-functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@rsbuild/core": "^2.0.1",
"@rsbuild/core": "^2.0.8",
"@rsbuild/plugin-react": "^2.0.0",
"@tailwindcss/postcss": "^4.2.2",
"@tailwindcss/vite": "^4.2.2",
Expand Down
2 changes: 1 addition & 1 deletion e2e/solid-router/rspack-basic-file-based/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@rsbuild/core": "^2.0.1",
"@rsbuild/core": "^2.0.8",
"@rsbuild/plugin-babel": "^1.1.2",
"@rsbuild/plugin-solid": "^1.1.1",
"@tailwindcss/postcss": "^4.2.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@rsbuild/core": "^2.0.1",
"@rsbuild/core": "^2.0.8",
"@rsbuild/plugin-babel": "^1.1.2",
"@rsbuild/plugin-solid": "^1.1.1",
"@tailwindcss/postcss": "^4.2.2",
Expand Down
2 changes: 1 addition & 1 deletion e2e/solid-start/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@rsbuild/core": "^2.0.1",
"@rsbuild/core": "^2.0.8",
"@rsbuild/plugin-babel": "^1.1.2",
"@rsbuild/plugin-solid": "^1.1.1",
"@tailwindcss/postcss": "^4.2.2",
Expand Down
4 changes: 3 additions & 1 deletion e2e/solid-start/deferred-hydration/tests/hydration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,9 @@ async function documentModulePreloadHrefs(page: Page) {

function isHydrateBoundaryResource(url: string) {
return (
url.includes('/assets/components-') || url.includes('/static/js/async/')
url.includes('/assets/components-') ||
url.includes('/assets/js/async/') ||
url.includes('/static/js/async/')
)
}

Expand Down
2 changes: 1 addition & 1 deletion e2e/vue-router/rspack-basic-file-based/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@rsbuild/core": "^2.0.1",
"@rsbuild/core": "^2.0.8",
"@rsbuild/plugin-babel": "^1.1.2",
"@rsbuild/plugin-vue": "^1.2.7",
"@rsbuild/plugin-vue-jsx": "^2.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@rsbuild/core": "^2.0.1",
"@rsbuild/core": "^2.0.8",
"@rsbuild/plugin-babel": "^1.1.2",
"@rsbuild/plugin-vue": "^1.2.7",
"@rsbuild/plugin-vue-jsx": "^2.0.0",
Expand Down
2 changes: 1 addition & 1 deletion e2e/vue-start/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@rsbuild/core": "^2.0.1",
"@rsbuild/core": "^2.0.8",
"@rsbuild/plugin-babel": "^1.0.5",
"@rsbuild/plugin-vue": "^1.2.2",
"@rsbuild/plugin-vue-jsx": "^1.1.1",
Expand Down
2 changes: 1 addition & 1 deletion examples/react/quickstart-rspack-file-based/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"tailwindcss": "^4.2.2"
},
"devDependencies": {
"@rsbuild/core": "^2.0.1",
"@rsbuild/core": "^2.0.8",
"@rsbuild/plugin-react": "^2.0.0",
"@tanstack/router-plugin": "^1.168.12",
"@types/react": "^19.0.8",
Expand Down
2 changes: 1 addition & 1 deletion examples/solid/quickstart-rspack-file-based/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"tailwindcss": "^4.2.2"
},
"devDependencies": {
"@rsbuild/core": "^2.0.1",
"@rsbuild/core": "^2.0.8",
"@rsbuild/plugin-babel": "^1.1.2",
"@rsbuild/plugin-solid": "^1.1.1",
"@tanstack/router-plugin": "^1.168.12",
Expand Down
5 changes: 4 additions & 1 deletion packages/react-start-rsc/src/awaitLazyElements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ function* findPendingLazyPayloads(
el.type === 'link' &&
el.props?.rel === 'stylesheet'
) {
const cssHref = el.props['data-rsc-css-href'] as string | undefined
let cssHref: string | undefined
if ('data-rsc-css-href' in el.props) {
cssHref = el.props.href
}
if (cssHref && cssCollector) {
cssCollector(cssHref)
}
Expand Down
Loading
Loading