Skip to content

Commit 846d02d

Browse files
Separate streamServerRenderedReactComponent from ReactOnRails (#1680)
* separate streamServerRenderedReactComponent from ReactOnRails * add changelog entry * Export stream functions only for node bundles (#1681) * Add export for ReactOnRails for Node.js which supports streaming * adding default export from ReactOnRails module in ReactOnRails.node --------- Co-authored-by: Abanoub Ghadban <[email protected]>
1 parent fc789d9 commit 846d02d

File tree

13 files changed

+213
-183
lines changed

13 files changed

+213
-183
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ Please follow the recommendations outlined at [keepachangelog.com](http://keepac
1818
### [Unreleased]
1919
Changes since the last non-beta release.
2020

21+
### [14.1.1] - 2025-01-15
22+
23+
#### Fixed
24+
25+
- Separated streamServerRenderedReactComponent from the ReactOnRails object in order to stop users from getting errors during webpack compilation about needing the `stream-browserify` package. [PR 1680](https://github.com/shakacode/react_on_rails/pull/1680) by [judahmeek](https://github.com/judahmeek).
26+
2127
### [14.1.0] - 2025-01-06
2228

2329
#### Fixed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import ReactOnRails from './ReactOnRails';
2+
import streamServerRenderedReactComponent from './streamServerRenderedReactComponent';
3+
4+
ReactOnRails.streamServerRenderedReactComponent = streamServerRenderedReactComponent;
5+
6+
export * from './ReactOnRails';
7+
export { default } from './ReactOnRails';

node_package/src/ReactOnRails.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import type { ReactElement } from 'react';
2-
import type { Readable } from 'stream';
32

43
import * as ClientStartup from './clientStartup';
54
import handleError from './handleError';
65
import ComponentRegistry from './ComponentRegistry';
76
import StoreRegistry from './StoreRegistry';
8-
import serverRenderReactComponent, { streamServerRenderedReactComponent } from './serverRenderReactComponent';
7+
import serverRenderReactComponent from './serverRenderReactComponent';
98
import buildConsoleReplay from './buildConsoleReplay';
109
import createReactOutput from './createReactOutput';
1110
import Authenticity from './Authenticity';
@@ -252,8 +251,8 @@ ctx.ReactOnRails = {
252251
* Used by server rendering by Rails
253252
* @param options
254253
*/
255-
streamServerRenderedReactComponent(options: RenderParams): Readable {
256-
return streamServerRenderedReactComponent(options);
254+
streamServerRenderedReactComponent() {
255+
throw new Error('streamServerRenderedReactComponent is only supported when using a bundle built for Node.js environments');
257256
},
258257

259258
/**
Lines changed: 3 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,13 @@
1-
import ReactDOMServer, { type PipeableStream } from 'react-dom/server';
2-
import { PassThrough, Readable } from 'stream';
1+
import ReactDOMServer from 'react-dom/server';
32
import type { ReactElement } from 'react';
43

54
import ComponentRegistry from './ComponentRegistry';
65
import createReactOutput from './createReactOutput';
76
import { isPromise, isServerRenderHash } from './isServerRenderResult';
87
import buildConsoleReplay from './buildConsoleReplay';
98
import handleError from './handleError';
10-
import type { CreateReactOutputResult, RegisteredComponent, RenderParams, RenderResult, RenderingError, ServerRenderResult } from './types';
11-
12-
type RenderState = {
13-
result: null | string | Promise<string>;
14-
hasErrors: boolean;
15-
error?: RenderingError;
16-
};
17-
18-
type StreamRenderState = Omit<RenderState, 'result'> & {
19-
result: null | Readable;
20-
isShellReady: boolean;
21-
};
22-
23-
type RenderOptions = {
24-
componentName: string;
25-
domNodeId?: string;
26-
trace?: boolean;
27-
renderingReturnsPromises: boolean;
28-
};
29-
30-
function convertToError(e: unknown): Error {
31-
return e instanceof Error ? e : new Error(String(e));
32-
}
33-
34-
function validateComponent(componentObj: RegisteredComponent, componentName: string) {
35-
if (componentObj.isRenderer) {
36-
throw new Error(`Detected a renderer while server rendering component '${componentName}'. See https://github.com/shakacode/react_on_rails#renderer-functions`);
37-
}
38-
}
9+
import { createResultObject, convertToError, validateComponent } from './serverRenderUtils';
10+
import type { CreateReactOutputResult, RenderParams, RenderResult, RenderState, RenderOptions, ServerRenderResult } from './types';
3911

4012
function processServerRenderHash(result: ServerRenderResult, options: RenderOptions): RenderState {
4113
const { redirectLocation, routeError } = result;
@@ -104,16 +76,6 @@ function handleRenderingError(e: unknown, options: { componentName: string, thro
10476
};
10577
}
10678

107-
function createResultObject(html: string | null, consoleReplayScript: string, renderState: RenderState | StreamRenderState): RenderResult {
108-
return {
109-
html,
110-
consoleReplayScript,
111-
hasErrors: renderState.hasErrors,
112-
renderingError: renderState.error && { message: renderState.error.message, stack: renderState.error.stack },
113-
isShellReady: 'isShellReady' in renderState ? renderState.isShellReady : undefined,
114-
};
115-
}
116-
11779
async function createPromiseResult(
11880
renderState: RenderState & { result: Promise<string> },
11981
componentName: string,
@@ -203,131 +165,4 @@ const serverRenderReactComponent: typeof serverRenderReactComponentInternal = (o
203165
}
204166
};
205167

206-
const stringToStream = (str: string): Readable => {
207-
const stream = new PassThrough();
208-
stream.write(str);
209-
stream.end();
210-
return stream;
211-
};
212-
213-
const transformRenderStreamChunksToResultObject = (renderState: StreamRenderState) => {
214-
const consoleHistory = console.history;
215-
let previouslyReplayedConsoleMessages = 0;
216-
217-
const transformStream = new PassThrough({
218-
transform(chunk, _, callback) {
219-
const htmlChunk = chunk.toString();
220-
const consoleReplayScript = buildConsoleReplay(consoleHistory, previouslyReplayedConsoleMessages);
221-
previouslyReplayedConsoleMessages = consoleHistory?.length || 0;
222-
223-
const jsonChunk = JSON.stringify(createResultObject(htmlChunk, consoleReplayScript, renderState));
224-
225-
this.push(`${jsonChunk}\n`);
226-
callback();
227-
}
228-
});
229-
230-
let pipedStream: PipeableStream | null = null;
231-
const pipeToTransform = (pipeableStream: PipeableStream) => {
232-
pipeableStream.pipe(transformStream);
233-
pipedStream = pipeableStream;
234-
};
235-
// We need to wrap the transformStream in a Readable stream to properly handle errors:
236-
// 1. If we returned transformStream directly, we couldn't emit errors into it externally
237-
// 2. If an error is emitted into the transformStream, it would cause the render to fail
238-
// 3. By wrapping in Readable.from(), we can explicitly emit errors into the readableStream without affecting the transformStream
239-
// Note: Readable.from can merge multiple chunks into a single chunk, so we need to ensure that we can separate them later
240-
const readableStream = Readable.from(transformStream);
241-
242-
const writeChunk = (chunk: string) => transformStream.write(chunk);
243-
const emitError = (error: unknown) => readableStream.emit('error', error);
244-
const endStream = () => {
245-
transformStream.end();
246-
pipedStream?.abort();
247-
}
248-
return { readableStream, pipeToTransform, writeChunk, emitError, endStream };
249-
}
250-
251-
const streamRenderReactComponent = (reactRenderingResult: ReactElement, options: RenderParams) => {
252-
const { name: componentName, throwJsErrors } = options;
253-
const renderState: StreamRenderState = {
254-
result: null,
255-
hasErrors: false,
256-
isShellReady: false
257-
};
258-
259-
const {
260-
readableStream,
261-
pipeToTransform,
262-
writeChunk,
263-
emitError,
264-
endStream
265-
} = transformRenderStreamChunksToResultObject(renderState);
266-
267-
const renderingStream = ReactDOMServer.renderToPipeableStream(reactRenderingResult, {
268-
onShellError(e) {
269-
const error = convertToError(e);
270-
renderState.hasErrors = true;
271-
renderState.error = error;
272-
273-
if (throwJsErrors) {
274-
emitError(error);
275-
}
276-
277-
const errorHtml = handleError({ e: error, name: componentName, serverSide: true });
278-
writeChunk(errorHtml);
279-
endStream();
280-
},
281-
onShellReady() {
282-
renderState.isShellReady = true;
283-
pipeToTransform(renderingStream);
284-
},
285-
onError(e) {
286-
if (!renderState.isShellReady) {
287-
return;
288-
}
289-
const error = convertToError(e);
290-
if (throwJsErrors) {
291-
emitError(error);
292-
}
293-
renderState.hasErrors = true;
294-
renderState.error = error;
295-
},
296-
});
297-
298-
return readableStream;
299-
}
300-
301-
export const streamServerRenderedReactComponent = (options: RenderParams): Readable => {
302-
const { name: componentName, domNodeId, trace, props, railsContext, throwJsErrors } = options;
303-
304-
try {
305-
const componentObj = ComponentRegistry.get(componentName);
306-
validateComponent(componentObj, componentName);
307-
308-
const reactRenderingResult = createReactOutput({
309-
componentObj,
310-
domNodeId,
311-
trace,
312-
props,
313-
railsContext,
314-
});
315-
316-
if (isServerRenderHash(reactRenderingResult) || isPromise(reactRenderingResult)) {
317-
throw new Error('Server rendering of streams is not supported for server render hashes or promises.');
318-
}
319-
320-
return streamRenderReactComponent(reactRenderingResult, options);
321-
} catch (e) {
322-
if (throwJsErrors) {
323-
throw e;
324-
}
325-
326-
const error = convertToError(e);
327-
const htmlResult = handleError({ e: error, name: componentName, serverSide: true });
328-
const jsonResult = JSON.stringify(createResultObject(htmlResult, buildConsoleReplay(), { hasErrors: true, error, result: null }));
329-
return stringToStream(jsonResult);
330-
}
331-
};
332-
333168
export default serverRenderReactComponent;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
import type { RegisteredComponent, RenderResult, RenderState, StreamRenderState } from './types';
3+
4+
export function createResultObject(html: string | null, consoleReplayScript: string, renderState: RenderState | StreamRenderState): RenderResult {
5+
return {
6+
html,
7+
consoleReplayScript,
8+
hasErrors: renderState.hasErrors,
9+
renderingError: renderState.error && { message: renderState.error.message, stack: renderState.error.stack },
10+
isShellReady: 'isShellReady' in renderState ? renderState.isShellReady : undefined,
11+
};
12+
}
13+
14+
export function convertToError(e: unknown): Error {
15+
return e instanceof Error ? e : new Error(String(e));
16+
}
17+
18+
export function validateComponent(componentObj: RegisteredComponent, componentName: string) {
19+
if (componentObj.isRenderer) {
20+
throw new Error(`Detected a renderer while server rendering component '${componentName}'. See https://github.com/shakacode/react_on_rails#renderer-functions`);
21+
}
22+
}

0 commit comments

Comments
 (0)