Skip to content
Closed
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
21 changes: 21 additions & 0 deletions packages/plugin-rsc/e2e/react-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,27 @@ function defineTest(f: Fixture) {

await page.getByText('This is the home [edit] page.').click()
})

test('server hmr for a glob-loaded component under a client boundary', async ({
page,
}) => {
await page.goto(f.url('/'))
await waitForHydration(page)
await using _ = await expectNoReload(page)

const component = page.getByTestId('workflow-component')
await expect(component).toHaveText('Workflow component: before')

const editor = f.createEditor(
'app/routes/home-components/workflow-component.tsx',
)
await editor.edit((contents) => contents.replace('before', 'after'))

await expect(component).toHaveText('Workflow component: after')

await editor.reset()
await expect(component).toHaveText('Workflow component: before')
})
})

test('server css code split', async ({ page }) => {
Expand Down
21 changes: 21 additions & 0 deletions packages/plugin-rsc/e2e/server-module-hmr.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { expect, test } from '@playwright/test'
import { useFixture } from './fixture'
import { expectNoReload, waitForHydration } from './helper'

const f = useFixture({ root: 'examples/server-module-hmr', mode: 'dev' })

test('refreshes a glob-loaded server module behind a client boundary', async ({
page,
}) => {
await page.goto(f.url())
await waitForHydration(page)
await using _ = await expectNoReload(page)

const component = page.getByTestId('workflow-component')
await expect(component).toHaveText('Workflow component: before')

const editor = f.createEditor('app/workflow-components/demo/component.tsx')
await editor.edit((contents) => contents.replace('before', 'after'))

await expect(component).toHaveText('Workflow component: after')
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function WorkflowComponent() {
return <p data-testid="workflow-component">Workflow component: before</p>
}
18 changes: 16 additions & 2 deletions packages/plugin-rsc/examples/react-router/app/routes/home.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import type { ComponentType } from 'react'
import { sayHello } from './home.actions.ts'
import { PendingButton } from './home.client.tsx'
import './home.css'
import { PendingButton } from './home.client.tsx'
import { TestActionStateServer } from './test-action-state/server.tsx'

const Component = () => {
type WorkflowComponentModule = {
default: ComponentType
}

const workflowComponentModules = import.meta.glob<WorkflowComponentModule>(
'./home-components/*.tsx',
)
const loadWorkflowComponent =
workflowComponentModules['./home-components/workflow-component.tsx']!

const Component = async () => {
const WorkflowComponent = (await loadWorkflowComponent()).default

return (
<main className="container my-8 px-8 mx-auto">
<article className="paper prose max-w-none">
Expand Down Expand Up @@ -34,6 +47,7 @@ const Component = () => {
<div className="mt-4">
<TestActionStateServer message={`${new Date().toISOString()}`} />
</div>
<WorkflowComponent />
</article>
</main>
)
Expand Down
3 changes: 3 additions & 0 deletions packages/plugin-rsc/examples/server-module-hmr/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
build/
.react-router/
5 changes: 5 additions & 0 deletions packages/plugin-rsc/examples/server-module-hmr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# RSC Server Module HMR Reproduction

This app loads a server component through `import.meta.glob` and passes its rendered output to a client component.

Run `pnpm dev`, open the printed URL, and edit `app/workflow-components/demo/component.tsx`. Before the HMR fix, the page keeps the old text until a full reload.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use client'

import type { ReactNode } from 'react'

export function ClientPage({ children }: { children: ReactNode }) {
return <main>{children}</main>
}
18 changes: 18 additions & 0 deletions packages/plugin-rsc/examples/server-module-hmr/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Links, Meta, Outlet } from 'react-router'

export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<Meta />
<Links />
</head>
<body>{children}</body>
</html>
)
}

export default function Root() {
return <Outlet />
}
3 changes: 3 additions & 0 deletions packages/plugin-rsc/examples/server-module-hmr/app/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { index, layout } from '@react-router/dev/routes'

export default [layout('./routes/layout.tsx', [index('./routes/index.tsx')])]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Home() {
return <p>Home route</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { ReactNode } from 'react'
import { Outlet } from 'react-router'
import { ClientPage } from '../components/client-page'

type WorkflowComponentModule = {
default: () => ReactNode
}

const workflowModules = import.meta.glob<WorkflowComponentModule>(
'../workflow-components/*/component.tsx',
)

export function loader() {
return { ready: true }
}

export async function ServerComponent() {
const loadWorkflow =
workflowModules['../workflow-components/demo/component.tsx']!
const workflowModule = await loadWorkflow()
const WorkflowComponent = workflowModule.default

return (
<ClientPage>
<WorkflowComponent />
<Outlet />
</ClientPage>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function WorkflowComponent() {
return <p data-testid="workflow-component">Workflow component: before</p>
}
23 changes: 23 additions & 0 deletions packages/plugin-rsc/examples/server-module-hmr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@vitejs/plugin-rsc-examples-server-module-hmr",
"private": true,
"license": "MIT",
"type": "module",
"scripts": {
"dev": "react-router dev",
"build": "react-router build"
},
"dependencies": {
"@react-router/node": "8.5.0",
"react": "^19.3.1",
"react-dom": "^19.3.1",
"react-router": "8.5.0",
"react-server-dom-webpack": "^19.3.1"
},
"devDependencies": {
"@react-router/dev": "8.5.0",
"@vitejs/plugin-react": "workspace:*",
"@vitejs/plugin-rsc": "workspace:*",
"vite": "^8.3.4"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default {
appDirectory: 'app',
buildDirectory: 'build',
ssr: true,
routeDiscovery: {
mode: 'lazy',
},
future: {
unstable_enableNodeReadableStream: true,
unstable_optimizeDeps: true,
},
}
12 changes: 12 additions & 0 deletions packages/plugin-rsc/examples/server-module-hmr/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { unstable_reactRouterRSC as reactRouterRSC } from '@react-router/dev/vite'
import react from '@vitejs/plugin-react'
import rsc from '@vitejs/plugin-rsc'
import { defineConfig } from 'vite'

export default defineConfig({
plugins: [
reactRouterRSC({ __runningWithinTheReactRouterMonoRepo: true }),
react(),
rsc(),
],
})
8 changes: 7 additions & 1 deletion packages/plugin-rsc/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -823,7 +823,13 @@ export default function vitePluginRsc(
return mods.some((mod) => recurse(mod))
}

if (!isInsideClientBoundary(ctx.modules)) {
if (
!isInsideClientBoundary(ctx.modules) ||
(this.environment.name === 'rsc' &&
!ctx.modules.some(
(mod) => mod.id && manager.clientReferenceMetaMap[mod.id],
))
) {
if (this.environment.name === 'rsc') {
// detect if this module is only created as css deps (e.g. tailwind)
// (NOTE: this is not necessary since Vite 7.1.0-beta.0 https://github.com/vitejs/vite/pull/20391 )
Expand Down
Loading
Loading