Skip to content

Commit 97e942e

Browse files
authored
fix(gatsby): handle case of html and data files mismatch (#34225)
1 parent 10c8227 commit 97e942e

File tree

14 files changed

+245
-94
lines changed

14 files changed

+245
-94
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,179 @@
11
/* global cy */
22

3+
let spy
4+
Cypress.on(`window:before:load`, win => {
5+
spy = cy.spy(win.console, `error`).as(`errorMessage`)
6+
})
7+
8+
Cypress.on(`uncaught:exception`, (err, runnable) => {
9+
// returning false here prevents Cypress from
10+
// failing the test
11+
return false
12+
})
13+
314
const getRandomInt = (min, max) => {
415
min = Math.ceil(min)
516
max = Math.floor(max)
617
return Math.floor(Math.random() * (max - min)) + min
718
}
819

9-
const createMockCompilationHash = () =>
10-
[...Array(20)]
20+
const createMockCompilationHash = () => {
21+
const hash = [...Array(20)]
1122
.map(a => getRandomInt(0, 16))
1223
.map(k => k.toString(16))
1324
.join(``)
25+
cy.log({ hash })
26+
return hash
27+
}
1428

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

20-
// This covers the case where a user loads a gatsby site and then
21-
// the site is changed resulting in a webpack recompile and a
22-
// redeploy. This could result in a mismatch between the page-data
23-
// and the component. To protect against this, when gatsby loads a
24-
// new page-data.json, it refreshes the page if it's webpack
25-
// compilation hash differs from the one on on the window object
26-
// (which was set on initial page load)
27-
//
28-
// Since initial page load results in all links being prefetched, we
29-
// have to navigate to a non-prefetched page to test this. Thus the
30-
// `deep-link-page`.
31-
//
32-
// We simulate a rebuild by updating all page-data.jsons and page
33-
// htmls with the new hash. It's not pretty, but it's easier than
34-
// figuring out how to perform an actual rebuild while cypress is
35-
// running. See ../plugins/compilation-hash.js for the
36-
// implementation
37-
it.skip(`should reload page if build occurs in background`, () => {
38-
cy.window().then(window => {
39-
const oldHash = window.___webpackCompilationHash
40-
expect(oldHash).to.not.eq(undefined)
41-
34+
// Service worker is handling requests so this one is cached by previous runs
35+
if (!Cypress.env(`TEST_PLUGIN_OFFLINE`)) {
36+
// This covers the case where a user loads a gatsby site and then
37+
// the site is changed resulting in a webpack recompile and a
38+
// redeploy. This could result in a mismatch between the page-data
39+
// and the component. To protect against this, when gatsby loads a
40+
// new page-data.json, it refreshes the page if it's webpack
41+
// compilation hash differs from the one on on the window object
42+
// (which was set on initial page load)
43+
//
44+
// Since initial page load results in all links being prefetched, we
45+
// have to navigate to a non-prefetched page to test this. Thus the
46+
// `deep-link-page`.
47+
//
48+
// We simulate a rebuild by intercepting app-data request and responding with random hash
49+
it(`should reload page on navigation if build occurs in background`, () => {
4250
const mockHash = createMockCompilationHash()
4351

44-
// Simulate a new webpack build
45-
cy.task(`overwriteWebpackCompilationHash`, mockHash).then(() => {
46-
cy.getTestElement(`compilation-hash`).click()
47-
cy.waitForRouteChange()
52+
cy.visit(`/`).waitForRouteChange()
4853

49-
// Navigate into a non-prefetched page
50-
cy.getTestElement(`deep-link-page`).click()
51-
cy.waitForRouteChange()
54+
let didMock = false
55+
cy.intercept("/app-data.json", req => {
56+
if (!didMock) {
57+
req.reply({
58+
webpackCompilationHash: mockHash,
59+
})
60+
didMock = true
61+
}
62+
}).as(`appDataFetch`)
5263

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

58-
// Cleanup
59-
cy.task(`overwriteWebpackCompilationHash`, oldHash)
72+
cy.waitForRouteChange()
73+
74+
// we expect reload to happen so our window property shouldn't be set anymore
75+
cy.window().its(`notReloaded`).should(`not.equal`, true)
76+
77+
// let's make sure we actually see the content
78+
cy.contains(
79+
`StaticQuery in wrapRootElement test (should show site title):Gatsby Default Starter`
80+
)
6081
})
61-
})
82+
83+
// This covers the case where user user loads "outdated" html from some kind of cache
84+
// and our data files (page-data and app-data) are for newer built.
85+
// We will mock both app-data (to change the hash) as well as example page-data
86+
// to simulate changes to static query hashes between builds.
87+
it(`should force reload page if on initial load the html is not matching newest app/page-data`, () => {
88+
const mockHash = createMockCompilationHash()
89+
90+
// trying to intercept just `/` seems to intercept all routes
91+
// so intercepting same thing just with regex
92+
cy.intercept(/^\/$/).as(`indexFetch`)
93+
94+
// We will mock `app-data` and `page-data` json responses one time (for initial load)
95+
let shouldMockAppDataRequests = true
96+
let shouldMockPageDataRequests = true
97+
cy.intercept("/app-data.json", req => {
98+
if (shouldMockAppDataRequests) {
99+
req.reply({
100+
webpackCompilationHash: mockHash,
101+
})
102+
shouldMockAppDataRequests = false
103+
}
104+
}).as(`appDataFetch`)
105+
106+
cy.readFile(`public/page-data/compilation-hash/page-data.json`).then(
107+
originalPageData => {
108+
cy.intercept("/page-data/index/page-data.json", req => {
109+
if (shouldMockPageDataRequests) {
110+
req.reply({
111+
...originalPageData,
112+
// setting this to empty array should break runtime with
113+
// either placeholder "Loading (StaticQuery)" (for <StaticQuery> component)
114+
// or thrown error "The result of this StaticQuery could not be fetched." (for `useStaticQuery` hook)
115+
staticQueryHashes: [],
116+
})
117+
shouldMockPageDataRequests = false
118+
}
119+
}).as(`pageDataFetch`)
120+
}
121+
)
122+
123+
cy.visit(`/`)
124+
cy.wait(1500)
125+
126+
// <StaticQuery> component case
127+
cy.contains("Loading (StaticQuery)").should("not.exist")
128+
129+
// useStaticQuery hook case
130+
cy.get(`@errorMessage`).should(`not.called`)
131+
132+
// let's make sure we actually see the content
133+
cy.contains(
134+
`StaticQuery in wrapRootElement test (should show site title):Gatsby Default Starter`
135+
)
136+
137+
cy.get("@indexFetch.all").should("have.length", 2)
138+
cy.get("@appDataFetch.all").should("have.length", 2)
139+
cy.get("@pageDataFetch.all").should("have.length", 2)
140+
})
141+
142+
it(`should not force reload indefinitely`, () => {
143+
const mockHash = createMockCompilationHash()
144+
145+
// trying to intercept just `/` seems to intercept all routes
146+
// so intercepting same thing just with regex
147+
cy.intercept(/^\/$/).as(`indexFetch`)
148+
149+
// We will mock `app-data` and `page-data` json responses permanently
150+
cy.intercept("/app-data.json", req => {
151+
req.reply({
152+
webpackCompilationHash: mockHash,
153+
})
154+
}).as(`appDataFetch`)
155+
156+
cy.readFile(`public/page-data/index/page-data.json`).then(
157+
originalPageData => {
158+
cy.intercept("/page-data/index/page-data.json", req => {
159+
req.reply({
160+
...originalPageData,
161+
// setting this to empty array should break runtime with
162+
// either placeholder "Loading (StaticQuery)" (for <StaticQuery> component)
163+
// or thrown error "The result of this StaticQuery could not be fetched." (for `useStaticQuery` hook)
164+
staticQueryHashes: [],
165+
})
166+
}).as(`pageDataFetch`)
167+
}
168+
)
169+
170+
cy.visit(`/`)
171+
172+
cy.wait(1500)
173+
174+
cy.get("@indexFetch.all").should("have.length", 2)
175+
cy.get("@appDataFetch.all").should("have.length", 2)
176+
cy.get("@pageDataFetch.all").should("have.length", 2)
177+
})
178+
}
62179
})

e2e-tests/production-runtime/cypress/plugins/compilation-hash.js

-37
This file was deleted.

e2e-tests/production-runtime/cypress/plugins/index.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
const compilationHash = require(`./compilation-hash`)
21
const blockResources = require(`./block-resources`)
32

43
module.exports = (on, config) => {
@@ -16,5 +15,5 @@ module.exports = (on, config) => {
1615
return args
1716
})
1817

19-
on(`task`, Object.assign({}, compilationHash, blockResources))
18+
on(`task`, Object.assign({}, blockResources))
2019
}

packages/gatsby-plugin-offline/src/gatsby-node.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ exports.onPostBuild = (
133133
// since these files have unique URLs and their contents will never change
134134
dontCacheBustURLsMatching: /(\.js$|\.css$|static\/)/,
135135
runtimeCaching: [
136+
// ignore cypress endpoints (only for testing)
137+
process.env.CYPRESS_SUPPORT
138+
? {
139+
urlPattern: /\/__cypress\//,
140+
handler: `NetworkOnly`,
141+
}
142+
: false,
136143
{
137144
// Use cacheFirst since these don't need to be revalidated (same RegExp
138145
// and same reason as above)
@@ -156,7 +163,7 @@ exports.onPostBuild = (
156163
urlPattern: /^https?:\/\/fonts\.googleapis\.com\/css/,
157164
handler: `StaleWhileRevalidate`,
158165
},
159-
],
166+
].filter(Boolean),
160167
skipWaiting: true,
161168
clientsClaim: true,
162169
}

packages/gatsby-plugin-offline/src/sw-append.js

+18
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,24 @@ const MessageAPI = {
1414

1515
clearPathResources: event => {
1616
event.waitUntil(idbKeyval.clear())
17+
18+
// We detected compilation hash mismatch
19+
// we should clear runtime cache as data
20+
// files might be out of sync and we should
21+
// do fresh fetches for them
22+
event.waitUntil(
23+
caches.keys().then(function (keyList) {
24+
return Promise.all(
25+
keyList.map(function (key) {
26+
if (key && key.includes(`runtime`)) {
27+
return caches.delete(key)
28+
}
29+
30+
return Promise.resolve()
31+
})
32+
)
33+
})
34+
)
1735
},
1836

1937
enableOfflineShell: () => {

packages/gatsby/cache-dir/__tests__/__snapshots__/static-entry.js.snap

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,21 @@ exports[`develop-static-entry onPreRenderHTML can be used to replace preBodyComp
1414
1515
exports[`static-entry onPreRenderHTML can be used to replace headComponents 1`] = `
1616
Object {
17-
"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>",
17+
"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>",
1818
"unsafeBuiltinsUsage": Array [],
1919
}
2020
`;
2121
2222
exports[`static-entry onPreRenderHTML can be used to replace postBodyComponents 1`] = `
2323
Object {
24-
"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>",
24+
"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>",
2525
"unsafeBuiltinsUsage": Array [],
2626
}
2727
`;
2828
2929
exports[`static-entry onPreRenderHTML can be used to replace preBodyComponents 1`] = `
3030
Object {
31-
"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>",
31+
"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>",
3232
"unsafeBuiltinsUsage": Array [],
3333
}
3434
`;

packages/gatsby/cache-dir/__tests__/static-entry.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,18 @@ jest.mock(
6363
const pageDataMock = {
6464
componentChunkName: `page-component---src-pages-test-js`,
6565
path: `/about/`,
66-
webpackCompilationHash: `1234567890abcdef1234`,
6766
staticQueryHashes: [],
6867
}
6968

69+
const webpackCompilationHash = `1234567890abcdef1234`
70+
7071
const MOCK_FILE_INFO = {
7172
[`${process.cwd()}/public/webpack.stats.json`]: `{}`,
7273
[`${process.cwd()}/public/chunk-map.json`]: `{}`,
7374
[join(process.cwd(), `/public/page-data/about/page-data.json`)]:
7475
JSON.stringify(pageDataMock),
7576
[join(process.cwd(), `/public/page-data/app-data.json`)]: JSON.stringify({
76-
webpackCompilationHash: `1234567890abcdef1234`,
77+
webpackCompilationHash,
7778
}),
7879
}
7980

@@ -173,11 +174,10 @@ const SSR_DEV_MOCK_FILE_INFO = {
173174
[join(publicDir, `page-data/about/page-data.json`)]: JSON.stringify({
174175
componentChunkName: `page-component---src-pages-about-js`,
175176
path: `/about/`,
176-
webpackCompilationHash: `1234567890abcdef1234`,
177177
staticQueryHashes: [],
178178
}),
179179
[join(publicDir, `page-data/app-data.json`)]: JSON.stringify({
180-
webpackCompilationHash: `1234567890abcdef1234`,
180+
webpackCompilationHash,
181181
}),
182182
}
183183

@@ -411,6 +411,7 @@ describe(`static-entry`, () => {
411411
styles: [],
412412
reversedStyles: [],
413413
reversedScripts: [],
414+
webpackCompilationHash,
414415
}
415416

416417
beforeEach(() => {

0 commit comments

Comments
 (0)