forked from vitejs/vite-plugin-react
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathentry.rsc.tsx
More file actions
176 lines (163 loc) · 5.91 KB
/
Copy pathentry.rsc.tsx
File metadata and controls
176 lines (163 loc) · 5.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import {
renderToReadableStream,
createTemporaryReferenceSet,
decodeReply,
loadServerAction,
decodeAction,
decodeFormState,
} from '@vitejs/plugin-rsc/rsc'
import type React from 'react'
import type { ReactFormState } from 'react-dom/client'
import { parseRenderRequest } from './request.tsx'
// The schema of payload which is serialized into RSC stream on rsc environment
// and deserialized on ssr/client environments.
export type RscPayload = {
// this demo renders/serializes/deserizlies entire root html element
// but this mechanism can be changed to render/fetch different parts of components
// based on your own route conventions.
root: React.ReactNode
// server action return value of non-progressive enhancement case
returnValue?: { ok: boolean; data: unknown }
// server action form state (e.g. useActionState) of progressive enhancement case
formState?: ReactFormState
}
async function handleRequest({
request,
getRoot,
nonce,
}: {
request: Request
getRoot: () => React.ReactNode
nonce?: string
}): Promise<Response> {
// differentiate RSC, SSR, action, etc.
const renderRequest = parseRenderRequest(request)
request = renderRequest.request
// handle server function request
let returnValue: RscPayload['returnValue'] | undefined
let formState: ReactFormState | undefined
let temporaryReferences: unknown | undefined
let actionStatus: number | undefined
if (renderRequest.isAction === true) {
if (renderRequest.actionId) {
// action is called via `ReactClient.setServerCallback`.
const contentType = request.headers.get('content-type')
const body = contentType?.startsWith('multipart/form-data')
? await request.formData()
: await request.text()
temporaryReferences = createTemporaryReferenceSet()
const args = await decodeReply(body, { temporaryReferences })
const action = await loadServerAction(renderRequest.actionId)
try {
const data = await action.apply(null, args)
returnValue = { ok: true, data }
} catch (e) {
returnValue = { ok: false, data: e }
actionStatus = 500
}
} else {
// otherwise server function is called via `<form action={...}>`
// before hydration (e.g. when javascript is disabled).
// aka progressive enhancement.
const formData = await request.formData()
const decodedAction = await decodeAction(formData)
try {
const result = await decodedAction()
formState = await decodeFormState(result, formData)
} catch (e) {
// there's no single general obvious way to surface this error,
// so explicitly return classic 500 response.
return new Response('Internal Server Error: server action failed', {
status: 500,
})
}
}
}
const rscPayload: RscPayload = { root: getRoot(), formState, returnValue }
const rscOptions = { temporaryReferences }
const debugClientReferences: unknown[] = []
const rscStream = renderToReadableStream<RscPayload>(rscPayload, rscOptions, {
onClientReference(metadata) {
debugClientReferences.push(metadata)
},
})
// test `onClientReference` callback
if (renderRequest.url.pathname === '/__test_onClientReference') {
await rscStream.pipeTo(new WritableStream({ write() {} }))
return Response.json(debugClientReferences)
}
// Respond RSC stream without HTML rendering as decided by `RenderRequest`
if (renderRequest.isRsc) {
return new Response(rscStream, {
status: actionStatus,
headers: {
'content-type': 'text/x-component;charset=utf-8',
},
})
}
// Delegate to SSR environment for html rendering.
// The plugin provides `loadModule` helper to allow loading SSR environment entry module
// in RSC environment. however this can be customized by implementing own runtime communication
// e.g. `@cloudflare/vite-plugin`'s service binding.
const ssrEntryModule = await import.meta.viteRsc.loadModule<
typeof import('./entry.ssr.tsx')
>('ssr', 'index')
const ssrResult = await ssrEntryModule.renderHTML(rscStream, {
formState,
nonce,
// allow quick simulation of javascript disabled browser
debugNojs: renderRequest.url.searchParams.has('__nojs'),
})
// respond html
return new Response(ssrResult.stream, {
status: ssrResult.status,
headers: {
'content-type': 'text/html;charset=utf-8',
},
})
}
async function handler(request: Request): Promise<Response> {
const url = new URL(request.url)
if (url.pathname === '/__test_compatibility_manifest') {
const { default: compatibilityManifest } =
await import('virtual:vite-rsc/compatibility-manifest')
return Response.json(compatibilityManifest)
}
const { Root } = await import('../routes/root.tsx')
const nonce = !process.env.NO_CSP ? crypto.randomUUID() : undefined
// https://vite.dev/guide/features.html#content-security-policy-csp
// this isn't needed if `style-src: 'unsafe-inline'` (dev) and `script-src: 'self'`
const nonceMeta = nonce && <meta property="csp-nonce" nonce={nonce} />
const root = (
<>
{nonceMeta}
<Root url={url} />
</>
)
const response = await handleRequest({
request,
getRoot: () => root,
nonce,
})
if (nonce && response.headers.get('content-type')?.includes('text/html')) {
const cspValue = [
`default-src 'self';`,
// `unsafe-eval` is required during dev since React uses eval for findSourceMapURL feature
`script-src 'self' 'nonce-${nonce}' ${import.meta.env.DEV ? `'unsafe-eval'` : ``};`,
`style-src 'self' 'unsafe-inline';`,
`img-src 'self' data:;`,
// allow blob: worker for Vite server ping shared worker
import.meta.hot && `worker-src 'self' blob:;`,
]
.filter(Boolean)
.join('')
response.headers.set('content-security-policy', cspValue)
}
return response
}
export default {
fetch: handler,
}
if (import.meta.hot) {
import.meta.hot.accept()
}