gatsby
Version:
Blazing fast modern site generator for React
595 lines (516 loc) • 17 kB
JavaScript
const React = require(`react`)
const path = require(`path`)
const {
renderToStaticMarkup,
renderToPipeableStream,
} = 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 asyncRequires = require(`$virtual/async-requires`)
const { version: gatsbyVersion } = require(`gatsby/package.json`)
const { grabMatchParams } = require(`./find-path`)
const { headHandlerForSSR } = require(`./head/head-export-handler-for-ssr`)
const {
SlicesResultsContext,
SlicesContext,
SlicesMapContext,
SlicesPropsContext,
} = require(`./slice/context`)
const { ServerSliceRenderer } = require(`./slice/server-slice-renderer`)
// we want to force posix-style joins, so Windows doesn't produce backslashes for urls
const { join } = path.posix
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 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 })
}
/**
Reorder headComponents so meta tags are always at the top and aren't missed by crawlers by being pushed down by large inline styles, etc.
@see https://github.com/gatsbyjs/gatsby/issues/22206
*/
export const reorderHeadComponents = headComponents => {
const sorted = headComponents.sort((a, b) => {
if (a.type && a.type === `meta` && !(b.type && b.type === `meta`)) {
return -1
}
return 0
})
return sorted
}
const DEFAULT_CONTEXT = {
// whether or not we're building the site now
// usage in determining original build or engines
isDuringBuild: false,
}
export default async function staticPage({
pagePath,
pageData,
staticQueryContext,
styles,
scripts,
reversedStyles,
reversedScripts,
inlinePageData = false,
context = {},
webpackCompilationHash,
sliceData,
}) {
const renderContext = Object.assign(DEFAULT_CONTEXT, context)
// 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 = [
<meta
name="generator"
content={`Gatsby ${gatsbyVersion}`}
key={`generator-${gatsbyVersion}`}
/>,
]
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 { componentChunkName, slicesMap } = pageData
const pageComponent = await asyncRequires.components[componentChunkName]()
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(pageComponent.default, props)
const wrappedPage = apiRunner(
`wrapPageElement`,
{ element: pageElement, props },
pageElement,
({ result }) => {
return { element: result, props }
}
).pop()
return wrappedPage
}
}
const routerElement = (
<ServerLocation url={`${__BASE_PATH__}${pagePath}`}>
<Router id="gatsby-focus-wrapper" baseuri={__BASE_PATH__}>
<RouteHandler path="/*" />
</Router>
<div {...RouteAnnouncerProps} />
</ServerLocation>
)
const sliceProps = {}
let body = apiRunner(
`wrapRootElement`,
{ element: routerElement, pathname: pagePath },
routerElement,
({ result }) => {
return { element: result, pathname: pagePath }
}
).pop()
const slicesContext = {
// if we're in build now, we know we're on the server
// otherwise we're in an engine
renderEnvironment: renderContext.isDuringBuild ? `server` : `engines`,
}
if (process.env.GATSBY_SLICES) {
// if we're running in an engine, we need to manually wrap body with
// the results context to pass the map of slice name to component/data/context
if (slicesContext.renderEnvironment === `engines`) {
// this is the same name used in the browser
// since this immitates behavior
const slicesDb = new Map()
for (const sliceName of Object.values(slicesMap)) {
const slice = sliceData[sliceName]
const { default: SliceComponent } = await getPageChunk(slice)
const sliceObject = {
component: SliceComponent,
sliceContext: slice.result.sliceContext,
data: slice.result.data,
}
slicesDb.set(sliceName, sliceObject)
}
body = (
<SlicesResultsContext.Provider value={slicesDb}>
{body}
</SlicesResultsContext.Provider>
)
}
body = (
<SlicesContext.Provider value={slicesContext}>
<SlicesPropsContext.Provider value={sliceProps}>
<SlicesMapContext.Provider value={slicesMap}>
{body}
</SlicesMapContext.Provider>
</SlicesPropsContext.Provider>
</SlicesContext.Provider>
)
}
const bodyComponent = (
<StaticQueryContext.Provider value={staticQueryContext}>
{body}
</StaticQueryContext.Provider>
)
// 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 {
const writableStream = new WritableAsPromise()
const { pipe } = renderToPipeableStream(bodyComponent, {
onAllReady() {
pipe(writableStream)
},
onError(error) {
writableStream.destroy(error)
},
})
bodyHtml = await writableStream
} 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__,
})
// we want to run Head after onRenderBody, so Html and Body attributes
// from Head wins over global ones from onRenderBody
headHandlerForSSR({
pageComponent,
setHeadComponents,
setHtmlAttributes,
setBodyAttributes,
staticQueryContext,
pageData,
pagePath,
})
reversedScripts.forEach(script => {
// Add preload/prefetch <link>s magic comments
if (script.shouldGenerateLink) {
headComponents.push(
<link
as="script"
rel={script.rel}
key={script.name}
href={`${__PATH_PREFIX__}/${script.name}`}
/>
)
}
})
reversedStyles.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}`}
data-identity={`gatsby-global-css`}
dangerouslySetInnerHTML={{
__html: style.content,
}}
/>
)
}
})
// Add page metadata for the current page
const windowPageData = `/*<![CDATA[*/window.pagePath=${JSON.stringify(
pagePath
)};${
process.env.GATSBY_SLICES
? ``
: `window.___webpackCompilationHash="${webpackCompilationHash}";`
}${
inlinePageData ? `window.pageData=${JSON.stringify(pageData)};` : ``
}/*]]>*/`
postBodyComponents.push(
<script
key={`script-loader`}
id={`gatsby-script-loader`}
dangerouslySetInnerHTML={{
__html: windowPageData,
}}
/>
)
if (process.env.GATSBY_SLICES) {
postBodyComponents.push(
createElement(ServerSliceRenderer, {
sliceId: `_gatsby-scripts`,
})
)
} else {
const chunkMapping = require(`../public/chunk-map.json`)
// restore the old behavior
// 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,
}}
/>
)
let bodyScripts = []
if (chunkMapping[`polyfill`]) {
chunkMapping[`polyfill`].forEach(script => {
const scriptPath = `${__PATH_PREFIX__}${script}`
bodyScripts.push(
<script key={scriptPath} src={scriptPath} noModule={true} />
)
})
}
// Filter out prefetched bundles as adding them as a script tag
// would force high priority fetching.
bodyScripts = bodyScripts.concat(
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)
}
headComponents = reorderHeadComponents(headComponents)
apiRunner(`onPreRenderHTML`, {
getHeadComponents,
replaceHeadComponents,
getPreBodyComponents,
replacePreBodyComponents,
getPostBodyComponents,
replacePostBodyComponents,
pathname: pagePath,
pathPrefix: __PATH_PREFIX__,
})
let htmlElement = (
<Html
{...bodyProps}
headComponents={headComponents}
htmlAttributes={htmlAttributes}
bodyAttributes={bodyAttributes}
preBodyComponents={preBodyComponents}
postBodyComponents={postBodyComponents}
body={bodyHtml}
path={pagePath}
/>
)
if (process.env.GATSBY_SLICES) {
htmlElement = (
<SlicesContext.Provider value={slicesContext}>
{htmlElement}
</SlicesContext.Provider>
)
}
const html = `<!DOCTYPE html>${renderToStaticMarkup(htmlElement)}`
return {
html,
unsafeBuiltinsUsage: global.unsafeBuiltinUsage,
sliceData: sliceProps,
}
} catch (e) {
e.unsafeBuiltinsUsage = global.unsafeBuiltinUsage
throw e
}
}
export function getPageChunk({ componentChunkName }) {
return asyncRequires.components[componentChunkName]()
}
export { renderToPipeableStream } from "react-server-dom-webpack/writer.node.server"
export { StaticQueryContext, React }
export async function renderSlice({ slice, staticQueryContext, props = {} }) {
const { default: SliceComponent } = await getPageChunk(slice)
const slicesContext = {
// we are not yet supporting using <Slice /> placeholders within slice components
// setting this renderEnvironemnt to throw meaningful error on `<Slice />` usage
// `slices` renderEnvironment should be removed once we support nested `<Slice />` placeholders
renderEnvironment: `slices`,
sliceRoot: slice,
}
const sliceElement = (
<SliceComponent sliceContext={slice.context} {...props} />
)
const sliceWrappedWithWrapRootElement = apiRunner(
`wrapRootElement`,
{ element: sliceElement },
sliceElement,
({ result }) => {
return { element: result }
}
).pop()
const sliceWrappedWithWrapRootElementAndContexts = (
<SlicesContext.Provider value={slicesContext}>
<StaticQueryContext.Provider value={staticQueryContext}>
{sliceWrappedWithWrapRootElement}
</StaticQueryContext.Provider>
</SlicesContext.Provider>
)
const writableStream = new WritableAsPromise()
const { pipe } = renderToPipeableStream(
sliceWrappedWithWrapRootElementAndContexts,
{
onAllReady() {
pipe(writableStream)
},
onError(error) {
writableStream.destroy(error)
},
}
)
return await writableStream
}