Skip to content

Commit 34a8aad

Browse files
add tests for the behavior of returning ReactComponents from render functions
1 parent cc7a4c3 commit 34a8aad

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
@@ -82,7 +82,7 @@ function handleRenderingError(e: unknown, options: { componentName: string, thro
8282
}
8383

8484
async function createPromiseResult(
85-
renderState: RenderState & { result: Promise<string> },
85+
renderState: RenderState & { result: Promise<string | ReactElement> },
8686
componentName: string,
8787
throwJsErrors: boolean
8888
): 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
@@ -54,8 +54,24 @@ describe('streamServerRenderedReactComponent', () => {
5454
throwSyncError = false,
5555
throwJsErrors = false,
5656
throwAsyncError = false,
57+
componentType = 'reactComponent',
5758
} = {}) => {
58-
ComponentRegistry.register({ TestComponentForStreaming });
59+
switch (componentType) {
60+
case 'reactComponent':
61+
ComponentRegistry.register({ TestComponentForStreaming });
62+
break;
63+
case 'renderFunction':
64+
ComponentRegistry.register({
65+
TestComponentForStreaming: (props, _railsContext) => () => <TestComponentForStreaming {...props} />,
66+
});
67+
break;
68+
case 'asyncRenderFunction':
69+
ComponentRegistry.register({
70+
TestComponentForStreaming: (props, _railsContext) => () =>
71+
Promise.resolve(<TestComponentForStreaming {...props} />),
72+
});
73+
break;
74+
}
5975
const renderResult = streamServerRenderedReactComponent({
6076
name: 'TestComponentForStreaming',
6177
domNodeId: 'myDomId',
@@ -153,4 +169,47 @@ describe('streamServerRenderedReactComponent', () => {
153169
expect(chunks[1].hasErrors).toBe(true);
154170
expect(chunks[1].isShellReady).toBe(true);
155171
});
172+
173+
it.each(['asyncRenderFunction', 'renderFunction'])(
174+
'streams a component from a %s that resolves to a React component',
175+
async (componentType) => {
176+
const { renderResult, chunks } = setupStreamTest({ componentType });
177+
await new Promise((resolve) => renderResult.on('end', resolve));
178+
179+
expect(chunks).toHaveLength(2);
180+
expect(chunks[0].html).toContain('Header In The Shell');
181+
expect(chunks[0].consoleReplayScript).toBe('');
182+
expect(chunks[0].hasErrors).toBe(false);
183+
expect(chunks[0].isShellReady).toBe(true);
184+
expect(chunks[1].html).toContain('Async Content');
185+
expect(chunks[1].consoleReplayScript).toBe('');
186+
expect(chunks[1].hasErrors).toBe(false);
187+
expect(chunks[1].isShellReady).toBe(true);
188+
},
189+
);
190+
191+
it('streams a string from a Promise that resolves to a string', async () => {
192+
const StringPromiseComponent = () => Promise.resolve('<div>String from Promise</div>');
193+
ComponentRegistry.register({ StringPromiseComponent });
194+
195+
const renderResult = streamServerRenderedReactComponent({
196+
name: 'StringPromiseComponent',
197+
domNodeId: 'stringPromiseId',
198+
trace: false,
199+
throwJsErrors: false,
200+
});
201+
202+
const chunks = [];
203+
renderResult.on('data', (chunk) => {
204+
const decodedText = new TextDecoder().decode(chunk);
205+
chunks.push(expectStreamChunk(decodedText));
206+
});
207+
208+
await new Promise((resolve) => renderResult.on('end', resolve));
209+
210+
// Verify we have at least one chunk and it contains our string
211+
expect(chunks.length).toBeGreaterThan(0);
212+
expect(chunks[0].html).toContain('String from Promise');
213+
expect(chunks[0].hasErrors).toBe(false);
214+
});
156215
});

0 commit comments

Comments
 (0)