diff --git a/e2e-tests/development-runtime/cypress/integration/navigation/redirect.js b/e2e-tests/development-runtime/cypress/integration/navigation/redirect.js
index 293930eda4c16..d3621d61cfaa8 100644
--- a/e2e-tests/development-runtime/cypress/integration/navigation/redirect.js
+++ b/e2e-tests/development-runtime/cypress/integration/navigation/redirect.js
@@ -5,34 +5,38 @@ Cypress.on(`window:before:load`, win => {
const runTests = () => {
it(`should redirect page to index page when there is no such page`, () => {
- cy.visit(`/redirect-without-page`).waitForRouteChange()
+ cy.visit(`/redirect-without-page`, {
+ failOnStatusCode: false,
+ }).waitForRouteChange()
cy.location(`pathname`).should(`equal`, `/`)
cy.then(() => {
const calls = spy.getCalls()
- const callsAboutRedirectMatchingPage = calls.filter(call => {
- return call.args[0].includes(
- "matches both a page and a redirect; this is probably not intentional."
+ const callsAboutRedirectMatchingPage = calls.filter(call =>
+ call.args[0].includes(
+ `matches both a page and a redirect; this is probably not intentional.`
)
- })
+ )
expect(callsAboutRedirectMatchingPage.length).to.equal(0)
})
})
it(`should redirect page to index page even there is a such page`, () => {
- cy.visit(`/redirect`).waitForRouteChange()
+ cy.visit(`/redirect`, {
+ failOnStatusCode: false,
+ }).waitForRouteChange()
cy.location(`pathname`).should(`equal`, `/`)
cy.then(() => {
const calls = spy.getCalls()
- const callsAboutRedirectMatchingPage = calls.filter(call => {
- return call.args[0].includes(
- "matches both a page and a redirect; this is probably not intentional."
+ const callsAboutRedirectMatchingPage = calls.filter(call =>
+ call.args[0].includes(
+ `matches both a page and a redirect; this is probably not intentional.`
)
- })
+ )
expect(callsAboutRedirectMatchingPage.length).not.to.equal(0)
expect(spy).to.be.calledWith(
@@ -42,7 +46,9 @@ const runTests = () => {
})
it(`should redirect to a dynamically-created replacement page`, () => {
- cy.visit(`/redirect-me/`).waitForRouteChange()
+ cy.visit(`/redirect-me/`, {
+ failOnStatusCode: false,
+ }).waitForRouteChange()
cy.location(`pathname`).should(`equal`, `/pt/redirect-me/`)
cy.then(() => {
@@ -50,7 +56,7 @@ const runTests = () => {
`The route "/redirect" matches both a page and a redirect; this is probably not intentional.`
)
- cy.findByText("This should be at /pt/redirect-me/", {
+ cy.findByText(`This should be at /pt/redirect-me/`, {
exact: false,
}).should(`exist`)
})
@@ -58,27 +64,29 @@ const runTests = () => {
}
describe(`redirect`, () => {
- describe("404 is present", () => {
+ describe(`404 is present`, () => {
before(() => {
cy.task(`restoreAllBlockedResources`)
})
// this is sanity check for this group
it(`make sure 404 is present`, () => {
- cy.visit(`/______not_existing_page`).waitForRouteChange()
- cy.findByText("Preview custom 404 page").click()
- cy.findByText("A custom 404 page wasn't detected", {
+ cy.visit(`/______not_existing_page`, {
+ failOnStatusCode: false,
+ }).waitForRouteChange()
+ cy.findByText(`Preview custom 404 page`).click()
+ cy.findByText(`A custom 404 page wasn't detected`, {
exact: false,
}).should(`not.exist`)
cy.findByText(
- "You just hit a route that does not exist... the sadness."
+ `You just hit a route that does not exist... the sadness.`
).should(`exist`)
})
runTests()
})
- describe("no 404", () => {
+ describe(`no 404`, () => {
before(() => {
cy.task(`restoreAllBlockedResources`)
@@ -100,13 +108,15 @@ describe(`redirect`, () => {
})
it(`make sure 404 is NOT present`, () => {
- cy.visit(`/______not_existing_page`).waitForRouteChange()
- cy.findByText("Preview custom 404 page").click()
- cy.findByText("A custom 404 page wasn't detected", {
+ cy.visit(`/______not_existing_page`, {
+ failOnStatusCode: false,
+ }).waitForRouteChange()
+ cy.findByText(`Preview custom 404 page`).click()
+ cy.findByText(`A custom 404 page wasn't detected`, {
exact: false,
}).should(`exist`)
cy.findByText(
- "You just hit a route that does not exist... the sadness.",
+ `You just hit a route that does not exist... the sadness.`,
{ exact: false }
).should(`not.exist`)
})
diff --git a/e2e-tests/development-runtime/cypress/integration/page-not-found/404.js b/e2e-tests/development-runtime/cypress/integration/page-not-found/404.js
index 400e7c13a27e1..28e6bba1cca4e 100644
--- a/e2e-tests/development-runtime/cypress/integration/page-not-found/404.js
+++ b/e2e-tests/development-runtime/cypress/integration/page-not-found/404.js
@@ -1,6 +1,8 @@
describe(`page not found`, () => {
beforeEach(() => {
- cy.visit(`/__404__`)
+ cy.visit(`/__404__`, {
+ failOnStatusCode: false,
+ })
})
it(`should display message `, () => {
cy.get(`h1`).invoke(`text`).should(`eq`, `Gatsby.js development 404 page`)
diff --git a/integration-tests/ssr/__tests__/__snapshots__/ssr.js.snap b/integration-tests/ssr/__tests__/__snapshots__/ssr.js.snap
index 1de25d640ab7c..57990a718a79c 100644
--- a/integration-tests/ssr/__tests__/__snapshots__/ssr.js.snap
+++ b/integration-tests/ssr/__tests__/__snapshots__/ssr.js.snap
@@ -3,134 +3,15 @@
exports[`SSR is run for a page when it is requested 1`] = `"
"`;
exports[`SSR it generates an error page correctly 1`] = `
-"
- Develop SSR Error
-
-
- Error
- The page didn't server render (SSR) correctly
-
- React components in Gatsby must render successfully in the browser and in a
- node.js environment. When we tried to render your page component in
- node.js, it errored.
-
-
- URL path: /bad-page/
- File path: src/pages/bad-page.js
-
- error
- window is not defined
- 2 |
- 3 | const Component = () => {
-> 4 | const a = window. width
- | ^
- 5 |
- 6 | return < div > hi< / div >
- 7 | }
- For help debugging SSR errors, see this docs page: https://www.gatsbyjs.com/docs/debugging-html-builds/
- Skip SSR
-
- If you don't wish to fix the SSR error at the moment, press the
- button below to reload the page without attempting SSR
-
- Note : this error will show up in when you build your site so must be fixed before then.
-
- Caveat : SSR errors in module scope i.e. outside of your components can't be skipped so will need fixed before you can continue
- Skip SSR
-
- "
+"Failed to Server Render (SSR) Error message: window is not defined
File: src/pages/bad-page.js:4:13
Stack: ReferenceError: window is not defined
+ at Component (/public/webpack:/ssr/src/pages/bad-page.js:4:13)
+ at d (/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:36:498)
+ at $a (/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:39:16)
+ at a.b.render (/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:44:476)
+ at a.b.read (/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:44:18)
+ at renderToString (/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:54:364)
+ at generateBodyHTML (/public/webpack:/ssr/.cache/ssr-develop-static-entry.js:293:34)
+ at Object.__WEBPACK_DEFAULT_EXPORT__ [as default] (/public/webpack:/ssr/.cache/ssr-develop-static-entry.js:323:19)
+ at /node_modules/gatsby/src/utils/dev-ssr/render-dev-html-child.js:95:9
+ at new Promise (<anonymous>)
"
`;
diff --git a/integration-tests/ssr/__tests__/ssr.js b/integration-tests/ssr/__tests__/ssr.js
index d82b19c1dc9b2..0395743a970dd 100644
--- a/integration-tests/ssr/__tests__/ssr.js
+++ b/integration-tests/ssr/__tests__/ssr.js
@@ -38,7 +38,7 @@ describe(`SSR`, () => {
const pageUrl = `http://localhost:8000/bad-page/`
// Poll until the new page is bundled (so starts returning a non-404 status).
const rawDevHtml = await fetchUntil(pageUrl, res => {
- return res.status === 500
+ return res
}).then(res => res.text())
expect(rawDevHtml).toMatchSnapshot()
await fs.remove(dest)
diff --git a/integration-tests/ssr/jest.config.js b/integration-tests/ssr/jest.config.js
index 57d3e1a6afb2b..881e96b65584b 100644
--- a/integration-tests/ssr/jest.config.js
+++ b/integration-tests/ssr/jest.config.js
@@ -1,4 +1,5 @@
module.exports = {
+ snapshotSerializers: [`jest-serializer-path`],
testPathIgnorePatterns: [
`/node_modules/`,
`__tests__/fixtures`,
diff --git a/integration-tests/ssr/package.json b/integration-tests/ssr/package.json
index 24a7d7323e54b..e221a38cc7d18 100644
--- a/integration-tests/ssr/package.json
+++ b/integration-tests/ssr/package.json
@@ -18,6 +18,7 @@
"fs-extra": "^9.0.0",
"jest": "^24.0.0",
"jest-diff": "^24.0.0",
+ "jest-serializer-path": "^0.1.15",
"npm-run-all": "4.1.5",
"start-server-and-test": "^1.11.3"
},
@@ -30,7 +31,7 @@
"scripts": {
"build": "gatsby build",
"clean": "gatsby clean",
- "develop": "cross-env GATSBY_EXPERIMENTAL_DEV_SSR=true gatsby develop",
+ "develop": "cross-env GATSBY_EXPERIMENTAL_DEV_SSR=true CI=1 FORCE_COLOR=0 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",
diff --git a/packages/gatsby-cli/src/structured-errors/error-map.ts b/packages/gatsby-cli/src/structured-errors/error-map.ts
index 8460f93c84ff3..7f05bd9c9fc30 100644
--- a/packages/gatsby-cli/src/structured-errors/error-map.ts
+++ b/packages/gatsby-cli/src/structured-errors/error-map.ts
@@ -549,18 +549,29 @@ const errors = {
docsUrl: `https://www.gatsbyjs.com/docs/reference/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.`,
+ text: (context): string =>
+ stripIndent(`
+ The path "${context.path}" errored during SSR.
+ Edit its component ${context.filePath}${
+ context.line ? `:${context.line}:${context.column}` : ``
+ } to resolve the error.`),
level: Level.WARNING,
- docsUrl: `https://gatsby.dev/debug-html`,
+ },
+ "11615": {
+ text: (context): string =>
+ stripIndent(`
+ There was an error while trying to load dev-404-page:
+ ${context.sourceMessage}`),
+ level: Level.ERROR,
+ category: ErrorCategory.SYSTEM,
+ },
+ "11616": {
+ text: (context): string =>
+ stripIndent(`
+ There was an error while trying to create the client-only shell for displaying SSR errors:
+ ${context.sourceMessage}`),
+ level: Level.ERROR,
+ category: ErrorCategory.SYSTEM,
},
// Watchdog
"11701": {
diff --git a/packages/gatsby/cache-dir/__tests__/static-entry.js b/packages/gatsby/cache-dir/__tests__/static-entry.js
index 638dc0d6f32ac..eba0a4c5c3a86 100644
--- a/packages/gatsby/cache-dir/__tests__/static-entry.js
+++ b/packages/gatsby/cache-dir/__tests__/static-entry.js
@@ -195,7 +195,7 @@ describe(`develop-static-entry`, () => {
test(`SSR: onPreRenderHTML can be used to replace headComponents`, done => {
global.plugins = [fakeStylesPlugin, reverseHeadersPlugin]
- ssrDevelopStaticEntry(`/about/`, false, publicDir, (_, html) => {
+ ssrDevelopStaticEntry(`/about/`, false, publicDir, undefined, (_, html) => {
expect(html).toMatchSnapshot()
done()
})
@@ -207,7 +207,7 @@ describe(`develop-static-entry`, () => {
reverseBodyComponentsPluginFactory(`Post`),
]
- ssrDevelopStaticEntry(`/about/`, false, publicDir, (_, html) => {
+ ssrDevelopStaticEntry(`/about/`, false, publicDir, undefined, (_, html) => {
expect(html).toMatchSnapshot()
done()
})
@@ -219,14 +219,14 @@ describe(`develop-static-entry`, () => {
reverseBodyComponentsPluginFactory(`Pre`),
]
- ssrDevelopStaticEntry(`/about/`, false, publicDir, (_, html) => {
+ ssrDevelopStaticEntry(`/about/`, false, publicDir, undefined, (_, html) => {
expect(html).toMatchSnapshot()
done()
})
})
test(`SSR: onPreRenderHTML adds metatag note for development environment`, done => {
- ssrDevelopStaticEntry(`/about/`, false, publicDir, (_, html) => {
+ ssrDevelopStaticEntry(`/about/`, false, publicDir, undefined, (_, html) => {
expect(html).toContain(
` `
)
@@ -237,7 +237,7 @@ describe(`develop-static-entry`, () => {
test(`SSR: onPreRenderHTML adds metatag note for development environment after replaceHeadComponents`, done => {
global.plugins = [reverseHeadersPlugin]
- ssrDevelopStaticEntry(`/about/`, false, publicDir, (_, html) => {
+ ssrDevelopStaticEntry(`/about/`, false, publicDir, undefined, (_, html) => {
expect(html).toContain(
` `
)
diff --git a/packages/gatsby/cache-dir/fast-refresh-overlay/components/dev-ssr-error.js b/packages/gatsby/cache-dir/fast-refresh-overlay/components/dev-ssr-error.js
new file mode 100644
index 0000000000000..fd0776451b3a1
--- /dev/null
+++ b/packages/gatsby/cache-dir/fast-refresh-overlay/components/dev-ssr-error.js
@@ -0,0 +1,66 @@
+import * as React from "react"
+import { Overlay, Header, Body, Footer, HeaderOpenClose } from "./overlay"
+import { CodeFrame } from "./code-frame"
+import { prettifyStack, openInEditor, skipSSR, reloadPage } from "../utils"
+
+export function DevSsrError({ error }) {
+ const { codeFrame, source, line } = error
+ const decoded = prettifyStack(codeFrame)
+
+ return (
+
+
+
+
+ React Components in Gatsby must render both successfully in the
+ browser and in a Node.js environment. When we tried to render your
+ page component in Node.js, it errored.
+
+ Source
+
+
+
+ If you fixed the error, saved your file, and want to retry server
+ rendering this page, please reload the page.
+
+ reloadPage()}
+ data-gatsby-overlay="primary-button"
+ >
+ Reload page
+
+ Skip Server Render
+
+ If you don't wish to fix the SSR error at the moment, press the button
+ below to reload the page without attempting to do SSR.
+
+ skipSSR()} data-gatsby-overlay="primary-button">
+ Skip SSR
+
+
+ Note: This error will show up
+ during "gatsby build" so it must be fixed before then. SSR errors in
+ module scope, e.g. outside of your own React components can't be
+ skipped.
+
+
+
+ )
+}
diff --git a/packages/gatsby/cache-dir/fast-refresh-overlay/components/overlay.js b/packages/gatsby/cache-dir/fast-refresh-overlay/components/overlay.js
index 78b936e5788db..9360e391f2af1 100644
--- a/packages/gatsby/cache-dir/fast-refresh-overlay/components/overlay.js
+++ b/packages/gatsby/cache-dir/fast-refresh-overlay/components/overlay.js
@@ -73,7 +73,7 @@ export function Overlay({ children }) {
export function CloseButton({ dismiss }) {
return (
-
+
Close
{open && (
-
+
Open in editor
)}
diff --git a/packages/gatsby/cache-dir/fast-refresh-overlay/index.js b/packages/gatsby/cache-dir/fast-refresh-overlay/index.js
index 640392c246928..7fdca80e19b55 100644
--- a/packages/gatsby/cache-dir/fast-refresh-overlay/index.js
+++ b/packages/gatsby/cache-dir/fast-refresh-overlay/index.js
@@ -5,6 +5,7 @@ import { Style } from "./style"
import { BuildError } from "./components/build-error"
import { RuntimeErrors } from "./components/runtime-errors"
import { GraphqlErrors } from "./components/graphql-errors"
+import { DevSsrError } from "./components/dev-ssr-error"
const reducer = (state, event) => {
switch (event.action) {
@@ -14,9 +15,15 @@ const reducer = (state, event) => {
case `CLEAR_RUNTIME_ERRORS`: {
return { ...state, errors: [] }
}
+ case `CLEAR_DEV_SSR_ERROR`: {
+ return { ...state, devSsrError: null }
+ }
case `SHOW_COMPILE_ERROR`: {
return { ...state, buildError: event.payload }
}
+ case `SHOW_DEV_SSR_ERROR`: {
+ return { ...state, devSsrError: event.payload }
+ }
case `HANDLE_RUNTIME_ERROR`:
case `SHOW_RUNTIME_ERRORS`: {
return { ...state, errors: state.errors.concat(event.payload) }
@@ -47,6 +54,7 @@ const reducer = (state, event) => {
const initialState = {
errors: [],
buildError: null,
+ devSsrError: null,
graphqlErrors: [],
}
@@ -81,7 +89,9 @@ function DevOverlay({ children }) {
const hasBuildError = state.buildError !== null
const hasRuntimeErrors = Boolean(state.errors.length)
const hasGraphqlErrors = Boolean(state.graphqlErrors.length)
- const hasErrors = hasBuildError || hasRuntimeErrors || hasGraphqlErrors
+ const hasDevSsrError = state.devSsrError !== null
+ const hasErrors =
+ hasBuildError || hasRuntimeErrors || hasGraphqlErrors || hasDevSsrError
// This component has a deliberate order (priority)
const ErrorComponent = () => {
@@ -94,6 +104,9 @@ function DevOverlay({ children }) {
if (hasGraphqlErrors) {
return
}
+ if (hasDevSsrError) {
+ return
+ }
return null
}
diff --git a/packages/gatsby/cache-dir/fast-refresh-overlay/style.js b/packages/gatsby/cache-dir/fast-refresh-overlay/style.js
index 0364a60b53a86..f3c848641a3f4 100644
--- a/packages/gatsby/cache-dir/fast-refresh-overlay/style.js
+++ b/packages/gatsby/cache-dir/fast-refresh-overlay/style.js
@@ -183,7 +183,7 @@ export const Style = () => (
font-weight: 500;
}
- [data-gatsby-overlay="header__open-in-editor"] {
+ [data-gatsby-overlay="primary-button"] {
--ring-opacity: 0.9;
--ring-color: rgba(54, 32, 102, var(--ring-opacity));
align-items: center;
@@ -234,7 +234,7 @@ export const Style = () => (
margin-bottom: 0.5em;
}
- [data-gatsby-overlay="header__close-button"] {
+ [data-gatsby-overlay="close-button"] {
--ring-opacity: 0.9;
--ring-color: rgba(54, 32, 102, var(--ring-opacity));
cursor: pointer;
@@ -402,8 +402,8 @@ export const Style = () => (
--ring-color: rgba(217, 186, 232, var(--ring-opacity));
}
- [data-gatsby-overlay="header__close-button"],
- [data-gatsby-overlay="header__open-in-editor"] {
+ [data-gatsby-overlay="close-button"],
+ [data-gatsby-overlay="primary-button"] {
--ring-color: rgba(177, 122, 204, var(--ring-opacity));
}
}
diff --git a/packages/gatsby/cache-dir/fast-refresh-overlay/utils.js b/packages/gatsby/cache-dir/fast-refresh-overlay/utils.js
index 0338ae16a59a6..a9183a3f2719c 100644
--- a/packages/gatsby/cache-dir/fast-refresh-overlay/utils.js
+++ b/packages/gatsby/cache-dir/fast-refresh-overlay/utils.js
@@ -1,7 +1,5 @@
import Anser from "anser"
-const enterRegex = /^\s$/
-
export function prettifyStack(errorInformation) {
let txt
if (Array.isArray(errorInformation)) {
@@ -9,17 +7,11 @@ export function prettifyStack(errorInformation) {
} else {
txt = errorInformation
}
- const generated = Anser.ansiToJson(txt, {
+ return Anser.ansiToJson(txt, {
remove_empty: true,
use_classes: true,
json: true,
})
- // Sometimes the first line/entry is an "Enter", so we need to filter this out
- const [firstLine, ...rest] = generated
- if (enterRegex.test(firstLine.content)) {
- return rest
- }
- return generated
}
export function openInEditor(file, lineNumber = 1) {
@@ -31,6 +23,18 @@ export function openInEditor(file, lineNumber = 1) {
)
}
+export function reloadPage() {
+ window.location.reload()
+}
+
+export function skipSSR() {
+ if (`URLSearchParams` in window) {
+ const searchParams = new URLSearchParams(window.location.search)
+ searchParams.set(`skip-ssr`, `true`)
+ window.location.search = searchParams.toString()
+ }
+}
+
export function getCodeFrameInformation(stackTrace) {
const callSite = stackTrace.find(CallSite => CallSite.getFileName())
if (!callSite) {
diff --git a/packages/gatsby/cache-dir/ssr-develop-static-entry.js b/packages/gatsby/cache-dir/ssr-develop-static-entry.js
index 38ded4c3966d6..70d1b78b004dd 100644
--- a/packages/gatsby/cache-dir/ssr-develop-static-entry.js
+++ b/packages/gatsby/cache-dir/ssr-develop-static-entry.js
@@ -47,7 +47,7 @@ try {
Html = Html && Html.__esModule ? Html.default : Html
-export default (pagePath, isClientOnlyPage, publicDir, callback) => {
+export default (pagePath, isClientOnlyPage, publicDir, error, callback) => {
let bodyHtml = ``
let headComponents = [
,
@@ -58,6 +58,32 @@ export default (pagePath, isClientOnlyPage, publicDir, callback) => {
let postBodyComponents = []
let bodyProps = {}
+ if (error) {
+ postBodyComponents.push([
+ ,
+
+ Failed to Server Render (SSR)
+ Error message:
+ {error.sourceMessage}
+ File:
+
+ {error.source}:{error.line}:{error.column}
+
+ Stack:
+
+ {error.stack}
+
+ ,
+ ])
+ }
+
const generateBodyHTML = () => {
const setHeadComponents = components => {
headComponents = headComponents.concat(components)
diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json
index 4aa80d4eec49f..6933039f2845e 100644
--- a/packages/gatsby/package.json
+++ b/packages/gatsby/package.json
@@ -27,7 +27,6 @@
"@typescript-eslint/parser": "^4.15.2",
"address": "1.1.2",
"anser": "^2.0.1",
- "ansi-html": "^0.0.7",
"autoprefixer": "^10.2.4",
"axios": "^0.21.1",
"babel-loader": "^8.2.2",
diff --git a/packages/gatsby/src/commands/build-html.ts b/packages/gatsby/src/commands/build-html.ts
index 9c4de6eb86485..8746be8ac00cb 100644
--- a/packages/gatsby/src/commands/build-html.ts
+++ b/packages/gatsby/src/commands/build-html.ts
@@ -58,7 +58,10 @@ const runWebpack = (
stage: Stage,
directory,
parentSpan?: Span
-): Bluebird<{ stats: webpack.Stats; waitForCompilerClose: Promise }> =>
+): Bluebird<{
+ stats: webpack.Stats | undefined
+ waitForCompilerClose: Promise
+}> =>
new Bluebird((resolve, reject) => {
if (!process.env.GATSBY_EXPERIMENTAL_DEV_SSR || stage === `build-html`) {
const compiler = webpack(compilerConfig)
@@ -96,7 +99,7 @@ const runWebpack = (
stage === `develop-html`
) {
devssrWebpackCompiler = webpack(compilerConfig)
- devssrWebpackCompiler.hooks.invalid.tap(`ssr file invalidation`, file => {
+ devssrWebpackCompiler.hooks.invalid.tap(`ssr file invalidation`, () => {
needToRecompileSSRBundle = true
})
devssrWebpackWatcher = devssrWebpackCompiler.watch(
@@ -111,7 +114,7 @@ const runWebpack = (
if (err) {
return reject(err)
} else {
- newHash = stats.hash || ``
+ newHash = stats?.hash || ``
const {
restartWorker,
@@ -142,17 +145,17 @@ const doBuildRenderer = async (
directory,
parentSpan
)
- if (stats.hasErrors()) {
+ if (stats?.hasErrors()) {
reporter.panic(structureWebpackErrors(stage, stats.compilation.errors))
}
if (
stage === `build-html` &&
- store.getState().html.ssrCompilationHash !== stats.hash
+ store.getState().html.ssrCompilationHash !== stats?.hash
) {
store.dispatch({
type: `SET_SSR_WEBPACK_COMPILATION_HASH`,
- payload: stats.hash,
+ payload: stats?.hash,
})
}
diff --git a/packages/gatsby/src/utils/dev-ssr/__tests__/develop-html-route.ts b/packages/gatsby/src/utils/dev-ssr/__tests__/develop-html-route.ts
deleted file mode 100644
index d80c46b574c22..0000000000000
--- a/packages/gatsby/src/utils/dev-ssr/__tests__/develop-html-route.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import * as os from "os"
-import { parseError } from "../render-dev-html-child"
-import error from "./fixtures/error-object"
-
-describe(`error parsing`, () => {
- it(`returns an object w/ the parsed error & codeframe`, () => {
- // stack traces have real paths so \\, the fixture does not have them
- if (os.platform() === `win32`) {
- error.stack = error.stack.replace(/\//g, `\\`)
- }
-
- const parsedError = parseError({
- err: error,
- directory: __dirname,
- componentPath: __filename,
- })
-
- expect(parsedError).toMatchObject({
- filename: `fixtures/blog-post.js`,
- message: `window is not defined`,
- type: `ReferenceError`,
- stack: expect.any(String),
- })
- })
-})
diff --git a/packages/gatsby/src/utils/dev-ssr/__tests__/fixtures/error-object.js b/packages/gatsby/src/utils/dev-ssr/__tests__/fixtures/error-object.js
deleted file mode 100644
index 2bd7b3325ca29..0000000000000
--- a/packages/gatsby/src/utils/dev-ssr/__tests__/fixtures/error-object.js
+++ /dev/null
@@ -1,33 +0,0 @@
-module.exports = {
- stack: `ReferenceError: window is not defined
- at BlogPostRoute.render (/programs/blog/public/webpack:/lib/fixtures/blog-post.js:16:17)
- at processChild (/programs/blog/node_modules/react-dom/cjs/react-dom-server.node.development.js:3134:18)
- at resolve (/programs/blog/node_modules/react-dom/cjs/react-dom-server.node.development.js:2960:5)
- at ReactDOMServerRenderer.render (/programs/blog/node_modules/react-dom/cjs/react-dom-server.node.development.js:3435:22)
- at ReactDOMServerRenderer.read (/programs/blog/node_modules/react-dom/cjs/react-dom-server.node.development.js:3373:29)
- at renderToString (/programs/blog/node_modules/react-dom/cjs/react-dom-server.node.development.js:3988:27)
- at Module.default (/programs/blog/public/webpack:/lib/.cache/develop-static-entry.js:248:32)
- at /programs/blog/node_modules/gatsby/src/utils/worker/render-html.ts:32:11
- at /programs/blog/node_modules/gatsby/src/utils/worker/render-html.ts:25:7
-From previous event:
- at renderHTML (/programs/blog/node_modules/gatsby/src/utils/worker/render-html.ts:22:18)
- at /programs/blog/node_modules/gatsby/src/utils/develop-html-route.ts:140:36
- at Layer.handle [as handle_request] (/programs/blog/node_modules/express/lib/router/layer.js:95:5)
- at next (/programs/blog/node_modules/express/lib/router/route.js:137:13)
- at Route.dispatch (/programs/blog/node_modules/express/lib/router/route.js:112:3)
- at Layer.handle [as handle_request] (/programs/blog/node_modules/express/lib/router/layer.js:95:5)
- at /programs/blog/node_modules/express/lib/router/index.js:281:22
- at param (/programs/blog/node_modules/express/lib/router/index.js:354:14)
- at param (/programs/blog/node_modules/express/lib/router/index.js:365:14)
- at Function.process_params (/programs/blog/node_modules/express/lib/router/index.js:410:3)
- at next (/programs/blog/node_modules/express/lib/router/index.js:275:10)
- at cors (/programs/blog/node_modules/cors/lib/index.js:188:7)
- at /programs/blog/node_modules/cors/lib/index.js:224:17
- at originCallback (/programs/blog/node_modules/cors/lib/index.js:214:15)
- at /programs/blog/node_modules/cors/lib/index.js:219:13
- at optionsCallback (/programs/blog/node_modules/cors/lib/index.js:199:9)
- at corsMiddleware (/programs/blog/node_modules/cors/lib/index.js:204:7)
- at Layer.handle [as handle_request] (/programs/blog/node_modules/express/lib/router/layer.js:95:5)`,
- message: `window is not defined`,
- type: `ReferenceError`,
-}
diff --git a/packages/gatsby/src/utils/dev-ssr/develop-html-route.ts b/packages/gatsby/src/utils/dev-ssr/develop-html-route.ts
deleted file mode 100644
index 4a3b8f2596673..0000000000000
--- a/packages/gatsby/src/utils/dev-ssr/develop-html-route.ts
+++ /dev/null
@@ -1,199 +0,0 @@
-import report from "gatsby-cli/lib/reporter"
-import { trackFeatureIsUsed } from "gatsby-telemetry"
-
-import { findPageByPath } from "../find-page-by-path"
-import { renderDevHTML } from "./render-dev-html"
-import { appendPreloadHeaders } from "../develop-preload-headers"
-
-export const route = ({ app, program, store }): any =>
- // Render an HTML page and serve it.
- app.get(`*`, async (req, res, next) => {
- trackFeatureIsUsed(`GATSBY_EXPERIMENTAL_DEV_SSR`)
-
- const pathObj = findPageByPath(store.getState(), decodeURI(req.path))
-
- if (!pathObj) {
- return next()
- }
-
- await appendPreloadHeaders(pathObj.path, res)
-
- const htmlActivity = report.phantomActivity(`building HTML for path`, {})
- htmlActivity.start()
-
- try {
- const renderResponse = await renderDevHTML({
- path: pathObj.path,
- page: pathObj,
- skipSsr: req.query[`skip-ssr`] || false,
- store,
- htmlComponentRendererPath: `${program.directory}/public/render-page.js`,
- directory: program.directory,
- })
- res.status(200).send(renderResponse)
- } catch (error) {
- // THe page errored but couldn't read the page component.
- // This is a race condition when a page is deleted but its requested
- // immediately after before anything can recompile.
- if (error === `404 page`) {
- return next()
- }
-
- report.error({
- id: `11614`,
- filePath: error.filename,
- location: {
- start: {
- line: error.line,
- column: error.row,
- },
- },
- context: {
- path: pathObj.path,
- filePath: error.filename,
- line: error.line,
- column: error.row,
- },
- })
- let errorHtml = `
- Develop SSR Error
-
-
- Error
- The page didn't server render (SSR) correctly
-
- React components in Gatsby must render successfully in the browser and in a
- node.js environment. When we tried to render your page component in
- node.js, it errored.
-
-
- URL path: ${pathObj.path}
- File path: ${error.filename}
-
- error
- ${error.message}
- `
-
- if (error.codeFrame) {
- errorHtml += `${error.codeFrame} `
- }
-
- // Add link to help page
- errorHtml += `
- For help debugging SSR errors, see this docs page: https://www.gatsbyjs.com/docs/debugging-html-builds/
`
-
- // Add skip ssr button
- errorHtml += `
- Skip SSR
-
- If you don't wish to fix the SSR error at the moment, press the
- button below to reload the page without attempting SSR
-
- Note : this error will show up in when you build your site so must be fixed before then.
-
- Caveat : SSR errors in module scope i.e. outside of your components can't be skipped so will need fixed before you can continue
- Skip SSR
-
- `
- res.status(500).send(errorHtml)
- }
-
- htmlActivity.end()
-
- // Make eslint happy
- return null
- })
diff --git a/packages/gatsby/src/utils/dev-ssr/render-dev-html-child.js b/packages/gatsby/src/utils/dev-ssr/render-dev-html-child.js
index 04dd05732c1b1..d9d36e65a7c9d 100644
--- a/packages/gatsby/src/utils/dev-ssr/render-dev-html-child.js
+++ b/packages/gatsby/src/utils/dev-ssr/render-dev-html-child.js
@@ -1,14 +1,12 @@
require(`source-map-support`).install()
-const { codeFrameColumns } = require(`@babel/code-frame`)
-const ansiHTML = require(`ansi-html`)
-const fs = require(`fs-extra`)
const sysPath = require(`path`)
+const fs = require(`fs-extra`)
const { slash } = require(`gatsby-core-utils`)
const getPosition = function (stackObject) {
let filename
let line
- let row
+ let column
// Because the JavaScript error stack has not yet been standardized,
// wrap the stack parsing in a try/catch for a soft fail if an
// unexpected stack is encountered.
@@ -27,29 +25,18 @@ const getPosition = function (stackObject) {
const splitLength = splitLine.length
filename = splitLine[splitLength - 3]
line = Number(splitLine[splitLength - 2])
- row = Number(splitLine[splitLength - 1])
+ column = Number(splitLine[splitLength - 1])
} catch (err) {
filename = ``
line = 0
- row = 0
+ column = 0
}
return {
filename,
line,
- row,
+ column,
}
}
-// Colors taken from Gatsby's design tokens
-// https://github.com/gatsbyjs/gatsby/blob/d8acab3a135fa8250a0eb3a47c67300dde6eae32/packages/gatsby-design-tokens/src/colors.js#L185-L205
-const colors = {
- background: `fdfaf6`,
- text: `452475`,
- green: `137886`,
- darkGreen: `006500`,
- comment: `527713`,
- keyword: `096fb3`,
- yellow: `DB3A00`,
-}
// Code borrowed and modified from https://github.com/watilde/parse-error
const parseError = function ({ err, directory, componentPath }) {
@@ -65,9 +52,12 @@ const parseError = function ({ err, directory, componentPath }) {
...position.filename.split(sysPath.sep).slice(2)
)
- const splitMessage = err.message ? err.message.split(`\n`) : [``]
- const message = splitMessage[splitMessage.length - 1]
- const type = err.type ? err.type : err.name
+ let sourceContent
+ try {
+ sourceContent = fs.readFileSync(filename, `utf-8`)
+ } catch (e) {
+ sourceContent = null
+ }
// We prefer the file path from the stack trace as the error might not be in the
// component — but if source-maps fail and we just get public/render-page.js as
@@ -76,51 +66,15 @@ const parseError = function ({ err, directory, componentPath }) {
const trueFileName = filename.includes(`render-page`)
? componentPath
: filename
- const data = {
+
+ return {
filename: slash(sysPath.relative(directory, trueFileName)),
- message: message,
- type: type,
+ sourceContent,
+ message: err.message,
stack: stack,
+ line: position.line,
+ column: position.column,
}
-
- // Try to generate a codeFrame
- try {
- const code = fs.readFileSync(filename, `utf-8`)
- const line = position.line
- const row = position.row
- ansiHTML.setColors({
- reset: [colors.text, `ffffff`], // [FOREGROUND-COLOR, BACKGROUND-COLOR]
- black: `aaa`, // String
- red: colors.keyword,
- green: colors.green,
- yellow: colors.yellow,
- blue: `eee`,
- magenta: `fff`,
- cyan: colors.darkGreen,
- lightgrey: `888`,
- darkgrey: colors.comment,
- })
- const codeFrame = ansiHTML(
- codeFrameColumns(
- code,
- {
- start: { line: line, column: row },
- },
- { forceColor: true }
- )
- )
-
- data.line = line
- data.row = row
- data.codeFrame = codeFrame
- } catch (e) {
- console.log(
- `Couldn't read the file ${filename}, possibly due to source maps failing`
- )
- console.log(`original error`, err)
- }
-
- return data
}
exports.parseError = parseError
@@ -131,6 +85,7 @@ exports.renderHTML = ({
htmlComponentRendererPath,
publicDir,
isClientOnlyPage = false,
+ error = undefined,
directory,
}) =>
new Promise((resolve, reject) => {
@@ -141,6 +96,7 @@ exports.renderHTML = ({
path,
isClientOnlyPage,
publicDir,
+ error,
(_throwAway, htmlString) => {
resolve(htmlString)
}
@@ -151,23 +107,8 @@ exports.renderHTML = ({
})
}
} catch (err) {
- const stack = err.stack ? err.stack : ``
-
- // Only generate error pages for webpack errors. If it's not a webpack
- // error, it's not a user error so probably a system error so we'll just
- // panic and quit.
- const regex = /webpack:[/\\]/gm
- const moduleBuildFailed = /Module.build.failed/gm
- if (stack.match(moduleBuildFailed)) {
- reject(`500 page`)
- } else if (!stack.match(regex)) {
- console.log(`unexpected error while SSRing the path: ${path}`)
- console.log(err)
- reject(err)
- } else {
- const error = parseError({ err, directory, componentPath })
- reject(error)
- }
+ const error = parseError({ err, directory, componentPath })
+ reject(error)
}
})
diff --git a/packages/gatsby/src/utils/dev-ssr/render-dev-html.ts b/packages/gatsby/src/utils/dev-ssr/render-dev-html.ts
index a8640cd81c62e..17e31cfc9ee67 100644
--- a/packages/gatsby/src/utils/dev-ssr/render-dev-html.ts
+++ b/packages/gatsby/src/utils/dev-ssr/render-dev-html.ts
@@ -3,15 +3,15 @@ import fs from "fs-extra"
import nodePath from "path"
import report from "gatsby-cli/lib/reporter"
import { isCI } from "gatsby-core-utils"
-
+import { Stats } from "webpack"
import { startListener } from "../../bootstrap/requires-writer"
import { findPageByPath } from "../find-page-by-path"
import { getPageData as getPageDataExperimental } from "../get-page-data"
import { getDevSSRWebpack } from "../../commands/build-html"
-import { emitter } from "../../redux"
-import { Stats } from "webpack"
+import { emitter, GatsbyReduxStore } from "../../redux"
+import { IGatsbyPage } from "../../redux/types"
-const startWorker = (): any => {
+const startWorker = (): JestWorker => {
const newWorker = new JestWorker(require.resolve(`./render-dev-html-child`), {
exposedMethods: [`renderHTML`, `deleteModuleCache`, `warmup`],
numWorkers: 1,
@@ -20,8 +20,8 @@ const startWorker = (): any => {
env: {
...process.env,
NODE_ENV: isCI() ? `production` : `development`,
- forceColors: true,
- GATSBY_EXPERIMENTAL_DEV_SSR: true,
+ forceColors: `true`,
+ GATSBY_EXPERIMENTAL_DEV_SSR: `true`,
},
},
})
@@ -40,11 +40,11 @@ export const initDevWorkerPool = (): void => {
}
let changeCount = 0
-export const restartWorker = (htmlComponentRendererPath): void => {
+export const restartWorker = (htmlComponentRendererPath: string): void => {
changeCount += 1
// Forking is expensive — each time we re-require the outputted webpack
// file, memory grows ~10 mb — 25 regenerations means ~250mb which seems
- // like an accepatable amount of memory to grow before we reclaim it
+ // like an acceptable amount of memory to grow before we reclaim it
// by rebooting the worker process.
if (changeCount > 25) {
const oldWorker = worker
@@ -57,7 +57,10 @@ export const restartWorker = (htmlComponentRendererPath): void => {
}
}
-const searchFileForString = (substring, filePath): Promise =>
+const searchFileForString = (
+ substring: string,
+ filePath: string
+): Promise =>
new Promise(resolve => {
const escapedSubString = substring.replace(/[.*+?^${}()|[\]\\]/g, `\\$&`)
@@ -86,7 +89,7 @@ const searchFileForString = (substring, filePath): Promise =>
const ensurePathComponentInSSRBundle = async (
page,
directory
-): Promise => {
+): Promise => {
// This shouldn't happen.
if (!page) {
report.panic(`page not found`, page)
@@ -128,14 +131,32 @@ const ensurePathComponentInSSRBundle = async (
return found
}
+interface IRenderDevHtmlProps {
+ path: string
+ page: IGatsbyPage
+ skipSsr?: boolean
+ store: GatsbyReduxStore
+ error?: {
+ codeFrame: string
+ source: string
+ line: number
+ column: number
+ sourceMessage?: string
+ stack?: string
+ }
+ htmlComponentRendererPath: string
+ directory: string
+}
+
export const renderDevHTML = ({
path,
page,
skipSsr = false,
store,
+ error = undefined,
htmlComponentRendererPath,
directory,
-}): Promise =>
+}: IRenderDevHtmlProps): Promise =>
// eslint-disable-next-line no-async-promise-executor
new Promise(async (resolve, reject) => {
startListener()
@@ -229,6 +250,7 @@ export const renderDevHTML = ({
directory,
publicDir,
isClientOnlyPage,
+ error,
})
return resolve(htmlString)
} catch (error) {
diff --git a/packages/gatsby/src/utils/start-server.ts b/packages/gatsby/src/utils/start-server.ts
index 374f954de1914..8dc51732a9302 100644
--- a/packages/gatsby/src/utils/start-server.ts
+++ b/packages/gatsby/src/utils/start-server.ts
@@ -48,6 +48,7 @@ import {
routeLoadingIndicatorRequests,
writeVirtualLoadingIndicatorModule,
} from "./loading-indicator"
+import { renderDevHTML } from "./dev-ssr/render-dev-html"
type ActivityTracker = any // TODO: Replace this with proper type once reporter is typed
@@ -487,9 +488,125 @@ module.exports = {
// Render an HTML page and serve it.
if (process.env.GATSBY_EXPERIMENTAL_DEV_SSR) {
- // Setup HTML route.
- const { route } = require(`./dev-ssr/develop-html-route`)
- route({ app, program, store })
+ app.get(`*`, async (req, res, next) => {
+ telemetry.trackFeatureIsUsed(`GATSBY_EXPERIMENTAL_DEV_SSR`)
+
+ const pathObj = findPageByPath(store.getState(), decodeURI(req.path))
+
+ if (!pathObj) {
+ return next()
+ }
+
+ await appendPreloadHeaders(pathObj.path, res)
+
+ const htmlActivity = report.phantomActivity(`building HTML for path`, {})
+ htmlActivity.start()
+
+ try {
+ const renderResponse = await renderDevHTML({
+ path: pathObj.path,
+ page: pathObj,
+ skipSsr: req.query[`skip-ssr`] || false,
+ store,
+ htmlComponentRendererPath: `${program.directory}/public/render-page.js`,
+ directory: program.directory,
+ })
+ res.status(200).send(renderResponse)
+ } catch (error) {
+ // The page errored but couldn't read the page component.
+ // This is a race condition when a page is deleted but its requested
+ // immediately after before anything can recompile.
+ if (error === `404 page`) {
+ return next()
+ }
+
+ // renderDevHTML throws an error with these information
+ const lineNumber = error?.line as number
+ const columnNumber = error?.column as number
+ const filePath = error?.filename as string
+ const sourceContent = error?.sourceContent as string
+
+ report.error({
+ id: `11614`,
+ context: {
+ path: pathObj.path,
+ filePath: filePath,
+ line: lineNumber,
+ column: columnNumber,
+ },
+ })
+
+ const emptyResponse = {
+ codeFrame: `No codeFrame could be generated`,
+ sourcePosition: null,
+ sourceContent: null,
+ }
+
+ if (!sourceContent || !lineNumber) {
+ res.json(emptyResponse)
+ return null
+ }
+
+ const codeFrame = codeFrameColumns(
+ sourceContent,
+ {
+ start: {
+ line: lineNumber,
+ column: columnNumber ?? 0,
+ },
+ },
+ {
+ highlightCode: true,
+ }
+ )
+
+ const message = {
+ codeFrame,
+ source: filePath,
+ line: lineNumber,
+ column: columnNumber ?? 0,
+ sourceMessage: error?.message,
+ stack: error?.stack,
+ }
+
+ try {
+ // Generate a shell for client-only content -- for the error overlay
+ const clientOnlyShell = await renderDevHTML({
+ path: pathObj.path,
+ page: pathObj,
+ skipSsr: true,
+ store,
+ error: message,
+ htmlComponentRendererPath: `${program.directory}/public/render-page.js`,
+ directory: program.directory,
+ })
+
+ res.send(clientOnlyShell)
+ } catch (e) {
+ report.error({
+ id: `11616`,
+ context: {
+ sourceMessage: e.message,
+ },
+ filePath: e.filename,
+ location: {
+ start: {
+ line: e.line,
+ column: e.column,
+ },
+ },
+ })
+
+ const minimalHTML = `Failed to Server Render (SSR) Failed to Server Render (SSR) Error message: ${e.message}
File: ${e.filename}:${e.line}:${e.column}
Stack: ${e.stack}
`
+
+ res.send(minimalHTML).status(500)
+ }
+ }
+
+ htmlActivity.end()
+
+ return null
+ })
}
if (
@@ -527,10 +644,21 @@ module.exports = {
htmlComponentRendererPath: pageRenderer,
directory: program.directory,
})
- const status = process.env.GATSBY_EXPERIMENTAL_DEV_SSR ? 404 : 200
- res.status(status).send(renderResponse)
+ res.status(404).send(renderResponse)
} catch (e) {
- report.error(e)
+ report.error({
+ id: `11615`,
+ context: {
+ sourceMessage: e.message,
+ },
+ filePath: e.filename,
+ location: {
+ start: {
+ line: e.line,
+ column: e.column,
+ },
+ },
+ })
res.send(e).status(500)
}
} else {