@shopify/react-server
Version:
Utilities for React server-side rendering
146 lines (142 loc) • 5.54 kB
JavaScript
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 };