Skip to content

Commit c2d42ec

Browse files
KyleAMathewsvladar
andauthored
feat(gatsby): Don't crash the build process when in preview mode (#33184)
* feat(gatsby): Don't crash the build process when in preview mode This causes problems to a number of Gatsby Cloud customers. When doing preview builds, there's often nodes that are missing information as the user is still filling out the content form. We do want to crash production builds in this case as we don't want 1/2 formed pages but for preview, we should error but not crash. Fixes this Gatsby Cloud feature request https://gatsby.canny.io/gatsby-cloud/p/errors-shouldnt-necessarily-break-preview The error page is very spartan but perhaps good enough for now. * Add more to error & customize it for Preview * make this conditional * cloud sets booleans as strings... * Better error html message * Revert chunkSize change + make pretty structured errors * Add the error name/stack to the generated error html page * restore breaking prod builds * just need stack * Improve error * fix lint error * Fix typescript errors * remove console.logs * Add page data & component id to preview error page * Update packages/gatsby/src/utils/worker/child/render-html.ts Co-authored-by: Vladimir Razuvaev <[email protected]> * fixes for reviews Co-authored-by: Vladimir Razuvaev <[email protected]>
1 parent 279f470 commit c2d42ec

File tree

3 files changed

+112
-5
lines changed

3 files changed

+112
-5
lines changed

packages/gatsby-cli/src/structured-errors/error-map.ts

+5
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ const errors = {
4747
level: Level.ERROR,
4848
docsUrl: `https://gatsby.dev/debug-html`,
4949
},
50+
"95314": {
51+
text: (context): string => context.errorMessage,
52+
level: Level.ERROR,
53+
docsUrl: `https://gatsby.dev/debug-html`,
54+
},
5055
"98123": {
5156
text: (context): string =>
5257
`${context.stageLabel} failed\n\n${

packages/gatsby/src/commands/build-html.ts

+62-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import { processNodeManifests } from "../utils/node-manifest"
2424
import type { GatsbyWorkerPool } from "../utils/worker/pool"
2525
type IActivity = any // TODO
2626

27+
const isPreview = process.env.GATSBY_IS_PREVIEW === `true`
28+
2729
export interface IBuildArgs extends IProgram {
2830
directory: string
2931
sitePackageJson: PackageJson
@@ -170,6 +172,7 @@ export const deleteRenderer = async (rendererPath: string): Promise<void> => {
170172
}
171173
export interface IRenderHtmlResult {
172174
unsafeBuiltinsUsageByPagePath: Record<string, Array<string>>
175+
previewErrors: Record<string, any>
173176
}
174177

175178
const renderHTMLQueue = async (
@@ -207,6 +210,50 @@ const renderHTMLQueue = async (
207210
sessionId,
208211
})
209212

213+
if (isPreview) {
214+
const htmlRenderMeta = renderHTMLResult as IRenderHtmlResult
215+
const seenErrors = new Set()
216+
const errorMessages = new Map()
217+
await Promise.all(
218+
Object.entries(htmlRenderMeta.previewErrors).map(
219+
async ([pagePath, error]) => {
220+
if (!seenErrors.has(error.stack)) {
221+
errorMessages.set(error.stack, {
222+
pagePaths: [pagePath],
223+
})
224+
seenErrors.add(error.stack)
225+
const prettyError = await createErrorFromString(
226+
error.stack,
227+
`${htmlComponentRendererPath}.map`
228+
)
229+
230+
const errorMessageStr = `${prettyError.stack}${
231+
prettyError.codeFrame ? `\n\n${prettyError.codeFrame}\n` : ``
232+
}`
233+
234+
const errorMessage = errorMessages.get(error.stack)
235+
errorMessage.errorMessage = errorMessageStr
236+
errorMessages.set(error.stack, errorMessage)
237+
} else {
238+
const errorMessage = errorMessages.get(error.stack)
239+
errorMessage.pagePaths.push(pagePath)
240+
errorMessages.set(error.stack, errorMessage)
241+
}
242+
}
243+
)
244+
)
245+
246+
for (const value of errorMessages.values()) {
247+
const errorMessage = `The following page(s) saw this error when building their HTML:\n\n${value.pagePaths
248+
.map(p => `- ${p}`)
249+
.join(`\n`)}\n\n${value.errorMessage}`
250+
reporter.error({
251+
id: `95314`,
252+
context: { errorMessage },
253+
})
254+
}
255+
}
256+
210257
if (stage === `build-html`) {
211258
const htmlRenderMeta = renderHTMLResult as IRenderHtmlResult
212259
store.dispatch({
@@ -321,10 +368,23 @@ export const doBuildPages = async (
321368
error.context.path
322369
}": ${JSON.stringify(truncatedPageData, null, 2)}`
323370

324-
reporter.error(pageDataMessage)
371+
// This is our only error during preview so customize it a bit + add the
372+
// pretty build error.
373+
if (isPreview) {
374+
reporter.error({
375+
id: `95314`,
376+
context: { pageData: pageDataMessage },
377+
error: buildError,
378+
})
379+
} else {
380+
reporter.error(pageDataMessage)
381+
}
325382
}
326383

327-
throw buildError
384+
// Don't crash the builder when we're in preview-mode.
385+
if (!isPreview) {
386+
throw buildError
387+
}
328388
}
329389
}
330390

packages/gatsby/src/utils/worker/child/render-html.ts

+45-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import fs from "fs-extra"
44
import Bluebird from "bluebird"
55
import * as path from "path"
66
import { generateHtmlPath, fixedPagePath } from "gatsby-core-utils"
7+
import { truncate } from "lodash"
78

89
import {
910
readWebpackStats,
@@ -111,6 +112,20 @@ async function getResourcesForTemplate(
111112
return resources
112113
}
113114

115+
const truncateObjStrings = (obj): IPageDataWithQueryResult => {
116+
// Recursively truncate strings nested in object
117+
// These objs can be quite large, but we want to preserve each field
118+
for (const key in obj) {
119+
if (typeof obj[key] === `object` && obj[key] !== null) {
120+
truncateObjStrings(obj[key])
121+
} else if (typeof obj[key] === `string`) {
122+
obj[key] = truncate(obj[key], { length: 250 })
123+
}
124+
}
125+
126+
return obj
127+
}
128+
114129
export const renderHTMLProd = async ({
115130
htmlComponentRendererPath,
116131
paths,
@@ -123,8 +138,10 @@ export const renderHTMLProd = async ({
123138
sessionId: number
124139
}): Promise<IRenderHtmlResult> => {
125140
const publicDir = join(process.cwd(), `public`)
141+
const isPreview = process.env.GATSBY_IS_PREVIEW === `true`
126142

127143
const unsafeBuiltinsUsageByPagePath = {}
144+
const previewErrors = {}
128145

129146
// Check if we need to do setup and cache clearing. Within same session we can reuse memoized data,
130147
// but it's not safe to reuse them in different sessions. Check description of `lastSessionId` for more details
@@ -165,7 +182,7 @@ export const renderHTMLProd = async ({
165182
unsafeBuiltinsUsageByPagePath[pagePath] = unsafeBuiltinsUsage
166183
}
167184

168-
return fs.outputFile(generateHtmlPath(publicDir, pagePath), html)
185+
await fs.outputFile(generateHtmlPath(publicDir, pagePath), html)
169186
} catch (e) {
170187
if (e.unsafeBuiltinsUsage && e.unsafeBuiltinsUsage.length > 0) {
171188
unsafeBuiltinsUsageByPagePath[pagePath] = e.unsafeBuiltinsUsage
@@ -175,13 +192,38 @@ export const renderHTMLProd = async ({
175192
path: pagePath,
176193
unsafeBuiltinsUsageByPagePath,
177194
}
178-
throw e
195+
196+
// If we're in Preview-mode, write out a simple error html file.
197+
if (isPreview) {
198+
const pageData = await readPageData(publicDir, pagePath)
199+
const truncatedPageData = truncateObjStrings(pageData)
200+
201+
const html = `<h1>Preview build error</h1>
202+
<p>There was an error when building the preview page for this page ("${pagePath}").</p>
203+
<h3>Error</h3>
204+
<pre><code>${e.stack}</code></pre>
205+
<h3>Page component id</h3>
206+
<p><code>${pageData.componentChunkName}</code></p>
207+
<h3>Page data</h3>
208+
<pre><code>${JSON.stringify(truncatedPageData, null, 4)}</code></pre>`
209+
210+
await fs.outputFile(generateHtmlPath(publicDir, pagePath), html)
211+
previewErrors[pagePath] = {
212+
e,
213+
message: e.message,
214+
code: e.code,
215+
stack: e.stack,
216+
name: e.name,
217+
}
218+
} else {
219+
throw e
220+
}
179221
}
180222
},
181223
{ concurrency: 2 }
182224
)
183225

184-
return { unsafeBuiltinsUsageByPagePath }
226+
return { unsafeBuiltinsUsageByPagePath, previewErrors }
185227
}
186228

187229
// TODO: remove when DEV_SSR is done

0 commit comments

Comments
 (0)