From 4a01c6dede55647dac858601ab777ae6e7c40f88 Mon Sep 17 00:00:00 2001 From: David Bailey <4248177+davidbailey00@users.noreply.github.com> Date: Mon, 28 Jan 2019 16:54:36 +0000 Subject: [PATCH] feat(gatsby-plugin-offline): reload when missing resources and SW was updated + add "onServiceWorkerUpdateReady" API (#10432) --- ...d-offline-support-with-a-service-worker.md | 4 +-- packages/gatsby-plugin-offline/.gitignore | 1 - packages/gatsby-plugin-offline/package.json | 9 +++-- .../src/gatsby-browser.js | 11 +++++++ .../{ => src}/sw-append.js | 31 +++++++++-------- packages/gatsby/cache-dir/ensure-resources.js | 2 ++ packages/gatsby/cache-dir/navigation.js | 5 +-- .../cache-dir/register-service-worker.js | 11 ++++++- packages/gatsby/src/utils/api-browser-docs.js | 8 +++++ yarn.lock | 33 +++++++++++++++++-- 10 files changed, 88 insertions(+), 27 deletions(-) rename packages/gatsby-plugin-offline/{ => src}/sw-append.js (78%) diff --git a/docs/docs/add-offline-support-with-a-service-worker.md b/docs/docs/add-offline-support-with-a-service-worker.md index 1947f985da6da..731d27deb9b8e 100644 --- a/docs/docs/add-offline-support-with-a-service-worker.md +++ b/docs/docs/add-offline-support-with-a-service-worker.md @@ -45,10 +45,10 @@ Note: Service worker registers only in production builds (`gatsby build`). ### Displaying a message when a service worker updates -To display a custom message once your service worker finds an update, you can use the [`onServiceWorkerUpdateFound`](/docs/browser-apis/#onServiceWorkerUpdateFound) browser API in your `gatsby-browser.js` file. The following code will display a confirm prompt asking the user whether they would like to refresh the page when an update is found: +To display a custom message once your service worker finds an update, you can use the [`onServiceWorkerUpdateReady`](/docs/browser-apis/#onServiceWorkerUpdateReady) browser API in your `gatsby-browser.js` file. The following code will display a confirm prompt asking the user whether they would like to refresh the page when an update is found: ```javascript:title=gatsby-browser.js -exports.onServiceWorkerUpdateFound = () => { +exports.onServiceWorkerUpdateReady = () => { const answer = window.confirm( `This application has been updated. ` + `Reload to display the latest version?` diff --git a/packages/gatsby-plugin-offline/.gitignore b/packages/gatsby-plugin-offline/.gitignore index 10cf84f0d2dc8..8c9686624a187 100644 --- a/packages/gatsby-plugin-offline/.gitignore +++ b/packages/gatsby-plugin-offline/.gitignore @@ -1,4 +1,3 @@ /*.js !index.js -!sw-append.js yarn.lock diff --git a/packages/gatsby-plugin-offline/package.json b/packages/gatsby-plugin-offline/package.json index 3793abbf556a8..433db8a0fa786 100644 --- a/packages/gatsby-plugin-offline/package.json +++ b/packages/gatsby-plugin-offline/package.json @@ -9,6 +9,7 @@ "dependencies": { "@babel/runtime": "^7.0.0", "cheerio": "^1.0.0-rc.2", + "cpx": "^1.5.0", "idb-keyval": "^3.1.0", "lodash": "^4.17.10", "workbox-build": "^3.6.3" @@ -30,12 +31,14 @@ "license": "MIT", "main": "index.js", "peerDependencies": { - "gatsby": ">=2.0.53" + "gatsby": "^2.0.100" }, "repository": "https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-offline", "scripts": { - "build": "babel src --out-dir . --ignore **/__tests__", + "build": "npm run build:src && npm run build:sw-append", + "build:src": "babel src --out-dir . --ignore **/__tests__,src/sw-append.js", + "build:sw-append": "cpx -v src/sw-append.js .", "prepare": "cross-env NODE_ENV=production npm run build", - "watch": "babel -w src --out-dir . --ignore **/__tests__" + "watch": "npm run build:sw-append -- --watch & npm run build:src -- --watch" } } diff --git a/packages/gatsby-plugin-offline/src/gatsby-browser.js b/packages/gatsby-plugin-offline/src/gatsby-browser.js index 947a2bc2649e6..a9ee410438ff9 100644 --- a/packages/gatsby-plugin-offline/src/gatsby-browser.js +++ b/packages/gatsby-plugin-offline/src/gatsby-browser.js @@ -7,6 +7,13 @@ exports.onServiceWorkerActive = ({ getResourceURLsForPathname, serviceWorker, }) => { + // if the SW has just updated then reset whitelisted paths and don't cache + // stuff, since we're on the old revision until we navigate to another page + if (window.___swUpdated) { + serviceWorker.active.postMessage({ gatsbyApi: `resetWhitelist` }) + return + } + // grab nodes from head of document const nodes = document.querySelectorAll(` head > script[src], @@ -63,6 +70,10 @@ function whitelistPathname(pathname, includesPrefix) { } exports.onPostPrefetchPathname = ({ pathname }) => { + // do nothing if the SW has just updated, since we still have old pages in + // memory which we don't want to be whitelisted + if (window.___swUpdated) return + whitelistPathname(pathname, false) // if SW is not installed, we need to record any prefetches diff --git a/packages/gatsby-plugin-offline/sw-append.js b/packages/gatsby-plugin-offline/src/sw-append.js similarity index 78% rename from packages/gatsby-plugin-offline/sw-append.js rename to packages/gatsby-plugin-offline/src/sw-append.js index b209013dd20d8..e4d296e70fc6d 100644 --- a/packages/gatsby-plugin-offline/sw-append.js +++ b/packages/gatsby-plugin-offline/src/sw-append.js @@ -13,20 +13,23 @@ const navigationRoute = new workbox.routing.NavigationRoute(({ event }) => { const cacheName = workbox.core.cacheNames.precache return caches.match(offlineShell, { cacheName }).then(cachedResponse => { - if (!cachedResponse) { - return fetch(offlineShell).then(response => { - if (response.ok) { - return caches.open(cacheName).then(cache => - // Clone is needed because put() consumes the response body. - cache.put(offlineShell, response.clone()).then(() => response) - ) - } else { - return fetch(event.request) - } - }) - } - - return cachedResponse + if (cachedResponse) return cachedResponse + + console.error( + `The offline shell (${offlineShell}) was not found ` + + `while attempting to serve a response for ${pathname}` + ) + + return fetch(offlineShell).then(response => { + if (response.ok) { + return caches.open(cacheName).then(cache => + // Clone is needed because put() consumes the response body. + cache.put(offlineShell, response.clone()).then(() => response) + ) + } else { + return fetch(event.request) + } + }) }) } diff --git a/packages/gatsby/cache-dir/ensure-resources.js b/packages/gatsby/cache-dir/ensure-resources.js index 7b2f38244c043..0c37dd50333d6 100644 --- a/packages/gatsby/cache-dir/ensure-resources.js +++ b/packages/gatsby/cache-dir/ensure-resources.js @@ -129,6 +129,8 @@ class EnsureResources extends React.Component { render() { if (!this.hasResources(this.state.pageResources) && isInitialRender) { + window.___failedResources = true + // prevent hydrating throw new Error(`Missing resources for ${this.state.location.pathname}`) } diff --git a/packages/gatsby/cache-dir/navigation.js b/packages/gatsby/cache-dir/navigation.js index 9b50ec8b57ca4..798caa7e2aa76 100644 --- a/packages/gatsby/cache-dir/navigation.js +++ b/packages/gatsby/cache-dir/navigation.js @@ -67,10 +67,7 @@ const navigate = (to, options = {}) => { // If we had a service worker update, no matter the path, reload window and // reset the pathname whitelist - if (window.GATSBY_SW_UPDATED) { - const { controller } = navigator.serviceWorker - controller.postMessage({ gatsbyApi: `resetWhitelist` }) - + if (window.___swUpdated) { window.location = pathname return } diff --git a/packages/gatsby/cache-dir/register-service-worker.js b/packages/gatsby/cache-dir/register-service-worker.js index b2edd20569d4d..acb6347b450b0 100644 --- a/packages/gatsby/cache-dir/register-service-worker.js +++ b/packages/gatsby/cache-dir/register-service-worker.js @@ -23,8 +23,17 @@ if ( if (navigator.serviceWorker.controller) { // At this point, the old content will have been purged and the fresh content will // have been added to the cache. + // We set a flag so Gatsby Link knows to refresh the page on next navigation attempt - window.GATSBY_SW_UPDATED = true + window.___swUpdated = true + // We call the onServiceWorkerUpdateReady API so users can show update prompts. + apiRunner(`onServiceWorkerUpdateReady`, { serviceWorker: reg }) + + // If resources failed for the current page, reload. + if (window.___failedResources) { + console.log(`resources failed, SW updated - reloading`) + window.location.reload() + } } else { // At this point, everything has been precached. // It's the perfect time to display a "Content is cached for offline use." message. diff --git a/packages/gatsby/src/utils/api-browser-docs.js b/packages/gatsby/src/utils/api-browser-docs.js index 170f8eb7fecc0..cce1fe014c78a 100644 --- a/packages/gatsby/src/utils/api-browser-docs.js +++ b/packages/gatsby/src/utils/api-browser-docs.js @@ -211,6 +211,14 @@ exports.onServiceWorkerInstalled = true */ exports.onServiceWorkerUpdateFound = true +/** + * Inform plugins when a service worker has been updated in the background + * and the page is ready to reload to apply changes. + * @param {object} $0 + * @param {object} $0.serviceWorker The service worker instance. + */ +exports.onServiceWorkerUpdateReady = true + /** * Inform plugins when a service worker has become active. * @param {object} $0 diff --git a/yarn.lock b/yarn.lock index 8c425b086ba93..928cc88dd015f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3665,7 +3665,7 @@ babel-register@^6.26.0, babel-register@^6.9.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0: +babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0, babel-runtime@^6.9.2: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= @@ -4662,7 +4662,7 @@ cheerio@^1.0.0-rc.2: lodash "^4.15.0" parse5 "^3.0.1" -chokidar@^1.4.2, chokidar@^1.7.0: +chokidar@^1.4.2, chokidar@^1.6.0, chokidar@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" integrity sha1-eY5ol3gVHIB2tLNg5e3SjNortGg= @@ -5487,6 +5487,23 @@ cosmiconfig@^5.0.0, cosmiconfig@^5.0.2, cosmiconfig@^5.0.5, cosmiconfig@^5.0.6: js-yaml "^3.9.0" parse-json "^4.0.0" +cpx@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/cpx/-/cpx-1.5.0.tgz#185be018511d87270dedccc293171e37655ab88f" + integrity sha1-GFvgGFEdhycN7czCkxceN2VauI8= + dependencies: + babel-runtime "^6.9.2" + chokidar "^1.6.0" + duplexer "^0.1.1" + glob "^7.0.5" + glob2base "^0.0.12" + minimatch "^3.0.2" + mkdirp "^0.5.1" + resolve "^1.1.7" + safe-buffer "^5.0.1" + shell-quote "^1.6.1" + subarg "^1.0.0" + crc-32@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208" @@ -8023,6 +8040,11 @@ find-cache-dir@^2.0.0: make-dir "^1.0.0" pkg-dir "^3.0.0" +find-index@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4" + integrity sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ= + find-npm-prefix@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/find-npm-prefix/-/find-npm-prefix-1.0.2.tgz#8d8ce2c78b3b4b9e66c8acc6a37c231eb841cfdf" @@ -8649,6 +8671,13 @@ glob-to-regexp@^0.3.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= +glob2base@^0.0.12: + version "0.0.12" + resolved "https://registry.yarnpkg.com/glob2base/-/glob2base-0.0.12.tgz#9d419b3e28f12e83a362164a277055922c9c0d56" + integrity sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY= + dependencies: + find-index "^0.1.1" + glob@6.0.4: version "6.0.4" resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"