From f21b54faaee5fa1d9f5c1377721333013261c8c3 Mon Sep 17 00:00:00 2001 From: Ty Hopp Date: Fri, 14 Oct 2022 19:31:21 +0800 Subject: [PATCH] feat(gatsby,gatsby-cli): Slice HTML rendering error handling (#36822) --- .../gatsby-cli/__tests__/build-ssr-errors.js | 2 +- .../__tests__/__snapshots__/index.ts.snap | 4 +- .../src/reporter/__tests__/index.ts | 2 +- .../__tests__/construct-error.ts | 2 +- .../src/structured-errors/error-map.ts | 29 ++++++++- packages/gatsby/src/commands/build-html.ts | 34 ++++++++--- .../__tests__/extract-undefined-global.ts | 26 ++++++++ .../src/utils/extract-undefined-global.ts | 14 +++++ .../src/utils/worker/child/render-html.ts | 61 +++++++++++++------ 9 files changed, 140 insertions(+), 34 deletions(-) create mode 100644 packages/gatsby/src/utils/__tests__/extract-undefined-global.ts create mode 100644 packages/gatsby/src/utils/extract-undefined-global.ts diff --git a/integration-tests/gatsby-cli/__tests__/build-ssr-errors.js b/integration-tests/gatsby-cli/__tests__/build-ssr-errors.js index a1a96319c393c..c1f6f4a4fa4a7 100644 --- a/integration-tests/gatsby-cli/__tests__/build-ssr-errors.js +++ b/integration-tests/gatsby-cli/__tests__/build-ssr-errors.js @@ -16,7 +16,7 @@ describe(`gatsby build (SSR errors)`, () => { logs.should.contain(`failed Building static HTML for pages`) logs.should.contain(`ERROR #95312`) logs.should.contain( - `"window" is not available during Server-Side Rendering.` + `"window" is not available during server-side rendering.` ) logs.should.contain( `See our docs page for more info on this error: https://gatsby.dev/debug-html` diff --git a/packages/gatsby-cli/src/reporter/__tests__/__snapshots__/index.ts.snap b/packages/gatsby-cli/src/reporter/__tests__/__snapshots__/index.ts.snap index 73625445e5fd0..ee613448c50ce 100644 --- a/packages/gatsby-cli/src/reporter/__tests__/__snapshots__/index.ts.snap +++ b/packages/gatsby-cli/src/reporter/__tests__/__snapshots__/index.ts.snap @@ -125,12 +125,12 @@ Object { "category": "USER", "code": "95312", "context": Object { - "ref": "navigator", + "undefinedGlobal": "navigator", }, "docsUrl": "https://gatsby.dev/debug-html", "level": "ERROR", "stack": Array [], - "text": "\\"navigator\\" is not available during Server-Side Rendering. Enable \\"DEV_SSR\\" to debug this during \\"gatsby develop\\".", + "text": "\\"navigator\\" is not available during server-side rendering. Enable \\"DEV_SSR\\" to debug this during \\"gatsby develop\\".", } `; diff --git a/packages/gatsby-cli/src/reporter/__tests__/index.ts b/packages/gatsby-cli/src/reporter/__tests__/index.ts index 0d806d60c4e92..ea48500da41e7 100644 --- a/packages/gatsby-cli/src/reporter/__tests__/index.ts +++ b/packages/gatsby-cli/src/reporter/__tests__/index.ts @@ -77,7 +77,7 @@ describe(`report.error`, () => { reporter.error({ id: `95312`, context: { - ref: `navigator`, + undefinedGlobal: `navigator`, }, }) const generatedError = getErrorMessages( diff --git a/packages/gatsby-cli/src/structured-errors/__tests__/construct-error.ts b/packages/gatsby-cli/src/structured-errors/__tests__/construct-error.ts index 365493ba70947..7567c1523b6e0 100644 --- a/packages/gatsby-cli/src/structured-errors/__tests__/construct-error.ts +++ b/packages/gatsby-cli/src/structured-errors/__tests__/construct-error.ts @@ -58,7 +58,7 @@ test(`it constructs an error from the supplied errorMap`, () => { test(`it does not overwrite internal error map`, () => { const error = constructError( - { details: { id: `95312`, context: { ref: `Error!` } } }, + { details: { id: `95312`, context: { undefinedGlobal: `window` } } }, { "95312": { text: (context): string => `Error text is ${context.someProp} `, diff --git a/packages/gatsby-cli/src/structured-errors/error-map.ts b/packages/gatsby-cli/src/structured-errors/error-map.ts index c108cca89bf44..2bcc8e84a4f71 100644 --- a/packages/gatsby-cli/src/structured-errors/error-map.ts +++ b/packages/gatsby-cli/src/structured-errors/error-map.ts @@ -34,7 +34,7 @@ const errors = { }, "95312": { text: (context): string => - `"${context.ref}" is not available during Server-Side Rendering. Enable "DEV_SSR" to debug this during "gatsby develop".`, + `"${context.undefinedGlobal}" is not available during server-side rendering. Enable "DEV_SSR" to debug this during "gatsby develop".`, level: Level.ERROR, docsUrl: `https://gatsby.dev/debug-html`, category: ErrorCategory.USER, @@ -650,6 +650,33 @@ const errors = { // TODO: change domain to gatsbyjs.com when it's released docsUrl: `https://v5.gatsbyjs.com/docs/reference/config-files/actions#createSlice`, }, + "11339": { + text: (context): string => + [ + `Building static HTML failed for slice "${context.sliceName}".`, + `Slice metadata: ${JSON.stringify(context?.sliceData || {}, null, 2)}`, + `Slice props: ${JSON.stringify(context?.sliceProps || {}, null, 2)}`, + ] + .filter(Boolean) + .join(`\n\n`), + level: Level.ERROR, + category: ErrorCategory.USER, + // TODO: change domain to gatsbyjs.com when it's released + docsUrl: `https://v5.gatsbyjs.com/docs/reference/config-files/actions#createSlice`, + }, + "11340": { + text: (context): string => + [ + `Building static HTML failed for slice "${context.sliceName}".`, + `"${context.undefinedGlobal}" is not available during server-side rendering. Enable "DEV_SSR" to debug this during "gatsby develop".`, + ] + .filter(Boolean) + .join(`\n\n`), + level: Level.ERROR, + category: ErrorCategory.USER, + // TODO: change domain to gatsbyjs.com when it's released + docsUrl: `https://v5.gatsbyjs.com/docs/reference/config-files/actions#createSlice`, + }, // node object didn't pass validation "11467": { text: (context): string => diff --git a/packages/gatsby/src/commands/build-html.ts b/packages/gatsby/src/commands/build-html.ts index 57c1ac98430fa..0add33012555a 100644 --- a/packages/gatsby/src/commands/build-html.ts +++ b/packages/gatsby/src/commands/build-html.ts @@ -25,6 +25,7 @@ import type { GatsbyWorkerPool } from "../utils/worker/pool" import { stitchSliceForAPage } from "../utils/slices/stitching" import type { ISlicePropsEntry } from "../utils/worker/child/render-html" import { getPageMode } from "../utils/page-mode" +import { extractUndefinedGlobal } from "../utils/extract-undefined-global" type IActivity = any // TODO @@ -662,15 +663,14 @@ export async function buildHTMLPagesAndDeleteStaleArtifacts({ let id = `95313` // TODO: verify error IDs exist const context = { errorPath: err.context && err.context.path, - ref: ``, + undefinedGlobal: ``, } - const match = err.message.match( - /ReferenceError: (window|document|localStorage|navigator|alert|location) is not defined/i - ) - if (match && match[1]) { + const undefinedGlobal = extractUndefinedGlobal(err) + + if (undefinedGlobal) { id = `95312` - context.ref = match[1] + context.undefinedGlobal = undefinedGlobal } buildHTMLActivityProgress.panic({ @@ -800,8 +800,26 @@ export async function buildSlices({ slices, slicesProps, }) - } catch (e) { - buildHTMLActivityProgress.panic(e) + } catch (err) { + const prettyError = createErrorFromString( + err.stack, + `${htmlComponentRendererPath}.map` + ) + + const undefinedGlobal = extractUndefinedGlobal(err) + + let id = `11339` + + if (undefinedGlobal) { + id = `11340` + err.context.undefinedGlobal = undefinedGlobal + } + + buildHTMLActivityProgress.panic({ + id, + context: err.context, + error: prettyError, + }) } finally { buildHTMLActivityProgress.end() } diff --git a/packages/gatsby/src/utils/__tests__/extract-undefined-global.ts b/packages/gatsby/src/utils/__tests__/extract-undefined-global.ts new file mode 100644 index 0000000000000..ebe6c3907d679 --- /dev/null +++ b/packages/gatsby/src/utils/__tests__/extract-undefined-global.ts @@ -0,0 +1,26 @@ +import { extractUndefinedGlobal } from "../extract-undefined-global" + +const globals = [ + `window`, + `document`, + `localStorage`, + `navigator`, + `alert`, + `location`, +] + +it.each(globals)(`extracts %s`, global => { + const extractedGlobal = extractUndefinedGlobal( + new ReferenceError(`${global} is not defined`) + ) + + expect(extractedGlobal).toEqual(global) +}) + +it(`returns an empty string if no known global found`, () => { + const extractedGlobal = extractUndefinedGlobal( + new ReferenceError(`foo is not defined`) + ) + + expect(extractedGlobal).toEqual(``) +}) diff --git a/packages/gatsby/src/utils/extract-undefined-global.ts b/packages/gatsby/src/utils/extract-undefined-global.ts new file mode 100644 index 0000000000000..dfae26ad4b5b7 --- /dev/null +++ b/packages/gatsby/src/utils/extract-undefined-global.ts @@ -0,0 +1,14 @@ +/** + * Extract undefined global variables used in server context from a reference error. + */ +export function extractUndefinedGlobal(error: ReferenceError): string { + const match = error.message.match( + /(window|document|localStorage|navigator|alert|location) is not defined/i + ) + + if (match && match[1]) { + return match[1] + } + + return `` +} diff --git a/packages/gatsby/src/utils/worker/child/render-html.ts b/packages/gatsby/src/utils/worker/child/render-html.ts index 2f960aba1374d..2b9c9abb270db 100644 --- a/packages/gatsby/src/utils/worker/child/render-html.ts +++ b/packages/gatsby/src/utils/worker/child/render-html.ts @@ -40,7 +40,6 @@ declare global { } } -// Best effort typing the shape of errors we might throw interface IRenderHTMLError extends Error { message: string name: string @@ -465,6 +464,18 @@ export interface ISlicePropsEntry { hasChildren: boolean } +interface IRenderSliceHTMLError extends Error { + message: string + name: string + code?: string + stack?: string + context?: { + sliceName?: string + sliceData: unknown + sliceProps: unknown + } +} + export async function renderSlices({ slices, htmlComponentRendererPath, @@ -495,25 +506,35 @@ export async function renderSlices({ const MAGIC_CHILDREN_STRING = `__DO_NOT_USE_OR_ELSE__` const sliceData = await readSliceData(publicDir, slice.name) - const html = await htmlComponentRenderer.renderSlice({ - slice, - staticQueryContext, - props: { - data: sliceData?.result?.data, - ...(hasChildren ? { children: MAGIC_CHILDREN_STRING } : {}), - ...props, - }, - }) - const split = html.split(MAGIC_CHILDREN_STRING) - - // TODO always generate both for now - let index = 1 - for (const htmlChunk of split) { - await ensureFileContent( - path.join(publicDir, `_gatsby`, `slices`, `${sliceId}-${index}.html`), - htmlChunk - ) - index++ + try { + const html = await htmlComponentRenderer.renderSlice({ + slice, + staticQueryContext, + props: { + data: sliceData?.result?.data, + ...(hasChildren ? { children: MAGIC_CHILDREN_STRING } : {}), + ...props, + }, + }) + const split = html.split(MAGIC_CHILDREN_STRING) + + // TODO always generate both for now + let index = 1 + for (const htmlChunk of split) { + await ensureFileContent( + path.join(publicDir, `_gatsby`, `slices`, `${sliceId}-${index}.html`), + htmlChunk + ) + index++ + } + } catch (err) { + const renderSliceError: IRenderSliceHTMLError = err + renderSliceError.context = { + sliceName, + sliceData, + sliceProps: props, + } + throw renderSliceError } } }