Skip to content

Commit eea1c42

Browse files
chore: upgrade @webcontainer/react and fix RSC examples
1 parent 0c58a15 commit eea1c42

File tree

13 files changed

+4574
-51
lines changed

13 files changed

+4574
-51
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"@docsearch/react": "^3.8.3",
3232
"@headlessui/react": "^1.7.0",
3333
"@radix-ui/react-context-menu": "^2.1.5",
34-
"@webcontainer/react": "^0.0.1",
34+
"@webcontainer/react": "^0.0.2",
3535
"body-scroll-lock": "^3.1.3",
3636
"classnames": "^2.2.6",
3737
"debounce": "^1.2.1",

src/components/MDX/Sandpack/SandpackRSCRoot.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ import {SandpackProvider} from '@webcontainer/react';
1515
import {CustomPreset} from './CustomPreset';
1616
import {createFileMap} from './createFileMap';
1717
import {CustomTheme} from './Themes';
18-
import {templateRSC} from './templateRSC';
19-
import {RscFileBridge} from './sandpack-rsc/RscFileBridge';
18+
import {viteRscTemplate} from './templates/viteRscTemplate';
2019

2120
type SandpackProps = {
2221
children: React.ReactNode;
@@ -93,17 +92,17 @@ function SandpackRSCRoot(props: SandpackProps) {
9392
return (
9493
<div className="sandpack sandpack--playground w-full my-8" dir="ltr">
9594
<SandpackProvider
96-
files={{...templateRSC, ...files}}
95+
template={viteRscTemplate}
96+
files={files}
9797
theme={CustomTheme}
9898
options={{
9999
autorun,
100100
initMode: 'user-visible',
101101
initModeObserverOptions: {rootMargin: '1400px 0px'},
102102
}}>
103-
<RscFileBridge />
104103
<CustomPreset
105104
providedFiles={Object.keys(files)}
106-
showOpenInCodeSandbox={false}
105+
// showOpenInCodeSandbox={false}
107106
/>
108107
</SandpackProvider>
109108
</div>

src/components/MDX/Sandpack/SandpackRoot.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {SandpackProvider} from '@webcontainer/react';
1515
import {CustomPreset} from './CustomPreset';
1616
import {createFileMap} from './createFileMap';
1717
import {CustomTheme} from './Themes';
18+
import {viteReactTemplate} from './templates/viteReactTemplate';
1819

1920
type SandpackProps = {
2021
children: React.ReactNode;
@@ -91,6 +92,7 @@ function SandpackRoot(props: SandpackProps) {
9192
return (
9293
<div className="sandpack sandpack--playground w-full my-8" dir="ltr">
9394
<SandpackProvider
95+
template={viteReactTemplate}
9496
files={files}
9597
theme={CustomTheme}
9698
options={{

src/components/MDX/Sandpack/sandpack-rsc/RscFileBridge.tsx

Lines changed: 0 additions & 40 deletions
This file was deleted.
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import {
2+
createFromReadableStream,
3+
createFromFetch,
4+
setServerCallback,
5+
createTemporaryReferenceSet,
6+
encodeReply,
7+
} from '@vitejs/plugin-rsc/browser';
8+
import React from 'react';
9+
import {createRoot, hydrateRoot} from 'react-dom/client';
10+
import {rscStream} from 'rsc-html-stream/client';
11+
import {GlobalErrorBoundary} from './error-boundary';
12+
import {createRscRenderRequest} from './request';
13+
14+
async function main() {
15+
let setPayload;
16+
17+
const initialPayload = await createFromReadableStream(rscStream);
18+
19+
function BrowserRoot() {
20+
const [payload, setPayload_] = React.useState(initialPayload);
21+
22+
React.useEffect(() => {
23+
setPayload = (v) => React.startTransition(() => setPayload_(v));
24+
}, [setPayload_]);
25+
26+
React.useEffect(() => {
27+
return listenNavigation(() => fetchRscPayload());
28+
}, []);
29+
30+
return payload.root;
31+
}
32+
33+
// re-fetch RSC and trigger re-rendering
34+
async function fetchRscPayload() {
35+
const renderRequest = createRscRenderRequest(window.location.href);
36+
const payload = await createFromFetch(fetch(renderRequest));
37+
38+
setPayload(payload);
39+
}
40+
41+
setServerCallback(async (id, args) => {
42+
const temporaryReferences = createTemporaryReferenceSet();
43+
const renderRequest = createRscRenderRequest(window.location.href, {
44+
id,
45+
body: await encodeReply(args, {temporaryReferences}),
46+
});
47+
const payload = await createFromFetch(fetch(renderRequest), {
48+
temporaryReferences,
49+
});
50+
51+
setPayload(payload);
52+
53+
const {ok, data} = payload.returnValue;
54+
55+
if (!ok) {
56+
throw data;
57+
}
58+
59+
return data;
60+
});
61+
62+
const browserRoot = (
63+
<React.StrictMode>
64+
<GlobalErrorBoundary>
65+
<BrowserRoot />
66+
</GlobalErrorBoundary>
67+
</React.StrictMode>
68+
);
69+
70+
if ('__NO_HYDRATE' in globalThis) {
71+
createRoot(document).render(browserRoot);
72+
} else {
73+
hydrateRoot(document, browserRoot, {
74+
formState: initialPayload.formState,
75+
});
76+
}
77+
78+
if (import.meta.hot) {
79+
import.meta.hot.on('rsc:update', () => {
80+
fetchRscPayload();
81+
});
82+
}
83+
}
84+
85+
function listenNavigation(onNavigation) {
86+
window.addEventListener('popstate', onNavigation);
87+
88+
const oldPushState = window.history.pushState;
89+
window.history.pushState = function (...args) {
90+
const res = oldPushState.apply(this, args);
91+
onNavigation();
92+
return res;
93+
};
94+
95+
const oldReplaceState = window.history.replaceState;
96+
window.history.replaceState = function (...args) {
97+
const res = oldReplaceState.apply(this, args);
98+
onNavigation();
99+
return res;
100+
};
101+
102+
function onClick(e) {
103+
let link = e.target.closest('a');
104+
if (
105+
link &&
106+
link instanceof HTMLAnchorElement &&
107+
link.href &&
108+
(!link.target || link.target === '_self') &&
109+
link.origin === location.origin &&
110+
!link.hasAttribute('download') &&
111+
e.button === 0 &&
112+
!e.metaKey &&
113+
!e.ctrlKey &&
114+
!e.altKey &&
115+
!e.shiftKey &&
116+
!e.defaultPrevented
117+
) {
118+
e.preventDefault();
119+
history.pushState(null, '', link.href);
120+
}
121+
}
122+
document.addEventListener('click', onClick);
123+
124+
return () => {
125+
document.removeEventListener('click', onClick);
126+
window.removeEventListener('popstate', onNavigation);
127+
window.history.pushState = oldPushState;
128+
window.history.replaceState = oldReplaceState;
129+
};
130+
}
131+
132+
main();
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import {
2+
renderToReadableStream,
3+
createTemporaryReferenceSet,
4+
decodeReply,
5+
loadServerAction,
6+
decodeAction,
7+
decodeFormState,
8+
} from '@vitejs/plugin-rsc/rsc';
9+
import {Root} from '../root.jsx';
10+
import {parseRenderRequest} from './request.jsx';
11+
12+
export default {fetch: handler};
13+
14+
async function handler(request) {
15+
const renderRequest = parseRenderRequest(request);
16+
request = renderRequest.request;
17+
18+
let returnValue;
19+
let formState;
20+
let temporaryReferences;
21+
let actionStatus;
22+
23+
if (renderRequest.isAction === true) {
24+
if (renderRequest.actionId) {
25+
const contentType = request.headers.get('content-type');
26+
const body = contentType?.startsWith('multipart/form-data')
27+
? await request.formData()
28+
: await request.text();
29+
temporaryReferences = createTemporaryReferenceSet();
30+
const args = await decodeReply(body, {temporaryReferences});
31+
const action = await loadServerAction(renderRequest.actionId);
32+
try {
33+
const data = await action.apply(null, args);
34+
returnValue = {ok: true, data};
35+
} catch (e) {
36+
returnValue = {ok: false, data: e};
37+
actionStatus = 500;
38+
}
39+
} else {
40+
const formData = await request.formData();
41+
const decodedAction = await decodeAction(formData);
42+
try {
43+
const result = await decodedAction();
44+
formState = await decodeFormState(result, formData);
45+
} catch (e) {
46+
return new Response('Internal Server Error: server action failed', {
47+
status: 500,
48+
});
49+
}
50+
}
51+
}
52+
53+
const rscPayload = {
54+
root: <Root url={renderRequest.url} />,
55+
formState,
56+
returnValue,
57+
};
58+
const rscOptions = {temporaryReferences};
59+
const rscStream = renderToReadableStream(rscPayload, rscOptions);
60+
61+
if (renderRequest.isRsc) {
62+
return new Response(rscStream, {
63+
status: actionStatus,
64+
headers: {
65+
'content-type': 'text/x-component;charset=utf-8',
66+
},
67+
});
68+
}
69+
70+
const ssrEntryModule = await import.meta.viteRsc.loadModule('ssr', 'index');
71+
const ssrResult = await ssrEntryModule.renderHTML(rscStream, {
72+
formState,
73+
debugNojs: renderRequest.url.searchParams.has('__nojs'),
74+
});
75+
76+
return new Response(ssrResult.stream, {
77+
status: ssrResult.status,
78+
headers: {
79+
'Content-type': 'text/html',
80+
},
81+
});
82+
}
83+
84+
if (import.meta.hot) {
85+
import.meta.hot.accept();
86+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {createFromReadableStream} from '@vitejs/plugin-rsc/ssr';
2+
import React from 'react';
3+
import {renderToReadableStream} from 'react-dom/server.edge';
4+
import {injectRSCPayload} from 'rsc-html-stream/server';
5+
6+
export async function renderHTML(rscStream, options) {
7+
const [rscStream1, rscStream2] = rscStream.tee();
8+
9+
let payload;
10+
function SsrRoot() {
11+
payload ??= createFromReadableStream(rscStream1);
12+
return React.use(payload).root;
13+
}
14+
15+
const bootstrapScriptContent =
16+
await import.meta.viteRsc.loadBootstrapScriptContent('index');
17+
let htmlStream;
18+
let status;
19+
try {
20+
htmlStream = await renderToReadableStream(<SsrRoot />, {
21+
bootstrapScriptContent: options?.debugNojs
22+
? undefined
23+
: bootstrapScriptContent,
24+
nonce: options?.nonce,
25+
formState: options?.formState,
26+
});
27+
} catch (e) {
28+
status = 500;
29+
htmlStream = await renderToReadableStream(
30+
<html>
31+
<body>
32+
<noscript>Internal Server Error: SSR failed</noscript>
33+
</body>
34+
</html>,
35+
{
36+
bootstrapScriptContent:
37+
`self.__NO_HYDRATE=1;` +
38+
(options?.debugNojs ? '' : bootstrapScriptContent),
39+
nonce: options?.nonce,
40+
}
41+
);
42+
}
43+
44+
let responseStream = htmlStream;
45+
if (!options?.debugNojs) {
46+
responseStream = responseStream.pipeThrough(
47+
injectRSCPayload(rscStream2, {
48+
nonce: options?.nonce,
49+
})
50+
);
51+
}
52+
53+
return {stream: responseStream, status};
54+
}

0 commit comments

Comments
 (0)