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