@@ -6,154 +6,168 @@ 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 : null | 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+ name : string ;
19+ domNodeId ?: string ;
20+ trace ?: boolean ;
21+ renderingReturnsPromises : boolean ;
22+ } ;
23+
24+ function validateComponent ( componentObj : RegisteredComponent , name : string ) {
25+ if ( componentObj . isRenderer ) {
26+ throw new Error ( `Detected a renderer while server rendering component '${ name } '. See https://github.com/shakacode/react_on_rails#renderer-functions` ) ;
27+ }
28+ }
1529
16- let renderResult : null | string | Promise < string > = null ;
17- let hasErrors = false ;
18- let renderingError : null | RenderingError = null ;
30+ function processServerRenderHash ( result : ServerRenderResult , options : RenderOptions ) : string {
31+ const { redirectLocation , routeError } = result ;
32+ const hasErrors = ! ! routeError ;
1933
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` ) ;
34+ if ( hasErrors ) {
35+ console . error ( `React Router ERROR: ${ JSON . stringify ( routeError ) } ` ) ;
36+ }
37+
38+ if ( redirectLocation ) {
39+ if ( options . trace ) {
40+ const redirectPath = redirectLocation . pathname + redirectLocation . search ;
41+ console . log ( `ROUTER REDIRECT: ${ options . name } to dom node with id: ${ options . domNodeId } , redirect to ${ redirectPath } ` ) ;
2642 }
43+ return '' ;
44+ }
2745
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- } ;
46+ return result . renderedHtml as string ;
47+ }
6148
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- } ;
49+ function processPromise ( result : Promise < unknown > , renderingReturnsPromises : boolean ) : Promise < string > | string {
50+ if ( ! renderingReturnsPromises ) {
51+ console . error ( 'Your render function returned a Promise, which is only supported by a node renderer, not ExecJS.' ) ;
52+ // If the app is using server rendering with ExecJS, then the promise will not be awaited.
53+ // And when a promise is passed to JSON.stringify, it will be converted to '{}'.
54+ return '{}' ;
55+ }
56+ return result as Promise < string > ;
57+ }
6858
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
59+ function processReactElement ( result : ReactElement ) : string {
60+ try {
61+ return ReactDOMServer . renderToString ( result ) ;
62+ } catch ( error ) {
63+ console . error ( `Invalid call to renderToString. Possibly you have a renderFunction, a function that already
7464calls renderToString, that takes one parameter. You need to add an extra unused parameter to identify this function
7565as 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- }
66+ throw error ;
67+ }
68+ }
9169
92- hasErrors = true ;
93- renderResult = handleError ( {
94- e,
95- name,
96- serverSide : true ,
97- } ) ;
98- renderingError = e ;
70+ function processRenderingResult ( result : CreateReactOutputResult , options : RenderOptions ) : string | Promise < string > {
71+ if ( isServerRenderHash ( result ) ) {
72+ return processServerRenderHash ( result , options ) ;
9973 }
74+ if ( isPromise ( result ) ) {
75+ return processPromise ( result , options . renderingReturnsPromises ) ;
76+ }
77+ return processReactElement ( result ) ;
78+ }
10079
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- } ;
80+ function handleRenderingError ( e : Error , renderState : RenderState , options : { name : string , throwJsErrors : boolean } ) {
81+ if ( options . throwJsErrors ) {
82+ throw e ;
83+ }
84+ return {
85+ ...renderState ,
86+ hasErrors : true ,
87+ result : handleError ( { e, name : options . name , serverSide : true } ) ,
88+ error : e ,
10889 } ;
90+ }
10991
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 ;
92+ function createResultObject ( html : string | null , consoleReplayScript : string , hasErrors : boolean , error : RenderingError | null ) : RenderResult {
93+ const result : RenderResult = { html, consoleReplayScript, hasErrors } ;
94+ if ( error ) {
95+ result . renderingError = {
96+ message : error . message ,
97+ stack : error . stack ,
14198 } ;
99+ }
100+ return result ;
101+ }
102+
103+ function createSyncResult ( renderState : RenderState & { result : string | null } , consoleReplayScript : string ) : RenderResult {
104+ return createResultObject ( renderState . result , consoleReplayScript , renderState . hasErrors , renderState . error ) ;
105+ }
142106
143- return resolveRenderResult ( ) ;
107+ function createPromiseResult ( renderState : RenderState & { result : Promise < string > } , consoleReplayScript : string ) : Promise < RenderResult > {
108+ return ( async ( ) => {
109+ try {
110+ const html = await renderState . result ;
111+ return createResultObject ( html , consoleReplayScript , renderState . hasErrors , renderState . error ) ;
112+ } catch ( e : unknown ) {
113+ const error = e instanceof Error ? e : new Error ( String ( e ) ) ;
114+ const html = handleError ( { e : error , name : 'Unknown' , serverSide : true } ) ;
115+ return createResultObject ( html , consoleReplayScript , true , error ) ;
116+ }
117+ } ) ( ) ;
118+ }
119+
120+ function createFinalResult ( renderState : RenderState ) : null | string | Promise < RenderResult > {
121+ const consoleReplayScript = buildConsoleReplay ( ) ;
122+
123+ const { result } = renderState ;
124+ if ( isPromise ( result ) ) {
125+ return createPromiseResult ( { ...renderState , result } , consoleReplayScript ) ;
144126 }
145127
146- const result : RenderResult = {
147- html : renderResult as string ,
148- consoleReplayScript,
149- hasErrors,
128+ return JSON . stringify ( createSyncResult ( { ...renderState , result } , consoleReplayScript ) ) ;
129+ }
130+
131+ function serverRenderReactComponentInternal ( options : RenderParams ) : null | string | Promise < RenderResult > {
132+ const { name, domNodeId, trace, props, railsContext, renderingReturnsPromises, throwJsErrors } = options ;
133+
134+ let renderState : RenderState = {
135+ result : null ,
136+ hasErrors : false ,
137+ error : null ,
150138 } ;
151139
152- if ( renderingError ) {
153- addRenderingErrors ( result , renderingError ) ;
140+ try {
141+ const componentObj = ComponentRegistry . get ( name ) ;
142+ validateComponent ( componentObj , name ) ;
143+
144+ // Renders the component or executes the render function
145+ // - If the registered component is a React element or component, it renders it
146+ // - If it's a render function, it executes the function and processes the result:
147+ // - For React elements or components, it renders them
148+ // - For promises, it returns them without awaiting (for async rendering)
149+ // - For other values (e.g., strings), it returns them directly
150+ // Note: Only synchronous operations are performed at this stage
151+ const reactRenderingResult = createReactOutput ( { componentObj, domNodeId, trace, props, railsContext } ) ;
152+ // Processes the result from createReactOutput:
153+ // 1. Converts React elements to HTML strings
154+ // 2. Returns rendered HTML from serverRenderHash
155+ // 3. Handles promises for async rendering
156+ renderState . result = processRenderingResult ( reactRenderingResult , { name, domNodeId, trace, renderingReturnsPromises } ) ;
157+ } catch ( e ) {
158+ renderState = handleRenderingError ( e as Error , renderState , { name, throwJsErrors } ) ;
154159 }
155160
156- return JSON . stringify ( result ) ;
161+ // Finalize the rendering result and prepare it for server response
162+ // 1. Builds the consoleReplayScript for client-side console replay
163+ // 2. Handles both synchronous and asynchronous (Promise) results
164+ // 3. Constructs a JSON object with the following properties:
165+ // - html: string | null (The rendered component HTML)
166+ // - consoleReplayScript: string (Script to replay console outputs on the client)
167+ // - hasErrors: boolean (Indicates if any errors occurred during rendering)
168+ // - renderingError: Error | null (The error object if an error occurred, null otherwise)
169+ // 4. For Promise results, it awaits resolution before creating the final JSON
170+ return createFinalResult ( renderState ) ;
157171}
158172
159173const serverRenderReactComponent : typeof serverRenderReactComponentInternal = ( options ) => {
@@ -165,4 +179,5 @@ const serverRenderReactComponent: typeof serverRenderReactComponentInternal = (o
165179 console . history = [ ] ;
166180 }
167181} ;
182+
168183export default serverRenderReactComponent ;
0 commit comments