From 31a23d873d4b1fa76d4c432fcecfe54876c0af1d Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Wed, 17 Feb 2021 09:58:01 +0100 Subject: [PATCH 01/91] fix(conditional-page-builds): track page data and not page query (#29498) * fix(conditional-page-builds): track page-data and not just page query result * test(artifacts): test page-data change (and not just page query result change) --- .../artifacts/__tests__/index.js | 70 +++++++++++++++++++ integration-tests/artifacts/gatsby-node.js | 22 +++++- .../templates/deps-page-query-alternative.js | 19 +++++ .../src/templates/deps-page-query.js | 7 +- .../__tests__/__snapshots__/index.js.snap | 2 +- packages/gatsby/src/redux/reducers/html.ts | 34 ++++----- packages/gatsby/src/redux/types.ts | 4 +- packages/gatsby/src/utils/page-data.ts | 3 + 8 files changed, 141 insertions(+), 20 deletions(-) create mode 100644 integration-tests/artifacts/src/templates/deps-page-query-alternative.js diff --git a/integration-tests/artifacts/__tests__/index.js b/integration-tests/artifacts/__tests__/index.js index 49b2e8d8f80fa..2f05bd5619e76 100644 --- a/integration-tests/artifacts/__tests__/index.js +++ b/integration-tests/artifacts/__tests__/index.js @@ -162,6 +162,63 @@ function assertWebpackBundleChanges({ browser, ssr, runNumber }) { }) } +function assertHTMLCorrectness(runNumber) { + describe(`/page-query-template-change/`, () => { + const expectedComponentChunkName = + runNumber <= 1 + ? `component---src-templates-deps-page-query-js` + : `component---src-templates-deps-page-query-alternative-js` + + let pageDataContent + beforeAll(() => { + pageDataContent = fs.readJsonSync( + path.join( + process.cwd(), + `public`, + `page-data`, + `page-query-template-change`, + `page-data.json` + ) + ) + }) + + it(`html built is using correct template (${ + runNumber <= 1 ? `default` : `alternative` + })`, () => { + const htmlContent = fs.readFileSync( + path.join( + process.cwd(), + `public`, + `page-query-template-change`, + `index.html` + ), + `utf-8` + ) + + expect(htmlContent).toContain( + runNumber <= 1 + ? `

Default template for depPageQuery

` + : `

Alternative template for depPageQuery

` + ) + }) + + it(`page data use correct componentChunkName (${expectedComponentChunkName})`, () => { + expect(pageDataContent.componentChunkName).toEqual( + expectedComponentChunkName + ) + }) + + it(`page query result is the same in every build`, () => { + // this test is only to assert that page query result doesn't change and something else + // cause this html file to rebuild + expect(pageDataContent.result).toEqual({ + data: { depPageQuery: { label: `Stable (always created)` } }, + pageContext: { id: `page-query-template-change` }, + }) + }) + }) +} + beforeAll(done => { fs.removeSync(path.join(__dirname, `__debug__`)) @@ -350,6 +407,8 @@ describe(`First run (baseline)`, () => { // first run - this means bundles changed (from nothing to something) assertWebpackBundleChanges({ browser: true, ssr: true, runNumber }) + + assertHTMLCorrectness(runNumber) }) describe(`Second run (different pages created, data changed)`, () => { @@ -360,6 +419,7 @@ describe(`Second run (different pages created, data changed)`, () => { `/page-query-changing-data-but-not-id/`, `/page-query-dynamic-2/`, `/static-query-result-tracking/should-invalidate/`, + `/page-query-template-change/`, ] const expectedPagesToRemainFromPreviousBuild = [ @@ -434,6 +494,8 @@ describe(`Second run (different pages created, data changed)`, () => { // second run - only data changed and no bundle should have changed assertWebpackBundleChanges({ browser: false, ssr: false, runNumber }) + + assertHTMLCorrectness(runNumber) }) describe(`Third run (js change, all pages are recreated)`, () => { @@ -521,6 +583,8 @@ describe(`Third run (js change, all pages are recreated)`, () => { // third run - we modify module used by both ssr and browser bundle - both bundles should change assertWebpackBundleChanges({ browser: true, ssr: true, runNumber }) + + assertHTMLCorrectness(runNumber) }) describe(`Fourth run (gatsby-browser change - cache get invalidated)`, () => { @@ -605,6 +669,8 @@ describe(`Fourth run (gatsby-browser change - cache get invalidated)`, () => { // Fourth run - we change gatsby-browser, so browser bundle change, // but ssr bundle also change because chunk-map file is changed due to browser bundle change assertWebpackBundleChanges({ browser: true, ssr: true, runNumber }) + + assertHTMLCorrectness(runNumber) }) describe(`Fifth run (.cache is deleted but public isn't)`, () => { @@ -677,6 +743,8 @@ describe(`Fifth run (.cache is deleted but public isn't)`, () => { // Fifth run - because cache was deleted before run - both browser and ssr bundles were "invalidated" (because there was nothing before) assertWebpackBundleChanges({ browser: true, ssr: true, runNumber }) + + assertHTMLCorrectness(runNumber) }) describe(`Sixth run (ssr-only change - only ssr compilation hash changes)`, () => { @@ -770,4 +838,6 @@ describe(`Sixth run (ssr-only change - only ssr compilation hash changes)`, () = // Sixth run - only ssr bundle should change as only file used by ssr was changed assertWebpackBundleChanges({ browser: false, ssr: true, runNumber }) + + assertHTMLCorrectness(runNumber) }) diff --git a/integration-tests/artifacts/gatsby-node.js b/integration-tests/artifacts/gatsby-node.js index afce20acb5b03..ca61039d8d646 100644 --- a/integration-tests/artifacts/gatsby-node.js +++ b/integration-tests/artifacts/gatsby-node.js @@ -49,6 +49,7 @@ exports.sourceNodes = ({ function createNodeHelper(type, nodePartial) { const node = { + template: `default`, ...nodePartial, internal: { type, @@ -65,6 +66,21 @@ exports.sourceNodes = ({ label: `Stable (always created)`, }) + // used to create pages and queried by them + createNodeHelper(`DepPageQuery`, { + id: `page-query-stable-alternative`, + label: `Stable (always created)`, + // this is just so we always create at least one page with alternative template to avoid changing compilation hash (async-requires change) (sigh: we should be able to mark module as template even without any pages to avoid that warning) + template: `alternative`, + }) + + createNodeHelper(`DepPageQuery`, { + id: `page-query-template-change`, + label: `Stable (always created)`, + // use default template in first run, but alternative in next ones + template: runNumber <= 1 ? `default` : `alternative`, + }) + createNodeHelper(`DepPageQuery`, { id: `page-query-changing-but-not-invalidating-html`, label: `Stable (always created)`, @@ -134,6 +150,7 @@ exports.createPages = async ({ actions, graphql }) => { allDepPageQuery { nodes { id + template } } } @@ -142,7 +159,10 @@ exports.createPages = async ({ actions, graphql }) => { for (const depPageQueryNode of data.allDepPageQuery.nodes) { actions.createPage({ path: `/${depPageQueryNode.id}/`, - component: require.resolve(`./src/templates/deps-page-query`), + component: + depPageQueryNode.template === `alternative` + ? require.resolve(`./src/templates/deps-page-query-alternative`) + : require.resolve(`./src/templates/deps-page-query`), context: { id: depPageQueryNode.id, }, diff --git a/integration-tests/artifacts/src/templates/deps-page-query-alternative.js b/integration-tests/artifacts/src/templates/deps-page-query-alternative.js new file mode 100644 index 0000000000000..b9d8bdaad854b --- /dev/null +++ b/integration-tests/artifacts/src/templates/deps-page-query-alternative.js @@ -0,0 +1,19 @@ +import React from "react" +import { graphql } from "gatsby" + +export default function DepPageQueryAlternativePage({ data }) { + return ( + <> +

Alternative template for depPageQuery

+
{JSON.stringify(data, null, 2)}
+ + ) +} + +export const query = graphql` + query($id: String) { + depPageQuery(id: { eq: $id }) { + label + } + } +` diff --git a/integration-tests/artifacts/src/templates/deps-page-query.js b/integration-tests/artifacts/src/templates/deps-page-query.js index 6f4fc1941d8d3..d88cdb8f23fa0 100644 --- a/integration-tests/artifacts/src/templates/deps-page-query.js +++ b/integration-tests/artifacts/src/templates/deps-page-query.js @@ -2,7 +2,12 @@ import React from "react" import { graphql } from "gatsby" export default function DepPageQueryPage({ data }) { - return
{JSON.stringify(data, null, 2)}
+ return ( + <> +

Default template for depPageQuery

+
{JSON.stringify(data, null, 2)}
+ + ) } export const query = graphql` diff --git a/packages/gatsby/src/redux/__tests__/__snapshots__/index.js.snap b/packages/gatsby/src/redux/__tests__/__snapshots__/index.js.snap index 3c0f7b755e9e4..4d9523c081bc0 100644 --- a/packages/gatsby/src/redux/__tests__/__snapshots__/index.js.snap +++ b/packages/gatsby/src/redux/__tests__/__snapshots__/index.js.snap @@ -19,7 +19,7 @@ Object { "/my-sweet-new-page/" => Object { "dirty": 2, "isDeleted": false, - "pageQueryHash": "", + "pageDataHash": "", }, }, "trackedStaticQueryResults": Map {}, diff --git a/packages/gatsby/src/redux/reducers/html.ts b/packages/gatsby/src/redux/reducers/html.ts index fa1a42a934840..153ded86acd6b 100644 --- a/packages/gatsby/src/redux/reducers/html.ts +++ b/packages/gatsby/src/redux/reducers/html.ts @@ -7,7 +7,7 @@ import { const FLAG_DIRTY_CLEARED_CACHE = 0b0000001 const FLAG_DIRTY_NEW_PAGE = 0b0000010 -const FLAG_DIRTY_PAGE_QUERY_CHANGED = 0b0000100 // TODO: this need to be PAGE_DATA and not PAGE_QUERY, but requires some shuffling +const FLAG_DIRTY_PAGE_DATA_CHANGED = 0b0000100 const FLAG_DIRTY_STATIC_QUERY_FIRST_RUN = 0b0001000 const FLAG_DIRTY_STATIC_QUERY_RESULT_CHANGED = 0b0010000 const FLAG_DIRTY_BROWSER_COMPILATION_HASH = 0b0100000 @@ -58,7 +58,7 @@ export function htmlReducer( htmlFile = { dirty: FLAG_DIRTY_NEW_PAGE, isDeleted: false, - pageQueryHash: ``, + pageDataHash: ``, } state.trackedHtmlFiles.set(path, htmlFile) } else if (htmlFile.isDeleted) { @@ -87,20 +87,7 @@ export function htmlReducer( } case `PAGE_QUERY_RUN`: { - if (action.payload.isPage) { - const htmlFile = state.trackedHtmlFiles.get(action.payload.path) - if (!htmlFile) { - // invariant - throw new Error( - `[html reducer] I received event that query for a page finished running, but I'm not aware of the page it ran for (?)` - ) - } - - if (htmlFile.pageQueryHash !== action.payload.resultHash) { - htmlFile.pageQueryHash = action.payload.resultHash - htmlFile.dirty |= FLAG_DIRTY_PAGE_QUERY_CHANGED - } - } else { + if (!action.payload.isPage) { // static query case let staticQueryResult = state.trackedStaticQueryResults.get( action.payload.queryHash @@ -123,6 +110,21 @@ export function htmlReducer( return state } + case `ADD_PAGE_DATA_STATS`: { + const htmlFile = state.trackedHtmlFiles.get(action.payload.pagePath) + if (!htmlFile) { + // invariant + throw new Error( + `[html reducer] I received event that query for a page finished running, but I'm not aware of the page it ran for (?)` + ) + } + + if (htmlFile.pageDataHash !== action.payload.pageDataHash) { + htmlFile.pageDataHash = action.payload.pageDataHash + htmlFile.dirty |= FLAG_DIRTY_PAGE_DATA_CHANGED + } + return state + } case `SET_WEBPACK_COMPILATION_HASH`: { if (state.browserCompilationHash !== action.payload) { diff --git a/packages/gatsby/src/redux/types.ts b/packages/gatsby/src/redux/types.ts index 563d894a63ec9..d466256b24c29 100644 --- a/packages/gatsby/src/redux/types.ts +++ b/packages/gatsby/src/redux/types.ts @@ -167,7 +167,7 @@ export interface IComponentState { export interface IHtmlFileState { dirty: number isDeleted: boolean - pageQueryHash: string // TODO: change to page-data hash + pageDataHash: string } export interface IStaticQueryResultState { @@ -788,8 +788,10 @@ export interface ISetResolvedNodesAction { export interface IAddPageDataStatsAction { type: `ADD_PAGE_DATA_STATS` payload: { + pagePath: string filePath: SystemPath size: number + pageDataHash: string } } diff --git a/packages/gatsby/src/utils/page-data.ts b/packages/gatsby/src/utils/page-data.ts index e69b5c1952c1d..ee50d503d20b3 100644 --- a/packages/gatsby/src/utils/page-data.ts +++ b/packages/gatsby/src/utils/page-data.ts @@ -3,6 +3,7 @@ import fs from "fs-extra" import reporter from "gatsby-cli/lib/reporter" import fastq from "fastq" import path from "path" +import { createContentDigest } from "gatsby-core-utils" import { IGatsbyPage } from "../redux/types" import { websocketManager } from "./websocket-manager" import { isWebpackStatusPending } from "./webpack-status" @@ -100,8 +101,10 @@ export async function writePageData( store.dispatch({ type: `ADD_PAGE_DATA_STATS`, payload: { + pagePath, filePath: outputFilePath, size: pageDataSize, + pageDataHash: createContentDigest(bodyStr), }, }) From a6be92eb80bb98c308310a0fb3c340dab8e104aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Wed, 17 Feb 2021 10:53:53 +0100 Subject: [PATCH 02/91] fix(gatsby-plugin-styled-components): Support `topLevelImportPaths` option (#29544) --- packages/gatsby-plugin-styled-components/src/gatsby-node.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/gatsby-plugin-styled-components/src/gatsby-node.js b/packages/gatsby-plugin-styled-components/src/gatsby-node.js index 2f6815d1f9303..bfc75ecc23a5d 100644 --- a/packages/gatsby-plugin-styled-components/src/gatsby-node.js +++ b/packages/gatsby-plugin-styled-components/src/gatsby-node.js @@ -28,6 +28,10 @@ exports.pluginOptionsSchema = ({ Joi }) => transpileTemplateLiterals: Joi.boolean() .default(true) .description(`Transpile tagged template literals into optimized code.`), + topLevelImportPaths: Joi.array() + .default([]) + .items(Joi.string()) + .description(`Top level import paths allowed to identify library`), pure: Joi.boolean() .default(false) .description( From 1bf8d23ba38ca90d556398fc3df1a3d371dcca7f Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Wed, 17 Feb 2021 11:06:27 +0100 Subject: [PATCH 03/91] chore(build): move html generation to separate function (#29499) * refactor(build): move html building and deleting out to separate function * feat(build): don't create html writing progress bar if there is 0 pages to build --- packages/gatsby/src/commands/build-html.ts | 107 ++++++++++++++++++++- packages/gatsby/src/commands/build.ts | 96 +++--------------- 2 files changed, 121 insertions(+), 82 deletions(-) diff --git a/packages/gatsby/src/commands/build-html.ts b/packages/gatsby/src/commands/build-html.ts index 4f66fd03f4cca..399a736d3dba1 100644 --- a/packages/gatsby/src/commands/build-html.ts +++ b/packages/gatsby/src/commands/build-html.ts @@ -4,12 +4,16 @@ import reporter from "gatsby-cli/lib/reporter" import { createErrorFromString } from "gatsby-cli/lib/reporter/errors" import { chunk } from "lodash" import webpack from "webpack" +import * as path from "path" import { emitter, store } from "../redux" import webpackConfig from "../utils/webpack.config" import { structureWebpackErrors } from "../utils/webpack-error-utils" +import * as buildUtils from "./build-utils" +import { Span } from "opentracing" import { IProgram, Stage } from "./types" +import { PackageJson } from "../.." type IActivity = any // TODO type IWorkerPool = any // TODO @@ -19,6 +23,17 @@ export interface IWebpackWatchingPauseResume extends webpack.Watching { resume: () => void } +export interface IBuildArgs extends IProgram { + directory: string + sitePackageJson: PackageJson + prefixPaths: boolean + noUglify: boolean + profile: boolean + graphqlTracing: boolean + openTracingConfigFile: string + keepPageRenderer: boolean +} + let devssrWebpackCompiler: webpack.Compiler let devssrWebpackWatcher: IWebpackWatchingPauseResume let needToRecompileSSRBundle = true @@ -104,7 +119,7 @@ const doBuildRenderer = async ( } if ( - stage === Stage.BuildHTML && + stage === `build-html` && store.getState().html.ssrCompilationHash !== stats.hash ) { store.dispatch({ @@ -239,3 +254,93 @@ export const buildHTML = async ({ await doBuildPages(rendererPath, pagePaths, activity, workerPool, stage) await deleteRenderer(rendererPath) } + +export async function buildHTMLPagesAndDeleteStaleArtifacts({ + pageRenderer, + workerPool, + buildSpan, + program, +}: { + pageRenderer: string + workerPool: IWorkerPool + buildSpan?: Span + program: IBuildArgs +}): Promise<{ + toRegenerate: Array + toDelete: Array +}> { + buildUtils.markHtmlDirtyIfResultOfUsedStaticQueryChanged() + + const { toRegenerate, toDelete } = process.env + .GATSBY_EXPERIMENTAL_PAGE_BUILD_ON_DATA_CHANGES + ? buildUtils.calcDirtyHtmlFiles(store.getState()) + : { + toRegenerate: [...store.getState().pages.keys()], + toDelete: [], + } + + if (toRegenerate.length > 0) { + const buildHTMLActivityProgress = reporter.createProgress( + `Building static HTML for pages`, + toRegenerate.length, + 0, + { + parentSpan: buildSpan, + } + ) + buildHTMLActivityProgress.start() + try { + await doBuildPages( + pageRenderer, + toRegenerate, + buildHTMLActivityProgress, + workerPool, + Stage.BuildHTML + ) + } catch (err) { + let id = `95313` // TODO: verify error IDs exist + const context = { + errorPath: err.context && err.context.path, + ref: ``, + } + + const match = err.message.match( + /ReferenceError: (window|document|localStorage|navigator|alert|location) is not defined/i + ) + if (match && match[1]) { + id = `95312` + context.ref = match[1] + } + + buildHTMLActivityProgress.panic({ + id, + context, + error: err, + }) + } + buildHTMLActivityProgress.end() + } else { + reporter.info(`There are no new or changed html files to build.`) + } + + if (!program.keepPageRenderer) { + try { + await deleteRenderer(pageRenderer) + } catch (err) { + // pass through + } + } + + if (toDelete.length > 0) { + const publicDir = path.join(program.directory, `public`) + const deletePageDataActivityTimer = reporter.activityTimer( + `Delete previous page data` + ) + deletePageDataActivityTimer.start() + await buildUtils.removePageFiles(publicDir, toDelete) + + deletePageDataActivityTimer.end() + } + + return { toRegenerate, toDelete } +} diff --git a/packages/gatsby/src/commands/build.ts b/packages/gatsby/src/commands/build.ts index cf974699d0b2a..79c99a74827ce 100644 --- a/packages/gatsby/src/commands/build.ts +++ b/packages/gatsby/src/commands/build.ts @@ -4,7 +4,11 @@ import signalExit from "signal-exit" import fs from "fs-extra" import telemetry from "gatsby-telemetry" -import { doBuildPages, buildRenderer, deleteRenderer } from "./build-html" +import { + buildRenderer, + buildHTMLPagesAndDeleteStaleArtifacts, + IBuildArgs, +} from "./build-html" import { buildProductionBundle } from "./build-javascript" import { bootstrap } from "../bootstrap" import apiRunnerNode from "../utils/api-runner-node" @@ -26,8 +30,7 @@ import { import * as buildUtils from "./build-utils" import { actions } from "../redux/actions" import { waitUntilAllJobsComplete } from "../utils/wait-until-jobs-complete" -import { IProgram, Stage } from "./types" -import { PackageJson } from "../.." +import { Stage } from "./types" import { calculateDirtyQueries, runStaticQueries, @@ -40,17 +43,6 @@ import { } from "../utils/webpack-status" import { updateSiteMetadata } from "gatsby-core-utils" -interface IBuildArgs extends IProgram { - directory: string - sitePackageJson: PackageJson - prefixPaths: boolean - noUglify: boolean - profile: boolean - graphqlTracing: boolean - openTracingConfigFile: string - keepPageRenderer: boolean -} - module.exports = async function build(program: IBuildArgs): Promise { report.setVerbose(program.verbose) @@ -202,79 +194,21 @@ module.exports = async function build(program: IBuildArgs): Promise { buildSSRBundleActivityProgress.end() } - buildUtils.markHtmlDirtyIfResultOfUsedStaticQueryChanged() - - const { toRegenerate, toDelete } = process.env - .GATSBY_EXPERIMENTAL_PAGE_BUILD_ON_DATA_CHANGES - ? buildUtils.calcDirtyHtmlFiles(store.getState()) - : { - toRegenerate: [...store.getState().pages.keys()], - toDelete: [], - } + const { + toRegenerate, + toDelete, + } = await buildHTMLPagesAndDeleteStaleArtifacts({ + program, + pageRenderer, + workerPool, + buildSpan, + }) telemetry.addSiteMeasurement(`BUILD_END`, { pagesCount: toRegenerate.length, // number of html files that will be written totalPagesCount: store.getState().pages.size, // total number of pages }) - const buildHTMLActivityProgress = report.createProgress( - `Building static HTML for pages`, - toRegenerate.length, - 0, - { - parentSpan: buildSpan, - } - ) - buildHTMLActivityProgress.start() - try { - await doBuildPages( - pageRenderer, - toRegenerate, - buildHTMLActivityProgress, - workerPool, - Stage.BuildHTML - ) - } catch (err) { - let id = `95313` // TODO: verify error IDs exist - const context = { - errorPath: err.context && err.context.path, - ref: ``, - } - - const match = err.message.match( - /ReferenceError: (window|document|localStorage|navigator|alert|location) is not defined/i - ) - if (match && match[1]) { - id = `95312` - context.ref = match[1] - } - - buildHTMLActivityProgress.panic({ - id, - context, - error: err, - }) - } - buildHTMLActivityProgress.end() - - if (!program.keepPageRenderer) { - try { - await deleteRenderer(pageRenderer) - } catch (err) { - // pass through - } - } - - if (toDelete.length > 0) { - const deletePageDataActivityTimer = report.activityTimer( - `Delete previous page data` - ) - deletePageDataActivityTimer.start() - await buildUtils.removePageFiles(publicDir, toDelete) - - deletePageDataActivityTimer.end() - } - const postBuildActivityTimer = report.activityTimer(`onPostBuild`, { parentSpan: buildSpan, }) From a7a3991ca2ad450abaacdc168e9900dad995b584 Mon Sep 17 00:00:00 2001 From: Ward Peeters Date: Wed, 17 Feb 2021 16:47:41 +0100 Subject: [PATCH 04/91] feat(gatsby): upgrade webpack to version 5 (#29145) Co-authored-by: Michal Piechowiak --- .../limited-exports-page-templates.js | 17 +- .../no-anonymous-exports-page-templates.js | 23 +- .../development-runtime/gatsby-config.js | 3 + .../use-static-query/destructuring.js | 3 +- integration-tests/artifacts/gatsby-browser.js | 14 +- .../gatsby-cli/__tests__/build-errors.js | 4 +- .../gatsby-cli/__tests__/build-ssr-errors.js | 4 +- package.json | 4 +- packages/gatsby-admin/gatsby-node.js | 8 +- packages/gatsby-admin/package.json | 3 +- .../__snapshots__/static-entry.js.snap | 6 +- .../gatsby/cache-dir/create-react-context.js | 26 - .../gatsby/cache-dir/develop-static-entry.js | 1 + .../cache-dir/ssr-develop-static-entry.js | 2 +- packages/gatsby/package.json | 47 +- packages/gatsby/src/commands/build-html.ts | 17 +- .../gatsby/src/commands/build-javascript.ts | 75 +- packages/gatsby/src/commands/build.ts | 9 +- packages/gatsby/src/redux/actions/public.js | 38 + packages/gatsby/src/redux/types.ts | 18 +- .../src/services/start-webpack-server.ts | 53 +- .../__snapshots__/develop-html-route.ts.snap | 47 - .../__snapshots__/webpack-utils.ts.snap | 6 + .../src/utils/__tests__/develop-html-route.ts | 14 - .../map-templates-to-static-query-hashes.js | 78 -- .../utils/__tests__/webpack-error-utils.ts | 20 +- .../src/utils/__tests__/webpack-utils.ts | 4 +- .../dev-ssr/__tests__/develop-html-route.ts | 25 + .../__tests__/fixtures/error-object.js | 0 .../utils/dev-ssr/render-dev-html-child.js | 13 +- .../gatsby/src/utils/eslint-rules-helpers.ts | 4 +- .../map-templates-to-static-query-hashes.ts | 213 --- packages/gatsby/src/utils/start-server.ts | 42 +- .../gatsby/src/utils/webpack-error-utils.ts | 171 ++- packages/gatsby/src/utils/webpack-plugins.ts | 24 +- packages/gatsby/src/utils/webpack-utils.ts | 152 +- packages/gatsby/src/utils/webpack.config.js | 216 +-- .../src/utils/webpack/static-query-mapper.ts | 274 ++++ ...-mini-css-extract-contenthash-overwrite.ts | 67 + .../gatsby/src/utils/worker/render-html.ts | 15 +- yarn.lock | 1221 +++++++++++------ 41 files changed, 1690 insertions(+), 1291 deletions(-) delete mode 100644 packages/gatsby/cache-dir/create-react-context.js delete mode 100644 packages/gatsby/src/utils/__tests__/__snapshots__/develop-html-route.ts.snap delete mode 100644 packages/gatsby/src/utils/__tests__/develop-html-route.ts delete mode 100644 packages/gatsby/src/utils/__tests__/map-templates-to-static-query-hashes.js create mode 100644 packages/gatsby/src/utils/dev-ssr/__tests__/develop-html-route.ts rename packages/gatsby/src/utils/{ => dev-ssr}/__tests__/fixtures/error-object.js (100%) delete mode 100644 packages/gatsby/src/utils/map-templates-to-static-query-hashes.ts create mode 100644 packages/gatsby/src/utils/webpack/static-query-mapper.ts create mode 100644 packages/gatsby/src/utils/webpack/tmp-mini-css-extract-contenthash-overwrite.ts diff --git a/e2e-tests/development-runtime/cypress/integration/eslint-rules/limited-exports-page-templates.js b/e2e-tests/development-runtime/cypress/integration/eslint-rules/limited-exports-page-templates.js index 3464d8de60a7a..5c32d2bd47cb6 100644 --- a/e2e-tests/development-runtime/cypress/integration/eslint-rules/limited-exports-page-templates.js +++ b/e2e-tests/development-runtime/cypress/integration/eslint-rules/limited-exports-page-templates.js @@ -1,14 +1,21 @@ if (Cypress.env("HOT_LOADER") === `fast-refresh`) { describe(`limited-exports-page-templates`, () => { - it(`should log warning to console for invalid export`, () => { + // Skipped because HMR not show warnings because of https://github.com/webpack-contrib/webpack-hot-middleware/pull/397 + it.skip(`should log warning to console for invalid export`, () => { cy.visit(`/eslint-rules/limited-exports-page-templates`, { onBeforeLoad(win) { - cy.stub(win.console, 'log').as(`consoleLog`) - } + cy.stub(win.console, "log").as(`consoleLog`) + }, }).waitForRouteChange() - cy.get(`@consoleLog`).should(`be.calledWithMatch`, /15:1 warning In page templates only a default export of a valid React component and the named export of a page query is allowed./i) - cy.get(`@consoleLog`).should(`not.be.calledWithMatch`, /17:1 warning In page templates only a default export of a valid React component and the named export of a page query is allowed./i) + cy.get(`@consoleLog`).should( + `be.calledWithMatch`, + /15:1 warning In page templates only a default export of a valid React component and the named export of a page query is allowed./i + ) + cy.get(`@consoleLog`).should( + `not.be.calledWithMatch`, + /17:1 warning In page templates only a default export of a valid React component and the named export of a page query is allowed./i + ) }) }) } diff --git a/e2e-tests/development-runtime/cypress/integration/eslint-rules/no-anonymous-exports-page-templates.js b/e2e-tests/development-runtime/cypress/integration/eslint-rules/no-anonymous-exports-page-templates.js index 53aacb74fea8b..04d7a521b5d87 100644 --- a/e2e-tests/development-runtime/cypress/integration/eslint-rules/no-anonymous-exports-page-templates.js +++ b/e2e-tests/development-runtime/cypress/integration/eslint-rules/no-anonymous-exports-page-templates.js @@ -1,22 +1,29 @@ if (Cypress.env("HOT_LOADER") === `fast-refresh`) { describe(`no-anonymous-exports-page-templates`, () => { - it(`should log warning to console for arrow functions`, () => { + // Skipped because HMR not show warnings because of https://github.com/webpack-contrib/webpack-hot-middleware/pull/397 + it.skip(`should log warning to console for arrow functions`, () => { cy.visit(`/eslint-rules/no-anonymous-exports-page-templates`, { onBeforeLoad(win) { - cy.stub(win.console, 'log').as(`consoleLog`) - } + cy.stub(win.console, "log").as(`consoleLog`) + }, }).waitForRouteChange() - cy.get(`@consoleLog`).should(`be.calledWithMatch`, /Anonymous arrow functions cause Fast Refresh to not preserve local component state./i) + cy.get(`@consoleLog`).should( + `be.calledWithMatch`, + /Anonymous arrow functions cause Fast Refresh to not preserve local component state./i + ) }) - it(`should log warning to console for function declarations`, () => { + it.skip(`should log warning to console for function declarations`, () => { cy.visit(`/eslint-rules/no-anonymous-exports-page-templates-function`, { onBeforeLoad(win) { - cy.stub(win.console, 'log').as(`consoleLog`) - } + cy.stub(win.console, "log").as(`consoleLog`) + }, }).waitForRouteChange() - cy.get(`@consoleLog`).should(`be.calledWithMatch`, /Anonymous function declarations cause Fast Refresh to not preserve local component state./i) + cy.get(`@consoleLog`).should( + `be.calledWithMatch`, + /Anonymous function declarations cause Fast Refresh to not preserve local component state./i + ) }) }) } diff --git a/e2e-tests/development-runtime/gatsby-config.js b/e2e-tests/development-runtime/gatsby-config.js index 907d2f3e3c910..186b0e809a2c0 100644 --- a/e2e-tests/development-runtime/gatsby-config.js +++ b/e2e-tests/development-runtime/gatsby-config.js @@ -6,6 +6,9 @@ module.exports = { twitter: `kylemathews`, }, }, + flags: { + DEV_SSR: false, + }, plugins: [ `gatsby-plugin-react-helmet`, { diff --git a/e2e-tests/development-runtime/src/components/static-query/use-static-query/destructuring.js b/e2e-tests/development-runtime/src/components/static-query/use-static-query/destructuring.js index eb068622e0dac..05f692c214866 100644 --- a/e2e-tests/development-runtime/src/components/static-query/use-static-query/destructuring.js +++ b/e2e-tests/development-runtime/src/components/static-query/use-static-query/destructuring.js @@ -12,7 +12,7 @@ function DestructuringQuery(props) { return (
    {plugins.map(plugin => ( -
  • +
  • {plugin.name}: {plugin.version}
  • ))} @@ -25,6 +25,7 @@ const variableQuery = graphql` allSitePlugin { edges { node { + id name version pluginFilepath diff --git a/integration-tests/artifacts/gatsby-browser.js b/integration-tests/artifacts/gatsby-browser.js index ac57b42fe1cc4..af169d50fce67 100644 --- a/integration-tests/artifacts/gatsby-browser.js +++ b/integration-tests/artifacts/gatsby-browser.js @@ -2,12 +2,14 @@ const React = require(`react`) const { useMoreInfoQuery } = require("./src/hooks/use-more-info-query") const Github = require(`./src/components/github`).default -exports.wrapRootElement = ({ element }) => ( - <> - - {element} - -) +exports.wrapRootElement = ({ element }) => { + return ( + <> + + {element} + + ) +} function PageWrapper({ children }) { const data = useMoreInfoQuery() diff --git a/integration-tests/gatsby-cli/__tests__/build-errors.js b/integration-tests/gatsby-cli/__tests__/build-errors.js index 3578d1d188aab..6d5fec5880d8e 100644 --- a/integration-tests/gatsby-cli/__tests__/build-errors.js +++ b/integration-tests/gatsby-cli/__tests__/build-errors.js @@ -34,7 +34,9 @@ describe(`gatsby build (errors)`, () => { `WebpackError: TypeError: Cannot read property 'bar' of null` ) logs.should.contain(`- index.js:5`) - logs.should.contain(` src/pages/index.js:5:5`) + logs.should.contain( + ` gatsby-starter-default-build-errors/src/pages/index.js:5:5` + ) expect(code).not.toBe(0) }) diff --git a/integration-tests/gatsby-cli/__tests__/build-ssr-errors.js b/integration-tests/gatsby-cli/__tests__/build-ssr-errors.js index b942bafd3feb1..d6c5a13758341 100644 --- a/integration-tests/gatsby-cli/__tests__/build-ssr-errors.js +++ b/integration-tests/gatsby-cli/__tests__/build-ssr-errors.js @@ -35,7 +35,9 @@ describe(`gatsby build (SSR errors)`, () => { // // Stack trace logs.should.contain(`WebpackError: ReferenceError: window is not defined`) logs.should.contain(`- gatsby-ssr.js:3`) - logs.should.contain(` gatsby-ssr.js:3:3`) + logs.should.contain( + ` gatsby-starter-default-build-ssr-errors/gatsby-ssr.js:3:3` + ) expect(code).not.toBe(0) }) diff --git a/package.json b/package.json index 0a97c408b6e37..a48d10eed274b 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "@types/bluebird": "^3.5.33", "@types/cache-manager": "^2.10.3", "@types/common-tags": "^1.8.0", - "@types/eslint": "^6.8.1", "@types/express": "^4.17.3", "@types/fs-extra": "^8.1.1", "@types/got": "^9.6.11", @@ -25,7 +24,6 @@ "@types/semver": "^7.3.4", "@types/signal-exit": "^3.0.0", "@types/stack-trace": "^0.0.29", - "@types/webpack": "^4.41.24", "@types/webpack-merge": "^4.1.5", "@typescript-eslint/eslint-plugin": "^2.34.0", "@typescript-eslint/parser": "^2.34.0", @@ -161,4 +159,4 @@ "workspaces": [ "packages/*" ] -} +} \ No newline at end of file diff --git a/packages/gatsby-admin/gatsby-node.js b/packages/gatsby-admin/gatsby-node.js index a82ea6cf8c268..f8701b58a67a8 100644 --- a/packages/gatsby-admin/gatsby-node.js +++ b/packages/gatsby-admin/gatsby-node.js @@ -1,7 +1,9 @@ exports.onCreateWebpackConfig = ({ actions }) => { actions.setWebpackConfig({ - node: { - fs: 'empty' + resolve: { + alias: { + path: require.resolve("path-browserify") + } } }) - } \ No newline at end of file + } diff --git a/packages/gatsby-admin/package.json b/packages/gatsby-admin/package.json index 1c6036afd1dd0..204e6b7e1b4ea 100644 --- a/packages/gatsby-admin/package.json +++ b/packages/gatsby-admin/package.json @@ -29,6 +29,7 @@ "ncp": "^2.0.0", "nodemon": "^2.0.7", "normalize.css": "^8.0.1", + "path-browserify": "^1.0.1", "prism-react-renderer": "^1.1.1", "query-string": "^6.13.8", "react": "^16.12.0", @@ -55,4 +56,4 @@ "postbuild": "ncp public ../gatsby/gatsby-admin-public", "watch": "nodemon --watch src --ext js,ts,tsx,json --exec \"yarn run build\"" } -} +} \ 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 1a2b6021ebb85..3cf65cc12c462 100644 --- a/packages/gatsby/cache-dir/__tests__/__snapshots__/static-entry.js.snap +++ b/packages/gatsby/cache-dir/__tests__/__snapshots__/static-entry.js.snap @@ -6,11 +6,11 @@ exports[`develop-static-entry SSR: onPreRenderHTML can be used to replace postBo 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 headComponents 1`] = `"
    "`; -exports[`develop-static-entry onPreRenderHTML can be used to replace postBodyComponents 1`] = `"
    div3
    div2
    div1
    "`; +exports[`develop-static-entry onPreRenderHTML can be used to replace postBodyComponents 1`] = `"
    div3
    div2
    div1
    "`; -exports[`develop-static-entry onPreRenderHTML can be used to replace preBodyComponents 1`] = `"
    div3
    div2
    div1
    "`; +exports[`develop-static-entry onPreRenderHTML can be used to replace preBodyComponents 1`] = `"
    div3
    div2
    div1
    "`; exports[`static-entry onPreRenderHTML can be used to replace headComponents 1`] = `"
    "`; diff --git a/packages/gatsby/cache-dir/create-react-context.js b/packages/gatsby/cache-dir/create-react-context.js deleted file mode 100644 index 8e4f972d134c0..0000000000000 --- a/packages/gatsby/cache-dir/create-react-context.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - Why commonjs and not ES imports/exports? - - This module is used to alias `create-react-context` package, but drop the the actual implementation part - because Gatsby requires version of react that has implementatoin baked in. - - Package source is using ES modules: - - https://github.com/jamiebuilds/create-react-context/blob/v0.3.0/src/index.js - - But to build this package `babel-plugin-add-module-exports` is used ( https://www.npmjs.com/package/babel-plugin-add-module-exports). - Which result in both `module.exports` and `exports.default` being set to same thing. - - We don't use that babel plugin so we only have `exports.default`. - - This cause problems in various 3rd party react components that rely on `module.exports` being set. - See https://github.com/gatsbyjs/gatsby/issues/23645 for example of it. - - Instead of adding same babel plugin we mimic output here. Adding babel plugin just for this would: - a) unnecesairly slow down compilation for all other files (if we just apply it everywhere) - b) or complicate babel-loader configuration with overwrite specifically for this file -*/ - -const { createContext } = require(`react`) - -module.exports = createContext -module.exports.default = createContext diff --git a/packages/gatsby/cache-dir/develop-static-entry.js b/packages/gatsby/cache-dir/develop-static-entry.js index 3b0493a4fed4a..923de768a9f1b 100644 --- a/packages/gatsby/cache-dir/develop-static-entry.js +++ b/packages/gatsby/cache-dir/develop-static-entry.js @@ -104,6 +104,7 @@ export default ({ pagePath }) => { body: ``, headComponents: headComponents.concat([
    div3
    div2
    div1
    "`; -exports[`static-entry onPreRenderHTML can be used to replace headComponents 1`] = `"
    "`; - -exports[`static-entry onPreRenderHTML can be used to replace postBodyComponents 1`] = `"
    div3
    div2
    div1
    "`; - -exports[`static-entry onPreRenderHTML can be used to replace preBodyComponents 1`] = `"
    div3
    div2
    div1
    "`; +exports[`static-entry onPreRenderHTML can be used to replace headComponents 1`] = ` +Object { + "html": "
    ", + "unsafeBuiltinsUsage": Array [], +} +`; + +exports[`static-entry onPreRenderHTML can be used to replace postBodyComponents 1`] = ` +Object { + "html": "
    div3
    div2
    div1
    ", + "unsafeBuiltinsUsage": Array [], +} +`; + +exports[`static-entry onPreRenderHTML can be used to replace preBodyComponents 1`] = ` +Object { + "html": "
    div3
    div2
    div1
    ", + "unsafeBuiltinsUsage": Array [], +} +`; diff --git a/packages/gatsby/cache-dir/ssr-builtin-trackers/child_process.js b/packages/gatsby/cache-dir/ssr-builtin-trackers/child_process.js new file mode 100644 index 0000000000000..e953efda99e0e --- /dev/null +++ b/packages/gatsby/cache-dir/ssr-builtin-trackers/child_process.js @@ -0,0 +1,4 @@ +/* eslint-disable filenames/match-regex */ +const { wrapModuleWithTracking } = require(`./tracking-unsafe-module-wrapper`) + +module.exports = wrapModuleWithTracking(`child_process`) diff --git a/packages/gatsby/cache-dir/ssr-builtin-trackers/fs.js b/packages/gatsby/cache-dir/ssr-builtin-trackers/fs.js new file mode 100644 index 0000000000000..4ed58f29e01cb --- /dev/null +++ b/packages/gatsby/cache-dir/ssr-builtin-trackers/fs.js @@ -0,0 +1,3 @@ +const { wrapModuleWithTracking } = require(`./tracking-unsafe-module-wrapper`) + +module.exports = wrapModuleWithTracking(`fs`) diff --git a/packages/gatsby/cache-dir/ssr-builtin-trackers/http.js b/packages/gatsby/cache-dir/ssr-builtin-trackers/http.js new file mode 100644 index 0000000000000..7393f9e40d32e --- /dev/null +++ b/packages/gatsby/cache-dir/ssr-builtin-trackers/http.js @@ -0,0 +1,3 @@ +const { wrapModuleWithTracking } = require(`./tracking-unsafe-module-wrapper`) + +module.exports = wrapModuleWithTracking(`http`) diff --git a/packages/gatsby/cache-dir/ssr-builtin-trackers/http2.js b/packages/gatsby/cache-dir/ssr-builtin-trackers/http2.js new file mode 100644 index 0000000000000..30d956d6e6237 --- /dev/null +++ b/packages/gatsby/cache-dir/ssr-builtin-trackers/http2.js @@ -0,0 +1,3 @@ +const { wrapModuleWithTracking } = require(`./tracking-unsafe-module-wrapper`) + +module.exports = wrapModuleWithTracking(`http2`) diff --git a/packages/gatsby/cache-dir/ssr-builtin-trackers/https.js b/packages/gatsby/cache-dir/ssr-builtin-trackers/https.js new file mode 100644 index 0000000000000..fa4b85606e09a --- /dev/null +++ b/packages/gatsby/cache-dir/ssr-builtin-trackers/https.js @@ -0,0 +1,3 @@ +const { wrapModuleWithTracking } = require(`./tracking-unsafe-module-wrapper`) + +module.exports = wrapModuleWithTracking(`https`) diff --git a/packages/gatsby/cache-dir/ssr-builtin-trackers/tracking-unsafe-module-wrapper.js b/packages/gatsby/cache-dir/ssr-builtin-trackers/tracking-unsafe-module-wrapper.js new file mode 100644 index 0000000000000..df374dab131ad --- /dev/null +++ b/packages/gatsby/cache-dir/ssr-builtin-trackers/tracking-unsafe-module-wrapper.js @@ -0,0 +1,37 @@ +// initializing global here for unsafe builtin usage at import time +global.unsafeBuiltinUsage = [] + +function createProxyHandler(prefix) { + return { + get: function (target, key) { + const value = target[key] + if (typeof value === `function`) { + return function wrapper(...args) { + const myErrorHolder = { + name: `Unsafe builtin usage ${prefix}.${key}`, + } + Error.captureStackTrace(myErrorHolder, wrapper) + + global.unsafeBuiltinUsage.push(myErrorHolder.stack) + return value.apply(target, args) + } + } else if (typeof value === `object` && value !== null) { + return new Proxy( + value, + createProxyHandler( + key && key.toString ? `${prefix}.${key.toString()}` : prefix + ) + ) + } + + return value + }, + } +} + +function wrapModuleWithTracking(moduleName) { + const mod = require(moduleName) + return new Proxy(mod, createProxyHandler(moduleName)) +} + +exports.wrapModuleWithTracking = wrapModuleWithTracking diff --git a/packages/gatsby/cache-dir/static-entry.js b/packages/gatsby/cache-dir/static-entry.js index 015226a4e2298..86d68faf02356 100644 --- a/packages/gatsby/cache-dir/static-entry.js +++ b/packages/gatsby/cache-dir/static-entry.js @@ -103,315 +103,325 @@ export default ({ reversedStyles, reversedScripts, }) => { - let bodyHtml = `` - let headComponents = [ - , - ] - let htmlAttributes = {} - let bodyAttributes = {} - let preBodyComponents = [] - let postBodyComponents = [] - let bodyProps = {} - - const replaceBodyHTMLString = body => { - bodyHtml = body - } - - const setHeadComponents = components => { - headComponents = headComponents.concat(sanitizeComponents(components)) - } - - const setHtmlAttributes = attributes => { - htmlAttributes = merge(htmlAttributes, attributes) - } - - const setBodyAttributes = attributes => { - bodyAttributes = merge(bodyAttributes, attributes) - } - - const setPreBodyComponents = components => { - preBodyComponents = preBodyComponents.concat(sanitizeComponents(components)) - } + // for this to work we need this function to be sync or at least ensure there is single execution of it at a time + global.unsafeBuiltinUsage = [] + + try { + let bodyHtml = `` + let headComponents = [ + , + ] + let htmlAttributes = {} + let bodyAttributes = {} + let preBodyComponents = [] + let postBodyComponents = [] + let bodyProps = {} + + const replaceBodyHTMLString = body => { + bodyHtml = body + } - const setPostBodyComponents = components => { - postBodyComponents = postBodyComponents.concat( - sanitizeComponents(components) - ) - } + const setHeadComponents = components => { + headComponents = headComponents.concat(sanitizeComponents(components)) + } - const setBodyProps = props => { - bodyProps = merge({}, bodyProps, props) - } + const setHtmlAttributes = attributes => { + htmlAttributes = merge(htmlAttributes, attributes) + } - const getHeadComponents = () => headComponents + const setBodyAttributes = attributes => { + bodyAttributes = merge(bodyAttributes, attributes) + } - const replaceHeadComponents = components => { - headComponents = sanitizeComponents(components) - } + const setPreBodyComponents = components => { + preBodyComponents = preBodyComponents.concat( + sanitizeComponents(components) + ) + } - const getPreBodyComponents = () => preBodyComponents + const setPostBodyComponents = components => { + postBodyComponents = postBodyComponents.concat( + sanitizeComponents(components) + ) + } - const replacePreBodyComponents = components => { - preBodyComponents = sanitizeComponents(components) - } + const setBodyProps = props => { + bodyProps = merge({}, bodyProps, props) + } - const getPostBodyComponents = () => postBodyComponents + const getHeadComponents = () => headComponents - const replacePostBodyComponents = components => { - postBodyComponents = sanitizeComponents(components) - } + const replaceHeadComponents = components => { + headComponents = sanitizeComponents(components) + } - const pageDataUrl = getPageDataUrl(pagePath) + const getPreBodyComponents = () => preBodyComponents - const { componentChunkName, staticQueryHashes = [] } = pageData + const replacePreBodyComponents = components => { + preBodyComponents = sanitizeComponents(components) + } - const staticQueryUrls = staticQueryHashes.map(getStaticQueryUrl) + const getPostBodyComponents = () => postBodyComponents - class RouteHandler extends React.Component { - render() { - const props = { - ...this.props, - ...pageData.result, - params: { - ...grabMatchParams(this.props.location.pathname), - ...(pageData.result?.pageContext?.__params || {}), - }, - } + const replacePostBodyComponents = components => { + postBodyComponents = sanitizeComponents(components) + } - const pageElement = createElement( - syncRequires.components[componentChunkName], - props - ) + const pageDataUrl = getPageDataUrl(pagePath) - const wrappedPage = apiRunner( - `wrapPageElement`, - { element: pageElement, props }, - pageElement, - ({ result }) => { - return { element: result, props } - } - ).pop() + const { componentChunkName, staticQueryHashes = [] } = pageData - return wrappedPage - } - } + const staticQueryUrls = staticQueryHashes.map(getStaticQueryUrl) - const routerElement = ( - - - - -
    - - ) - - const bodyComponent = ( - - {apiRunner( - `wrapRootElement`, - { element: routerElement, pathname: pagePath }, - routerElement, - ({ result }) => { - return { element: result, pathname: pagePath } + class RouteHandler extends React.Component { + render() { + const props = { + ...this.props, + ...pageData.result, + params: { + ...grabMatchParams(this.props.location.pathname), + ...(pageData.result?.pageContext?.__params || {}), + }, } - ).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 + const pageElement = createElement( + syncRequires.components[componentChunkName], + props + ) + + const wrappedPage = apiRunner( + `wrapPageElement`, + { element: pageElement, props }, + pageElement, + ({ result }) => { + return { element: result, props } + } + ).pop() + + return wrappedPage + } } - } - apiRunner(`onRenderBody`, { - setHeadComponents, - setHtmlAttributes, - setBodyAttributes, - setPreBodyComponents, - setPostBodyComponents, - setBodyProps, - pathname: pagePath, - loadPageDataSync, - bodyHtml, - scripts, - styles, - pathPrefix: __PATH_PREFIX__, - }) - - reversedScripts.forEach(script => { - // Add preload/prefetch s for scripts. - headComponents.push( - + const routerElement = ( + + + + +
    + ) - }) - if (pageData) { - headComponents.push( - - ) - } - staticQueryUrls.forEach(staticQueryUrl => - headComponents.push( - + const bodyComponent = ( + + {apiRunner( + `wrapRootElement`, + { element: routerElement, pathname: pagePath }, + routerElement, + ({ result }) => { + return { element: result, pathname: pagePath } + } + ).pop()} + ) - ) - - const appDataUrl = getAppDataUrl() - if (appDataUrl) { - headComponents.push( - - ) - } - reversedStyles.forEach(style => { - // Add s for styles that should be prefetched - // otherwise, inline as a
    "`; +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 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 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 headComponents 1`] = `"
    "`; -exports[`develop-static-entry onPreRenderHTML can be used to replace postBodyComponents 1`] = `"
    div3
    div2
    div1
    "`; +exports[`develop-static-entry onPreRenderHTML can be used to replace postBodyComponents 1`] = `"
    div3
    div2
    div1
    "`; -exports[`develop-static-entry onPreRenderHTML can be used to replace preBodyComponents 1`] = `"
    div3
    div2
    div1
    "`; +exports[`develop-static-entry onPreRenderHTML can be used to replace preBodyComponents 1`] = `"
    div3
    div2
    div1
    "`; exports[`static-entry onPreRenderHTML can be used to replace headComponents 1`] = ` Object { diff --git a/packages/gatsby/cache-dir/__tests__/dev-loader.js b/packages/gatsby/cache-dir/__tests__/dev-loader.js index 62ffacb02dac6..0fde1ce6f08dc 100644 --- a/packages/gatsby/cache-dir/__tests__/dev-loader.js +++ b/packages/gatsby/cache-dir/__tests__/dev-loader.js @@ -21,6 +21,10 @@ describe(`Dev loader`, () => { global.__PATH_PREFIX__ = originalPathPrefix }) + const asyncRequires = { + components: {}, + } + describe(`loadPageDataJson`, () => { let xhrCount @@ -63,7 +67,7 @@ describe(`Dev loader`, () => { }) it(`should return a pageData json on success`, async () => { - const devLoader = new DevLoader(null, []) + const devLoader = new DevLoader(asyncRequires, []) mockPageData(`/mypage`, 200, defaultPayload, true) @@ -78,7 +82,7 @@ describe(`Dev loader`, () => { }) it(`should return a pageData json on success without contentType`, async () => { - const devLoader = new DevLoader(null, []) + const devLoader = new DevLoader(asyncRequires, []) mockPageData(`/mypage`, 200, defaultPayload) @@ -93,7 +97,7 @@ describe(`Dev loader`, () => { }) it(`should return a pageData json with an empty compilation hash (gatsby develop)`, async () => { - const devLoader = new DevLoader(null, []) + const devLoader = new DevLoader(asyncRequires, []) const payload = { ...defaultPayload, webpackCompilationHash: `` } mockPageData(`/mypage`, 200, payload) @@ -109,7 +113,7 @@ describe(`Dev loader`, () => { }) it(`should load a 404 page when page-path file is not a gatsby json`, async () => { - const devLoader = new DevLoader(null, []) + const devLoader = new DevLoader(asyncRequires, []) const payload = { ...defaultPayload, path: `/404.html/` } mockPageData(`/unknown-page`, 200, { random: `string` }, true) @@ -129,7 +133,7 @@ describe(`Dev loader`, () => { }) it(`should load a 404 page when page-path file is not a json`, async () => { - const devLoader = new DevLoader(null, []) + const devLoader = new DevLoader(asyncRequires, []) const payload = { ...defaultPayload, path: `/404.html/` } mockPageData(`/unknown-page`, 200) @@ -149,7 +153,7 @@ describe(`Dev loader`, () => { }) it(`should load a 404 page when path returns a 404`, async () => { - const devLoader = new DevLoader(null, []) + const devLoader = new DevLoader(asyncRequires, []) const payload = { ...defaultPayload, path: `/404.html/` } mockPageData(`/unknown-page`, 200) @@ -169,7 +173,7 @@ describe(`Dev loader`, () => { }) it(`should return the dev-404-page when no 404 page can be found`, async () => { - const devLoader = new DevLoader(null, []) + const devLoader = new DevLoader(asyncRequires, []) const payload = { ...defaultPayload, path: `/dev-404-page/` } mockPageData(`/unknown-page`, 404) @@ -195,7 +199,7 @@ describe(`Dev loader`, () => { }) it(`should return an error when status is 500`, async () => { - const devLoader = new DevLoader(null, []) + const devLoader = new DevLoader(asyncRequires, []) mockPageData(`/error-page`, 500) @@ -213,7 +217,7 @@ describe(`Dev loader`, () => { }) it(`should retry 3 times before returning an error`, async () => { - const devLoader = new DevLoader(null, []) + const devLoader = new DevLoader(asyncRequires, []) mockPageData(`/blocked-page`, 0) @@ -232,7 +236,7 @@ describe(`Dev loader`, () => { }) it(`should recover if we get 1 failure`, async () => { - const devLoader = new DevLoader(null, []) + const devLoader = new DevLoader(asyncRequires, []) const payload = { path: `/blocked-page/`, } @@ -261,7 +265,7 @@ describe(`Dev loader`, () => { }) it(`shouldn't load pageData multiple times`, async () => { - const devLoader = new DevLoader(null, []) + const devLoader = new DevLoader(asyncRequires, []) mockPageData(`/mypage`, 200, defaultPayload, true) @@ -272,7 +276,7 @@ describe(`Dev loader`, () => { }) describe(`loadPage`, () => { - const createSyncRequires = components => { + const createAsyncRequires = components => { return { components, } @@ -303,10 +307,10 @@ describe(`Dev loader`, () => { }) it(`should be successful when component can be loaded`, async () => { - const syncRequires = createSyncRequires({ - chunk: `instance`, + const asyncRequires = createAsyncRequires({ + chunk: () => Promise.resolve({ default: () => {} }), }) - const devLoader = new DevLoader(syncRequires, []) + const devLoader = new DevLoader(asyncRequires, []) const pageData = { path: `/mypage/`, componentChunkName: `chunk`, @@ -344,10 +348,10 @@ describe(`Dev loader`, () => { }) it(`should set not found on finalResult`, async () => { - const syncRequires = createSyncRequires({ - chunk: `instance`, + const asyncRequires = createAsyncRequires({ + chunk: () => Promise.resolve({ default: () => {} }), }) - const devLoader = new DevLoader(syncRequires, []) + const devLoader = new DevLoader(asyncRequires, []) const pageData = { path: `/mypage/`, componentChunkName: `chunk`, @@ -371,10 +375,10 @@ describe(`Dev loader`, () => { }) it(`should return an error when component cannot be loaded`, async () => { - const syncRequires = createSyncRequires({ + const asyncRequires = createAsyncRequires({ chunk: false, }) - const devLoader = new DevLoader(syncRequires, []) + const devLoader = new DevLoader(asyncRequires, []) const pageData = { path: `/mypage/`, componentChunkName: `chunk`, @@ -394,10 +398,10 @@ describe(`Dev loader`, () => { }) it(`should return an error pageData contains an error`, async () => { - const syncRequires = createSyncRequires({ - chunk: `instance`, + const asyncRequires = createAsyncRequires({ + chunk: () => Promise.resolve({ default: () => {} }), }) - const devLoader = new DevLoader(syncRequires, []) + const devLoader = new DevLoader(asyncRequires, []) const pageData = { path: `/mypage/`, componentChunkName: `chunk`, @@ -416,7 +420,7 @@ describe(`Dev loader`, () => { }) it(`should log an error when 404 cannot be fetched`, async () => { - const devLoader = new DevLoader(null, []) + const devLoader = new DevLoader(asyncRequires, []) const consoleErrorSpy = jest.spyOn(console, `error`) const defaultXHRMockErrorHandler = XMLHttpRequest.errorCallback mock.error(() => {}) @@ -436,10 +440,10 @@ describe(`Dev loader`, () => { }) it(`should cache the result of loadPage`, async () => { - const syncRequires = createSyncRequires({ - chunk: `instance`, + const asyncRequires = createAsyncRequires({ + chunk: () => Promise.resolve({ default: () => {} }), }) - const devLoader = new DevLoader(syncRequires, []) + const devLoader = new DevLoader(asyncRequires, []) devLoader.loadPageDataJson = jest.fn(() => Promise.resolve({ payload: { @@ -458,14 +462,14 @@ describe(`Dev loader`, () => { describe(`loadPageSync`, () => { it(`returns page resources when already fetched`, () => { - const devLoader = new DevLoader(null, []) + const devLoader = new DevLoader(asyncRequires, []) devLoader.pageDb.set(`/mypage`, { payload: true }) expect(devLoader.loadPageSync(`/mypage/`)).toBe(true) }) it(`returns page resources when already fetched`, () => { - const devLoader = new DevLoader(null, []) + const devLoader = new DevLoader(asyncRequires, []) expect(devLoader.loadPageSync(`/mypage/`)).toBeUndefined() }) @@ -475,7 +479,7 @@ describe(`Dev loader`, () => { const flushPromises = () => new Promise(resolve => setImmediate(resolve)) it(`shouldn't prefetch when shouldPrefetch is false`, () => { - const devLoader = new DevLoader(null, []) + const devLoader = new DevLoader(asyncRequires, []) devLoader.shouldPrefetch = jest.fn(() => false) devLoader.doPrefetch = jest.fn() devLoader.apiRunner = jest.fn() @@ -487,7 +491,7 @@ describe(`Dev loader`, () => { }) it(`should trigger custom prefetch logic when core is disabled`, () => { - const devLoader = new DevLoader(null, []) + const devLoader = new DevLoader(asyncRequires, []) devLoader.shouldPrefetch = jest.fn(() => true) devLoader.doPrefetch = jest.fn() devLoader.apiRunner = jest.fn() @@ -503,7 +507,7 @@ describe(`Dev loader`, () => { it(`should prefetch when not yet triggered`, async () => { jest.useFakeTimers() - const devLoader = new DevLoader(null, []) + const devLoader = new DevLoader(asyncRequires, []) devLoader.shouldPrefetch = jest.fn(() => true) devLoader.apiRunner = jest.fn() devLoader.doPrefetch = jest.fn(() => Promise.resolve({})) @@ -526,7 +530,7 @@ describe(`Dev loader`, () => { }) it(`should only run apis once`, async () => { - const devLoader = new DevLoader(null, []) + const devLoader = new DevLoader(asyncRequires, []) devLoader.shouldPrefetch = jest.fn(() => true) devLoader.apiRunner = jest.fn() devLoader.doPrefetch = jest.fn(() => Promise.resolve({})) diff --git a/packages/gatsby/cache-dir/app.js b/packages/gatsby/cache-dir/app.js index 32fc4f87aca9f..34b2420139206 100644 --- a/packages/gatsby/cache-dir/app.js +++ b/packages/gatsby/cache-dir/app.js @@ -9,18 +9,18 @@ import { apiRunner, apiRunnerAsync } from "./api-runner-browser" import { setLoader, publicLoader } from "./loader" import { Indicator } from "./loading-indicator/indicator" import DevLoader from "./dev-loader" -import syncRequires from "$virtual/sync-requires" +import asyncRequires from "$virtual/async-requires" // Generated during bootstrap import matchPaths from "$virtual/match-paths.json" // Enable fast-refresh for virtual sync-requires -module.hot.accept(`$virtual/sync-requires`, () => { +module.hot.accept(`$virtual/async-requires`, () => { // Manually reload }) window.___emitter = emitter -const loader = new DevLoader(syncRequires, matchPaths) +const loader = new DevLoader(asyncRequires, matchPaths) setLoader(loader) loader.setApiRunner(apiRunner) diff --git a/packages/gatsby/cache-dir/dev-loader.js b/packages/gatsby/cache-dir/dev-loader.js index 5f8e84b2fef0f..d3a32968f5c19 100644 --- a/packages/gatsby/cache-dir/dev-loader.js +++ b/packages/gatsby/cache-dir/dev-loader.js @@ -7,6 +7,8 @@ import normalizePagePath from "./normalize-page-path" // TODO move away from lodash import isEqual from "lodash/isEqual" +const preferDefault = m => (m && m.default) || m + function mergePageEntry(cachedPage, newPageData) { return { ...cachedPage, @@ -22,10 +24,14 @@ function mergePageEntry(cachedPage, newPageData) { } class DevLoader extends BaseLoader { - constructor(syncRequires, matchPaths) { + constructor(asyncRequires, matchPaths) { const loadComponent = chunkName => - Promise.resolve(syncRequires.components[chunkName]) - + asyncRequires.components[chunkName] + ? asyncRequires.components[chunkName]() + .then(preferDefault) + // loader will handle the case when component is null + .catch(() => null) + : Promise.resolve() super(loadComponent, matchPaths) const socket = getSocket() diff --git a/packages/gatsby/cache-dir/develop-static-entry.js b/packages/gatsby/cache-dir/develop-static-entry.js index 923de768a9f1b..b00d7250a11a8 100644 --- a/packages/gatsby/cache-dir/develop-static-entry.js +++ b/packages/gatsby/cache-dir/develop-static-entry.js @@ -111,6 +111,7 @@ export default ({ pagePath }) => { preBodyComponents, postBodyComponents: postBodyComponents.concat([