const React = require(`react`) const fs = require(`fs`) const { join } = require(`path`) const { renderToString, renderToStaticMarkup } = require(`react-dom/server`) const { ServerLocation, Router, isRedirect } = require(`@reach/router`) const { get, merge, isObject, flatten, uniqBy, flattenDeep, replace, concat, memoize, } = require(`lodash`) const apiRunner = require(`./api-runner-ssr`) const syncRequires = require(`./sync-requires`) const { version: gatsbyVersion } = require(`gatsby/package.json`) const stats = JSON.parse( fs.readFileSync(`${process.cwd()}/public/webpack.stats.json`, `utf-8`) ) const chunkMapping = JSON.parse( fs.readFileSync(`${process.cwd()}/public/chunk-map.json`, `utf-8`) ) // const testRequireError = require("./test-require-error") // For some extremely mysterious reason, webpack adds the above module *after* // this module so that when this code runs, testRequireError is undefined. // So in the meantime, we'll just inline it. const testRequireError = (moduleName, err) => { const regex = new RegExp(`Error: Cannot find module\\s.${moduleName}`) const firstLine = err.toString().split(`\n`)[0] return regex.test(firstLine) } let Html try { Html = require(`../src/html`) } catch (err) { if (testRequireError(`../src/html`, err)) { Html = require(`./default-html`) } else { throw err } } Html = Html && Html.__esModule ? Html.default : Html const getPageDataPath = path => { const fixedPagePath = path === `/` ? `index` : path return join(`page-data`, fixedPagePath, `page-data.json`) } const getPageDataUrl = pagePath => { const pageDataPath = getPageDataPath(pagePath) return `${__PATH_PREFIX__}/${pageDataPath}` } const getPageData = pagePath => { const pageDataPath = getPageDataPath(pagePath) const absolutePageDataPath = join(process.cwd(), `public`, pageDataPath) const pageDataRaw = fs.readFileSync(absolutePageDataPath) try { return JSON.parse(pageDataRaw.toString()) } catch (err) { return null } } const appDataPath = join(`page-data`, `app-data.json`) const getAppDataUrl = memoize(() => { let appData try { const absoluteAppDataPath = join(process.cwd(), `public`, appDataPath) const appDataRaw = fs.readFileSync(absoluteAppDataPath) appData = JSON.parse(appDataRaw.toString()) if (!appData) { return null } } catch (err) { return null } return `${__PATH_PREFIX__}/${appDataPath}` }) const loadPageDataSync = pagePath => { const pageDataPath = getPageDataPath(pagePath) const pageDataFile = join(process.cwd(), `public`, pageDataPath) try { const pageDataJson = fs.readFileSync(pageDataFile) return JSON.parse(pageDataJson) } catch (error) { // not an error if file is not found. There's just no page data return null } } const createElement = React.createElement export const sanitizeComponents = components => { const componentsArray = ensureArray(components) return componentsArray.map(component => { // Ensure manifest is always loaded from content server // And not asset server when an assetPrefix is used if (__ASSET_PREFIX__ && component.props.rel === `manifest`) { return React.cloneElement(component, { href: replace(component.props.href, __ASSET_PREFIX__, ``), }) } return component }) } const ensureArray = components => { if (Array.isArray(components)) { // remove falsy items and flatten return flattenDeep( components.filter(val => (Array.isArray(val) ? val.length > 0 : val)) ) } else { // we also accept single components, so we need to handle this case as well return components ? [components] : [] } } export default (pagePath, callback) => { let bodyHtml = `` let headComponents = [ , ] let htmlAttributes = {} let bodyAttributes = {} let preBodyComponents = [] let postBodyComponents = [] let bodyProps = {} const replaceBodyHTMLString = body => { bodyHtml = body } const setHeadComponents = components => { headComponents = headComponents.concat(sanitizeComponents(components)) } const setHtmlAttributes = attributes => { htmlAttributes = merge(htmlAttributes, attributes) } const setBodyAttributes = attributes => { bodyAttributes = merge(bodyAttributes, attributes) } const setPreBodyComponents = components => { preBodyComponents = preBodyComponents.concat(sanitizeComponents(components)) } const setPostBodyComponents = components => { postBodyComponents = postBodyComponents.concat( sanitizeComponents(components) ) } const setBodyProps = props => { bodyProps = merge({}, bodyProps, props) } const getHeadComponents = () => headComponents const replaceHeadComponents = components => { headComponents = sanitizeComponents(components) } const getPreBodyComponents = () => preBodyComponents const replacePreBodyComponents = components => { preBodyComponents = sanitizeComponents(components) } const getPostBodyComponents = () => postBodyComponents const replacePostBodyComponents = components => { postBodyComponents = sanitizeComponents(components) } const pageData = getPageData(pagePath) const pageDataUrl = getPageDataUrl(pagePath) const appDataUrl = getAppDataUrl() const { componentChunkName } = pageData class RouteHandler extends React.Component { render() { const props = { ...this.props, ...pageData.result, // pathContext was deprecated in v2. Renamed to pageContext pathContext: pageData.result ? pageData.result.pageContext : undefined, } const pageElement = createElement( syncRequires.components[componentChunkName], props ) const wrappedPage = apiRunner( `wrapPageElement`, { element: pageElement, props }, pageElement, ({ result }) => { return { element: result, props } } ).pop() return wrappedPage } } const routerElement = createElement( ServerLocation, { url: `${__BASE_PATH__}${pagePath}` }, createElement( Router, { id: `gatsby-focus-wrapper`, baseuri: `${__BASE_PATH__}`, }, createElement(RouteHandler, { path: `/*` }) ) ) const bodyComponent = apiRunner( `wrapRootElement`, { element: routerElement, pathname: pagePath }, routerElement, ({ result }) => { return { element: result, pathname: pagePath } } ).pop() // Let the site or plugin render the page component. apiRunner(`replaceRenderer`, { bodyComponent, replaceBodyHTMLString, setHeadComponents, setHtmlAttributes, setBodyAttributes, setPreBodyComponents, setPostBodyComponents, setBodyProps, pathname: pagePath, pathPrefix: __PATH_PREFIX__, }) // If no one stepped up, we'll handle it. if (!bodyHtml) { try { bodyHtml = renderToString(bodyComponent) } catch (e) { // ignore @reach/router redirect errors if (!isRedirect(e)) throw e } } // Create paths to scripts let scriptsAndStyles = flatten( [`app`, componentChunkName].map(s => { const fetchKey = `assetsByChunkName[${s}]` let chunks = get(stats, fetchKey) const namedChunkGroups = get(stats, `namedChunkGroups`) if (!chunks) { return null } chunks = chunks.map(chunk => { if (chunk === `/`) { return null } return { rel: `preload`, name: chunk } }) namedChunkGroups[s].assets.forEach(asset => chunks.push({ rel: `preload`, name: asset }) ) const childAssets = namedChunkGroups[s].childAssets for (const rel in childAssets) { chunks = concat( chunks, childAssets[rel].map(chunk => { return { rel, name: chunk } }) ) } return chunks }) ) .filter(s => isObject(s)) .sort((s1, s2) => (s1.rel == `preload` ? -1 : 1)) // given priority to preload scriptsAndStyles = uniqBy(scriptsAndStyles, item => item.name) const scripts = scriptsAndStyles.filter( script => script.name && script.name.endsWith(`.js`) ) const styles = scriptsAndStyles.filter( style => style.name && style.name.endsWith(`.css`) ) apiRunner(`onRenderBody`, { setHeadComponents, setHtmlAttributes, setBodyAttributes, setPreBodyComponents, setPostBodyComponents, setBodyProps, pathname: pagePath, loadPageDataSync, bodyHtml, scripts, styles, pathPrefix: __PATH_PREFIX__, }) scripts .slice(0) .reverse() .forEach(script => { // Add preload/prefetch s for scripts. headComponents.push( ) }) if (pageData) { headComponents.push( ) } if (appDataUrl) { headComponents.push( ) } styles .slice(0) .reverse() .forEach(style => { // Add s for styles that should be prefetched // otherwise, inline as a