Skip to content

Commit

Permalink
fix(gatsby): refresh browser if webpack rebuild occurs (gatsbyjs#13871)
Browse files Browse the repository at this point in the history
* move query running into build and develop

* save build compilation hash

* write compilationHash to page data

* reload browser if rebuild occurs in background

* add test to ensure that browser is reloaded if rebuild occurs

* update page-datas when compilation hash changes

* use worker pool to udpate page data compilation hash

* update tests snapshot

* reset plugin offline whitelist if compilation hash changes

* prettier: remove global Cypress

* separate page for testing compilation-hash

* fix case typo

* mock out static entry test webpackCompilationHash field

* consolidate jest-worker calls into a central worker pool
  • Loading branch information
Moocar committed Jun 2, 2019
1 parent 253e6bc commit 0200949
Show file tree
Hide file tree
Showing 33 changed files with 462 additions and 152 deletions.
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
32 changes: 32 additions & 0 deletions e2e-tests/production-runtime/cypress/plugins/compilation-hash.js
Original file line number Diff line number Diff line change
@@ -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,
}
15 changes: 3 additions & 12 deletions e2e-tests/production-runtime/cypress/plugins/index.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -27,4 +16,6 @@ module.exports = (on, config) => {
return args
})
}

on(`task`, Object.assign({}, compilationHash))
}
1 change: 1 addition & 0 deletions e2e-tests/production-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
19 changes: 19 additions & 0 deletions e2e-tests/production-runtime/src/pages/compilation-hash.js
Original file line number Diff line number Diff line change
@@ -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 = () => (
<Layout>
<h1>Hi from Compilation Hash page</h1>
<p>Used by integration/compilation-hash.js test</p>
<p>
<Link to="/deep-link-page/" data-testid="deep-link-page">
To deeply linked page
</Link>
</p>
</Layout>
)

export default InstrumentPage(CompilationHashPage)
16 changes: 16 additions & 0 deletions e2e-tests/production-runtime/src/pages/deep-link-page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react'

import Layout from '../components/layout'
import InstrumentPage from '../utils/instrument-page'

const DeepLinkPage = () => (
<Layout>
<h1>Hi from a deeply linked page</h1>
<p>
Used to navigate to a non prefetched page by
integrations/compilation-hash.js tests
</p>
</Layout>
)

export default InstrumentPage(DeepLinkPage)
5 changes: 5 additions & 0 deletions e2e-tests/production-runtime/src/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ const IndexPage = ({ pageContext }) => (
StaticQuery and useStaticQuery
</Link>
</li>
<li>
<Link to="/compilation-hash/" data-testid="compilation-hash">
Compilation Hash Page
</Link>
</li>
</ul>
</Layout>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`] = `"<!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\\"/><script src=\\"/socket.io/socket.io.js\\"></script></head><body><div> div3 </div><div> div2 </div><div> div1 </div><noscript id=\\"gatsby-noscript\\">This app works best with JavaScript enabled.</noscript><div id=\\"___gatsby\\"></div><script src=\\"/commons.js\\"></script></body></html>"`;
exports[`static-entry onPreRenderHTML can be used to replace headComponents 1`] = `"<!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/about/page-data.json\\" crossorigin=\\"use-credentials\\"/><style> .style3 </style><style> .style2 </style><style> .style1 </style><meta name=\\"generator\\" content=\\"Gatsby 2.0.0\\"/></head><body><noscript id=\\"gatsby-noscript\\">This app works best with JavaScript enabled.</noscript><div id=\\"___gatsby\\"><div style=\\"outline:none\\" tabindex=\\"-1\\" role=\\"group\\"></div></div><script id=\\"gatsby-script-loader\\">/*<![CDATA[*/window.pagePath=\\"/about/\\";/*]]>*/</script><script id=\\"gatsby-chunk-mapping\\">/*<![CDATA[*/window.___chunkMapping={};/*]]>*/</script></body></html>"`;
exports[`static-entry onPreRenderHTML can be used to replace headComponents 1`] = `"<!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/about/page-data.json\\" crossorigin=\\"use-credentials\\"/><style> .style3 </style><style> .style2 </style><style> .style1 </style><meta name=\\"generator\\" content=\\"Gatsby 2.0.0\\"/></head><body><noscript id=\\"gatsby-noscript\\">This app works best with JavaScript enabled.</noscript><div id=\\"___gatsby\\"><div style=\\"outline:none\\" tabindex=\\"-1\\" role=\\"group\\"></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>"`;
exports[`static-entry onPreRenderHTML can be used to replace postBodyComponents 1`] = `"<!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=\\"use-credentials\\"/></head><body><noscript id=\\"gatsby-noscript\\">This app works best with JavaScript enabled.</noscript><div id=\\"___gatsby\\"><div style=\\"outline:none\\" tabindex=\\"-1\\" role=\\"group\\"></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>"`;
exports[`static-entry onPreRenderHTML can be used to replace postBodyComponents 1`] = `"<!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=\\"use-credentials\\"/></head><body><noscript id=\\"gatsby-noscript\\">This app works best with JavaScript enabled.</noscript><div id=\\"___gatsby\\"><div style=\\"outline:none\\" tabindex=\\"-1\\" role=\\"group\\"></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>"`;
exports[`static-entry onPreRenderHTML can be used to replace preBodyComponents 1`] = `"<!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=\\"use-credentials\\"/></head><body><div> div3 </div><div> div2 </div><div> div1 </div><noscript id=\\"gatsby-noscript\\">This app works best with JavaScript enabled.</noscript><div id=\\"___gatsby\\"><div style=\\"outline:none\\" tabindex=\\"-1\\" role=\\"group\\"></div></div><script id=\\"gatsby-script-loader\\">/*<![CDATA[*/window.pagePath=\\"/about/\\";/*]]>*/</script><script id=\\"gatsby-chunk-mapping\\">/*<![CDATA[*/window.___chunkMapping={};/*]]>*/</script></body></html>"`;
exports[`static-entry onPreRenderHTML can be used to replace preBodyComponents 1`] = `"<!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=\\"use-credentials\\"/></head><body><div> div3 </div><div> div2 </div><div> div1 </div><noscript id=\\"gatsby-noscript\\">This app works best with JavaScript enabled.</noscript><div id=\\"___gatsby\\"><div style=\\"outline:none\\" tabindex=\\"-1\\" role=\\"group\\"></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>"`;
1 change: 1 addition & 0 deletions packages/gatsby/cache-dir/__tests__/static-entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const MOCK_FILE_INFO = {
componentChunkName: `page-component---src-pages-test-js`,
path: `/about/`,
jsonName: `about.json`,
webpackCompilationHash: `1234567890abcdef1234`,
}),
}

Expand Down
1 change: 1 addition & 0 deletions packages/gatsby/cache-dir/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ const queue = {
const page = {
componentChunkName: pageData.componentChunkName,
path: pageData.path,
webpackCompilationHash: pageData.webpackCompilationHash,
}

const jsonData = pageData.result
Expand Down
22 changes: 22 additions & 0 deletions packages/gatsby/cache-dir/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down
1 change: 1 addition & 0 deletions packages/gatsby/cache-dir/production-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions packages/gatsby/cache-dir/static-entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,15 +338,17 @@ export default (pagePath, callback) => {
}
})

const webpackCompilationHash = pageData.webpackCompilationHash

// Add page metadata for the current page
const windowPagePath = `/*<![CDATA[*/window.pagePath="${pagePath}";/*]]>*/`
const windowPageData = `/*<![CDATA[*/window.pagePath="${pagePath}";window.webpackCompilationHash="${webpackCompilationHash}";/*]]>*/`

postBodyComponents.push(
<script
key={`script-loader`}
id={`gatsby-script-loader`}
dangerouslySetInnerHTML={{
__html: windowPagePath,
__html: windowPageData,
}}
/>
)
Expand Down
62 changes: 5 additions & 57 deletions packages/gatsby/src/bootstrap/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ process.on(`unhandledRejection`, (reason, p) => {
})

const { extractQueries } = require(`../query/query-watcher`)
const queryUtil = require(`../query`)
const requiresWriter = require(`./requires-writer`)
const { writeRedirects } = require(`./redirects-writer`)

Expand Down Expand Up @@ -447,42 +446,15 @@ module.exports = async (args: BootstrapArgs) => {
await extractQueries()
activity.end()

// Start the createPages hot reloader.
if (process.env.NODE_ENV !== `production`) {
require(`./page-hot-reloader`)(graphqlRunner)
}

const queryIds = queryUtil.calcInitialDirtyQueryIds(store.getState())
const { staticQueryIds, pageQueryIds } = queryUtil.groupQueryIds(queryIds)

activity = report.activityTimer(`run static queries`, {
parentSpan: bootstrapSpan,
})
activity.start()
await queryUtil.processStaticQueries(staticQueryIds, {
activity,
state: store.getState(),
})
activity.end()

activity = report.activityTimer(`run page queries`)
activity.start()
await queryUtil.processPageQueries(pageQueryIds, { activity })
activity.end()

require(`../redux/actions`).boundActionCreators.setProgramStatus(
`BOOTSTRAP_QUERY_RUNNING_FINISHED`
)

// Write out files.
activity = report.activityTimer(`write out page data`, {
activity = report.activityTimer(`write out requires`, {
parentSpan: bootstrapSpan,
})
activity.start()
try {
await requiresWriter.writeAll(store.getState())
} catch (err) {
report.panic(`Failed to write out page data`, err)
report.panic(`Failed to write out requires`, err)
}
activity.end()

Expand All @@ -494,33 +466,7 @@ module.exports = async (args: BootstrapArgs) => {
await writeRedirects()
activity.end()

let onEndJob

const checkJobsDone = _.debounce(async resolve => {
const state = store.getState()
if (state.jobs.active.length === 0) {
emitter.off(`END_JOB`, onEndJob)

await finishBootstrap(bootstrapSpan)
resolve({ graphqlRunner })
}
}, 100)

if (store.getState().jobs.active.length === 0) {
await finishBootstrap(bootstrapSpan)
return { graphqlRunner }
} else {
return new Promise(resolve => {
// Wait until all side effect jobs are finished.
onEndJob = () => checkJobsDone(resolve)
emitter.on(`END_JOB`, onEndJob)
})
}
}

const finishBootstrap = async bootstrapSpan => {
// onPostBootstrap
const activity = report.activityTimer(`onPostBootstrap`, {
activity = report.activityTimer(`onPostBootstrap`, {
parentSpan: bootstrapSpan,
})
activity.start()
Expand All @@ -536,4 +482,6 @@ const finishBootstrap = async bootstrapSpan => {
)

bootstrapSpan.finish()

return { graphqlRunner }
}
Loading

0 comments on commit 0200949

Please sign in to comment.