diff --git a/e2e-tests/production-runtime/cypress/integration/1-production.js b/e2e-tests/production-runtime/cypress/integration/1-production.js
index 3e1da2d76876b..3e7c0d172e5eb 100644
--- a/e2e-tests/production-runtime/cypress/integration/1-production.js
+++ b/e2e-tests/production-runtime/cypress/integration/1-production.js
@@ -1,5 +1,14 @@
/* global Cypress, cy */
+// NOTE: This needs to be run before any other integration tests as it
+// sets up the service worker in offline mode. Therefore, if you want
+// to test an individual integration test, you must run this
+// first. E.g to run `compilation-hash.js` test, run
+//
+// cypress run -s \
+// "cypress/integration/1-production.js,cypress/integration/compilation-hash.js" \
+// -b chrome
+
describe(`Production build tests`, () => {
it(`should render properly`, () => {
cy.visit(`/`).waitForRouteChange()
diff --git a/e2e-tests/production-runtime/cypress/integration/compilation-hash.js b/e2e-tests/production-runtime/cypress/integration/compilation-hash.js
new file mode 100644
index 0000000000000..84fb3213fad29
--- /dev/null
+++ b/e2e-tests/production-runtime/cypress/integration/compilation-hash.js
@@ -0,0 +1,64 @@
+/* global cy */
+
+const getRandomInt = (min, max) => {
+ min = Math.ceil(min)
+ max = Math.floor(max)
+ return Math.floor(Math.random() * (max - min)) + min
+}
+
+const createMockCompilationHash = () =>
+ [...Array(20)]
+ .map(a => getRandomInt(0, 16))
+ .map(k => k.toString(16))
+ .join(``)
+
+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(`should reload page if build occurs in background`, () => {
+ cy.window().then(window => {
+ const oldHash = window.___webpackCompilationHash
+ expect(oldHash).to.not.eq(undefined)
+
+ const mockHash = createMockCompilationHash()
+
+ // Simulate a new webpack build
+ cy.task(`overwriteWebpackCompilationHash`, mockHash).then(() => {
+ cy.getTestElement(`compilation-hash`).click()
+ cy.waitForAPIorTimeout(`onRouteUpdate`, { timeout: 3000 })
+
+ // Navigate into a non-prefetched page
+ cy.getTestElement(`deep-link-page`).click()
+ cy.waitForAPIorTimeout(`onRouteUpdate`, { timeout: 3000 })
+
+ // If the window compilation hash has changed, we know the
+ // page was refreshed
+ cy.window()
+ .its(`___webpackCompilationHash`)
+ .should(`equal`, mockHash)
+ })
+
+ // Cleanup
+ cy.task(`overwriteWebpackCompilationHash`, oldHash)
+ })
+ })
+})
diff --git a/e2e-tests/production-runtime/cypress/plugins/compilation-hash.js b/e2e-tests/production-runtime/cypress/plugins/compilation-hash.js
new file mode 100644
index 0000000000000..61e38d642cc9a
--- /dev/null
+++ b/e2e-tests/production-runtime/cypress/plugins/compilation-hash.js
@@ -0,0 +1,32 @@
+const fs = require(`fs-extra`)
+const path = require(`path`)
+const glob = require(`glob`)
+
+const replaceHtmlCompilationHash = (filename, newHash) => {
+ const html = fs.readFileSync(filename, `utf-8`)
+ const regex = /window\.webpackCompilationHash="\w*"/
+ const replace = `window.webpackCompilationHash="${newHash}"`
+ fs.writeFileSync(filename, html.replace(regex, replace), `utf-8`)
+}
+
+const replacePageDataCompilationHash = (filename, newHash) => {
+ const pageData = JSON.parse(fs.readFileSync(filename, `utf-8`))
+ pageData.webpackCompilationHash = newHash
+ fs.writeFileSync(filename, JSON.stringify(pageData), `utf-8`)
+}
+
+const overwriteWebpackCompilationHash = newHash => {
+ glob
+ .sync(path.join(__dirname, `../../public/page-data/**/page-data.json`))
+ .forEach(filename => replacePageDataCompilationHash(filename, newHash))
+ glob
+ .sync(path.join(__dirname, `../../public/**/index.html`))
+ .forEach(filename => replaceHtmlCompilationHash(filename, newHash))
+
+ // cypress requires that null be returned instead of undefined
+ return null
+}
+
+module.exports = {
+ overwriteWebpackCompilationHash,
+}
diff --git a/e2e-tests/production-runtime/cypress/plugins/index.js b/e2e-tests/production-runtime/cypress/plugins/index.js
index 871f0fb6a83c5..9592c0abff476 100644
--- a/e2e-tests/production-runtime/cypress/plugins/index.js
+++ b/e2e-tests/production-runtime/cypress/plugins/index.js
@@ -1,15 +1,4 @@
-// ***********************************************************
-// This example plugins/index.js can be used to load plugins
-//
-// You can change the location of this file or turn off loading
-// the plugins file with the 'pluginsFile' configuration option.
-//
-// You can read more here:
-// https://on.cypress.io/plugins-guide
-// ***********************************************************
-
-// This function is called when a project is opened or re-opened (e.g. due to
-// the project's config changing)
+const compilationHash = require(`./compilation-hash`)
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
@@ -27,4 +16,6 @@ module.exports = (on, config) => {
return args
})
}
+
+ on(`task`, Object.assign({}, compilationHash))
}
diff --git a/e2e-tests/production-runtime/package.json b/e2e-tests/production-runtime/package.json
index 662a8baed0781..46619625f23ad 100644
--- a/e2e-tests/production-runtime/package.json
+++ b/e2e-tests/production-runtime/package.json
@@ -9,6 +9,7 @@
"gatsby-plugin-manifest": "^2.0.17",
"gatsby-plugin-offline": "^2.0.23",
"gatsby-plugin-react-helmet": "^3.0.6",
+ "glob": "^7.1.3",
"react": "^16.8.0",
"react-dom": "^16.8.0",
"react-helmet": "^5.2.0"
diff --git a/e2e-tests/production-runtime/src/pages/compilation-hash.js b/e2e-tests/production-runtime/src/pages/compilation-hash.js
new file mode 100644
index 0000000000000..0fccb86d27ae7
--- /dev/null
+++ b/e2e-tests/production-runtime/src/pages/compilation-hash.js
@@ -0,0 +1,19 @@
+import React from 'react'
+import { Link } from 'gatsby'
+
+import Layout from '../components/layout'
+import InstrumentPage from '../utils/instrument-page'
+
+const CompilationHashPage = () => (
+
+
Hi from Compilation Hash page
+
Used by integration/compilation-hash.js test
+
+
+ To deeply linked page
+
+
+
+)
+
+export default InstrumentPage(CompilationHashPage)
diff --git a/e2e-tests/production-runtime/src/pages/deep-link-page.js b/e2e-tests/production-runtime/src/pages/deep-link-page.js
new file mode 100644
index 0000000000000..6d1b881f8fb0e
--- /dev/null
+++ b/e2e-tests/production-runtime/src/pages/deep-link-page.js
@@ -0,0 +1,16 @@
+import React from 'react'
+
+import Layout from '../components/layout'
+import InstrumentPage from '../utils/instrument-page'
+
+const DeepLinkPage = () => (
+
+
Hi from a deeply linked page
+
+ Used to navigate to a non prefetched page by
+ integrations/compilation-hash.js tests
+
)
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 ebd71a1b4df16..9b381e0865bc3 100644
--- a/packages/gatsby/cache-dir/__tests__/__snapshots__/static-entry.js.snap
+++ b/packages/gatsby/cache-dir/__tests__/__snapshots__/static-entry.js.snap
@@ -6,8 +6,8 @@ exports[`develop-static-entry onPreRenderHTML can be used to replace postBodyCom
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`] = `"
"`;
+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 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 preBodyComponents 1`] = `"