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 +

+
+) + +export default InstrumentPage(DeepLinkPage) diff --git a/e2e-tests/production-runtime/src/pages/index.js b/e2e-tests/production-runtime/src/pages/index.js index 6e211b8981b69..227a12903e829 100644 --- a/e2e-tests/production-runtime/src/pages/index.js +++ b/e2e-tests/production-runtime/src/pages/index.js @@ -46,6 +46,11 @@ const IndexPage = ({ pageContext }) => ( StaticQuery and useStaticQuery +
  • + + Compilation Hash Page + +
  • ) 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`] = `"
    div3
    div2
    div1
    "`; diff --git a/packages/gatsby/cache-dir/__tests__/static-entry.js b/packages/gatsby/cache-dir/__tests__/static-entry.js index 0f5253ccf1c08..1796f9913d667 100644 --- a/packages/gatsby/cache-dir/__tests__/static-entry.js +++ b/packages/gatsby/cache-dir/__tests__/static-entry.js @@ -37,6 +37,7 @@ const MOCK_FILE_INFO = { componentChunkName: `page-component---src-pages-test-js`, path: `/about/`, jsonName: `about.json`, + webpackCompilationHash: `1234567890abcdef1234`, }), } diff --git a/packages/gatsby/cache-dir/loader.js b/packages/gatsby/cache-dir/loader.js index 3423ec76f4b82..6584cc30bf8b8 100644 --- a/packages/gatsby/cache-dir/loader.js +++ b/packages/gatsby/cache-dir/loader.js @@ -292,6 +292,7 @@ const queue = { const page = { componentChunkName: pageData.componentChunkName, path: pageData.path, + webpackCompilationHash: pageData.webpackCompilationHash, } const jsonData = pageData.result diff --git a/packages/gatsby/cache-dir/navigation.js b/packages/gatsby/cache-dir/navigation.js index 85808d6d10251..9201a89855c72 100644 --- a/packages/gatsby/cache-dir/navigation.js +++ b/packages/gatsby/cache-dir/navigation.js @@ -82,6 +82,28 @@ const navigate = (to, options = {}) => { }, 1000) loader.loadPage(pathname).then(pageResources => { + // If the loaded page has a different compilation hash to the + // window, then a rebuild has occurred on the server. Reload. + if (process.env.NODE_ENV === `production` && pageResources) { + if ( + pageResources.page.webpackCompilationHash !== + window.___webpackCompilationHash + ) { + // Purge plugin-offline cache + if ( + `serviceWorker` in navigator && + navigator.serviceWorker.controller !== null && + navigator.serviceWorker.controller.state === `activated` + ) { + navigator.serviceWorker.controller.postMessage({ + gatsbyApi: `resetWhitelist`, + }) + } + + console.log(`Site has changed on server. Reloading browser`) + window.location = pathname + } + } reachNavigate(to, options) clearTimeout(timeoutId) }) diff --git a/packages/gatsby/cache-dir/production-app.js b/packages/gatsby/cache-dir/production-app.js index e1a77480ddc73..4f4b1efd363b4 100644 --- a/packages/gatsby/cache-dir/production-app.js +++ b/packages/gatsby/cache-dir/production-app.js @@ -18,6 +18,7 @@ import EnsureResources from "./ensure-resources" window.asyncRequires = asyncRequires window.___emitter = emitter window.___loader = loader +window.___webpackCompilationHash = window.webpackCompilationHash loader.addProdRequires(asyncRequires) setApiRunnerForLoader(apiRunner) diff --git a/packages/gatsby/cache-dir/static-entry.js b/packages/gatsby/cache-dir/static-entry.js index 0805e93fb6c63..4e0ddfa83622b 100644 --- a/packages/gatsby/cache-dir/static-entry.js +++ b/packages/gatsby/cache-dir/static-entry.js @@ -338,15 +338,17 @@ export default (pagePath, callback) => { } }) + const webpackCompilationHash = pageData.webpackCompilationHash + // Add page metadata for the current page - const windowPagePath = `/**/` + const windowPageData = `/**/` postBodyComponents.push(