Skip to content

Commit

Permalink
fix(gatsby): handle case of html and data files mismatch (#34225)
Browse files Browse the repository at this point in the history
  • Loading branch information
pieh committed Dec 17, 2021
1 parent 10c8227 commit 97e942e
Show file tree
Hide file tree
Showing 14 changed files with 245 additions and 94 deletions.
191 changes: 154 additions & 37 deletions e2e-tests/production-runtime/cypress/integration/compilation-hash.js
Original file line number Diff line number Diff line change
@@ -1,62 +1,179 @@
/* global cy */

let spy
Cypress.on(`window:before:load`, win => {
spy = cy.spy(win.console, `error`).as(`errorMessage`)
})

Cypress.on(`uncaught:exception`, (err, runnable) => {
// returning false here prevents Cypress from
// failing the test
return false
})

const getRandomInt = (min, max) => {
min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(Math.random() * (max - min)) + min
}

const createMockCompilationHash = () =>
[...Array(20)]
const createMockCompilationHash = () => {
const hash = [...Array(20)]
.map(a => getRandomInt(0, 16))
.map(k => k.toString(16))
.join(``)
cy.log({ hash })
return hash
}

describe(`Webpack Compilation Hash tests`, () => {
it(`should render properly`, () => {
cy.visit(`/`).waitForRouteChange()
})

// This covers the case where a user loads a gatsby site and then
// the site is changed resulting in a webpack recompile and a
// redeploy. This could result in a mismatch between the page-data
// and the component. To protect against this, when gatsby loads a
// new page-data.json, it refreshes the page if it's webpack
// compilation hash differs from the one on on the window object
// (which was set on initial page load)
//
// Since initial page load results in all links being prefetched, we
// have to navigate to a non-prefetched page to test this. Thus the
// `deep-link-page`.
//
// We simulate a rebuild by updating all page-data.jsons and page
// htmls with the new hash. It's not pretty, but it's easier than
// figuring out how to perform an actual rebuild while cypress is
// running. See ../plugins/compilation-hash.js for the
// implementation
it.skip(`should reload page if build occurs in background`, () => {
cy.window().then(window => {
const oldHash = window.___webpackCompilationHash
expect(oldHash).to.not.eq(undefined)

// Service worker is handling requests so this one is cached by previous runs
if (!Cypress.env(`TEST_PLUGIN_OFFLINE`)) {
// This covers the case where a user loads a gatsby site and then
// the site is changed resulting in a webpack recompile and a
// redeploy. This could result in a mismatch between the page-data
// and the component. To protect against this, when gatsby loads a
// new page-data.json, it refreshes the page if it's webpack
// compilation hash differs from the one on on the window object
// (which was set on initial page load)
//
// Since initial page load results in all links being prefetched, we
// have to navigate to a non-prefetched page to test this. Thus the
// `deep-link-page`.
//
// We simulate a rebuild by intercepting app-data request and responding with random hash
it(`should reload page on navigation if build occurs in background`, () => {
const mockHash = createMockCompilationHash()

// Simulate a new webpack build
cy.task(`overwriteWebpackCompilationHash`, mockHash).then(() => {
cy.getTestElement(`compilation-hash`).click()
cy.waitForRouteChange()
cy.visit(`/`).waitForRouteChange()

// Navigate into a non-prefetched page
cy.getTestElement(`deep-link-page`).click()
cy.waitForRouteChange()
let didMock = false
cy.intercept("/app-data.json", req => {
if (!didMock) {
req.reply({
webpackCompilationHash: mockHash,
})
didMock = true
}
}).as(`appDataFetch`)

// If the window compilation hash has changed, we know the
// page was refreshed
cy.window().its(`___webpackCompilationHash`).should(`equal`, mockHash)
cy.window().then(window => {
// just setting some property on the window
// we will later assert that property to know wether
// browser reload happened or not.
window.notReloaded = true
window.___navigate(`/deep-link-page/`)
})

// Cleanup
cy.task(`overwriteWebpackCompilationHash`, oldHash)
cy.waitForRouteChange()

// we expect reload to happen so our window property shouldn't be set anymore
cy.window().its(`notReloaded`).should(`not.equal`, true)

// let's make sure we actually see the content
cy.contains(
`StaticQuery in wrapRootElement test (should show site title):Gatsby Default Starter`
)
})
})

// This covers the case where user user loads "outdated" html from some kind of cache
// and our data files (page-data and app-data) are for newer built.
// We will mock both app-data (to change the hash) as well as example page-data
// to simulate changes to static query hashes between builds.
it(`should force reload page if on initial load the html is not matching newest app/page-data`, () => {
const mockHash = createMockCompilationHash()

// trying to intercept just `/` seems to intercept all routes
// so intercepting same thing just with regex
cy.intercept(/^\/$/).as(`indexFetch`)

// We will mock `app-data` and `page-data` json responses one time (for initial load)
let shouldMockAppDataRequests = true
let shouldMockPageDataRequests = true
cy.intercept("/app-data.json", req => {
if (shouldMockAppDataRequests) {
req.reply({
webpackCompilationHash: mockHash,
})
shouldMockAppDataRequests = false
}
}).as(`appDataFetch`)

cy.readFile(`public/page-data/compilation-hash/page-data.json`).then(
originalPageData => {
cy.intercept("/page-data/index/page-data.json", req => {
if (shouldMockPageDataRequests) {
req.reply({
...originalPageData,
// setting this to empty array should break runtime with
// either placeholder "Loading (StaticQuery)" (for <StaticQuery> component)
// or thrown error "The result of this StaticQuery could not be fetched." (for `useStaticQuery` hook)
staticQueryHashes: [],
})
shouldMockPageDataRequests = false
}
}).as(`pageDataFetch`)
}
)

cy.visit(`/`)
cy.wait(1500)

// <StaticQuery> component case
cy.contains("Loading (StaticQuery)").should("not.exist")

// useStaticQuery hook case
cy.get(`@errorMessage`).should(`not.called`)

// let's make sure we actually see the content
cy.contains(
`StaticQuery in wrapRootElement test (should show site title):Gatsby Default Starter`
)

cy.get("@indexFetch.all").should("have.length", 2)
cy.get("@appDataFetch.all").should("have.length", 2)
cy.get("@pageDataFetch.all").should("have.length", 2)
})

it(`should not force reload indefinitely`, () => {
const mockHash = createMockCompilationHash()

// trying to intercept just `/` seems to intercept all routes
// so intercepting same thing just with regex
cy.intercept(/^\/$/).as(`indexFetch`)

// We will mock `app-data` and `page-data` json responses permanently
cy.intercept("/app-data.json", req => {
req.reply({
webpackCompilationHash: mockHash,
})
}).as(`appDataFetch`)

cy.readFile(`public/page-data/index/page-data.json`).then(
originalPageData => {
cy.intercept("/page-data/index/page-data.json", req => {
req.reply({
...originalPageData,
// setting this to empty array should break runtime with
// either placeholder "Loading (StaticQuery)" (for <StaticQuery> component)
// or thrown error "The result of this StaticQuery could not be fetched." (for `useStaticQuery` hook)
staticQueryHashes: [],
})
}).as(`pageDataFetch`)
}
)

cy.visit(`/`)

cy.wait(1500)

cy.get("@indexFetch.all").should("have.length", 2)
cy.get("@appDataFetch.all").should("have.length", 2)
cy.get("@pageDataFetch.all").should("have.length", 2)
})
}
})
37 changes: 0 additions & 37 deletions e2e-tests/production-runtime/cypress/plugins/compilation-hash.js

This file was deleted.

3 changes: 1 addition & 2 deletions e2e-tests/production-runtime/cypress/plugins/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
const compilationHash = require(`./compilation-hash`)
const blockResources = require(`./block-resources`)

module.exports = (on, config) => {
Expand All @@ -16,5 +15,5 @@ module.exports = (on, config) => {
return args
})

on(`task`, Object.assign({}, compilationHash, blockResources))
on(`task`, Object.assign({}, blockResources))
}
9 changes: 8 additions & 1 deletion packages/gatsby-plugin-offline/src/gatsby-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ exports.onPostBuild = (
// since these files have unique URLs and their contents will never change
dontCacheBustURLsMatching: /(\.js$|\.css$|static\/)/,
runtimeCaching: [
// ignore cypress endpoints (only for testing)
process.env.CYPRESS_SUPPORT
? {
urlPattern: /\/__cypress\//,
handler: `NetworkOnly`,
}
: false,
{
// Use cacheFirst since these don't need to be revalidated (same RegExp
// and same reason as above)
Expand All @@ -156,7 +163,7 @@ exports.onPostBuild = (
urlPattern: /^https?:\/\/fonts\.googleapis\.com\/css/,
handler: `StaleWhileRevalidate`,
},
],
].filter(Boolean),
skipWaiting: true,
clientsClaim: true,
}
Expand Down
18 changes: 18 additions & 0 deletions packages/gatsby-plugin-offline/src/sw-append.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,24 @@ const MessageAPI = {

clearPathResources: event => {
event.waitUntil(idbKeyval.clear())

// We detected compilation hash mismatch
// we should clear runtime cache as data
// files might be out of sync and we should
// do fresh fetches for them
event.waitUntil(
caches.keys().then(function (keyList) {
return Promise.all(
keyList.map(function (key) {
if (key && key.includes(`runtime`)) {
return caches.delete(key)
}

return Promise.resolve()
})
)
})
)
},

enableOfflineShell: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@ exports[`develop-static-entry onPreRenderHTML can be used to replace preBodyComp
exports[`static-entry onPreRenderHTML can be used to replace headComponents 1`] = `
Object {
"html": "<!DOCTYPE html><html><head><meta charSet=\\"utf-8\\"/><meta http-equiv=\\"x-ua-compatible\\" content=\\"ie=edge\\"/><meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1, shrink-to-fit=no\\"/><link as=\\"fetch\\" rel=\\"preload\\" href=\\"/page-data/app-data.json\\" crossorigin=\\"anonymous\\"/><link as=\\"fetch\\" rel=\\"preload\\" href=\\"/page-data/about/page-data.json\\" crossorigin=\\"anonymous\\"/><style> .style3 </style><style> .style2 </style><style> .style1 </style><meta name=\\"generator\\" content=\\"Gatsby 2.0.0\\"/></head><body><div id=\\"___gatsby\\"><div style=\\"outline:none\\" tabindex=\\"-1\\" id=\\"gatsby-focus-wrapper\\"></div><div id=\\"gatsby-announcer\\" style=\\"position:absolute;top:0;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0\\" aria-live=\\"assertive\\" aria-atomic=\\"true\\"></div></div><script id=\\"gatsby-script-loader\\">/*<![CDATA[*/window.pagePath=\\"/about/\\";/*]]>*/</script><script id=\\"gatsby-chunk-mapping\\">/*<![CDATA[*/window.___chunkMapping={};/*]]>*/</script></body></html>",
"html": "<!DOCTYPE html><html><head><meta charSet=\\"utf-8\\"/><meta http-equiv=\\"x-ua-compatible\\" content=\\"ie=edge\\"/><meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1, shrink-to-fit=no\\"/><link as=\\"fetch\\" rel=\\"preload\\" href=\\"/page-data/app-data.json\\" crossorigin=\\"anonymous\\"/><link as=\\"fetch\\" rel=\\"preload\\" href=\\"/page-data/about/page-data.json\\" crossorigin=\\"anonymous\\"/><style> .style3 </style><style> .style2 </style><style> .style1 </style><meta name=\\"generator\\" content=\\"Gatsby 2.0.0\\"/></head><body><div id=\\"___gatsby\\"><div style=\\"outline:none\\" tabindex=\\"-1\\" id=\\"gatsby-focus-wrapper\\"></div><div id=\\"gatsby-announcer\\" style=\\"position:absolute;top:0;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0\\" aria-live=\\"assertive\\" aria-atomic=\\"true\\"></div></div><script id=\\"gatsby-script-loader\\">/*<![CDATA[*/window.pagePath=\\"/about/\\";window.___webpackCompilationHash=\\"1234567890abcdef1234\\";/*]]>*/</script><script id=\\"gatsby-chunk-mapping\\">/*<![CDATA[*/window.___chunkMapping={};/*]]>*/</script></body></html>",
"unsafeBuiltinsUsage": Array [],
}
`;
exports[`static-entry onPreRenderHTML can be used to replace postBodyComponents 1`] = `
Object {
"html": "<!DOCTYPE html><html><head><meta charSet=\\"utf-8\\"/><meta http-equiv=\\"x-ua-compatible\\" content=\\"ie=edge\\"/><meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1, shrink-to-fit=no\\"/><meta name=\\"generator\\" content=\\"Gatsby 2.0.0\\"/><link as=\\"fetch\\" rel=\\"preload\\" href=\\"/page-data/about/page-data.json\\" crossorigin=\\"anonymous\\"/><link as=\\"fetch\\" rel=\\"preload\\" href=\\"/page-data/app-data.json\\" crossorigin=\\"anonymous\\"/></head><body><div id=\\"___gatsby\\"><div style=\\"outline:none\\" tabindex=\\"-1\\" id=\\"gatsby-focus-wrapper\\"></div><div id=\\"gatsby-announcer\\" style=\\"position:absolute;top:0;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0\\" aria-live=\\"assertive\\" aria-atomic=\\"true\\"></div></div><script id=\\"gatsby-chunk-mapping\\">/*<![CDATA[*/window.___chunkMapping={};/*]]>*/</script><script id=\\"gatsby-script-loader\\">/*<![CDATA[*/window.pagePath=\\"/about/\\";/*]]>*/</script><div> div3 </div><div> div2 </div><div> div1 </div></body></html>",
"html": "<!DOCTYPE html><html><head><meta charSet=\\"utf-8\\"/><meta http-equiv=\\"x-ua-compatible\\" content=\\"ie=edge\\"/><meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1, shrink-to-fit=no\\"/><meta name=\\"generator\\" content=\\"Gatsby 2.0.0\\"/><link as=\\"fetch\\" rel=\\"preload\\" href=\\"/page-data/about/page-data.json\\" crossorigin=\\"anonymous\\"/><link as=\\"fetch\\" rel=\\"preload\\" href=\\"/page-data/app-data.json\\" crossorigin=\\"anonymous\\"/></head><body><div id=\\"___gatsby\\"><div style=\\"outline:none\\" tabindex=\\"-1\\" id=\\"gatsby-focus-wrapper\\"></div><div id=\\"gatsby-announcer\\" style=\\"position:absolute;top:0;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0\\" aria-live=\\"assertive\\" aria-atomic=\\"true\\"></div></div><script id=\\"gatsby-chunk-mapping\\">/*<![CDATA[*/window.___chunkMapping={};/*]]>*/</script><script id=\\"gatsby-script-loader\\">/*<![CDATA[*/window.pagePath=\\"/about/\\";window.___webpackCompilationHash=\\"1234567890abcdef1234\\";/*]]>*/</script><div> div3 </div><div> div2 </div><div> div1 </div></body></html>",
"unsafeBuiltinsUsage": Array [],
}
`;
exports[`static-entry onPreRenderHTML can be used to replace preBodyComponents 1`] = `
Object {
"html": "<!DOCTYPE html><html><head><meta charSet=\\"utf-8\\"/><meta http-equiv=\\"x-ua-compatible\\" content=\\"ie=edge\\"/><meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1, shrink-to-fit=no\\"/><meta name=\\"generator\\" content=\\"Gatsby 2.0.0\\"/><link as=\\"fetch\\" rel=\\"preload\\" href=\\"/page-data/about/page-data.json\\" crossorigin=\\"anonymous\\"/><link as=\\"fetch\\" rel=\\"preload\\" href=\\"/page-data/app-data.json\\" crossorigin=\\"anonymous\\"/></head><body><div> div3 </div><div> div2 </div><div> div1 </div><div id=\\"___gatsby\\"><div style=\\"outline:none\\" tabindex=\\"-1\\" id=\\"gatsby-focus-wrapper\\"></div><div id=\\"gatsby-announcer\\" style=\\"position:absolute;top:0;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0\\" aria-live=\\"assertive\\" aria-atomic=\\"true\\"></div></div><script id=\\"gatsby-script-loader\\">/*<![CDATA[*/window.pagePath=\\"/about/\\";/*]]>*/</script><script id=\\"gatsby-chunk-mapping\\">/*<![CDATA[*/window.___chunkMapping={};/*]]>*/</script></body></html>",
"html": "<!DOCTYPE html><html><head><meta charSet=\\"utf-8\\"/><meta http-equiv=\\"x-ua-compatible\\" content=\\"ie=edge\\"/><meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1, shrink-to-fit=no\\"/><meta name=\\"generator\\" content=\\"Gatsby 2.0.0\\"/><link as=\\"fetch\\" rel=\\"preload\\" href=\\"/page-data/about/page-data.json\\" crossorigin=\\"anonymous\\"/><link as=\\"fetch\\" rel=\\"preload\\" href=\\"/page-data/app-data.json\\" crossorigin=\\"anonymous\\"/></head><body><div> div3 </div><div> div2 </div><div> div1 </div><div id=\\"___gatsby\\"><div style=\\"outline:none\\" tabindex=\\"-1\\" id=\\"gatsby-focus-wrapper\\"></div><div id=\\"gatsby-announcer\\" style=\\"position:absolute;top:0;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0\\" aria-live=\\"assertive\\" aria-atomic=\\"true\\"></div></div><script id=\\"gatsby-script-loader\\">/*<![CDATA[*/window.pagePath=\\"/about/\\";window.___webpackCompilationHash=\\"1234567890abcdef1234\\";/*]]>*/</script><script id=\\"gatsby-chunk-mapping\\">/*<![CDATA[*/window.___chunkMapping={};/*]]>*/</script></body></html>",
"unsafeBuiltinsUsage": Array [],
}
`;
9 changes: 5 additions & 4 deletions packages/gatsby/cache-dir/__tests__/static-entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,18 @@ jest.mock(
const pageDataMock = {
componentChunkName: `page-component---src-pages-test-js`,
path: `/about/`,
webpackCompilationHash: `1234567890abcdef1234`,
staticQueryHashes: [],
}

const webpackCompilationHash = `1234567890abcdef1234`

const MOCK_FILE_INFO = {
[`${process.cwd()}/public/webpack.stats.json`]: `{}`,
[`${process.cwd()}/public/chunk-map.json`]: `{}`,
[join(process.cwd(), `/public/page-data/about/page-data.json`)]:
JSON.stringify(pageDataMock),
[join(process.cwd(), `/public/page-data/app-data.json`)]: JSON.stringify({
webpackCompilationHash: `1234567890abcdef1234`,
webpackCompilationHash,
}),
}

Expand Down Expand Up @@ -173,11 +174,10 @@ const SSR_DEV_MOCK_FILE_INFO = {
[join(publicDir, `page-data/about/page-data.json`)]: JSON.stringify({
componentChunkName: `page-component---src-pages-about-js`,
path: `/about/`,
webpackCompilationHash: `1234567890abcdef1234`,
staticQueryHashes: [],
}),
[join(publicDir, `page-data/app-data.json`)]: JSON.stringify({
webpackCompilationHash: `1234567890abcdef1234`,
webpackCompilationHash,
}),
}

Expand Down Expand Up @@ -411,6 +411,7 @@ describe(`static-entry`, () => {
styles: [],
reversedStyles: [],
reversedScripts: [],
webpackCompilationHash,
}

beforeEach(() => {
Expand Down

0 comments on commit 97e942e

Please sign in to comment.