@@ -6,154 +6,180 @@ import createReactOutput from './createReactOutput';
66import { isPromise , isServerRenderHash } from './isServerRenderResult' ;
77import buildConsoleReplay from './buildConsoleReplay' ;
88import handleError from './handleError' ;
9- import type { RenderParams , RenderResult , RenderingError , ServerRenderResult } from './types' ;
9+ import type { CreateReactOutputResult , RegisteredComponent , RenderParams , RenderResult , RenderingError , ServerRenderResult } from './types' ;
1010
11- /* eslint-disable @typescript-eslint/no-explicit-any */
11+ type RenderState = {
12+ result : null | string | Promise < string > ;
13+ hasErrors : boolean ;
14+ error ?: RenderingError ;
15+ } ;
1216
13- function serverRenderReactComponentInternal ( options : RenderParams ) : null | string | Promise < RenderResult > {
14- const { name, domNodeId, trace, props, railsContext, renderingReturnsPromises, throwJsErrors } = options ;
17+ type RenderOptions = {
18+ componentName : string ;
19+ domNodeId ?: string ;
20+ trace ?: boolean ;
21+ renderingReturnsPromises : boolean ;
22+ } ;
1523
16- let renderResult : null | string | Promise < string > = null ;
17- let hasErrors = false ;
18- let renderingError : null | RenderingError = null ;
24+ function validateComponent ( componentObj : RegisteredComponent , componentName : string ) {
25+ if ( componentObj . isRenderer ) {
26+ throw new Error ( `Detected a renderer while server rendering component '${ componentName } '. See https://github.com/shakacode/react_on_rails#renderer-functions` ) ;
27+ }
28+ }
1929
20- try {
21- const componentObj = ComponentRegistry . get ( name ) ;
22- if ( componentObj . isRenderer ) {
23- throw new Error ( `\
24- Detected a renderer while server rendering component '${ name } '. \
25- See https://github.com/shakacode/react_on_rails#renderer-functions` ) ;
30+ function processServerRenderHash ( result : ServerRenderResult , options : RenderOptions ) : RenderState {
31+ const { redirectLocation, routeError } = result ;
32+ const hasErrors = ! ! routeError ;
33+
34+ if ( hasErrors ) {
35+ console . error ( `React Router ERROR: ${ JSON . stringify ( routeError ) } ` ) ;
36+ }
37+
38+ let htmlResult : string ;
39+ if ( redirectLocation ) {
40+ if ( options . trace ) {
41+ const redirectPath = redirectLocation . pathname + redirectLocation . search ;
42+ console . log ( `ROUTER REDIRECT: ${ options . componentName } to dom node with id: ${ options . domNodeId } , redirect to ${ redirectPath } ` ) ;
2643 }
44+ // For redirects on server rendering, we can't stop Rails from returning the same result.
45+ // Possibly, someday, we could have the Rails server redirect.
46+ htmlResult = '' ;
47+ } else {
48+ htmlResult = result . renderedHtml as string ;
49+ }
2750
28- const reactRenderingResult = createReactOutput ( {
29- componentObj,
30- domNodeId,
31- trace,
32- props,
33- railsContext,
34- } ) ;
35-
36- const processServerRenderHash = ( ) => {
37- // We let the client side handle any redirect
38- // Set hasErrors in case we want to throw a Rails exception
39- const { redirectLocation, routeError } = reactRenderingResult as ServerRenderResult ;
40- hasErrors = ! ! routeError ;
41-
42- if ( hasErrors ) {
43- console . error (
44- `React Router ERROR: ${ JSON . stringify ( routeError ) } ` ,
45- ) ;
46- }
47-
48- if ( redirectLocation ) {
49- if ( trace ) {
50- const redirectPath = redirectLocation . pathname + redirectLocation . search ;
51- console . log ( `\
52- ROUTER REDIRECT: ${ name } to dom node with id: ${ domNodeId } , redirect to ${ redirectPath } ` ,
53- ) ;
54- }
55- // For redirects on server rendering, we can't stop Rails from returning the same result.
56- // Possibly, someday, we could have the rails server redirect.
57- return '' ;
58- }
59- return ( reactRenderingResult as ServerRenderResult ) . renderedHtml as string ;
60- } ;
61-
62- const processPromise = ( ) => {
63- if ( ! renderingReturnsPromises ) {
64- console . error ( 'Your render function returned a Promise, which is only supported by a node renderer, not ExecJS.' ) ;
65- }
66- return reactRenderingResult ;
67- } ;
68-
69- const processReactElement = ( ) => {
70- try {
71- return ReactDOMServer . renderToString ( reactRenderingResult as ReactElement ) ;
72- } catch ( error ) {
73- console . error ( `Invalid call to renderToString. Possibly you have a renderFunction, a function that already
51+ return { result : htmlResult , hasErrors } ;
52+ }
53+
54+ function processPromise ( result : Promise < string > , renderingReturnsPromises : boolean ) : Promise < string > | string {
55+ if ( ! renderingReturnsPromises ) {
56+ console . error ( 'Your render function returned a Promise, which is only supported by a node renderer, not ExecJS.' ) ;
57+ // If the app is using server rendering with ExecJS, then the promise will not be awaited.
58+ // And when a promise is passed to JSON.stringify, it will be converted to '{}'.
59+ return '{}' ;
60+ }
61+ return result ;
62+ }
63+
64+ function processReactElement ( result : ReactElement ) : string {
65+ try {
66+ return ReactDOMServer . renderToString ( result ) ;
67+ } catch ( error ) {
68+ console . error ( `Invalid call to renderToString. Possibly you have a renderFunction, a function that already
7469calls renderToString, that takes one parameter. You need to add an extra unused parameter to identify this function
7570as a renderFunction and not a simple React Function Component.` ) ;
76- throw error ;
77- }
78- } ;
79-
80- if ( isServerRenderHash ( reactRenderingResult ) ) {
81- renderResult = processServerRenderHash ( ) ;
82- } else if ( isPromise ( reactRenderingResult ) ) {
83- renderResult = processPromise ( ) as Promise < string > ;
84- } else {
85- renderResult = processReactElement ( ) ;
86- }
87- } catch ( e : any ) {
88- if ( throwJsErrors ) {
89- throw e ;
90- }
71+ throw error ;
72+ }
73+ }
74+
75+ function processRenderingResult ( result : CreateReactOutputResult , options : RenderOptions ) : RenderState {
76+ if ( isServerRenderHash ( result ) ) {
77+ return processServerRenderHash ( result , options ) ;
78+ }
79+ if ( isPromise ( result ) ) {
80+ return { result : processPromise ( result , options . renderingReturnsPromises ) , hasErrors : false } ;
81+ }
82+ return { result : processReactElement ( result ) , hasErrors : false } ;
83+ }
9184
92- hasErrors = true ;
93- renderResult = handleError ( {
94- e,
95- name,
96- serverSide : true ,
97- } ) ;
98- renderingError = e ;
85+ function handleRenderingError ( e : unknown , options : { componentName : string , throwJsErrors : boolean } ) {
86+ if ( options . throwJsErrors ) {
87+ throw e ;
9988 }
89+ const error = e instanceof Error ? e : new Error ( String ( e ) ) ;
90+ return {
91+ hasErrors : true ,
92+ result : handleError ( { e : error , name : options . componentName , serverSide : true } ) ,
93+ error,
94+ } ;
95+ }
10096
101- const consoleReplayScript = buildConsoleReplay ( ) ;
102- const addRenderingErrors = ( resultObject : RenderResult , renderError : RenderingError ) => {
103- // Do not use `resultObject.renderingError = renderError` because JSON.stringify will turn it into '{}'.
104- resultObject . renderingError = { // eslint-disable-line no-param-reassign
105- message : renderError . message ,
106- stack : renderError . stack ,
107- } ;
97+ function createResultObject ( html : string | null , consoleReplayScript : string , renderState : RenderState ) : RenderResult {
98+ return {
99+ html,
100+ consoleReplayScript,
101+ hasErrors : renderState . hasErrors ,
102+ renderingError : renderState . error && { message : renderState . error . message , stack : renderState . error . stack } ,
108103 } ;
104+ }
109105
110- if ( renderingReturnsPromises ) {
111- const resolveRenderResult = async ( ) => {
112- let promiseResult : RenderResult ;
113-
114- try {
115- promiseResult = {
116- html : await renderResult ,
117- consoleReplayScript,
118- hasErrors,
119- } ;
120- } catch ( e : any ) {
121- if ( throwJsErrors ) {
122- throw e ;
123- }
124- promiseResult = {
125- html : handleError ( {
126- e,
127- name,
128- serverSide : true ,
129- } ) ,
130- consoleReplayScript,
131- hasErrors : true ,
132- } ;
133- renderingError = e ;
134- }
135-
136- if ( renderingError !== null ) {
137- addRenderingErrors ( promiseResult , renderingError ) ;
138- }
139-
140- return promiseResult ;
141- } ;
142-
143- return resolveRenderResult ( ) ;
106+ async function createPromiseResult (
107+ renderState : RenderState & { result : Promise < string > } ,
108+ consoleReplayScript : string ,
109+ componentName : string ,
110+ throwJsErrors : boolean
111+ ) : Promise < RenderResult > {
112+ try {
113+ const html = await renderState . result ;
114+ return createResultObject ( html , consoleReplayScript , renderState ) ;
115+ } catch ( e : unknown ) {
116+ const errorRenderState = handleRenderingError ( e , { componentName, throwJsErrors } ) ;
117+ return createResultObject ( errorRenderState . result , consoleReplayScript , errorRenderState ) ;
144118 }
119+ }
145120
146- const result : RenderResult = {
147- html : renderResult as string ,
148- consoleReplayScript,
149- hasErrors,
121+ function createFinalResult (
122+ renderState : RenderState ,
123+ componentName : string ,
124+ throwJsErrors : boolean
125+ ) : null | string | Promise < RenderResult > {
126+ // Console history is stored globally in `console.history`.
127+ // If node renderer is handling a render request that returns a promise,
128+ // It can handle another request while awaiting the promise.
129+ // To prevent cross-request console logs leakage between these requests,
130+ // we build the consoleReplayScript before awaiting any promises.
131+ // The console history is reset after the synchronous part of each request.
132+ // This causes console logs happening during async operations to not be captured.
133+ const consoleReplayScript = buildConsoleReplay ( ) ;
134+
135+ const { result } = renderState ;
136+ if ( isPromise ( result ) ) {
137+ return createPromiseResult ( { ...renderState , result } , consoleReplayScript , componentName , throwJsErrors ) ;
138+ }
139+
140+ return JSON . stringify ( createResultObject ( result , consoleReplayScript , renderState ) ) ;
141+ }
142+
143+ function serverRenderReactComponentInternal ( options : RenderParams ) : null | string | Promise < RenderResult > {
144+ const { name : componentName , domNodeId, trace, props, railsContext, renderingReturnsPromises, throwJsErrors } = options ;
145+
146+ let renderState : RenderState = {
147+ result : null ,
148+ hasErrors : false ,
150149 } ;
151150
152- if ( renderingError ) {
153- addRenderingErrors ( result , renderingError ) ;
151+ try {
152+ const componentObj = ComponentRegistry . get ( componentName ) ;
153+ validateComponent ( componentObj , componentName ) ;
154+
155+ // Renders the component or executes the render function
156+ // - If the registered component is a React element or component, it renders it
157+ // - If it's a render function, it executes the function and processes the result:
158+ // - For React elements or components, it renders them
159+ // - For promises, it returns them without awaiting (for async rendering)
160+ // - For other values (e.g., strings), it returns them directly
161+ // Note: Only synchronous operations are performed at this stage
162+ const reactRenderingResult = createReactOutput ( { componentObj, domNodeId, trace, props, railsContext } ) ;
163+
164+ // Processes the result from createReactOutput:
165+ // 1. Converts React elements to HTML strings
166+ // 2. Returns rendered HTML from serverRenderHash
167+ // 3. Handles promises for async rendering
168+ renderState = processRenderingResult ( reactRenderingResult , { componentName, domNodeId, trace, renderingReturnsPromises } ) ;
169+ } catch ( e : unknown ) {
170+ renderState = handleRenderingError ( e , { componentName, throwJsErrors } ) ;
154171 }
155172
156- return JSON . stringify ( result ) ;
173+ // Finalize the rendering result and prepare it for server response
174+ // 1. Builds the consoleReplayScript for client-side console replay
175+ // 2. Extract the result from promise (if needed) by awaiting it
176+ // 3. Constructs a JSON object with the following properties:
177+ // - html: string | null (The rendered component HTML)
178+ // - consoleReplayScript: string (Script to replay console outputs on the client)
179+ // - hasErrors: boolean (Indicates if any errors occurred during rendering)
180+ // - renderingError: Error | null (The error object if an error occurred, null otherwise)
181+ // 4. For Promise results, it awaits resolution before creating the final JSON
182+ return createFinalResult ( renderState , componentName , throwJsErrors ) ;
157183}
158184
159185const serverRenderReactComponent : typeof serverRenderReactComponentInternal = ( options ) => {
@@ -165,4 +191,5 @@ const serverRenderReactComponent: typeof serverRenderReactComponentInternal = (o
165191 console . history = [ ] ;
166192 }
167193} ;
194+
168195export default serverRenderReactComponent ;
0 commit comments