diff --git a/gatsby-ssr.tsx b/gatsby-ssr.tsx index 8da52b1e28..c34c350f36 100644 --- a/gatsby-ssr.tsx +++ b/gatsby-ssr.tsx @@ -1,7 +1,8 @@ import React from 'react'; +import type { GatsbySSR } from 'gatsby'; import { getSandpackCssText } from '@codesandbox/sandpack-react'; -const onRenderBody = ({ setHeadComponents }: { setHeadComponents: (components: React.ReactNode[]) => void }) => { +const onRenderBody: GatsbySSR['onRenderBody'] = ({ setHeadComponents }) => { const inlineScripts: React.ReactNode[] = []; // OneTrust consent management, inspiration taken from gatsby-google-tagmanager implementation @@ -45,10 +46,80 @@ const onRenderBody = ({ setHeadComponents }: { setHeadComponents: (components: R setHeadComponents(inlineScripts); }; +type StyleComponent = React.ReactElement< + { + 'data-href'?: string; + href?: string; + }, + 'style' +>; + +const isStyleComponent = (node: React.ReactNode): node is StyleComponent => + React.isValidElement(node) && node.type === 'style'; + +const getStyleHref = (node: StyleComponent): string | undefined => node.props?.['data-href'] ?? node.props?.href; + +// Only Gatsby-emitted stylesheet chunks have a data-href/href; inline styles +// like Sandpack's do not, and must not be reordered or replaced. +const isExtractableStyleNode = (node: React.ReactNode): node is StyleComponent => + isStyleComponent(node) && getStyleHref(node) !== undefined; + +const isGlobalStyleNode = (node: React.ReactNode): boolean => { + if (!isExtractableStyleNode(node)) { + return false; + } + // Heroku review apps set assetPrefix, which causes Gatsby to emit absolute + // URLs. Normalize to a pathname so the regex matches both forms. + try { + const stylePathname = new URL(getStyleHref(node) ?? '', 'http://localhost').pathname; + return /^\/styles\.[a-zA-Z0-9]+\.css$/.test(stylePathname); + } catch { + return false; + } +}; + +/** + * Gatsby inlines all styles from the app inside a `