Skip to content

Commit 26f0f20

Browse files
Copilothi-ogawa
andauthored
chore(rsc): example of rsc environment on browser module runner (#933)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: hi-ogawa <[email protected]> Co-authored-by: Hiroshi Ogawa <[email protected]>
1 parent e164644 commit 26f0f20

File tree

18 files changed

+638
-0
lines changed

18 files changed

+638
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { expect, test } from '@playwright/test'
2+
import { useFixture } from './fixture'
3+
import { defineStarterTest } from './starter'
4+
import path from 'node:path'
5+
import fs from 'node:fs'
6+
7+
// Webkit fails by
8+
// > TypeError: ReadableByteStreamController is not implemented
9+
test.skip(({ browserName }) => browserName === 'webkit')
10+
11+
test.describe('dev-browser', () => {
12+
const f = useFixture({ root: 'examples/browser', mode: 'dev' })
13+
defineStarterTest(f, 'no-ssr')
14+
})
15+
16+
test.describe('build-browser', () => {
17+
const f = useFixture({ root: 'examples/browser', mode: 'build' })
18+
defineStarterTest(f, 'no-ssr')
19+
20+
test('no ssr build', () => {
21+
expect(fs.existsSync(path.join(f.root, 'dist/ssr'))).toBe(false)
22+
})
23+
})
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# `rsc` environment on browser
2+
3+
See also https://github.com/hi-ogawa/vite-rsc-browser-example/
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>RSC on Browser</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
8+
<script async type="module" src="/src/framework/entry.browser.tsx"></script>
9+
</head>
10+
<body>
11+
<div id="root"></div>
12+
</body>
13+
</html>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
2+
3+
const runner = new ModuleRunner(
4+
{
5+
sourcemapInterceptor: false,
6+
transport: {
7+
invoke: async (payload) => {
8+
const response = await fetch(
9+
'/@vite/invoke-rsc?' +
10+
new URLSearchParams({
11+
data: JSON.stringify(payload),
12+
}),
13+
)
14+
return response.json()
15+
},
16+
},
17+
hmr: false,
18+
},
19+
new ESModulesEvaluator(),
20+
)
21+
22+
export default new Proxy(
23+
{},
24+
{
25+
get(_target, p, _receiver) {
26+
return async (...args: any[]) => {
27+
const module = await runner.import('/src/framework/entry.rsc')
28+
return module.default[p](...args)
29+
}
30+
},
31+
},
32+
)
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { rmSync } from 'node:fs'
2+
import path from 'node:path'
3+
import { normalizePath, type Plugin } from 'vite'
4+
5+
export default function vitePluginRscBrowser(): Plugin[] {
6+
return [
7+
{
8+
name: 'rsc-browser',
9+
config() {
10+
return {
11+
appType: 'spa',
12+
environments: {
13+
client: {
14+
build: {
15+
emptyOutDir: false,
16+
},
17+
},
18+
// TODO: server build is not hashed
19+
rsc: {
20+
build: {
21+
outDir: 'dist/client/__server',
22+
},
23+
keepProcessEnv: false,
24+
resolve: {
25+
noExternal: true,
26+
},
27+
optimizeDeps: {
28+
esbuildOptions: {
29+
platform: 'neutral',
30+
},
31+
},
32+
},
33+
},
34+
rsc: {
35+
serverHandler: false,
36+
},
37+
}
38+
},
39+
configResolved(config) {
40+
// avoid globalThis.AsyncLocalStorage injection in browser mode
41+
const plugin = config.plugins.find(
42+
(p) => p.name === 'rsc:inject-async-local-storage',
43+
)
44+
delete plugin!.transform
45+
},
46+
buildApp: {
47+
order: 'pre',
48+
async handler() {
49+
// clean up nested outDir
50+
rmSync('./dist', { recursive: true, force: true })
51+
},
52+
},
53+
configureServer(server) {
54+
server.middlewares.use(async (req, res, next) => {
55+
const url = new URL(req.url ?? '/', 'https://any.local')
56+
if (url.pathname === '/@vite/invoke-rsc') {
57+
const payload = JSON.parse(url.searchParams.get('data')!)
58+
const result =
59+
await server.environments['rsc']!.hot.handleInvoke(payload)
60+
res.setHeader('Content-Type', 'application/json')
61+
res.end(JSON.stringify(result))
62+
return
63+
}
64+
next()
65+
})
66+
},
67+
},
68+
{
69+
name: 'rsc-browser:load-rsc',
70+
resolveId(source) {
71+
if (source === 'virtual:vite-rsc-browser/load-rsc') {
72+
if (this.environment.mode === 'dev') {
73+
return this.resolve('/lib/dev-proxy')
74+
}
75+
return { id: source, external: true }
76+
}
77+
},
78+
renderChunk(code, chunk) {
79+
if (code.includes('virtual:vite-rsc-browser/load-rsc')) {
80+
const config = this.environment.getTopLevelConfig()
81+
const replacement = normalizeRelativePath(
82+
path.relative(
83+
path.join(
84+
config.environments.client.build.outDir,
85+
chunk.fileName,
86+
'..',
87+
),
88+
path.join(config.environments.rsc.build.outDir, 'index.js'),
89+
),
90+
)
91+
code = code.replaceAll(
92+
'virtual:vite-rsc-browser/load-rsc',
93+
() => replacement,
94+
)
95+
return { code }
96+
}
97+
},
98+
},
99+
]
100+
}
101+
102+
function normalizeRelativePath(s: string): string {
103+
s = normalizePath(s)
104+
return s[0] === '.' ? s : './' + s
105+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function loadEntryRsc() {
2+
return import('virtual:vite-rsc-browser/load-rsc' as any)
3+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "@vitejs/plugin-rsc-examples-browser",
3+
"version": "0.0.0",
4+
"private": true,
5+
"license": "MIT",
6+
"type": "module",
7+
"scripts": {
8+
"dev": "vite",
9+
"build": "vite build",
10+
"preview": "vite preview"
11+
},
12+
"dependencies": {
13+
"react": "^19.2.0",
14+
"react-dom": "^19.2.0"
15+
},
16+
"devDependencies": {
17+
"@types/react": "^19.2.2",
18+
"@types/react-dom": "^19.2.2",
19+
"@vitejs/plugin-react": "latest",
20+
"@vitejs/plugin-rsc": "latest",
21+
"vite": "^7.1.10"
22+
}
23+
}
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use server'
2+
3+
let serverCounter = 0
4+
5+
export async function getServerCounter() {
6+
return serverCounter
7+
}
8+
9+
export async function updateServerCounter(change: number) {
10+
serverCounter += change
11+
}
Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)