UNPKG

gatsby

Version:
266 lines (233 loc) • 8.38 kB
import { VALID_NODE_NAMES } from "./constants" /** * Filter the props coming from a page down to just the ones that are relevant for head. * This e.g. filters out properties that are undefined during SSR. */ export function filterHeadProps(input) { return { location: { pathname: input.location.pathname, }, params: input.params, data: input.data || {}, serverData: input.serverData, pageContext: input.pageContext, } } /** * Throw error if Head export is not a valid function */ export function headExportValidator(head) { if (typeof head !== `function`) throw new Error( `Expected "Head" export to be a function got "${typeof head}".` ) } /** * Warn once for same messsage */ let warnOnce = _ => {} if (process.env.NODE_ENV !== `production`) { const warnings = new Set() warnOnce = msg => { if (!warnings.has(msg)) { console.warn(msg) } warnings.add(msg) } } /** * Warn for invalid tags in Head which may have been directly added or introduced by `wrapRootElement` * @param {string} tagName */ export function warnForInvalidTag(tagName) { if (process.env.NODE_ENV !== `production`) { const warning = createWarningForInvalidTag(tagName) warnOnce(warning) } } function createWarningForInvalidTag(tagName) { return `<${tagName}> is not a valid head element. Please use one of the following: ${VALID_NODE_NAMES.join( `, ` )}.\n\nAlso make sure that wrapRootElement in gatsby-ssr/gatsby-browser doesn't contain UI elements: https://gatsby.dev/invalid-head-elements` } /** * When a `nonce` is present on an element, browsers such as Chrome and Firefox strip it out of the * actual HTML attributes for security reasons *when the element is added to the document*. Thus, * given two equivalent elements that have nonces, `Element,isEqualNode()` will return false if one * of those elements gets added to the document. Although the `element.nonce` property will be the * same for both elements, the one that was added to the document will return an empty string for * its nonce HTML attribute value. * * This custom `isEqualNode()` function therefore removes the nonce value from the `newTag` before * comparing it to `oldTag`, restoring it afterwards. * * For more information, see: * https://bugs.chromium.org/p/chromium/issues/detail?id=1211471#c12 */ export function isEqualNode(oldTag, newTag) { if (oldTag instanceof HTMLElement && newTag instanceof HTMLElement) { const nonce = newTag.getAttribute(`nonce`) // Only strip the nonce if `oldTag` has had it stripped. An element's nonce attribute will not // be stripped if there is no content security policy response header that includes a nonce. if (nonce && !oldTag.getAttribute(`nonce`)) { const cloneTag = newTag.cloneNode(true) cloneTag.setAttribute(`nonce`, ``) cloneTag.nonce = nonce return nonce === oldTag.nonce && oldTag.isEqualNode(cloneTag) } } return oldTag.isEqualNode(newTag) } export function diffNodes({ oldNodes, newNodes, onStale, onNew }) { for (const existingHeadElement of oldNodes) { const indexInNewNodes = newNodes.findIndex(e => isEqualNode(e, existingHeadElement) ) if (indexInNewNodes === -1) { onStale(existingHeadElement) } else { // this node is re-created as-is, so we keep old node, and remove it from list of new nodes (as we handled it already here) newNodes.splice(indexInNewNodes, 1) } } // remaing new nodes didn't have matching old node, so need to be added for (const newNode of newNodes) { onNew(newNode) } } export function getValidHeadNodesAndAttributes( 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 nodeName = node.nodeName.toLowerCase() const id = node.attributes?.id?.value if (!isElementType(node)) continue if (isValidNodeName(nodeName)) { // <html> and <body> tags are treated differently, in that we don't render them, we only extract the attributes and apply them separetely if (nodeName === `html` || nodeName === `body`) { for (const attribute of node.attributes) { const isStyleAttribute = attribute.name === `style` // Merge attributes for same nodeName from previous loop iteration htmlAndBodyAttributes[nodeName] = { ...htmlAndBodyAttributes[nodeName], } if (!isStyleAttribute) { htmlAndBodyAttributes[nodeName][attribute.name] = attribute.value } // If there is already a style attribute, we need to merge them as otherwise the last one will "win" if (isStyleAttribute) { htmlAndBodyAttributes[nodeName].style = `${ htmlAndBodyAttributes[nodeName]?.style ? htmlAndBodyAttributes[nodeName].style : `` }${attribute.value} ` } } } else { let clonedNode = node.cloneNode(true) clonedNode.setAttribute(`data-gatsby-head`, true) // // This is hack to make script tags work if (clonedNode.nodeName.toLowerCase() === `script`) { clonedNode = massageScript(clonedNode) } // Duplicate ids are not allowed in the head, so we need to dedupe them if (id) { if (!seenIds.has(id)) { validHeadNodes.push(clonedNode) seenIds.set(id, validHeadNodes.length - 1) } else { const indexOfPreviouslyInsertedNode = seenIds.get(id) validHeadNodes[ indexOfPreviouslyInsertedNode ].parentNode?.removeChild( validHeadNodes[indexOfPreviouslyInsertedNode] ) validHeadNodes[indexOfPreviouslyInsertedNode] = clonedNode } } else { validHeadNodes.push(clonedNode) } } } else { warnForInvalidTag(nodeName) } if (node.childNodes.length) { validHeadNodes.push( ...getValidHeadNodesAndAttributes(node, htmlAndBodyAttributes) .validHeadNodes ) } } return { validHeadNodes, htmlAndBodyAttributes } } function massageScript(node) { const script = document.createElement(`script`) for (const attr of node.attributes) { script.setAttribute(attr.name, attr.value) } script.innerHTML = node.innerHTML return script } export function isValidNodeName(nodeName) { return VALID_NODE_NAMES.includes(nodeName) } /* * For Head, we only care about element nodes(type = 1), so this util is used to skip over non-element nodes * For Node type, see https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType */ export function isElementType(node) { return node.nodeType === 1 } /** * Removes all the head elements that were added by `Head` */ export function removePrevHeadElements() { const prevHeadNodes = document.querySelectorAll(`[data-gatsby-head]`) for (const node of prevHeadNodes) { node.parentNode.removeChild(node) } } export function applyHtmlAndBodyAttributes(htmlAndBodyAttributes) { if (!htmlAndBodyAttributes) return const { html, body } = htmlAndBodyAttributes const htmlElement = document.querySelector(`html`) if (htmlElement) { Object.entries(html).forEach(([attributeName, attributeValue]) => { htmlElement.setAttribute(attributeName, attributeValue) }) } const bodyElement = document.querySelector(`body`) if (bodyElement) { Object.entries(body).forEach(([attributeName, attributeValue]) => { bodyElement.setAttribute(attributeName, attributeValue) }) } } export function removeHtmlAndBodyAttributes(htmlAndBodyattributeList) { if (!htmlAndBodyattributeList) return const { html, body } = htmlAndBodyattributeList if (html) { const htmlElement = document.querySelector(`html`) html.forEach(attributeName => { if (htmlElement) { htmlElement.removeAttribute(attributeName) } }) } if (body) { const bodyElement = document.querySelector(`body`) body.forEach(attributeName => { if (bodyElement) { bodyElement.removeAttribute(attributeName) } }) } }