const React = require(`react`) const path = require(`path`) const { renderToString, renderToStaticMarkup, pipeToNodeWritable, } = require(`react-dom/server`) const { ServerLocation, Router, isRedirect } = require(`@gatsbyjs/reach-router`) const merge = require(`deepmerge`) const { StaticQueryContext } = require(`gatsby`) const fs = require(`fs`) const { WritableAsPromise } = require(`./server-utils/writable-as-promise`) const { RouteAnnouncerProps } = require(`./route-announcer-props`) const { apiRunner, apiRunnerAsync } = require(`./api-runner-ssr`) const syncRequires = require(`$virtual/sync-requires`) const { version: gatsbyVersion } = require(`gatsby/package.json`) const { grabMatchParams } = require(`./find-path`) const chunkMapping = require(`../public/chunk-map.json`) // we want to force posix-style joins, so Windows doesn't produce backslashes for urls const { join } = path.posix // 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 getStaticQueryPath = hash => join(`page-data`, `sq`, `d`, `${hash}.json`) const getStaticQueryUrl = hash => `${__PATH_PREFIX__}/${getStaticQueryPath(hash)}` const getAppDataUrl = () => `${__PATH_PREFIX__}/${join(`page-data`, `app-data.json`)}` const createElement = React.createElement export const sanitizeComponents = components => { const componentsArray = [].concat(components).flat(Infinity).filter(Boolean) 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: component.props.href.replace(__ASSET_PREFIX__, ``), }) } return component }) } function deepMerge(a, b) { const combineMerge = (target, source, options) => { const destination = target.slice() source.forEach((item, index) => { if (typeof destination[index] === `undefined`) { destination[index] = options.cloneUnlessOtherwiseSpecified( item, options ) } else if (options.isMergeableObject(item)) { destination[index] = merge(target[index], item, options) } else if (target.indexOf(item) === -1) { destination.push(item) } }) return destination } return merge(a, b, { arrayMerge: combineMerge }) } export default async function staticPage({ pagePath, pageData, staticQueryContext, styles, scripts, reversedStyles, reversedScripts, inlinePageData = false, }) { // for this to work we need this function to be sync or at least ensure there is single execution of it at a time global.unsafeBuiltinUsage = [] try { let bodyHtml = `` let headComponents = [ , ] let htmlAttributes = {} let bodyAttributes = {} let preBodyComponents = [] let postBodyComponents = [] let bodyProps = {} function loadPageDataSync(_pagePath) { if (_pagePath === pagePath) { // no need to use fs if we are asking for pageData of current page return pageData } const pageDataPath = getPageDataPath(_pagePath) const pageDataFile = join(process.cwd(), `public`, pageDataPath) try { // deprecation notice const myErrorHolder = { name: `Usage of loadPageDataSync for page other than currently generated page disables incremental html generation in future builds`, } Error.captureStackTrace(myErrorHolder, loadPageDataSync) global.unsafeBuiltinUsage.push(myErrorHolder.stack) 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 replaceBodyHTMLString = body => { bodyHtml = body } const setHeadComponents = components => { headComponents = headComponents.concat(sanitizeComponents(components)) } const setHtmlAttributes = attributes => { // TODO - we should remove deep merges htmlAttributes = deepMerge(htmlAttributes, attributes) } const setBodyAttributes = attributes => { // TODO - we should remove deep merges bodyAttributes = deepMerge(bodyAttributes, attributes) } const setPreBodyComponents = components => { preBodyComponents = preBodyComponents.concat( sanitizeComponents(components) ) } const setPostBodyComponents = components => { postBodyComponents = postBodyComponents.concat( sanitizeComponents(components) ) } const setBodyProps = props => { // TODO - we should remove deep merges bodyProps = deepMerge({}, 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 pageDataUrl = getPageDataUrl(pagePath) const { componentChunkName, staticQueryHashes = [] } = pageData const staticQueryUrls = staticQueryHashes.map(getStaticQueryUrl) class RouteHandler extends React.Component { render() { const props = { ...this.props, ...pageData.result, params: { ...grabMatchParams(this.props.location.pathname), ...(pageData.result?.pageContext?.__params || {}), }, } 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 = (
) 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. await apiRunnerAsync(`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 { // react 18 enabled if (pipeToNodeWritable) { const writableStream = new WritableAsPromise() const { startWriting } = pipeToNodeWritable( bodyComponent, writableStream, { onCompleteAll() { startWriting() }, onError() {}, } ) bodyHtml = await writableStream } else { bodyHtml = renderToString(bodyComponent) } } catch (e) { // ignore @reach/router redirect errors if (!isRedirect(e)) throw e } } apiRunner(`onRenderBody`, { setHeadComponents, setHtmlAttributes, setBodyAttributes, setPreBodyComponents, setPostBodyComponents, setBodyProps, pathname: pagePath, loadPageDataSync, bodyHtml, scripts, styles, pathPrefix: __PATH_PREFIX__, }) reversedScripts.forEach(script => { // Add preload/prefetch s for scripts. headComponents.push( ) }) if (pageData && !inlinePageData) { headComponents.push( ) } staticQueryUrls.forEach(staticQueryUrl => headComponents.push( ) ) const appDataUrl = getAppDataUrl() if (appDataUrl) { headComponents.push( ) } reversedStyles.forEach(style => { // Add s for styles that should be prefetched // otherwise, inline as a