import path from 'path' ; import { LOADABLE_STATS_FILE } from '@modern-js/utils/constants' ; import React from 'react' ; import { RuntimeContext } from '@modern-js/runtime-core' ; import ReactDomServer from 'react-dom/server' ; import serialize from 'serialize-javascript' ; import ReactHelmet , { HelmetData } from 'react-helmet' ; import { Fragment , toFragments } from './template' ; import { ModernSSRReactComponent , RenderLevel , SSRServerContext , RenderResult , } from './type' ; import helmetReplace from './helmet' ; import { reduce } from './reduce' ; import * as loadableRenderer from './loadable' ; import * as styledComponentRenderer from './styledComponent' ; type EntryOptions = { name : string ; template : string ; App : ModernSSRReactComponent ; } ; const buildTemplateData = ( context , data , renderLevel ) => { const { request } = context ; return { data , context : { request : { params : request . params , query : request . query , pathname : request . pathname , headers : request . headers , } , } , renderLevel , } ; } ; export default class Entry { public entryName : string ; public result : RenderResult ; private readonly App : ModernSSRReactComponent ; private readonly fragments : Fragment [ ] ; constructor ( options ) { this . fragments = toFragments ( options . template ) ; this . entryName = options . name ; this . App = options . App ; this . result = { renderLevel : RenderLevel . CLIENT_RENDER , html : '' , chunksMap : { js : '' , css : '' , } , } ; } public async renderToHtml ( context ) { const { ssrContext } = context ; if ( ssrContext . redirection . url ) { return '' ; } const prefetchData = await this . prefetch ( context ) ; if ( ssrContext . redirection . url ) { return '' ; } if ( this . result . renderLevel >= RenderLevel . SERVER_PREFETCH ) { this . result . html = this . renderToString ( context ) ; } if ( ssrContext . redirection . url ) { return '' ; } let html = '' ; const templateData = buildTemplateData ( ssrContext , prefetchData , this . result . renderLevel ) ; const SSRData = this . getSSRDataScript ( templateData ) ; for ( const fragment of this . fragments ) { if ( fragment . isVariable && fragment . content === 'SSRDataScript' ) { html += fragment . getValue ( SSRData ) ; } else { html += fragment . getValue ( this . result ) ; } } const helmetData = ReactHelmet . renderStatic ( ) ; return helmetData ? helmetReplace ( html , helmetData ) : html ; } private async prefetch ( context ) { const { App : { prefetch } , } = this ; let prefetchData ; try { prefetchData = prefetch ? await prefetch ( context ) : null ; this . result . renderLevel = RenderLevel . SERVER_PREFETCH ; } catch ( e ) { this . result . renderLevel = RenderLevel . CLIENT_RENDER ; console . error ( 'SSR Error - App Prefetch error = %s' , e ) ; } return prefetchData || { } ; } private renderToString ( context ) { let html = '' ; try { const App = React . createElement ( this . App , { context : Object . assign ( context , { ssr : true } ) , } ) ; const renderContext = { loadableManifest : path . resolve ( context . ssrContext . distDir , LOADABLE_STATS_FILE ) , result : this . result , entryName : this . entryName , } ; html = reduce ( App , renderContext , [ loadableRenderer . toHtml , styledComponentRenderer . toHtml , ( jsx ) => ReactDomServer . renderToString ( jsx ) , ] ) ; this . result . renderLevel = RenderLevel . SERVER_RENDER ; } catch ( e ) { console . error ( 'SSR Error - App Render To HTML error = %s' , e ) ; } return html ; } private getSSRDataScript ( templateData ) { return { SSRDataScript : `
        <script>window._SSR_DATA = ${ serialize ( templateData , { isJSON : true , } ) }</script>
      ` , } ; } }