From 8ff6245f44882d891bc6f291363019047cbdb55f Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Mon, 21 Dec 2020 12:01:01 -0800 Subject: [PATCH] feature(gatsby): Pause dev-ssr watching between page loads to avoid slowing down regular develop-js HMR (#28394) * feature(gatsby): Pause dev-ssr watching between page loads to avoid slowing down regular develop-js HMR * update snapshot * Don't double-resolve + add activity for building the SSR bundle * Add timeout for tests to ensure that dev server has time to bundle SSR + remove activity timers as not helpful * Update packages/gatsby/src/commands/build-html.ts Co-authored-by: Michal Piechowiak * fix typo * Don't resume if nothing has changed * Update packages/gatsby/src/commands/build-html.ts Co-authored-by: Michal Piechowiak * Didn't need this Co-authored-by: Michal Piechowiak --- integration-tests/ssr/__tests__/ssr.js | 4 +- integration-tests/ssr/test-output.js | 9 +++++ packages/gatsby/src/commands/build-html.ts | 35 +++++++++++++++++- .../src/utils/dev-ssr/render-dev-html.ts | 37 +++++++++++++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/integration-tests/ssr/__tests__/ssr.js b/integration-tests/ssr/__tests__/ssr.js index 248426f972cec..c09a8923a5bcf 100644 --- a/integration-tests/ssr/__tests__/ssr.js +++ b/integration-tests/ssr/__tests__/ssr.js @@ -9,11 +9,13 @@ describe(`SSR`, () => { expect(html).toMatchSnapshot() }) + test(`dev & build outputs match`, async () => { const childProcess = await execa(`yarn`, [`test-output`]) expect(childProcess.code).toEqual(0) - }) + }, 15000) + 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`) diff --git a/integration-tests/ssr/test-output.js b/integration-tests/ssr/test-output.js index 884cae205fd68..d785a42f32c7e 100644 --- a/integration-tests/ssr/test-output.js +++ b/integration-tests/ssr/test-output.js @@ -42,6 +42,15 @@ ) ) + // Fetch once to trigger re-compilation. + await fetch(`${devSiteBasePath}/${path}`) + + // Then wait for 6 seconds to ensure it's ready to go. + // Otherwise, tests are flaky depending on the speed of the testing machine. + await new Promise(resolve => { + setTimeout(() => resolve(), 6000) + }) + let devStatus = 200 const rawDevHtml = await fetch(`${devSiteBasePath}/${path}`).then(res => { devStatus = res.status diff --git a/packages/gatsby/src/commands/build-html.ts b/packages/gatsby/src/commands/build-html.ts index 972cfe88c42ee..b89ab4a4f62d6 100644 --- a/packages/gatsby/src/commands/build-html.ts +++ b/packages/gatsby/src/commands/build-html.ts @@ -6,6 +6,7 @@ import telemetry from "gatsby-telemetry" import { chunk } from "lodash" import webpack from "webpack" +import { emitter } from "../redux" import webpackConfig from "../utils/webpack.config" import { structureWebpackErrors } from "../utils/webpack-error-utils" @@ -14,6 +15,30 @@ import { IProgram, Stage } from "./types" type IActivity = any // TODO type IWorkerPool = any // TODO +export interface IWebpackWatchingPauseResume extends webpack.Watching { + suspend: () => void + resume: () => void +} + +let devssrWebpackCompiler: webpack.Compiler +let devssrWebpackWatcher: IWebpackWatchingPauseResume +let needToRecompileSSRBundle = true +export const getDevSSRWebpack = (): Record< + IWebpackWatchingPauseResume, + webpack.Compiler, + needToRecompileSSRBundle +> => { + if (process.env.gatsby_executing_command !== `develop`) { + throw new Error(`This function can only be called in development`) + } + + return { + devssrWebpackWatcher, + devssrWebpackCompiler, + needToRecompileSSRBundle, + } +} + let oldHash = `` let newHash = `` const runWebpack = ( @@ -34,11 +59,19 @@ const runWebpack = ( process.env.GATSBY_EXPERIMENTAL_DEV_SSR && stage === `develop-html` ) { - webpack(compilerConfig).watch( + devssrWebpackCompiler = webpack(compilerConfig) + devssrWebpackCompiler.hooks.invalid.tap(`ssr file invalidation`, file => { + needToRecompileSSRBundle = true + }) + devssrWebpackWatcher = devssrWebpackCompiler.watch( { ignored: /node_modules/, }, (err, stats) => { + needToRecompileSSRBundle = false + emitter.emit(`DEV_SSR_COMPILATION_DONE`) + devssrWebpackWatcher.suspend() + if (err) { return reject(err) } else { 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 7424625285164..d549a530b5e10 100644 --- a/packages/gatsby/src/utils/dev-ssr/render-dev-html.ts +++ b/packages/gatsby/src/utils/dev-ssr/render-dev-html.ts @@ -6,6 +6,8 @@ import report from "gatsby-cli/lib/reporter" 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" const startWorker = (): any => { const newWorker = new JestWorker(require.resolve(`./render-dev-html-child`), { @@ -144,6 +146,41 @@ export const renderDevHTML = ({ return reject(`404 page`) } + // Resume the webpack watcher and wait for any compilation necessary to happen. + // We timeout after 1.5s as the user might not care per se about SSR. + // + // We pause and resume so there's no excess webpack activity during normal development. + const { + devssrWebpackCompiler, + devssrWebpackWatcher, + needToRecompileSSRBundle, + } = getDevSSRWebpack() + if ( + devssrWebpackWatcher && + devssrWebpackCompiler && + needToRecompileSSRBundle + ) { + let isResolved = false + await new Promise(resolve => { + function finish(stats: Stats): void { + emitter.off(`DEV_SSR_COMPILATION_DONE`, finish) + if (!isResolved) { + resolve(stats) + } + } + emitter.on(`DEV_SSR_COMPILATION_DONE`, finish) + devssrWebpackWatcher.resume() + // Suspending is just a flag, so it's safe to re-suspend right away + devssrWebpackWatcher.suspend() + + // Timeout after 1.5s. + setTimeout(() => { + isResolved = true + resolve() + }, 1500) + }) + } + // Wait for public/render-page.js to update w/ the page component. const found = await ensurePathComponentInSSRBundle(pageObj, directory)