Skip to content

Commit 370cf52

Browse files
add tests for the behavior of returning ReactComponents from render functions
1 parent acaef11 commit 370cf52

File tree

4 files changed

+142
-61
lines changed

4 files changed

+142
-61
lines changed

node_package/src/RSCServerRoot.ts

Lines changed: 58 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,58 @@
1-
import * as React from 'react';
2-
import { createFromNodeStream } from 'react-on-rails-rsc/client.node';
3-
import transformRSCStream from './transformRSCNodeStreamAndReplayConsoleLogs';
4-
import loadJsonFile from './loadJsonFile';
5-
6-
if (!('use' in React && typeof React.use === 'function')) {
7-
throw new Error('React.use is not defined. Please ensure you are using React 18 with experimental features enabled or React 19+ to use server components.');
8-
}
9-
10-
const { use } = React;
11-
12-
export type RSCServerRootProps = {
13-
getRscPromise: NodeJS.ReadableStream,
14-
reactClientManifestFileName: string,
15-
reactServerManifestFileName: string,
16-
}
17-
18-
const createFromFetch = (stream: NodeJS.ReadableStream, ssrManifest: Record<string, unknown>) => {
19-
const transformedStream = transformRSCStream(stream);
20-
return createFromNodeStream(transformedStream, ssrManifest);
21-
}
22-
23-
const createSSRManifest = (reactServerManifestFileName: string, reactClientManifestFileName: string) => {
24-
const reactServerManifest = loadJsonFile(reactServerManifestFileName);
25-
const reactClientManifest = loadJsonFile(reactClientManifestFileName);
26-
27-
const ssrManifest = {
28-
moduleLoading: {
29-
prefix: "/webpack/development/",
30-
crossOrigin: null,
31-
},
32-
moduleMap: {} as Record<string, unknown>,
33-
};
34-
35-
Object.entries(reactClientManifest).forEach(([aboluteFileUrl, clientFileBundlingInfo]) => {
36-
const serverFileBundlingInfo = reactServerManifest[aboluteFileUrl];
37-
ssrManifest.moduleMap[(clientFileBundlingInfo as { id: string }).id] = {
38-
'*': {
39-
id: (serverFileBundlingInfo as { id: string }).id,
40-
chunks: (serverFileBundlingInfo as { chunks: string[] }).chunks,
41-
name: '*',
42-
}
43-
};
44-
});
45-
46-
return ssrManifest;
47-
}
48-
49-
const RSCServerRoot = ({
50-
getRscPromise,
51-
reactClientManifestFileName,
52-
reactServerManifestFileName,
53-
}: RSCServerRootProps) => {
54-
const ssrManifest = createSSRManifest(reactServerManifestFileName, reactClientManifestFileName);
55-
return use(createFromFetch(getRscPromise, ssrManifest));
56-
};
57-
58-
export default RSCServerRoot;
1+
// import * as React from 'react';
2+
// import { createFromNodeStream } from 'react-on-rails-rsc/client.node';
3+
// import transformRSCStream from './transformRSCNodeStreamAndReplayConsoleLogs';
4+
// import loadJsonFile from './loadJsonFile';
5+
6+
// if (!('use' in React && typeof React.use === 'function')) {
7+
// throw new Error('React.use is not defined. Please ensure you are using React 18 with experimental features enabled or React 19+ to use server components.');
8+
// }
9+
10+
// const { use } = React;
11+
12+
// export type RSCServerRootProps = {
13+
// getRscPromise: NodeJS.ReadableStream,
14+
// reactClientManifestFileName: string,
15+
// reactServerManifestFileName: string,
16+
// }
17+
18+
// const createFromFetch = (stream: NodeJS.ReadableStream, ssrManifest: Record<string, unknown>) => {
19+
// const transformedStream = transformRSCStream(stream);
20+
// return createFromNodeStream(transformedStream, ssrManifest);
21+
// }
22+
23+
// const createSSRManifest = (reactServerManifestFileName: string, reactClientManifestFileName: string) => {
24+
// const reactServerManifest = loadJsonFile(reactServerManifestFileName);
25+
// const reactClientManifest = loadJsonFile(reactClientManifestFileName);
26+
27+
// const ssrManifest = {
28+
// moduleLoading: {
29+
// prefix: "/webpack/development/",
30+
// crossOrigin: null,
31+
// },
32+
// moduleMap: {} as Record<string, unknown>,
33+
// };
34+
35+
// Object.entries(reactClientManifest).forEach(([aboluteFileUrl, clientFileBundlingInfo]) => {
36+
// const serverFileBundlingInfo = reactServerManifest[aboluteFileUrl];
37+
// ssrManifest.moduleMap[(clientFileBundlingInfo as { id: string }).id] = {
38+
// '*': {
39+
// id: (serverFileBundlingInfo as { id: string }).id,
40+
// chunks: (serverFileBundlingInfo as { chunks: string[] }).chunks,
41+
// name: '*',
42+
// }
43+
// };
44+
// });
45+
46+
// return ssrManifest;
47+
// }
48+
49+
// const RSCServerRoot = ({
50+
// getRscPromise,
51+
// reactClientManifestFileName,
52+
// reactServerManifestFileName,
53+
// }: RSCServerRootProps) => {
54+
// const ssrManifest = createSSRManifest(reactServerManifestFileName, reactClientManifestFileName);
55+
// return use(createFromFetch(getRscPromise, ssrManifest));
56+
// };
57+
58+
// export default RSCServerRoot;

node_package/src/serverRenderReactComponent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ function handleRenderingError(e: unknown, options: { componentName: string; thro
9494
}
9595

9696
async function createPromiseResult(
97-
renderState: RenderState & { result: Promise<string> },
97+
renderState: RenderState & { result: Promise<string | ReactElement> },
9898
componentName: string,
9999
throwJsErrors: boolean,
100100
): Promise<RenderResult> {

node_package/tests/serverRenderReactComponent.test.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ describe('serverRenderReactComponent', () => {
6868
expect(result).toBeTruthy();
6969
});
7070

71-
it('serverRenderReactComponent renders promises', async () => {
71+
it('serverRenderReactComponent renders promises that return strings', async () => {
7272
expect.assertions(2);
7373
const expectedHtml = '<div>Hello</div>';
7474
const X5 = (props, _railsContext) => Promise.resolve(expectedHtml);
@@ -86,4 +86,26 @@ describe('serverRenderReactComponent', () => {
8686
expect(html).toEqual(expectedHtml);
8787
expect(renderResult.hasErrors).toBeFalsy();
8888
});
89+
90+
it('serverRenderReactComponent renders promises that return React components', async () => {
91+
expect.assertions(2);
92+
const AsyncComponent = () => <div>Async Component</div>;
93+
// Return a promise that resolves to a React component
94+
const X6 = (_, _railsContext) => Promise.resolve(AsyncComponent);
95+
96+
ComponentRegistry.register({ X6 });
97+
98+
const renderResult = await serverRenderReactComponent({
99+
name: 'X6',
100+
domNodeId: 'myDomId',
101+
trace: false,
102+
renderingReturnsPromises: true,
103+
});
104+
const html = await renderResult.html;
105+
106+
// Verify the component was rendered correctly
107+
const result = html.indexOf('>Async Component</div>') > 0;
108+
expect(result).toBeTruthy();
109+
expect(renderResult.hasErrors).toBeFalsy();
110+
});
89111
});

node_package/tests/streamServerRenderedReactComponent.test.jsx

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,24 @@ describe('streamServerRenderedReactComponent', () => {
6060
throwSyncError = false,
6161
throwJsErrors = false,
6262
throwAsyncError = false,
63+
componentType = 'reactComponent',
6364
} = {}) => {
64-
ComponentRegistry.register({ TestComponentForStreaming });
65+
switch (componentType) {
66+
case 'reactComponent':
67+
ComponentRegistry.register({ TestComponentForStreaming });
68+
break;
69+
case 'renderFunction':
70+
ComponentRegistry.register({
71+
TestComponentForStreaming: (props, _railsContext) => () => <TestComponentForStreaming {...props} />,
72+
});
73+
break;
74+
case 'asyncRenderFunction':
75+
ComponentRegistry.register({
76+
TestComponentForStreaming: (props, _railsContext) => () =>
77+
Promise.resolve(<TestComponentForStreaming {...props} />),
78+
});
79+
break;
80+
}
6581
const renderResult = streamServerRenderedReactComponent({
6682
name: 'TestComponentForStreaming',
6783
domNodeId: 'myDomId',
@@ -169,4 +185,47 @@ describe('streamServerRenderedReactComponent', () => {
169185
expect(chunks[1].hasErrors).toBe(true);
170186
expect(chunks[1].isShellReady).toBe(true);
171187
});
188+
189+
it.each(['asyncRenderFunction', 'renderFunction'])(
190+
'streams a component from a %s that resolves to a React component',
191+
async (componentType) => {
192+
const { renderResult, chunks } = setupStreamTest({ componentType });
193+
await new Promise((resolve) => renderResult.on('end', resolve));
194+
195+
expect(chunks).toHaveLength(2);
196+
expect(chunks[0].html).toContain('Header In The Shell');
197+
expect(chunks[0].consoleReplayScript).toBe('');
198+
expect(chunks[0].hasErrors).toBe(false);
199+
expect(chunks[0].isShellReady).toBe(true);
200+
expect(chunks[1].html).toContain('Async Content');
201+
expect(chunks[1].consoleReplayScript).toBe('');
202+
expect(chunks[1].hasErrors).toBe(false);
203+
expect(chunks[1].isShellReady).toBe(true);
204+
},
205+
);
206+
207+
it('streams a string from a Promise that resolves to a string', async () => {
208+
const StringPromiseComponent = () => Promise.resolve('<div>String from Promise</div>');
209+
ComponentRegistry.register({ StringPromiseComponent });
210+
211+
const renderResult = streamServerRenderedReactComponent({
212+
name: 'StringPromiseComponent',
213+
domNodeId: 'stringPromiseId',
214+
trace: false,
215+
throwJsErrors: false,
216+
});
217+
218+
const chunks = [];
219+
renderResult.on('data', (chunk) => {
220+
const decodedText = new TextDecoder().decode(chunk);
221+
chunks.push(expectStreamChunk(decodedText));
222+
});
223+
224+
await new Promise((resolve) => renderResult.on('end', resolve));
225+
226+
// Verify we have at least one chunk and it contains our string
227+
expect(chunks.length).toBeGreaterThan(0);
228+
expect(chunks[0].html).toContain('String from Promise');
229+
expect(chunks[0].hasErrors).toBe(false);
230+
});
172231
});

0 commit comments

Comments
 (0)