Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
95eaa8b
feat(rsc): support customizing `react-server` conditioned environment
hi-ogawa Jul 31, 2025
21183ba
chore: comment
hi-ogawa Jul 31, 2025
32d4756
wip: vitePluginUseClient
hi-ogawa Jul 31, 2025
2e36de8
wip vitePluginUseServer
hi-ogawa Jul 31, 2025
f480f3f
feat: expose more
hi-ogawa Jul 31, 2025
fa6353d
feat: expose more
hi-ogawa Jul 31, 2025
795f9fd
chore: tweak
hi-ogawa Jul 31, 2025
21c8cd6
feat: add vitePluginRscMinimal
hi-ogawa Jul 31, 2025
bb39264
fix: replace more
hi-ogawa Jul 31, 2025
ea048d2
chore: add examples/browser-mode
hi-ogawa Jul 31, 2025
743eaed
chore: cleanup
hi-ogawa Jul 31, 2025
d3b411a
wip: client component
hi-ogawa Jul 31, 2025
3e2f652
chore: cleanup
hi-ogawa Jul 31, 2025
162f501
refactor: align entry.{rsc,browser} pattern
hi-ogawa Jul 31, 2025
976afb6
chore: cleanup
hi-ogawa Jul 31, 2025
357e219
fix(rsc): use `/react/rsc` for use server transform runtime import
hi-ogawa Jul 31, 2025
2fa3a35
wip: server action
hi-ogawa Jul 31, 2025
f3f4932
chore: cleanup
hi-ogawa Jul 31, 2025
d84bd2a
fix: __vite_rsc_raw_import__ for server action
hi-ogawa Jul 31, 2025
f54f591
chore: tweak
hi-ogawa Jul 31, 2025
c9270c2
chore: readme
hi-ogawa Jul 31, 2025
9e4ced6
chore: cleanup
hi-ogawa Jul 31, 2025
30e8dc5
test: add e2e
hi-ogawa Jul 31, 2025
a66299f
test: tweak timeout
hi-ogawa Jul 31, 2025
2f80913
test: bye webkit
hi-ogawa Jul 31, 2025
801c349
fix: correct webkit test skip syntax in browser-mode tests
hi-ogawa Jul 31, 2025
0ba59e3
fix: fix action from client
hi-ogawa Jul 31, 2025
24c5769
chore: cleanup
hi-ogawa Jul 31, 2025
adbeee0
chore: cleanup
hi-ogawa Jul 31, 2025
b17dcd8
chore: cleanup
hi-ogawa Jul 31, 2025
edbdfca
fix: fix vitePluginDefineEncryptionKey
hi-ogawa Jul 31, 2025
07cc571
chore: cleanup
hi-ogawa Jul 31, 2025
8acadc4
chore: tweak
hi-ogawa Jul 31, 2025
2799acb
fix: use edge build
hi-ogawa Jul 31, 2025
07b3fbe
chore: comment
hi-ogawa Aug 1, 2025
8e28d18
test: tweak flaky
hi-ogawa Aug 1, 2025
7975e6d
refactor: rename option to `serverEnvironmentName`
hi-ogawa Aug 1, 2025
3798dc9
refactor: expose `environment.browser` option
hi-ogawa Aug 1, 2025
e794a0e
Merge branch 'main' into 07-31-feat_rsc_support_customizing_react-ser…
hi-ogawa Aug 1, 2025
ca2a1ff
Merge branch 'main' into 07-31-feat_rsc_support_customizing_react-ser…
hi-ogawa Aug 1, 2025
d35cf81
refactor: expose only vitePluginRscMinimal
hi-ogawa Aug 1, 2025
0b55603
fix: fix double
hi-ogawa Aug 1, 2025
7e6b57a
test: add e2e
hi-ogawa Aug 1, 2025
86fc3de
refactor: fix todo
hi-ogawa Aug 1, 2025
bf73261
fix: wrong browserEnvironmentName
hi-ogawa Aug 1, 2025
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
74 changes: 74 additions & 0 deletions packages/plugin-rsc/e2e/browser-mode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { expect, test, type Page } from '@playwright/test'
import { useFixture } from './fixture'
import { defineStarterTest } from './starter'

test.describe('dev-browser-mode', () => {
// Webkit fails by
// > TypeError: ReadableByteStreamController is not implemented
test.skip(({ browserName }) => browserName === 'webkit')

const f = useFixture({ root: 'examples/browser-mode', mode: 'dev' })
defineStarterTest(f, 'browser-mode')

// action-bind tests copied from basic.test.ts

test('action bind simple', async ({ page }) => {
await page.goto(f.url())
await testActionBindSimple(page)
})

async function testActionBindSimple(page: Page) {
await expect(page.getByTestId('test-server-action-bind-simple')).toHaveText(
'[?]',
)
await page
.getByRole('button', { name: 'test-server-action-bind-simple' })
.click()
await expect(page.getByTestId('test-server-action-bind-simple')).toHaveText(
'true',
)
await page
.getByRole('button', { name: 'test-server-action-bind-reset' })
.click()
}

test('action bind client', async ({ page }) => {
await page.goto(f.url())
await testActionBindClient(page)
})

async function testActionBindClient(page: Page) {
await expect(page.getByTestId('test-server-action-bind-client')).toHaveText(
'[?]',
)
await page
.getByRole('button', { name: 'test-server-action-bind-client' })
.click()
await expect(page.getByTestId('test-server-action-bind-client')).toHaveText(
'true',
)
await page
.getByRole('button', { name: 'test-server-action-bind-reset' })
.click()
}

test('action bind action', async ({ page }) => {
await page.goto(f.url())
await testActionBindAction(page)
})

async function testActionBindAction(page: Page) {
await expect(page.getByTestId('test-server-action-bind-action')).toHaveText(
'[?]',
)
await page
.getByRole('button', { name: 'test-server-action-bind-action' })
.click()
await expect(page.getByTestId('test-server-action-bind-action')).toHaveText(
'[true,true]',
)
await page
.getByRole('button', { name: 'test-server-action-bind-reset' })
.click()
}
})
2 changes: 1 addition & 1 deletion packages/plugin-rsc/e2e/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export async function waitForHydration(page: Page, locator: string = 'body') {
el &&
Object.keys(el).some((key) => key.startsWith('__reactFiber')),
),
{ timeout: 3000 },
{ timeout: 10000 },
)
.toBeTruthy()
}
Expand Down
30 changes: 17 additions & 13 deletions packages/plugin-rsc/e2e/starter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import {

export function defineStarterTest(
f: Fixture,
variant?: 'no-ssr' | 'dev-production',
variant?: 'no-ssr' | 'dev-production' | 'browser-mode',
) {
const waitForHydration: typeof waitForHydration_ = (page) =>
waitForHydration_(page, variant === 'no-ssr' ? '#root' : 'body')
waitForHydration_(
page,
variant === 'no-ssr' || variant === 'browser-mode' ? '#root' : 'body',
)

test('basic', async ({ page }) => {
using _ = expectNoPageError(page)
Expand Down Expand Up @@ -40,7 +43,7 @@ export function defineStarterTest(
})

testNoJs('server action @nojs', async ({ page }) => {
test.skip(variant === 'no-ssr')
test.skip(variant === 'no-ssr' || variant === 'browser-mode')

await page.goto(f.url())
await page.getByRole('button', { name: 'Server Counter: 1' }).click()
Expand All @@ -50,7 +53,11 @@ export function defineStarterTest(
})

test('client hmr', async ({ page }) => {
test.skip(f.mode === 'build' || variant === 'dev-production')
test.skip(
f.mode === 'build' ||
variant === 'dev-production' ||
variant === 'browser-mode',
)

await page.goto(f.url())
await waitForHydration(page)
Expand Down Expand Up @@ -80,7 +87,7 @@ export function defineStarterTest(
})

test.describe(() => {
test.skip(f.mode === 'build')
test.skip(f.mode === 'build' || variant === 'browser-mode')

test('server hmr', async ({ page }) => {
await page.goto(f.url())
Expand Down Expand Up @@ -113,20 +120,17 @@ export function defineStarterTest(
test('css @js', async ({ page }) => {
await page.goto(f.url())
await waitForHydration(page)
await expect(page.locator('.read-the-docs')).toHaveCSS(
'color',
'rgb(136, 136, 136)',
)
await expect(page.locator('.card').nth(0)).toHaveCSS('padding-left', '16px')
})

test.describe(() => {
test.skip(variant === 'no-ssr')
test.skip(variant === 'no-ssr' || variant === 'browser-mode')

testNoJs('css @nojs', async ({ page }) => {
await page.goto(f.url())
await expect(page.locator('.read-the-docs')).toHaveCSS(
'color',
'rgb(136, 136, 136)',
await expect(page.locator('.card').nth(0)).toHaveCSS(
'padding-left',
'16px',
)
})
})
Expand Down
4 changes: 2 additions & 2 deletions packages/plugin-rsc/e2e/syntax-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,11 @@ test.describe(() => {
)
await expect(async () => {
await page.goto(f.url())
await waitForHydration(page)
await expect(page.getByTestId('client-content')).toHaveText(
'client:fixed',
)
}).toPass()
await waitForHydration(page)
})
})

Expand Down Expand Up @@ -197,11 +197,11 @@ test.describe(() => {
)
await expect(async () => {
await page.goto(f.url())
await waitForHydration(page)
await expect(page.getByTestId('server-content')).toHaveText(
'server:fixed',
)
}).toPass()
await waitForHydration(page)
})
})
})
1 change: 1 addition & 0 deletions packages/plugin-rsc/examples/browser-mode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[examples/starter](https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc/examples/starter) but entirely on Browser. Inspired by https://github.com/kasperpeulen/vitest-plugin-rsc/
13 changes: 13 additions & 0 deletions packages/plugin-rsc/examples/browser-mode/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>RSC Browser Mode</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<script async type="module" src="/src/framework/main.tsx"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
23 changes: 23 additions & 0 deletions packages/plugin-rsc/examples/browser-mode/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@vitejs/plugin-rsc-examples-browser-mode",
"version": "0.0.0",
"private": true,
"license": "MIT",
"type": "module",
"scripts": {
"dev": "vite",
"build": "false && vite build",
"preview": "false && vite preview"
},
"dependencies": {
"@vitejs/plugin-rsc": "latest",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "latest",
"vite": "^7.0.6"
}
}
1 change: 1 addition & 0 deletions packages/plugin-rsc/examples/browser-mode/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use client'

import React from 'react'

export function ActionBindClient() {
const hydrated = React.useSyncExternalStore(
React.useCallback(() => () => {}, []),
() => true,
() => false,
)
return <>{String(hydrated)}</>
}
16 changes: 16 additions & 0 deletions packages/plugin-rsc/examples/browser-mode/src/action-bind/form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use client'

import React from 'react'

export function TestServerActionBindClientForm(props: {
action: () => Promise<React.ReactNode>
}) {
const [result, formAction] = React.useActionState(props.action, '[?]')

return (
<form action={formAction}>
<button>test-server-action-bind-client</button>
<span data-testid="test-server-action-bind-client">{result}</span>
</form>
)
}
107 changes: 107 additions & 0 deletions packages/plugin-rsc/examples/browser-mode/src/action-bind/server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// based on test cases in
// https://github.com/vercel/next.js/blob/ad898de735c393d98960a68c8d9eaeee32206c57/test/e2e/app-dir/actions/app/encryption/page.js

import { ActionBindClient } from './client'
import { TestServerActionBindClientForm } from './form'

export function TestActionBind() {
return (
<>
<TestServerActionBindReset />
<TestServerActionBindSimple />
<TestServerActionBindClient />
<TestServerActionBindAction />
</>
)
}

export function TestServerActionBindReset() {
return (
<form
action={async () => {
'use server'
testServerActionBindSimpleState = '[?]'
testServerActionBindActionState = '[?]'
testServerActionBindClientState++
}}
>
<button>test-server-action-bind-reset</button>
</form>
)
}

let testServerActionBindSimpleState = '[?]'

export function TestServerActionBindSimple() {
const outerValue = 'outerValue'

return (
<form
action={async (formData: FormData) => {
'use server'
const result = String(formData.get('value')) === outerValue
testServerActionBindSimpleState = JSON.stringify(result)
}}
>
<input type="hidden" name="value" value={outerValue} />
<button type="submit">test-server-action-bind-simple</button>
<span data-testid="test-server-action-bind-simple">
{testServerActionBindSimpleState}
</span>
</form>
)
}

let testServerActionBindClientState = 0

export function TestServerActionBindClient() {
// client element as server action bound argument
const client = <ActionBindClient />

const action = async () => {
'use server'
return client
}

return (
<TestServerActionBindClientForm
key={testServerActionBindClientState}
action={action}
/>
)
}

let testServerActionBindActionState = '[?]'

export function TestServerActionBindAction() {
async function otherAction() {
'use server'
return 'otherActionValue'
}

function wrapAction(value: string, action: () => Promise<string>) {
return async function (formValue: string) {
'use server'
const actionValue = await action()
return [actionValue === 'otherActionValue', formValue === value]
}
}

const action = wrapAction('ok', otherAction)

return (
<form
action={async (formData: FormData) => {
'use server'
const result = await action(String(formData.get('value')))
testServerActionBindActionState = JSON.stringify(result)
}}
>
<input type="hidden" name="value" value="ok" />
<button type="submit">test-server-action-bind-action</button>
<span data-testid="test-server-action-bind-action">
{testServerActionBindActionState}
</span>
</form>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use server'

export async function testActionState(prev: number) {
return prev + 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client'

import React from 'react'
import { testActionState } from './action'

export function TestUseActionState() {
const [state, formAction] = React.useActionState(testActionState, 0)

return (
<form action={formAction}>
<button data-testid="use-action-state">useActionState: {state}</button>
</form>
)
}
11 changes: 11 additions & 0 deletions packages/plugin-rsc/examples/browser-mode/src/action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use server'

let serverCounter = 0

export async function getServerCounter() {
return serverCounter
}

export async function updateServerCounter(change: number) {
serverCounter += change
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading