cl-react-graph
Version:
396 lines (339 loc) • 9.85 kB
JavaScript
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 } = require(`lodash`)
const apiRunner = require(`./api-runner-ssr`)
const syncRequires = require(`./sync-requires`)
const { dataPaths, pages } = require(`./data.json`)
const { version: gatsbyVersion } = require(`gatsby/package.json`)
// Speed up looking up pages.
const pagesObjectMap = new Map()
pages.forEach(p => pagesObjectMap.set(p.path, p))
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 getPage = path => pagesObjectMap.get(path)
const createElement = React.createElement
export default (pagePath, callback) => {
let bodyHtml = ``
let headComponents = [
<meta
name="generator"
content={`Gatsby ${gatsbyVersion}`}
key={`generator-${gatsbyVersion}`}
/>,
]
let htmlAttributes = {}
let bodyAttributes = {}
let preBodyComponents = []
let postBodyComponents = []
let bodyProps = {}
const replaceBodyHTMLString = body => {
bodyHtml = body
}
const setHeadComponents = components => {
headComponents = headComponents.concat(components)
}
const setHtmlAttributes = attributes => {
htmlAttributes = merge(htmlAttributes, attributes)
}
const setBodyAttributes = attributes => {
bodyAttributes = merge(bodyAttributes, attributes)
}
const setPreBodyComponents = components => {
preBodyComponents = preBodyComponents.concat(components)
}
const setPostBodyComponents = components => {
postBodyComponents = postBodyComponents.concat(components)
}
const setBodyProps = props => {
bodyProps = merge({}, bodyProps, props)
}
const getHeadComponents = () => headComponents
const replaceHeadComponents = components => {
headComponents = components
}
const getPreBodyComponents = () => preBodyComponents
const replacePreBodyComponents = components => {
preBodyComponents = components
}
const getPostBodyComponents = () => postBodyComponents
const replacePostBodyComponents = components => {
postBodyComponents = components
}
const page = getPage(pagePath)
let dataAndContext = {}
if (page.jsonName in dataPaths) {
const pathToJsonData = `../public/` + dataPaths[page.jsonName]
try {
dataAndContext = JSON.parse(
fs.readFileSync(
`${process.cwd()}/public/static/d/${dataPaths[page.jsonName]}.json`
)
)
} catch (e) {
console.log(`error`, pathToJsonData, e)
process.exit()
}
}
class RouteHandler extends React.Component {
render() {
const props = {
...this.props,
...dataAndContext,
pathContext: dataAndContext.pageContext,
}
const pageElement = createElement(
syncRequires.components[page.componentChunkName],
props
)
const wrappedPage = apiRunner(
`wrapPageElement`,
{ element: pageElement, props },
pageElement,
({ result }) => {
return { element: result, props }
}
).pop()
return wrappedPage
}
}
const routerElement = createElement(
ServerLocation,
{ url: `${__PATH_PREFIX__}${pagePath}` },
createElement(
Router,
{
baseuri: `${__PATH_PREFIX__}`,
},
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`, page.componentChunkName].map(s => {
const fetchKey = `assetsByChunkName[${s}]`
let chunks = get(stats, fetchKey)
let 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 = merge(
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,
bodyHtml,
scripts,
styles,
pathPrefix: __PATH_PREFIX__,
})
scripts
.slice(0)
.reverse()
.forEach(script => {
// Add preload/prefetch <link>s for scripts.
headComponents.push(
<link
as="script"
rel={script.rel}
key={script.name}
href={`${__PATH_PREFIX__}/${script.name}`}
/>
)
})
if (page.jsonName in dataPaths) {
const dataPath = `${__PATH_PREFIX__}/static/d/${
dataPaths[page.jsonName]
}.json`
headComponents.push(
<link
as="fetch"
rel="preload"
key={dataPath}
href={dataPath}
crossOrigin="use-credentials"
/>
)
}
styles
.slice(0)
.reverse()
.forEach(style => {
// Add <link>s for styles that should be prefetched
// otherwise, inline as a <style> tag
if (style.rel === `prefetch`) {
headComponents.push(
<link
as="style"
rel={style.rel}
key={style.name}
href={`${__PATH_PREFIX__}/${style.name}`}
/>
)
} else {
headComponents.unshift(
<style
data-href={`${__PATH_PREFIX__}/${style.name}`}
dangerouslySetInnerHTML={{
__html: fs.readFileSync(
join(process.cwd(), `public`, style.name),
`utf-8`
),
}}
/>
)
}
})
// Add page metadata for the current page
const windowData = `/*<![CDATA[*/window.page=${JSON.stringify(page)};${
page.jsonName in dataPaths
? `window.dataPath="${dataPaths[page.jsonName]}";`
: ``
}/*]]>*/`
postBodyComponents.push(
<script
key={`script-loader`}
id={`gatsby-script-loader`}
dangerouslySetInnerHTML={{
__html: windowData,
}}
/>
)
// Add chunk mapping metadata
const scriptChunkMapping = `/*<![CDATA[*/window.___chunkMapping=${JSON.stringify(
chunkMapping
)};/*]]>*/`
postBodyComponents.push(
<script
key={`chunk-mapping`}
id={`gatsby-chunk-mapping`}
dangerouslySetInnerHTML={{
__html: scriptChunkMapping,
}}
/>
)
// Filter out prefetched bundles as adding them as a script tag
// would force high priority fetching.
const bodyScripts = scripts
.filter(s => s.rel !== `prefetch`)
.map(s => {
const scriptPath = `${__PATH_PREFIX__}/${JSON.stringify(s.name).slice(
1,
-1
)}`
return <script key={scriptPath} src={scriptPath} async />
})
postBodyComponents.push(...bodyScripts)
apiRunner(`onPreRenderHTML`, {
getHeadComponents,
replaceHeadComponents,
getPreBodyComponents,
replacePreBodyComponents,
getPostBodyComponents,
replacePostBodyComponents,
pathname: pagePath,
pathPrefix: __PATH_PREFIX__,
})
const html = `<!DOCTYPE html>${renderToStaticMarkup(
<Html
{...bodyProps}
headComponents={headComponents}
htmlAttributes={htmlAttributes}
bodyAttributes={bodyAttributes}
preBodyComponents={preBodyComponents}
postBodyComponents={postBodyComponents}
body={bodyHtml}
path={pagePath}
/>
)}`
callback(null, html)
}