Skip to content

Commit

Permalink
feat(gatsby): enable replaceRenderer to be async (#32182)
Browse files Browse the repository at this point in the history
  • Loading branch information
wardpeet authored Jul 2, 2021
1 parent d56c1f1 commit d68148d
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 68 deletions.
14 changes: 0 additions & 14 deletions integration-tests/ssr/__tests__/__snapshots__/ssr.js.snap
Original file line number Diff line number Diff line change
@@ -1,17 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SSR is run for a page when it is requested 1`] = `"<!DOCTYPE html><html><head><meta charSet=\\"utf-8\\"/><meta http-equiv=\\"x-ua-compatible\\" content=\\"ie=edge\\"/><meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1, shrink-to-fit=no\\"/><link data-identity=\\"gatsby-dev-css\\" rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"/commons.css\\"/><meta name=\\"note\\" content=\\"environment=development\\"/><script src=\\"/socket.io/socket.io.js\\"></script></head><body><div id=\\"___gatsby\\"><div style=\\"outline:none\\" tabindex=\\"-1\\" id=\\"gatsby-focus-wrapper\\"><div><h1 class=\\"hi\\">Hello world</h1></div></div><div id=\\"gatsby-announcer\\" style=\\"position:absolute;top:0;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0\\" aria-live=\\"assertive\\" aria-atomic=\\"true\\"></div></div><script src=\\"/polyfill.js\\" nomodule=\\"\\"></script><script src=\\"/framework.js\\"></script><script src=\\"/commons.js\\"></script></body></html>"`;

exports[`SSR it generates an error page correctly 1`] = `
"<!DOCTYPE html><html><head><meta charSet=\\"utf-8\\"/><meta http-equiv=\\"x-ua-compatible\\" content=\\"ie=edge\\"/><meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1, shrink-to-fit=no\\"/><link data-identity=\\"gatsby-dev-css\\" rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"/commons.css\\"/><meta name=\\"note\\" content=\\"environment=development\\"/><script src=\\"/socket.io/socket.io.js\\"></script></head><body><div id=\\"___gatsby\\"><div style=\\"outline:none\\" tabindex=\\"-1\\" id=\\"gatsby-focus-wrapper\\"></div><div id=\\"gatsby-announcer\\" style=\\"position:absolute;top:0;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0\\" aria-live=\\"assertive\\" aria-atomic=\\"true\\"></div></div><script>window._gatsbyEvents = window._gatsbyEvents || []; window._gatsbyEvents.push([\\"FAST_REFRESH\\", { action: \\"SHOW_DEV_SSR_ERROR\\", payload: {\\"codeFrame\\":\\" 2 |/n 3 | const Component = () => {/n> 4 | const a = window.width/n | ^/n 5 |/n 6 | return <div>hi</div>/n 7 | }\\",\\"source\\":\\"src/pages/bad-page.js\\",\\"line\\":4,\\"column\\":13,\\"sourceMessage\\":\\"window is not defined\\",\\"stack\\":\\"ReferenceError: window is not defined/n at Component (<PROJECT_ROOT>/public/webpack:/ssr/src/pages/bad-page.js:4:13)/n at d (<PROJECT_ROOT>/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:36:498)/n at $a (<PROJECT_ROOT>/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:39:16)/n at a.b.render (<PROJECT_ROOT>/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:44:476)/n at a.b.read (<PROJECT_ROOT>/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:44:18)/n at renderToString (<PROJECT_ROOT>/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:54:364)/n at generateBodyHTML (<PROJECT_ROOT>/public/webpack:/ssr/.cache/ssr-develop-static-entry.js:294:34)/n at Object.__WEBPACK_DEFAULT_EXPORT__ [as default] (<PROJECT_ROOT>/public/webpack:/ssr/.cache/ssr-develop-static-entry.js:324:19)/n at <PROJECT_ROOT>/node_modules/gatsby/src/utils/dev-ssr/render-dev-html-child.js:95:9/n at new Promise (<anonymous>)\\"} }])</script><noscript><h1>Failed to Server Render (SSR)</h1><h2>Error message:</h2><p>window is not defined</p><h2>File:</h2><p>src/pages/bad-page.js:4:13</p><h2>Stack:</h2><pre><code>ReferenceError: window is not defined
at Component (<PROJECT_ROOT>/public/webpack:/ssr/src/pages/bad-page.js:4:13)
at d (<PROJECT_ROOT>/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:36:498)
at $a (<PROJECT_ROOT>/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:39:16)
at a.b.render (<PROJECT_ROOT>/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:44:476)
at a.b.read (<PROJECT_ROOT>/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:44:18)
at renderToString (<PROJECT_ROOT>/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:54:364)
at generateBodyHTML (<PROJECT_ROOT>/public/webpack:/ssr/.cache/ssr-develop-static-entry.js:294:34)
at Object.__WEBPACK_DEFAULT_EXPORT__ [as default] (<PROJECT_ROOT>/public/webpack:/ssr/.cache/ssr-develop-static-entry.js:324:19)
at <PROJECT_ROOT>/node_modules/gatsby/src/utils/dev-ssr/render-dev-html-child.js:95:9
at new Promise (&lt;anonymous&gt;)</code></pre></noscript><script src=\\"/polyfill.js\\" nomodule=\\"\\"></script><script src=\\"/framework.js\\"></script><script src=\\"/commons.js\\"></script></body></html>"
`;
7 changes: 6 additions & 1 deletion integration-tests/ssr/__tests__/ssr.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ describe(`SSR`, () => {
const rawDevHtml = await fetchUntil(pageUrl, res => {
return res
}).then(res => res.text())
expect(rawDevHtml).toMatchSnapshot()

expect(rawDevHtml).toMatch("<h1>Failed to Server Render (SSR)</h1>")
expect(rawDevHtml).toMatch("<h2>Error message:</h2>")
expect(rawDevHtml).toMatch("<p>window is not defined</p>")
// html should contain stacktrace to bad-page
expect(rawDevHtml).toMatch(/at Component \(.+?(?=bad-page.js)[^)]+\)/)
await fs.remove(dest)

// After the page is gone, it'll 404.
Expand Down
93 changes: 85 additions & 8 deletions packages/gatsby/cache-dir/__tests__/static-entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const { join } = require(`path`)

import developStaticEntry from "../develop-static-entry"

// TODO Move to @testing-library/dom

jest.mock(`fs`, () => {
const fs = jest.requireActual(`fs`)
return {
Expand Down Expand Up @@ -246,6 +248,48 @@ describe(`develop-static-entry`, () => {
})
})

test(`SSR: replaceRenderer can be sync`, done => {
global.plugins = [
{
plugin: {
replaceRenderer: ({ replaceBodyHTMLString }) =>
replaceBodyHTMLString(`i'm sync`),
},
},
]

ssrDevelopStaticEntry(`/about/`, false, publicDir, undefined, (_, html) => {
expect(html).toContain(`i'm sync`)

done()
})
})

test(`SSR: replaceRenderer can be async`, done => {
jest.useFakeTimers()
global.plugins = [
{
plugin: {
replaceRenderer: ({ replaceBodyHTMLString }) =>
new Promise(resolve => {
setTimeout(() => {
replaceBodyHTMLString(`i'm async`)
resolve()
}, 1000)
}),
},
},
]

ssrDevelopStaticEntry(`/about/`, false, publicDir, undefined, (_, html) => {
expect(html).toContain(`i'm async`)

done()
})
jest.runAllTimers()
jest.useRealTimers()
})

test(`onPreRenderHTML can be used to replace headComponents`, () => {
global.plugins = [fakeStylesPlugin, reverseHeadersPlugin]

Expand Down Expand Up @@ -374,39 +418,72 @@ describe(`static-entry`, () => {
fs.readFileSync.mockImplementation(file => MOCK_FILE_INFO[file])
})

test(`onPreRenderHTML can be used to replace headComponents`, () => {
test(`onPreRenderHTML can be used to replace headComponents`, async () => {
global.plugins = [fakeStylesPlugin, reverseHeadersPlugin]

const html = staticEntry(staticEntryFnArgs)
const html = await staticEntry(staticEntryFnArgs)
expect(html).toMatchSnapshot()
})

test(`onPreRenderHTML can be used to replace postBodyComponents`, () => {
test(`onPreRenderHTML can be used to replace postBodyComponents`, async () => {
global.plugins = [
fakeComponentsPluginFactory(`Post`),
reverseBodyComponentsPluginFactory(`Post`),
]

const html = staticEntry(staticEntryFnArgs)
const html = await staticEntry(staticEntryFnArgs)
expect(html).toMatchSnapshot()
})

test(`onPreRenderHTML can be used to replace preBodyComponents`, () => {
test(`onPreRenderHTML can be used to replace preBodyComponents`, async () => {
global.plugins = [
fakeComponentsPluginFactory(`Pre`),
reverseBodyComponentsPluginFactory(`Pre`),
]

const html = staticEntry(staticEntryFnArgs)
const html = await staticEntry(staticEntryFnArgs)
expect(html).toMatchSnapshot()
})

test(`onPreRenderHTML does not add metatag note for development environment`, () => {
const html = staticEntry(staticEntryFnArgs)
test(`onPreRenderHTML does not add metatag note for development environment`, async () => {
const { html } = await staticEntry(staticEntryFnArgs)
expect(html).not.toContain(
`<meta name="note" content="environment=development"/>`
)
})

test(`replaceRenderer does allow sync rendering`, async () => {
global.plugins = [
{
plugin: {
replaceRenderer: ({ replaceBodyHTMLString }) =>
replaceBodyHTMLString(`i'm sync`),
},
},
]

const { html } = await staticEntry(staticEntryFnArgs)
expect(html).toContain(`i'm sync`)
})

test(`replaceRenderer does allow async rendering`, async () => {
global.plugins = [
{
plugin: {
replaceRenderer: ({ replaceBodyHTMLString }) =>
new Promise(resolve => {
setTimeout(() => {
replaceBodyHTMLString(`i'm async`)
resolve()
}, 1000)
}),
},
},
]

const { html } = await staticEntry(staticEntryFnArgs)
expect(html).toContain(`async`)
})
})

describe(`sanitizeComponents`, () => {
Expand Down
80 changes: 58 additions & 22 deletions packages/gatsby/cache-dir/api-runner-ssr.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* global plugins */
// During bootstrap, we write requires at top of this file which looks like:
// var plugins = [
// {
Expand All @@ -12,41 +13,76 @@

const apis = require(`./api-ssr-docs`)

// Run the specified API in any plugins that have implemented it
module.exports = (api, args, defaultReturn, argTransform) => {
function augmentErrorWithPlugin(plugin, err) {
if (plugin.name !== `default-site-plugin`) {
// default-site-plugin is user code and will print proper stack trace,
// so no point in annotating error message pointing out which plugin is root of the problem
err.message += ` (from plugin: ${plugin.name})`
}

throw err
}

export function apiRunner(api, args, defaultReturn, argTransform) {
if (!apis[api]) {
console.log(`This API doesn't exist`, api)
}

// Run each plugin in series.
// eslint-disable-next-line no-undef
let results = plugins.map(plugin => {
if (!plugin.plugin[api]) {
return undefined
const results = []
plugins.forEach(plugin => {
const apiFn = plugin.plugin[api]
if (!apiFn) {
return
}

try {
const result = plugin.plugin[api](args, plugin.options)
const result = apiFn(args, plugin.options)

if (result && argTransform) {
args = argTransform({ args, result })
}
return result
} catch (e) {
if (plugin.name !== `default-site-plugin`) {
// default-site-plugin is user code and will print proper stack trace,
// so no point in annotating error message pointing out which plugin is root of the problem
e.message += ` (from plugin: ${plugin.name})`
}

throw e
// This if case keeps behaviour as before, we should allow undefined here as the api is defined
// TODO V4
if (typeof result !== `undefined`) {
results.push(result)
}
} catch (e) {
augmentErrorWithPlugin(plugin, e)
}
})

// Filter out undefined results.
results = results.filter(result => typeof result !== `undefined`)
return results.length ? results : [defaultReturn]
}

if (results.length > 0) {
return results
} else {
return [defaultReturn]
export async function apiRunnerAsync(api, args, defaultReturn, argTransform) {
if (!apis[api]) {
console.log(`This API doesn't exist`, api)
}

const results = []
for (const plugin of plugins) {
const apiFn = plugin.plugin[api]
if (!apiFn) {
continue
}

try {
const result = await apiFn(args, plugin.options)

if (result && argTransform) {
args = argTransform({ args, result })
}

// This if case keeps behaviour as before, we should allow undefined here as the api is defined
// TODO V4
if (typeof result !== `undefined`) {
results.push(result)
}
} catch (e) {
augmentErrorWithPlugin(plugin, e)
}
}

return results.length ? results : [defaultReturn]
}
2 changes: 1 addition & 1 deletion packages/gatsby/cache-dir/develop-static-entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import React from "react"
import { renderToStaticMarkup } from "react-dom/server"
import { merge } from "lodash"
import apiRunner from "./api-runner-ssr"
import { apiRunner } from "./api-runner-ssr"
// import testRequireError from "./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.
Expand Down
20 changes: 13 additions & 7 deletions packages/gatsby/cache-dir/ssr-develop-static-entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import fs from "fs"
import { renderToString, renderToStaticMarkup } from "react-dom/server"
import { get, merge, isObject, flatten, uniqBy, concat } from "lodash"
import nodePath from "path"
import apiRunner from "./api-runner-ssr"
import { apiRunner, apiRunnerAsync } from "./api-runner-ssr"
import { grabMatchParams } from "./find-path"
import syncRequires from "$virtual/ssr-sync-requires"

Expand Down Expand Up @@ -48,7 +48,13 @@ try {

Html = Html && Html.__esModule ? Html.default : Html

export default (pagePath, isClientOnlyPage, publicDir, error, callback) => {
export default async function staticPage(
pagePath,
isClientOnlyPage,
publicDir,
error,
callback
) {
let bodyHtml = ``
let headComponents = [
<meta key="environment" name="note" content="environment=development" />,
Expand Down Expand Up @@ -85,7 +91,7 @@ export default (pagePath, isClientOnlyPage, publicDir, error, callback) => {
])
}

const generateBodyHTML = () => {
const generateBodyHTML = async () => {
const setHeadComponents = components => {
headComponents = headComponents.concat(components)
}
Expand Down Expand Up @@ -151,7 +157,7 @@ export default (pagePath, isClientOnlyPage, publicDir, error, callback) => {

const pageData = getPageData(pagePath)

const { componentChunkName, staticQueryHashes = [] } = pageData
const { componentChunkName } = pageData

let scriptsAndStyles = flatten(
[`commons`].map(chunkKey => {
Expand Down Expand Up @@ -192,7 +198,7 @@ export default (pagePath, isClientOnlyPage, publicDir, error, callback) => {
})
)
.filter(s => isObject(s))
.sort((s1, s2) => (s1.rel == `preload` ? -1 : 1)) // given priority to preload
.sort((s1, _s2) => (s1.rel == `preload` ? -1 : 1)) // given priority to preload

scriptsAndStyles = uniqBy(scriptsAndStyles, item => item.name)

Expand Down Expand Up @@ -275,7 +281,7 @@ export default (pagePath, isClientOnlyPage, publicDir, error, callback) => {
).pop()

// Let the site or plugin render the page component.
apiRunner(`replaceRenderer`, {
await apiRunnerAsync(`replaceRenderer`, {
bodyComponent,
replaceBodyHTMLString,
setHeadComponents,
Expand Down Expand Up @@ -321,7 +327,7 @@ export default (pagePath, isClientOnlyPage, publicDir, error, callback) => {
return bodyHtml
}

const bodyStr = generateBodyHTML()
const bodyStr = await generateBodyHTML()

const htmlElement = React.createElement(Html, {
...bodyProps,
Expand Down
8 changes: 4 additions & 4 deletions packages/gatsby/cache-dir/static-entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const { StaticQueryContext } = require(`gatsby`)
const fs = require(`fs`)

const { RouteAnnouncerProps } = require(`./route-announcer-props`)
const apiRunner = require(`./api-runner-ssr`)
const { apiRunner, apiRunnerAsync } = require(`./api-runner-ssr`)
const syncRequires = require(`$virtual/sync-requires`)
const { version: gatsbyVersion } = require(`gatsby/package.json`)
const { grabMatchParams } = require(`./find-path`)
Expand Down Expand Up @@ -86,15 +86,15 @@ const ensureArray = components => {
}
}

export default ({
export default async function staticPage({
pagePath,
pageData,
staticQueryContext,
styles,
scripts,
reversedStyles,
reversedScripts,
}) => {
}) {
// 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 = []

Expand Down Expand Up @@ -244,7 +244,7 @@ export default ({
)

// Let the site or plugin render the page component.
apiRunner(`replaceRenderer`, {
await apiRunnerAsync(`replaceRenderer`, {
bodyComponent,
replaceBodyHTMLString,
setHeadComponents,
Expand Down
Loading

0 comments on commit d68148d

Please sign in to comment.