UNPKG

@shopify/react-server

Version:
146 lines (142 loc) 5.54 kB
import { join } from 'path'; import { existsSync } from 'fs'; import React from 'react'; import compose from 'koa-compose'; import { HtmlManager, HtmlContext, stream, Html, render } from '@shopify/react-html/server'; import { useSerialized } from '@shopify/react-html'; import { NetworkManager, applyToContext, NetworkContext } from '@shopify/react-network/server'; import { extract } from '@shopify/react-effect/server'; import { HydrationManager, HydrationContext } from '@shopify/react-hydrate'; import { AsyncAssetManager, AssetTiming, AsyncAssetContext } from '@shopify/react-async'; import { Header, StatusCode } from '@shopify/react-network'; import { middleware, getAssets } from '@shopify/sewing-kit-koa'; import { quiltDataMiddleware } from '../quilt-data/middleware.mjs'; import { getLogger } from '../logger/logger.mjs'; import { fallbackErrorMarkup } from './error/fallback-error-markup.mjs'; /** * Creates a Koa middleware for rendering an `@shopify/react-html` based React application defined by `render`. * @param render * @param options */ function createRender(render$1, options = {}) { const manifestPath = getManifestPath(process.cwd()); const { assetPrefix, assetName: assetNameInput = 'main', renderError, renderRawErrorMessage, htmlProps: htmlPropsInput = {} } = options; async function renderFunction(ctx) { const assetName = typeof assetNameInput === 'function' ? assetNameInput(ctx) : assetNameInput; const { scripts: additionalScripts = [], styles: additionalStyles = [], ...additionalHtmlProps } = typeof htmlPropsInput === 'function' ? htmlPropsInput(ctx) : htmlPropsInput; const logger = getLogger(ctx) || console; const assets = getAssets(ctx); const networkManager = new NetworkManager({ headers: ctx.headers, cookies: ctx.request.headers.cookie || '' }); const htmlManager = new HtmlManager(); const asyncAssetManager = new AsyncAssetManager(); const hydrationManager = new HydrationManager(); function Providers({ children }) { const [, Serialize] = useSerialized('quilt-data'); return /*#__PURE__*/React.createElement(AsyncAssetContext.Provider, { value: asyncAssetManager }, /*#__PURE__*/React.createElement(HydrationContext.Provider, { value: hydrationManager }, /*#__PURE__*/React.createElement(NetworkContext.Provider, { value: networkManager }, children, /*#__PURE__*/React.createElement(Serialize, { data: () => ctx.state.quiltData })))); } try { const app = render$1(ctx); await extract(app, { decorate(element) { return /*#__PURE__*/React.createElement(HtmlContext.Provider, { value: htmlManager }, /*#__PURE__*/React.createElement(Providers, null, element)); }, afterEachPass({ renderDuration, resolveDuration, index, finished }) { const pass = `Pass number ${index} ${finished ? ' (this was the final pass)' : ''}`; const rendering = `Rendering took ${renderDuration}ms`; const resolving = `Resolving promises took ${resolveDuration}ms`; logger.log(pass); logger.log(rendering); logger.log(resolving); }, ...options }); applyToContext(ctx, networkManager); const immediateAsyncAssets = asyncAssetManager.used(AssetTiming.Immediate); const [styles, scripts] = await Promise.all([assets.styles({ name: assetName, asyncAssets: immediateAsyncAssets }), assets.scripts({ name: assetName, asyncAssets: immediateAsyncAssets })]); styles.push(...additionalStyles); scripts.push(...additionalScripts); const response = stream( /*#__PURE__*/React.createElement(Html, Object.assign({}, additionalHtmlProps, { manager: htmlManager, styles: styles, scripts: scripts }), /*#__PURE__*/React.createElement(Providers, null, app))); ctx.set(Header.ContentType, 'text/html'); ctx.body = response; } catch (error) { const errorMessage = `React server-side rendering error:\n${error.stack || error.message}`; logger.log(errorMessage); ctx.status = StatusCode.InternalServerError; ctx.state.quiltError = error; if (renderRawErrorMessage) { ctx.body = errorMessage; } else { if (renderError) { const [styles, scripts] = await Promise.all([assets.styles({ name: 'error' }), assets.scripts({ name: 'error' })]); const response = render( /*#__PURE__*/React.createElement(Html, { manager: htmlManager, styles: styles, scripts: scripts }, renderError(ctx))); ctx.body = response; } else { ctx.body = fallbackErrorMarkup; ctx.set(Header.ContentType, 'text/html'); } ctx.throw(StatusCode.InternalServerError, error); } } } return compose([quiltDataMiddleware, middleware({ assetPrefix, manifestPath }), renderFunction]); } function getManifestPath(root) { const gemFileExists = existsSync(join(root, 'Gemfile')); if (!gemFileExists) { return; } // eslint-disable-next-line no-process-env return process.env.NODE_ENV === 'development' ? `tmp/sewing-kit/sewing-kit-manifest.json` : `public/bundles/sewing-kit-manifest.json`; } export { createRender };