From 23da2c3fb2e16b7e3fe1e15c19decd799000a212 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 17 Nov 2020 12:53:55 -0800 Subject: [PATCH] feat(gatsby): SSR pages during development (#27432) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Prototype SSR wip * Make it work * Fix issue with running two instances of webpack * Add a test suite for SSR * Linting * Run tests in CI * Lint * Show activity for HTML rendering + renable socket.io so server doesn't crash * Add error page when templates don't render correctly * Rebuild dev ssr bundle when source files change * Fix some lint errors * Fix building html * use gatsby colors for syntax highlighting * Add test script to compare dev & prod output * Add return types * Don't respond to dev server page loads until any sourcing/transforming is done * typings * fix types... maybe * maybe typescript happy * Add missing globals to tests & update snapshots * moer merry type work * Remove outdated typography.js test * Start migrating route handler to spawned service * back out moving dev html route into state machine * cleanups * more cleanups * yet moer cleanups * Add test for error parsing & codeframe creation * add return type * Feature flag functionality behind env variable * Lighten how much the dev ssr html webpack instance is watching * cleanup * track usage while we're in experimental stage * update snapshots * cleanup * Restore support for dev 404 page * fix test * Catch reading errors * Make static queries work in dev-ssr * Keep the renderer around * Send 'loading' page if webpack is busy * Show more error so can debug CI * lazily build dev 404 page so it happens after webpack/queries are run * just use / * Add ansi-html as depdnency * Run test w/ experimental flag * meaningless change to run tests again * use older version compatable with CI * Update packages/gatsby/cache-dir/develop-static-entry.js Co-authored-by: Peter van der Zee <209817+pvdz@users.noreply.github.com> * remove unnecessary changes * Consistently use imports * fix * Conditionally generate body str * remove unneeded change * Add find-page-by-path util from @pieh * make typescript happy * Switch to use @pieh's page finder util * fix find-page-by-path tests * Enable dev ssr for tests * Add build:types again * Do not await a flush * Only delete the render-page.js module cache when it changes * Try to reduce memory retention * Fix recreating dev 404 page on every request + cache requires * Add return * this wasn't necessary * Remove unused var * fix return type * Share cache across develop/develop-html instances of webpack * This caused a lot of runtime tests to fail * Use the webpack hash * This didn't work * fix lint error * Meaningless change to try tests again * SSR pages in jest-worker so memory doesn't accumulate in main process * fix lint * make typescript happy too * fix test import * Automatically fork the dev ssr renderer so it's ready to go when the user requests a page * Add structured logging on dev ssr failure * Need require.resolve I think * Add filepath + line/column to terminal error * try try again * Fixes hopefully * typescript 😱 * lint * Try tweaking jest settings * Debuggin * use default reporter * explicitly init dev html worker pool so it doesn't start during tests * restore original ci settings * sup * Update packages/gatsby-cli/src/structured-errors/error-map.ts Co-authored-by: Lennart * console.logs seem to break jest-worker on CI * Increase pageLoadTimeout * try taskTimeout * This might be confusing cypress * Don't re-spawn the worker process on every change as that's very expensive. Just delete the module cache for 25 edits before re-spawning * cleanups * Update packages/gatsby/cache-dir/develop-static-entry.js Co-authored-by: Ward Peeters * Cleanups suggested by @wardpeet * fix lint * Lazily compile page components This makes the initial creation of the dev ssr bundle ~85% faster. * fix typescript * mock /lazy-sync-requires * The lazy bundling created a race condition where two pages could be simultaneously requested but both would think they're done as soon as the first to arrive finishes — 'suspend' rendering until the pageComponent is found to avoid this * Add more pages to make sure we're going to hit the race condition * Check file directly that the page component has been added This is a lot simpler & more reliable * for some reason this lets log warnings from React not break jest-worker * fix test & comment * We can't use the gatsby reporter inside a child as it uses process.send for console.* which breaks jest-worker * Move writing lazyComponents to requires-writer & still use old develop-static-entry if no flag * update tests * Don't render body of pages w/ matchPath * use core util joinPath so works on windows * try again * try try again * remove mistakenly added file * fix pnp test * Move lazy bundling changes to https://github.com/gatsbyjs/gatsby/pull/27932 * More cleanup * more cleanups * Test both old & new develop-static-entry * fix lint * more lint fixes * Use execa * fix tests * Update names * Fail more gracefully when we source-maps don't work * fix test & remove testing code * Update snapshot * fix dependency check * remove unused import * Update packages/gatsby/src/utils/dev-ssr/develop-html-route.ts Co-authored-by: Ward Peeters * revert unnecessary change * restore more old behavior + move all new requires behind the flag * fix * fix problem w/ merge w/ master Co-authored-by: Sidhartha Chatterjee Co-authored-by: gatsbybot Co-authored-by: Peter van der Zee <209817+pvdz@users.noreply.github.com> Co-authored-by: Michal Piechowiak Co-authored-by: Lennart Co-authored-by: Ward Peeters --- .circleci/config.yml | 8 + integration-tests/ssr/LICENSE | 21 ++ integration-tests/ssr/README.md | 1 + .../ssr/__tests__/__snapshots__/ssr.js.snap | 20 ++ .../ssr/__tests__/fixtures/bad-page.js | 9 + integration-tests/ssr/__tests__/ssr.js | 55 ++++ integration-tests/ssr/gatsby-config.js | 10 + integration-tests/ssr/jest.config.js | 3 + integration-tests/ssr/package.json | 39 +++ integration-tests/ssr/src/pages/hi.js | 15 ++ integration-tests/ssr/src/pages/hi2.js | 15 ++ integration-tests/ssr/src/pages/hi3.js | 15 ++ integration-tests/ssr/src/pages/index.js | 15 ++ integration-tests/ssr/test-output.js | 96 +++++++ .../src/index.ts | 2 + .../src/structured-errors/error-map.ts | 14 ++ .../src/__tests__/gatsby-ssr.js | 6 - .../src/gatsby-ssr.js | 22 +- .../__snapshots__/package-json.test.js.snap | 12 +- .../npm/fixtures/package-json/package.json | 2 +- .../__snapshots__/static-entry.js.snap | 6 + .../cache-dir/__tests__/static-entry.js | 107 ++++++-- packages/gatsby/cache-dir/app.js | 2 + .../cache-dir/ssr-develop-static-entry.js | 236 ++++++++++++++++++ packages/gatsby/cache-dir/static-entry.js | 1 + packages/gatsby/package.json | 2 + .../gatsby/src/bootstrap/requires-writer.ts | 6 +- packages/gatsby/src/commands/build-html.ts | 72 ++++-- .../src/services/start-webpack-server.ts | 3 +- .../__snapshots__/develop-html-route.ts.snap | 47 ++++ .../src/utils/__tests__/develop-html-route.ts | 14 ++ .../src/utils/__tests__/fixtures/blog-post.js | 121 +++++++++ .../utils/__tests__/fixtures/error-object.js | 33 +++ .../src/utils/dev-ssr/develop-html-route.ts | 72 ++++++ .../utils/dev-ssr/render-dev-html-child.js | 168 +++++++++++++ .../src/utils/dev-ssr/render-dev-html.ts | 78 ++++++ packages/gatsby/src/utils/start-server.ts | 109 +++++--- .../gatsby/src/utils/webpack-error-utils.ts | 16 +- packages/gatsby/src/utils/webpack.config.js | 6 +- .../gatsby/src/utils/worker/render-html.ts | 24 +- 40 files changed, 1390 insertions(+), 113 deletions(-) create mode 100644 integration-tests/ssr/LICENSE create mode 100644 integration-tests/ssr/README.md create mode 100644 integration-tests/ssr/__tests__/__snapshots__/ssr.js.snap create mode 100644 integration-tests/ssr/__tests__/fixtures/bad-page.js create mode 100644 integration-tests/ssr/__tests__/ssr.js create mode 100644 integration-tests/ssr/gatsby-config.js create mode 100644 integration-tests/ssr/jest.config.js create mode 100644 integration-tests/ssr/package.json create mode 100644 integration-tests/ssr/src/pages/hi.js create mode 100644 integration-tests/ssr/src/pages/hi2.js create mode 100644 integration-tests/ssr/src/pages/hi3.js create mode 100644 integration-tests/ssr/src/pages/index.js create mode 100644 integration-tests/ssr/test-output.js create mode 100644 packages/gatsby/cache-dir/ssr-develop-static-entry.js create mode 100644 packages/gatsby/src/utils/__tests__/__snapshots__/develop-html-route.ts.snap create mode 100644 packages/gatsby/src/utils/__tests__/develop-html-route.ts create mode 100644 packages/gatsby/src/utils/__tests__/fixtures/blog-post.js create mode 100644 packages/gatsby/src/utils/__tests__/fixtures/error-object.js create mode 100644 packages/gatsby/src/utils/dev-ssr/develop-html-route.ts create mode 100644 packages/gatsby/src/utils/dev-ssr/render-dev-html-child.js create mode 100644 packages/gatsby/src/utils/dev-ssr/render-dev-html.ts 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`] = `"
Hello world
"`; + +exports[`SSR it generates an error page correctly 1`] = ` +"Develop SSR Error

Error

+

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([ +