gatsby
Version:
Blazing fast modern site generator for React
238 lines (203 loc) • 6.89 kB
JavaScript
const React = require(`react`)
const { grabMatchParams } = require(`../find-path`)
const { StaticQueryContext } = require(`gatsby`)
const {
headExportValidator,
filterHeadProps,
isElementType,
isValidNodeName,
warnForInvalidTag,
} = require(`./utils`)
const { ServerLocation, Router } = require(`@gatsbyjs/reach-router`)
const { renderToString } = require(`react-dom/server`)
const { parse } = require(`node-html-parser`)
const styleToOjbect = require(`style-to-object`)
import {
ITEM_PROP_WORKAROUND_KEY,
ITEM_PROP_WORKAROUND_VALUE,
HTML_BODY_ORIGINAL_TAG_ATTRIBUTE_KEY,
} from "./constants"
import { getValidHeadComponentReplacements } from "./components/head-components"
import { apiRunner } from "../api-runner-ssr"
export function applyHtmlAndBodyAttributesSSR(
htmlAndBodyAttributes,
{ setHtmlAttributes, setBodyAttributes }
) {
if (!htmlAndBodyAttributes) return
const { html, body } = htmlAndBodyAttributes
setHtmlAttributes(html)
setBodyAttributes(body)
}
export function getValidHeadNodesAndAttributesSSR(
rootNode,
htmlAndBodyAttributes = {
html: {},
body: {},
}
) {
const seenIds = new Map()
const validHeadNodes = []
// Filter out non-element nodes before looping since we don't care about them
for (const node of rootNode.childNodes) {
const { rawTagName } = node
const id = node.attributes?.id
if (!isElementType(node)) continue
const NodeName =
node.attributes?.[HTML_BODY_ORIGINAL_TAG_ATTRIBUTE_KEY] ?? rawTagName
if (isValidNodeName(NodeName)) {
if (NodeName === `html` || NodeName === `body`) {
const { style, ...nonStyleAttributes } = node.attributes
htmlAndBodyAttributes[NodeName] = {
...htmlAndBodyAttributes[NodeName],
...nonStyleAttributes,
}
// Unfortunately renderToString converts inline styles to a string, so we have to convert them back to an object
if (style) {
htmlAndBodyAttributes[NodeName].style = {
...htmlAndBodyAttributes[NodeName]?.style,
...styleToOjbect(style),
}
}
} else {
let element
const attributes = { ...node.attributes, "data-gatsby-head": true }
if (
attributes[ITEM_PROP_WORKAROUND_KEY] === ITEM_PROP_WORKAROUND_VALUE
) {
delete attributes[ITEM_PROP_WORKAROUND_KEY]
}
if (NodeName === `script` || NodeName === `style`) {
element = (
<NodeName
{...attributes}
dangerouslySetInnerHTML={{
__html: node.text,
}}
/>
)
} else {
element =
node.textContent.length > 0 ? (
<NodeName {...attributes}>{node.textContent}</NodeName>
) : (
<NodeName {...attributes} />
)
}
if (id) {
if (!seenIds.has(id)) {
validHeadNodes.push(element)
seenIds.set(id, validHeadNodes.length - 1)
} else {
const indexOfPreviouslyInsertedNode = seenIds.get(id)
validHeadNodes[indexOfPreviouslyInsertedNode] = element
}
} else {
validHeadNodes.push(element)
}
}
} else {
warnForInvalidTag(rawTagName)
}
if (node.childNodes.length) {
validHeadNodes.push(
...getValidHeadNodesAndAttributesSSR(node, htmlAndBodyAttributes)
.validHeadNodes
)
}
}
return { validHeadNodes, htmlAndBodyAttributes }
}
// see explanation in "head-export-handler-for-browser.js" module for reasons behind this patching
let applyCreateElementPatch = undefined
let revertCreateElementPatch = undefined
let needToRevertCreateElementPatch = false
const reactMajor = parseInt(React.version.split(`.`)[0], 10)
if (reactMajor !== 18) {
const originalCreateElement = React.createElement
const validHeadComponentReplacements = getValidHeadComponentReplacements(
originalCreateElement,
true
)
function patchedCreateElement(type, props, ...rest) {
const headReplacement = validHeadComponentReplacements.get(type)
if (headReplacement) {
type = headReplacement
}
return originalCreateElement.call(React, type, props, ...rest)
}
applyCreateElementPatch = () => {
needToRevertCreateElementPatch = true
React.createElement = patchedCreateElement
}
revertCreateElementPatch = () => {
if (needToRevertCreateElementPatch) {
React.createElement = originalCreateElement
needToRevertCreateElementPatch = false
}
}
}
// if this sync function is changed to async one, make sure to cover potential React.createElement patching as
// current handling relies on it being sync and we can just modify React.createElement temporarily without checking context of render
// (check how it's done in "head-export-handler-for-browser.js" module for potential solution)
export function headHandlerForSSR({
pageComponent,
setHeadComponents,
setHtmlAttributes,
setBodyAttributes,
staticQueryContext,
pageData,
pagePath,
}) {
if (pageComponent?.Head) {
try {
headExportValidator(pageComponent.Head)
function HeadRouteHandler(props) {
const _props = {
...props,
...pageData.result,
params: {
...grabMatchParams(props.location.pathname),
...(pageData.result?.pageContext?.__params || {}),
},
}
const HeadElement = <pageComponent.Head {...filterHeadProps(_props)} />
const headWithWrapRootElement = apiRunner(
`wrapRootElement`,
{ element: HeadElement },
HeadElement,
({ result }) => {
return { element: result }
}
).pop()
return headWithWrapRootElement
}
const routerElement = (
<StaticQueryContext.Provider value={staticQueryContext}>
<ServerLocation url={`${__BASE_PATH__}${pagePath}`}>
<Router
baseuri={__BASE_PATH__}
component={({ children }) => <>{children}</>}
>
<HeadRouteHandler path="/*" />
</Router>
</ServerLocation>
</StaticQueryContext.Provider>
)
applyCreateElementPatch?.()
const rawString = renderToString(routerElement)
revertCreateElementPatch?.()
const rootNode = parse(rawString)
const { validHeadNodes, htmlAndBodyAttributes } =
getValidHeadNodesAndAttributesSSR(rootNode)
applyHtmlAndBodyAttributesSSR(htmlAndBodyAttributes, {
setHtmlAttributes,
setBodyAttributes,
})
setHeadComponents(validHeadNodes)
} catch (e) {
// make sure that we don't leave this function without reverting the patch
revertCreateElementPatch?.()
throw e
}
}
}