2022-07-05 16:30:52 -06:00
'use strict' ;
// We use a child_process because we want to be able to exit the process after
// we receive the SSR results. We don't want Hydrogen to keep running after we
// get our initial rendered HTML.
const fork = require ( 'child_process' ) . fork ;
2022-07-06 18:24:29 -06:00
const assert = require ( 'assert' ) ;
2022-07-05 16:30:52 -06:00
const RethrownError = require ( '../lib/rethrown-error' ) ;
2022-08-29 13:13:13 -06:00
const { traceFunction } = require ( '../tracing/trace-utilities' ) ;
2022-07-05 16:30:52 -06:00
2022-08-29 18:13:56 -06:00
const config = require ( '../lib/config' ) ;
const logOutputFromChildProcesses = config . get ( 'logOutputFromChildProcesses' ) ;
2022-07-05 16:30:52 -06:00
// The render should be fast. If it's taking more than 5 seconds, something has
// gone really wrong.
const RENDER _TIMEOUT = 5000 ;
2022-08-29 18:13:56 -06:00
if ( ! logOutputFromChildProcesses ) {
console . warn (
` Silencing logs from child processes (config.logOutputFromChildProcesses = ${ logOutputFromChildProcesses } ) `
) ;
}
2022-09-02 17:49:45 -06:00
function assembleErrorAfterChildExitsWithErrors ( exitCode , childErrors ) {
assert ( childErrors ) ;
let extraErrorsMessage = '' ;
if ( childErrors . length > 1 ) {
extraErrorsMessage = ` (somehow we saw ${
childErrors . length
} errors but we really always expect 1 error ) \ n$ { childErrors
. map ( ( childError , index ) => ` ${ index } . ${ childError . stack } ` )
. join ( '\n' ) } ` ;
}
let childErrorToDisplay ;
if ( childErrors . length === 0 ) {
childErrorToDisplay = new Error ( 'No child errors' ) ;
// Clear the stack trace part of the stack string out because this is just a
// note about the lack of errors, not an actual error and is just noisy with
// that extra fluff.
childErrorToDisplay . stack = childErrorToDisplay . message ;
} else if ( childErrors . length === 1 ) {
childErrorToDisplay = childErrors [ 0 ] ;
} else {
childErrorToDisplay = new Error ( 'Multiple child errors listed above ^' ) ;
// Clear the stack trace part of the stack string out because this is just a
// note about the other errors, not an actual error and is just noisy with
// that extra fluff.
childErrorToDisplay . stack = childErrorToDisplay . message ;
}
const childErrorSummary = new RethrownError (
` Child process exited with code ${ exitCode } ${ extraErrorsMessage } ` ,
childErrorToDisplay
) ;
return childErrorSummary ;
}
2022-08-29 18:13:56 -06:00
async function renderHydrogenToString ( renderOptions ) {
2022-09-02 17:49:45 -06:00
let abortTimeoutId ;
2022-07-05 16:30:52 -06:00
try {
let childErrors = [ ] ;
2022-09-02 17:49:45 -06:00
let childExitCode = '(not set yet)' ;
2022-07-05 16:30:52 -06:00
const controller = new AbortController ( ) ;
const { signal } = controller ;
// We use a child_process because we want to be able to exit the process after
// we receive the SSR results.
2022-07-05 17:00:29 -06:00
const child = fork ( require . resolve ( './2-render-hydrogen-to-string-fork-script' ) , [ ] , {
signal ,
2022-08-29 18:13:56 -06:00
// Default to silencing logs from the child process. We already have
// proper instrumentation of any errors that might occur.
//
// This also makes `child.stderr` and `child.stdout` available
silent : true ,
2022-07-05 17:00:29 -06:00
//cwd: process.cwd(),
} ) ;
2022-08-29 18:13:56 -06:00
// Since we have to use the `silent` option for the `stderr` stuff below, we
// should also print out the `stdout` to our main console.
if ( logOutputFromChildProcesses ) {
child . stdout . on ( 'data' , function ( data ) {
console . log ( 'Child printed something to stdout:' , String ( data ) ) ;
} ) ;
child . stderr . on ( 'data' , function ( data ) {
console . log ( 'Child printed something to stderr:' , String ( data ) ) ;
} ) ;
}
// Pass the renderOptions to the child by sending instead of via argv because we
2022-07-05 17:00:29 -06:00
// will run into `Error: spawn E2BIG` and `Error: spawn ENAMETOOLONG` with
// argv.
2022-08-29 18:13:56 -06:00
child . send ( renderOptions ) ;
2022-07-05 16:30:52 -06:00
// Stops the child process if it takes too long
2022-09-02 17:49:45 -06:00
abortTimeoutId = setTimeout ( ( ) => {
2022-07-05 16:30:52 -06:00
controller . abort ( ) ;
} , RENDER _TIMEOUT ) ;
2022-09-02 17:49:45 -06:00
const returnedData = await new Promise ( ( resolve , reject ) => {
let data = '' ;
2022-08-29 18:13:56 -06:00
// Collect the data passed back by the child
child . on ( 'message' , function ( result ) {
if ( result . error ) {
// De-serialize the error
const childError = new Error ( ) ;
childError . name = result . name ;
childError . message = result . message ;
childError . stack = result . stack ;
// When an error happens while rendering Hydrogen, we only expect one
// error to come through here from the main line to render Hydrogen.
// But it's possible to get multiple errors from async out of context
// places since we also listen to `uncaughtException` and
// `unhandledRejection`.
childErrors . push ( childError ) ;
} else {
data += result . data ;
}
} ) ;
2022-07-05 16:30:52 -06:00
child . on ( 'close' , ( exitCode ) => {
2022-09-02 17:49:45 -06:00
childExitCode = exitCode ;
2022-07-05 16:30:52 -06:00
// Exited successfully
if ( exitCode === 0 ) {
resolve ( data ) ;
} else {
2022-09-02 17:49:45 -06:00
const childErrorSummary = assembleErrorAfterChildExitsWithErrors (
childExitCode ,
childErrors
2022-07-05 16:30:52 -06:00
) ;
2022-09-02 17:49:45 -06:00
reject ( childErrorSummary ) ;
2022-07-05 16:30:52 -06:00
}
} ) ;
// When a problem occurs when spawning the process or gets aborted
child . on ( 'error' , ( err ) => {
if ( err . name === 'AbortError' ) {
2022-09-02 17:49:45 -06:00
const childErrorSummary = assembleErrorAfterChildExitsWithErrors (
childExitCode ,
childErrors
) ;
reject (
new RethrownError (
` Timed out while rendering Hydrogen to string so we aborted the child process after ${ RENDER _TIMEOUT } ms. Any child errors? ( ${ childErrors . length } ) ` ,
childErrorSummary
)
2022-07-05 16:30:52 -06:00
) ;
2022-09-02 17:49:45 -06:00
} else {
reject ( err ) ;
2022-07-05 16:30:52 -06:00
}
} ) ;
} ) ;
2022-09-02 17:49:45 -06:00
if ( ! returnedData ) {
const childErrorSummary = assembleErrorAfterChildExitsWithErrors ( childExitCode , childErrors ) ;
throw new RethrownError (
` No HTML sent from child process to render Hydrogen. Any child errors? ( ${ childErrors . length } ) ` ,
childErrorSummary
) ;
}
2022-07-06 18:24:29 -06:00
2022-09-02 17:49:45 -06:00
return returnedData ;
2022-07-05 16:30:52 -06:00
} catch ( err ) {
throw new RethrownError (
` Failed to render Hydrogen to string. In order to reproduce, feed in these arguments into \` renderHydrogenToString(...) \` : \n renderToString arguments: ${ JSON . stringify (
2022-08-29 18:13:56 -06:00
renderOptions
2022-07-05 16:30:52 -06:00
) } ` ,
err
) ;
2022-09-02 17:49:45 -06:00
} finally {
// We don't have to add a undefined/null check here because `clearTimeout`
// works with any value you give it and doesn't throw an error.
clearTimeout ( abortTimeoutId ) ;
2022-07-05 16:30:52 -06:00
}
}
2022-08-29 13:13:13 -06:00
module . exports = traceFunction ( renderHydrogenToString ) ;