diff --git a/.circleci/config.yml b/.circleci/config.yml
index 44f7f089f7245..a9c4f595984ed 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -271,6 +271,12 @@ jobs:
- e2e-test:
test_path: integration-tests/artifacts
+ integration_tests_ssr:
+ executor: node
+ steps:
+ - e2e-test:
+ test_path: integration-tests/ssr
+
e2e_tests_path-prefix:
<<: *e2e-executor
environment:
@@ -582,6 +588,8 @@ workflows:
<<: *e2e-test-workflow
- integration_tests_artifacts:
<<: *e2e-test-workflow
+ - integration_tests_ssr:
+ <<: *e2e-test-workflow
- integration_tests_gatsby_cli:
requires:
- bootstrap
diff --git a/integration-tests/ssr/LICENSE b/integration-tests/ssr/LICENSE
new file mode 100644
index 0000000000000..20f91f2b3c52b
--- /dev/null
+++ b/integration-tests/ssr/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 gatsbyjs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/integration-tests/ssr/README.md b/integration-tests/ssr/README.md
new file mode 100644
index 0000000000000..7aca9ce8f432c
--- /dev/null
+++ b/integration-tests/ssr/README.md
@@ -0,0 +1 @@
+## SSR test suite
diff --git a/integration-tests/ssr/__tests__/__snapshots__/ssr.js.snap b/integration-tests/ssr/__tests__/__snapshots__/ssr.js.snap
new file mode 100644
index 0000000000000..5a4ebf200881f
--- /dev/null
+++ b/integration-tests/ssr/__tests__/__snapshots__/ssr.js.snap
@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SSR is run for a page when it is requested 1`] = `"
"`;
+
+exports[`SSR it generates an error page correctly 1`] = `
+"Develop SSR ErrorError
+ The page didn't SSR correctly
+
+ - URL path:
/bad-page/
+ - File path:
src/pages/bad-page.js
+
+ error message
+
window is not defined
2 |
+ 3 | const Component = () => {
+> 4 | const a = window.width
+ | ^
+ 5 |
+ 6 | return <div>hi</div>
+ 7 | }
"
+`;
diff --git a/integration-tests/ssr/__tests__/fixtures/bad-page.js b/integration-tests/ssr/__tests__/fixtures/bad-page.js
new file mode 100644
index 0000000000000..429c52813c049
--- /dev/null
+++ b/integration-tests/ssr/__tests__/fixtures/bad-page.js
@@ -0,0 +1,9 @@
+import React from "react"
+
+const Component = () => {
+ const a = window.width
+
+ return hi
+}
+
+export default Component
diff --git a/integration-tests/ssr/__tests__/ssr.js b/integration-tests/ssr/__tests__/ssr.js
new file mode 100644
index 0000000000000..248426f972cec
--- /dev/null
+++ b/integration-tests/ssr/__tests__/ssr.js
@@ -0,0 +1,55 @@
+const fetch = require(`node-fetch`)
+const execa = require(`execa`)
+const fs = require(`fs-extra`)
+const path = require(`path`)
+
+describe(`SSR`, () => {
+ test(`is run for a page when it is requested`, async () => {
+ const html = await fetch(`http://localhost:8000/`).then(res => res.text())
+
+ expect(html).toMatchSnapshot()
+ })
+ test(`dev & build outputs match`, async () => {
+ const childProcess = await execa(`yarn`, [`test-output`])
+
+ expect(childProcess.code).toEqual(0)
+ })
+ test(`it generates an error page correctly`, async () => {
+ const src = path.join(__dirname, `/fixtures/bad-page.js`)
+ const dest = path.join(__dirname, `../src/pages/bad-page.js`)
+ fs.copySync(src, dest)
+
+ const pageUrl = `http://localhost:8000/bad-page/`
+ await new Promise(resolve => {
+ // Poll until the new page is bundled (so starts returning a non-404 status).
+ const testInterval = setInterval(() => {
+ fetch(pageUrl).then(res => {
+ if (res.status !== 404) {
+ clearInterval(testInterval)
+ resolve()
+ }
+ })
+ }, 1000)
+ })
+
+ const rawDevHtml = await fetch(
+ `http://localhost:8000/bad-page/`
+ ).then(res => res.text())
+ expect(rawDevHtml).toMatchSnapshot()
+ fs.remove(dest)
+
+ // After the page is gone, it'll 404.
+ await new Promise(resolve => {
+ setTimeout(() => {
+ const testInterval = setInterval(() => {
+ fetch(pageUrl).then(res => {
+ if (res.status === 404) {
+ clearInterval(testInterval)
+ resolve()
+ }
+ })
+ }, 400)
+ }, 400)
+ })
+ })
+})
diff --git a/integration-tests/ssr/gatsby-config.js b/integration-tests/ssr/gatsby-config.js
new file mode 100644
index 0000000000000..cc785a9507538
--- /dev/null
+++ b/integration-tests/ssr/gatsby-config.js
@@ -0,0 +1,10 @@
+module.exports = {
+ siteMetadata: {
+ title: `Hello world`,
+ author: `Sid Chatterjee`,
+ twitter: `chatsidhartha`,
+ github: `sidharthachatterjee`,
+ moreInfo: `Sid is amazing`,
+ },
+ plugins: [],
+}
diff --git a/integration-tests/ssr/jest.config.js b/integration-tests/ssr/jest.config.js
new file mode 100644
index 0000000000000..4e5a78b25d7bf
--- /dev/null
+++ b/integration-tests/ssr/jest.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ testPathIgnorePatterns: [`/node_modules/`, `__tests__/fixtures`, `.cache`],
+}
diff --git a/integration-tests/ssr/package.json b/integration-tests/ssr/package.json
new file mode 100644
index 0000000000000..f6517f4147da6
--- /dev/null
+++ b/integration-tests/ssr/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "ssr",
+ "description": "A simplified bare-bones starter for Gatsby.",
+ "version": "0.1.0",
+ "author": "Sid Chatterjee",
+ "bugs": {
+ "url": "https://github.com/gatsbyjs/gatsby/issues"
+ },
+ "dependencies": {
+ "gatsby": "2.24.82-dev-1603131999086",
+ "react": "^16.12.0",
+ "react-dom": "^16.12.0"
+ },
+ "devDependencies": {
+ "cross-env": "^5.0.2",
+ "fs-extra": "^9.0.0",
+ "jest": "^24.0.0",
+ "jest-cli": "^24.0.0",
+ "jest-diff": "^24.0.0",
+ "npm-run-all": "4.1.5",
+ "start-server-and-test": "^1.11.3"
+ },
+ "license": "MIT",
+ "private": true,
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/gatsbyjs/gatsby-starter-hello-world"
+ },
+ "scripts": {
+ "build": "gatsby build",
+ "clean": "gatsby clean",
+ "develop": "gatsby develop",
+ "serve": "gatsby serve",
+ "start-dev-server": "start-server-and-test develop http://localhost:8000 test:jest",
+ "test": "cross-env GATSBY_EXPERIMENTAL_DEV_SSR=true npm-run-all -s build start-dev-server",
+ "test-output": "node test-output.js",
+ "test:jest": "jest --config=jest.config.js --runInBand"
+ }
+}
diff --git a/integration-tests/ssr/src/pages/hi.js b/integration-tests/ssr/src/pages/hi.js
new file mode 100644
index 0000000000000..20466045525dd
--- /dev/null
+++ b/integration-tests/ssr/src/pages/hi.js
@@ -0,0 +1,15 @@
+import React from "react"
+import { useStaticQuery, graphql } from "gatsby"
+
+export default function Inline() {
+ const { site } = useStaticQuery(graphql`
+ {
+ site {
+ siteMetadata {
+ title
+ }
+ }
+ }
+ `)
+ return hi1 {site.siteMetadata.title}
+}
diff --git a/integration-tests/ssr/src/pages/hi2.js b/integration-tests/ssr/src/pages/hi2.js
new file mode 100644
index 0000000000000..5cd1e99c3b450
--- /dev/null
+++ b/integration-tests/ssr/src/pages/hi2.js
@@ -0,0 +1,15 @@
+import React from "react"
+import { useStaticQuery, graphql } from "gatsby"
+
+export default function Inline() {
+ const { site } = useStaticQuery(graphql`
+ {
+ site {
+ siteMetadata {
+ title
+ }
+ }
+ }
+ `)
+ return hi2 {site.siteMetadata.title}
+}
diff --git a/integration-tests/ssr/src/pages/hi3.js b/integration-tests/ssr/src/pages/hi3.js
new file mode 100644
index 0000000000000..d8729091dc969
--- /dev/null
+++ b/integration-tests/ssr/src/pages/hi3.js
@@ -0,0 +1,15 @@
+import React from "react"
+import { useStaticQuery, graphql } from "gatsby"
+
+export default function Inline() {
+ const { site } = useStaticQuery(graphql`
+ {
+ site {
+ siteMetadata {
+ title
+ }
+ }
+ }
+ `)
+ return hi3{site.siteMetadata.title}
+}
diff --git a/integration-tests/ssr/src/pages/index.js b/integration-tests/ssr/src/pages/index.js
new file mode 100644
index 0000000000000..850e99d4638bb
--- /dev/null
+++ b/integration-tests/ssr/src/pages/index.js
@@ -0,0 +1,15 @@
+import React from "react"
+import { useStaticQuery, graphql } from "gatsby"
+
+export default function Inline() {
+ const { site } = useStaticQuery(graphql`
+ {
+ site {
+ siteMetadata {
+ title
+ }
+ }
+ }
+ `)
+ return {site.siteMetadata.title}
+}
diff --git a/integration-tests/ssr/test-output.js b/integration-tests/ssr/test-output.js
new file mode 100644
index 0000000000000..cf2d1731c387a
--- /dev/null
+++ b/integration-tests/ssr/test-output.js
@@ -0,0 +1,96 @@
+// To run the test script manually on a site (e.g. to test a plugin):
+// - build the site first
+// - start the develop server
+// - run this script
+;(async function () {
+ const { getPageHtmlFilePath } = require(`gatsby/dist/utils/page-html`)
+ const { join } = require(`path`)
+ const fs = require(`fs-extra`)
+ const fetch = require(`node-fetch`)
+ const diff = require(`jest-diff`)
+ const prettier = require(`prettier`)
+ const cheerio = require(`cheerio`)
+ const stripAnsi = require(`strip-ansi`)
+
+ const devSiteBasePath = `http://localhost:8000`
+
+ const comparePath = async path => {
+ const format = htmlStr => prettier.format(htmlStr, { parser: `html` })
+
+ const filterHtml = htmlStr => {
+ const $ = cheerio.load(htmlStr)
+ // There are many script tag differences
+ $(`script`).remove()
+ // Only added in production. Dev uses css-loader
+ $(`#gatsby-global-css`).remove()
+ // Only in prod
+ $(`link[rel="preload"]`).remove()
+ // Only in prod
+ $(`meta[name="generator"]`).remove()
+ // Only in dev
+ $(`meta[name="note"]`).remove()
+
+ return $.html()
+ }
+
+ const builtHtml = format(
+ filterHtml(
+ fs.readFileSync(
+ getPageHtmlFilePath(join(process.cwd(), `public`), path),
+ `utf-8`
+ )
+ )
+ )
+
+ const rawDevHtml = await fetch(`${devSiteBasePath}/${path}`).then(res =>
+ res.text()
+ )
+
+ const devHtml = format(filterHtml(rawDevHtml))
+ const diffResult = diff(devHtml, builtHtml, {
+ contextLines: 3,
+ expand: false,
+ })
+ if (
+ stripAnsi(diffResult) === `Compared values have no visual difference.`
+ ) {
+ return true
+ } else {
+ console.log(`path "${path}" has differences between dev & prod`)
+ console.log(diffResult)
+ return false
+ }
+ }
+
+ const response = await fetch(`${devSiteBasePath}/__graphql`, {
+ method: `POST`,
+ headers: { "Content-Type": `application/json` },
+ body: JSON.stringify({
+ query: `query MyQuery {
+ allSitePage {
+ nodes {
+ path
+ }
+ }
+}
+`,
+ }),
+ }).then(res => res.json()) // expecting a json response
+
+ const paths = response.data.allSitePage.nodes
+ .map(n => n.path)
+ .filter(p => p !== `/dev-404-page/`)
+
+ console.log(
+ `testing these paths for differences between dev & prod outputs`,
+ paths
+ )
+
+ const results = await Promise.all(paths.map(p => comparePath(p)))
+ // Test all true
+ if (results.every(r => r)) {
+ process.exit(0)
+ } else {
+ process.exit(1)
+ }
+})()
diff --git a/packages/babel-plugin-remove-graphql-queries/src/index.ts b/packages/babel-plugin-remove-graphql-queries/src/index.ts
index c9b93dffd27af..dd20cbf7a9a9c 100644
--- a/packages/babel-plugin-remove-graphql-queries/src/index.ts
+++ b/packages/babel-plugin-remove-graphql-queries/src/index.ts
@@ -273,6 +273,7 @@ export default function ({ types: t }): PluginObj {
JSXIdentifier(path2: NodePath): void {
if (
(process.env.NODE_ENV === `test` ||
+ state.opts.stage === `develop-html` ||
state.opts.stage === `build-html`) &&
path2.isJSXIdentifier({ name: `StaticQuery` }) &&
path2.referencesImport(`gatsby`, ``) &&
@@ -315,6 +316,7 @@ export default function ({ types: t }): PluginObj {
CallExpression(path2: NodePath): void {
if (
(process.env.NODE_ENV === `test` ||
+ state.opts.stage === `develop-html` ||
state.opts.stage === `build-html`) &&
isUseStaticQuery(path2)
) {
diff --git a/packages/gatsby-cli/src/structured-errors/error-map.ts b/packages/gatsby-cli/src/structured-errors/error-map.ts
index bde43bf84a0d9..a25efb6bad705 100644
--- a/packages/gatsby-cli/src/structured-errors/error-map.ts
+++ b/packages/gatsby-cli/src/structured-errors/error-map.ts
@@ -539,6 +539,20 @@ const errors = {
level: Level.ERROR,
docsUrl: `https://www.gatsbyjs.org/docs/gatsby-cli/#new`,
},
+ "11614": {
+ text: ({
+ path,
+ filePath,
+ line,
+ column,
+ }): string => `The path "${path}" errored during SSR.
+
+ Edit its component ${filePath}${
+ line ? `:${line}:${column}` : ``
+ } to resolve the error.`,
+ level: Level.WARNING,
+ docsUrl: `https://gatsby.dev/debug-html`,
+ },
// Watchdog
"11701": {
text: (context): string =>
diff --git a/packages/gatsby-plugin-typography/src/__tests__/gatsby-ssr.js b/packages/gatsby-plugin-typography/src/__tests__/gatsby-ssr.js
index dfbf50dc2adb9..ed708e9d75181 100644
--- a/packages/gatsby-plugin-typography/src/__tests__/gatsby-ssr.js
+++ b/packages/gatsby-plugin-typography/src/__tests__/gatsby-ssr.js
@@ -33,12 +33,6 @@ describe(`onRenderBody`, () => {
])
})
- it(`only invokes setHeadComponents if BUILD_STAGE is build-html`, () => {
- const api = setup({}, `develop`)
-
- expect(api.setHeadComponents).not.toHaveBeenCalled()
- })
-
it(`does not add google font if omitGoogleFont is passed`, () => {
const api = setup({
omitGoogleFont: true,
diff --git a/packages/gatsby-plugin-typography/src/gatsby-ssr.js b/packages/gatsby-plugin-typography/src/gatsby-ssr.js
index 3ec41f70ce26c..7a3bdbafe9f70 100644
--- a/packages/gatsby-plugin-typography/src/gatsby-ssr.js
+++ b/packages/gatsby-plugin-typography/src/gatsby-ssr.js
@@ -3,19 +3,17 @@ import { TypographyStyle, GoogleFont } from "react-typography"
import typography from "typography-plugin-cache-endpoint"
exports.onRenderBody = ({ setHeadComponents }, pluginOptions) => {
- if (process.env.BUILD_STAGE === `build-html`) {
- const googleFont = [].concat(
- pluginOptions.omitGoogleFont ? (
- []
- ) : (
-
- )
+ const googleFont = [].concat(
+ pluginOptions.omitGoogleFont ? (
+ []
+ ) : (
+
)
- setHeadComponents([
- ,
- ...googleFont,
- ])
- }
+ )
+ setHeadComponents([
+ ,
+ ...googleFont,
+ ])
}
// Move Typography.js styles to the top of the head section so they're loaded first
diff --git a/packages/gatsby-recipes/src/providers/npm/__snapshots__/package-json.test.js.snap b/packages/gatsby-recipes/src/providers/npm/__snapshots__/package-json.test.js.snap
index 30e1b37d8de15..9f9bc6d5ac66a 100644
--- a/packages/gatsby-recipes/src/providers/npm/__snapshots__/package-json.test.js.snap
+++ b/packages/gatsby-recipes/src/providers/npm/__snapshots__/package-json.test.js.snap
@@ -12,7 +12,7 @@ Object {
exports[`packageJson resource e2e package resource test: PackageJson create plan 1`] = `
Object {
"currentState": "{
- \\"name\\": \\"test\\",
+ \\"name\\": \\"test-npm-provider\\",
\\"scripts\\": {}
}",
"describe": "Add husky to package.json",
@@ -23,13 +23,13 @@ Object {
+ \\"husky\\": \\"{
+ /\\"hooks/\\": {}
+ }\\",
- \\"name\\": \\"test\\",
+ \\"name\\": \\"test-npm-provider\\",
\\"scripts\\": Object {},
}",
"id": "husky",
"name": "husky",
"newState": "{
- \\"name\\": \\"test\\",
+ \\"name\\": \\"test-npm-provider\\",
\\"scripts\\": {},
\\"husky\\": \\"{/n /\\"hooks/\\": {}/n}\\"
}",
@@ -50,7 +50,7 @@ Object {
exports[`packageJson resource e2e package resource test: PackageJson update plan 1`] = `
Object {
"currentState": "{
- \\"name\\": \\"test\\",
+ \\"name\\": \\"test-npm-provider\\",
\\"scripts\\": {},
\\"husky\\": \\"{/n /\\"hooks/\\": {}/n}\\"
}",
@@ -66,12 +66,12 @@ Object {
+ /\\"pre-commit/\\": /\\"lint-staged/\\"
+ }
}\\",
- \\"name\\": \\"test\\",
+ \\"name\\": \\"test-npm-provider\\",
\\"scripts\\": Object {},",
"id": "husky",
"name": "husky",
"newState": "{
- \\"name\\": \\"test\\",
+ \\"name\\": \\"test-npm-provider\\",
\\"scripts\\": {},
\\"husky\\": \\"{/n /\\"hooks/\\": {/n /\\"pre-commit/\\": /\\"lint-staged/\\"/n }/n}\\"
}",
diff --git a/packages/gatsby-recipes/src/providers/npm/fixtures/package-json/package.json b/packages/gatsby-recipes/src/providers/npm/fixtures/package-json/package.json
index 3e53932c9b0a5..7e184820f5c19 100644
--- a/packages/gatsby-recipes/src/providers/npm/fixtures/package-json/package.json
+++ b/packages/gatsby-recipes/src/providers/npm/fixtures/package-json/package.json
@@ -1,4 +1,4 @@
{
- "name": "test",
+ "name": "test-npm-provider",
"scripts": {}
}
\ No newline at end of file
diff --git a/packages/gatsby/cache-dir/__tests__/__snapshots__/static-entry.js.snap b/packages/gatsby/cache-dir/__tests__/__snapshots__/static-entry.js.snap
index a32218f3e0838..1a2b6021ebb85 100644
--- a/packages/gatsby/cache-dir/__tests__/__snapshots__/static-entry.js.snap
+++ b/packages/gatsby/cache-dir/__tests__/__snapshots__/static-entry.js.snap
@@ -1,5 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`develop-static-entry SSR: onPreRenderHTML can be used to replace headComponents 1`] = `""`;
+
+exports[`develop-static-entry SSR: onPreRenderHTML can be used to replace postBodyComponents 1`] = `" div3
div2
div1
"`;
+
+exports[`develop-static-entry SSR: onPreRenderHTML can be used to replace preBodyComponents 1`] = `" div3
div2
div1
"`;
+
exports[`develop-static-entry onPreRenderHTML can be used to replace headComponents 1`] = `""`;
exports[`develop-static-entry onPreRenderHTML can be used to replace postBodyComponents 1`] = `" div3
div2
div1
"`;
diff --git a/packages/gatsby/cache-dir/__tests__/static-entry.js b/packages/gatsby/cache-dir/__tests__/static-entry.js
index 7311c4ab1db4c..011e7b65ad3bb 100644
--- a/packages/gatsby/cache-dir/__tests__/static-entry.js
+++ b/packages/gatsby/cache-dir/__tests__/static-entry.js
@@ -2,7 +2,8 @@ import React from "react"
import fs from "fs"
const { join } = require(`path`)
-import DevelopStaticEntry from "../develop-static-entry"
+import ssrDevelopStaticEntry from "../ssr-develop-static-entry"
+import developStaticEntry from "../develop-static-entry"
jest.mock(`fs`, () => {
const fs = jest.requireActual(`fs`)
@@ -16,6 +17,19 @@ jest.mock(`gatsby/package.json`, () => {
version: `2.0.0`,
}
})
+jest.mock(
+ `$virtual/ssr-sync-requires`,
+ () => {
+ return {
+ components: {
+ "page-component---src-pages-test-js": () => null,
+ },
+ }
+ },
+ {
+ virtual: true,
+ }
+)
jest.mock(
`$virtual/sync-requires`,
@@ -48,10 +62,10 @@ const MOCK_FILE_INFO = {
}),
}
-let StaticEntry
+let staticEntry
beforeEach(() => {
fs.readFileSync.mockImplementation(file => MOCK_FILE_INFO[file])
- StaticEntry = require(`../static-entry`).default
+ staticEntry = require(`../static-entry`).default
})
const reverseHeadersPlugin = {
@@ -138,10 +152,69 @@ const fakeComponentsPluginFactory = type => {
}
describe(`develop-static-entry`, () => {
+ beforeEach(() => {
+ global.__PATH_PREFIX__ = ``
+ global.__BASE_PATH__ = ``
+ global.__ASSET_PREFIX__ = ``
+ })
+
+ test(`SSR: onPreRenderHTML can be used to replace headComponents`, done => {
+ global.plugins = [fakeStylesPlugin, reverseHeadersPlugin]
+
+ ssrDevelopStaticEntry(`/about/`, false, (_, html) => {
+ expect(html).toMatchSnapshot()
+ done()
+ })
+ })
+
+ test(`SSR: onPreRenderHTML can be used to replace postBodyComponents`, done => {
+ global.plugins = [
+ fakeComponentsPluginFactory(`Post`),
+ reverseBodyComponentsPluginFactory(`Post`),
+ ]
+
+ ssrDevelopStaticEntry(`/about/`, false, (_, html) => {
+ expect(html).toMatchSnapshot()
+ done()
+ })
+ })
+
+ test(`SSR: onPreRenderHTML can be used to replace preBodyComponents`, done => {
+ global.plugins = [
+ fakeComponentsPluginFactory(`Pre`),
+ reverseBodyComponentsPluginFactory(`Pre`),
+ ]
+
+ ssrDevelopStaticEntry(`/about/`, false, (_, html) => {
+ expect(html).toMatchSnapshot()
+ done()
+ })
+ })
+
+ test(`SSR: onPreRenderHTML adds metatag note for development environment`, done => {
+ ssrDevelopStaticEntry(`/about/`, false, (_, html) => {
+ expect(html).toContain(
+ ``
+ )
+ done()
+ })
+ })
+
+ test(`SSR: onPreRenderHTML adds metatag note for development environment after replaceHeadComponents`, done => {
+ global.plugins = [reverseHeadersPlugin]
+
+ ssrDevelopStaticEntry(`/about/`, false, (_, html) => {
+ expect(html).toContain(
+ ``
+ )
+ done()
+ })
+ })
+
test(`onPreRenderHTML can be used to replace headComponents`, done => {
global.plugins = [fakeStylesPlugin, reverseHeadersPlugin]
- DevelopStaticEntry(`/about/`, (_, html) => {
+ developStaticEntry(`/about/`, (_, html) => {
expect(html).toMatchSnapshot()
done()
})
@@ -153,7 +226,7 @@ describe(`develop-static-entry`, () => {
reverseBodyComponentsPluginFactory(`Post`),
]
- DevelopStaticEntry(`/about/`, (_, html) => {
+ developStaticEntry(`/about/`, (_, html) => {
expect(html).toMatchSnapshot()
done()
})
@@ -165,14 +238,14 @@ describe(`develop-static-entry`, () => {
reverseBodyComponentsPluginFactory(`Pre`),
]
- DevelopStaticEntry(`/about/`, (_, html) => {
+ developStaticEntry(`/about/`, (_, html) => {
expect(html).toMatchSnapshot()
done()
})
})
test(`onPreRenderHTML adds metatag note for development environment`, done => {
- DevelopStaticEntry(`/about/`, (_, html) => {
+ developStaticEntry(`/about/`, (_, html) => {
expect(html).toContain(
``
)
@@ -183,7 +256,7 @@ describe(`develop-static-entry`, () => {
test(`onPreRenderHTML adds metatag note for development environment after replaceHeadComponents`, done => {
global.plugins = [reverseHeadersPlugin]
- DevelopStaticEntry(`/about/`, (_, html) => {
+ developStaticEntry(`/about/`, (_, html) => {
expect(html).toContain(
``
)
@@ -210,7 +283,7 @@ describe(`static-entry sanity checks`, () => {
const plugin = injectValuePlugin(`onPreRenderHTML`, methodName, null)
global.plugins = [plugin, checkNonEmptyHeadersPlugin]
- StaticEntry(`/about/`, (_, html) => {
+ staticEntry(`/about/`, (_, html) => {
done()
})
})
@@ -222,7 +295,7 @@ describe(`static-entry sanity checks`, () => {
])
global.plugins = [plugin, checkNonEmptyHeadersPlugin]
- StaticEntry(`/about/`, (_, html) => {
+ staticEntry(`/about/`, (_, html) => {
done()
})
})
@@ -231,7 +304,7 @@ describe(`static-entry sanity checks`, () => {
const plugin = injectValuePlugin(`onPreRenderHTML`, methodName, [])
global.plugins = [plugin, checkNonEmptyHeadersPlugin]
- StaticEntry(`/about/`, (_, html) => {
+ staticEntry(`/about/`, (_, html) => {
done()
})
})
@@ -240,7 +313,7 @@ describe(`static-entry sanity checks`, () => {
const plugin = injectValuePlugin(`onPreRenderHTML`, methodName, [[], []])
global.plugins = [plugin, checkNonEmptyHeadersPlugin]
- StaticEntry(`/about/`, (_, html) => {
+ staticEntry(`/about/`, (_, html) => {
done()
})
})
@@ -254,7 +327,7 @@ describe(`static-entry sanity checks`, () => {
])
global.plugins = [plugin, checkNonEmptyHeadersPlugin]
- StaticEntry(`/about/`, (_, html) => {
+ staticEntry(`/about/`, (_, html) => {
done()
})
})
@@ -271,7 +344,7 @@ describe(`static-entry`, () => {
test(`onPreRenderHTML can be used to replace headComponents`, done => {
global.plugins = [fakeStylesPlugin, reverseHeadersPlugin]
- StaticEntry(`/about/`, (_, html) => {
+ staticEntry(`/about/`, (_, html) => {
expect(html).toMatchSnapshot()
done()
})
@@ -283,7 +356,7 @@ describe(`static-entry`, () => {
reverseBodyComponentsPluginFactory(`Post`),
]
- StaticEntry(`/about/`, (_, html) => {
+ staticEntry(`/about/`, (_, html) => {
expect(html).toMatchSnapshot()
done()
})
@@ -295,14 +368,14 @@ describe(`static-entry`, () => {
reverseBodyComponentsPluginFactory(`Pre`),
]
- StaticEntry(`/about/`, (_, html) => {
+ staticEntry(`/about/`, (_, html) => {
expect(html).toMatchSnapshot()
done()
})
})
test(`onPreRenderHTML does not add metatag note for development environment`, done => {
- StaticEntry(`/about/`, (_, html) => {
+ staticEntry(`/about/`, (_, html) => {
expect(html).not.toContain(
``
)
diff --git a/packages/gatsby/cache-dir/app.js b/packages/gatsby/cache-dir/app.js
index 5df61a462e89a..629a3aeefbac2 100644
--- a/packages/gatsby/cache-dir/app.js
+++ b/packages/gatsby/cache-dir/app.js
@@ -101,6 +101,8 @@ apiRunnerAsync(`onClientEntry`).then(() => {
const renderer = apiRunner(
`replaceHydrateFunction`,
undefined,
+ // TODO replace with hydrate once dev SSR is ready
+ // but only for SSRed pages.
ReactDOM.render
)[0]
diff --git a/packages/gatsby/cache-dir/ssr-develop-static-entry.js b/packages/gatsby/cache-dir/ssr-develop-static-entry.js
new file mode 100644
index 0000000000000..efd2083cd401d
--- /dev/null
+++ b/packages/gatsby/cache-dir/ssr-develop-static-entry.js
@@ -0,0 +1,236 @@
+import React from "react"
+import fs from "fs"
+import { renderToString, renderToStaticMarkup } from "react-dom/server"
+import { merge } from "lodash"
+import { join } from "path"
+import apiRunner from "./api-runner-ssr"
+import { grabMatchParams } from "./find-path"
+import syncRequires from "$virtual/ssr-sync-requires"
+
+import { RouteAnnouncerProps } from "./route-announcer-props"
+import { ServerLocation, Router, isRedirect } from "@reach/router"
+
+// 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.
+// So in the meantime, we'll just inline it.
+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 {
+ console.log(`There was an error requiring "src/html.js"\n\n`, err, `\n\n`)
+ process.exit()
+ }
+}
+
+Html = Html && Html.__esModule ? Html.default : Html
+
+export default (pagePath, isClientOnlyPage, callback) => {
+ let bodyHtml = ``
+ let headComponents = [
+ ,
+ ]
+ let htmlAttributes = {}
+ let bodyAttributes = {}
+ let preBodyComponents = []
+ let postBodyComponents = []
+ let bodyProps = {}
+
+ const generateBodyHTML = () => {
+ const setHeadComponents = components => {
+ headComponents = headComponents.concat(components)
+ }
+
+ const setHtmlAttributes = attributes => {
+ htmlAttributes = merge(htmlAttributes, attributes)
+ }
+
+ const setBodyAttributes = attributes => {
+ bodyAttributes = merge(bodyAttributes, attributes)
+ }
+
+ const setPreBodyComponents = components => {
+ preBodyComponents = preBodyComponents.concat(components)
+ }
+
+ const setPostBodyComponents = components => {
+ postBodyComponents = postBodyComponents.concat(components)
+ }
+
+ const setBodyProps = props => {
+ bodyProps = merge({}, bodyProps, props)
+ }
+
+ const getHeadComponents = () => headComponents
+
+ const replaceHeadComponents = components => {
+ headComponents = components
+ }
+
+ const replaceBodyHTMLString = body => {
+ bodyHtml = body
+ }
+
+ const getPreBodyComponents = () => preBodyComponents
+
+ const replacePreBodyComponents = components => {
+ preBodyComponents = components
+ }
+
+ const getPostBodyComponents = () => postBodyComponents
+
+ const replacePostBodyComponents = components => {
+ postBodyComponents = components
+ }
+
+ const getPageDataPath = path => {
+ const fixedPagePath = path === `/` ? `index` : path
+ return join(`page-data`, fixedPagePath, `page-data.json`)
+ }
+
+ const getPageData = pagePath => {
+ const pageDataPath = getPageDataPath(pagePath)
+ const absolutePageDataPath = join(process.cwd(), `public`, pageDataPath)
+ const pageDataJson = fs.readFileSync(absolutePageDataPath, `utf8`)
+
+ try {
+ return JSON.parse(pageDataJson)
+ } catch (err) {
+ return null
+ }
+ }
+
+ const pageData = getPageData(pagePath)
+
+ const componentChunkName = pageData?.componentChunkName
+
+ const createElement = React.createElement
+
+ class RouteHandler extends React.Component {
+ render() {
+ const props = {
+ ...this.props,
+ ...pageData.result,
+ params: {
+ ...grabMatchParams(this.props.location.pathname),
+ ...(pageData.result?.pageContext?.__params || {}),
+ },
+ // pathContext was deprecated in v2. Renamed to pageContext
+ pathContext: pageData.result
+ ? pageData.result.pageContext
+ : undefined,
+ }
+
+ const pageElement = createElement(
+ syncRequires.components[componentChunkName],
+ props
+ )
+
+ const wrappedPage = apiRunner(
+ `wrapPageElement`,
+ { element: pageElement, props },
+ pageElement,
+ ({ result }) => {
+ return { element: result, props }
+ }
+ ).pop()
+
+ return wrappedPage
+ }
+ }
+
+ const routerElement = (
+
+
+
+
+
+
+ )
+
+ const bodyComponent = apiRunner(
+ `wrapRootElement`,
+ { element: routerElement, pathname: pagePath },
+ routerElement,
+ ({ result }) => {
+ return { element: result, pathname: pagePath }
+ }
+ ).pop()
+
+ // Let the site or plugin render the page component.
+ apiRunner(`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 {
+ bodyHtml = renderToString(bodyComponent)
+ } catch (e) {
+ // ignore @reach/router redirect errors
+ if (!isRedirect(e)) throw e
+ }
+ }
+
+ apiRunner(`onRenderBody`, {
+ setHeadComponents,
+ setHtmlAttributes,
+ setBodyAttributes,
+ setPreBodyComponents,
+ setPostBodyComponents,
+ setBodyProps,
+ pathname: pagePath,
+ })
+
+ apiRunner(`onPreRenderHTML`, {
+ getHeadComponents,
+ replaceHeadComponents,
+ getPreBodyComponents,
+ replacePreBodyComponents,
+ getPostBodyComponents,
+ replacePostBodyComponents,
+ pathname: pagePath,
+ })
+
+ return bodyHtml
+ }
+
+ const bodyStr = isClientOnlyPage ? `` : generateBodyHTML()
+
+ const htmlElement = React.createElement(Html, {
+ ...bodyProps,
+ body: bodyStr,
+ headComponents: headComponents.concat([
+ ,
+ ]),
+ htmlAttributes,
+ bodyAttributes,
+ preBodyComponents,
+ postBodyComponents: postBodyComponents.concat([
+ ,
+ ,
+ ]),
+ })
+ let htmlStr = renderToStaticMarkup(htmlElement)
+ htmlStr = `${htmlStr}`
+
+ callback(null, htmlStr)
+}
diff --git a/packages/gatsby/cache-dir/static-entry.js b/packages/gatsby/cache-dir/static-entry.js
index c0f6b3577c783..61ff359538ac4 100644
--- a/packages/gatsby/cache-dir/static-entry.js
+++ b/packages/gatsby/cache-dir/static-entry.js
@@ -415,6 +415,7 @@ export default (pagePath, callback) => {
headComponents.unshift(