diff --git a/cli/src/lib/pwa/compileServiceWorker.js b/cli/src/lib/pwa/compileServiceWorker.js index 0a2be598f..8bb2df4ec 100644 --- a/cli/src/lib/pwa/compileServiceWorker.js +++ b/cli/src/lib/pwa/compileServiceWorker.js @@ -35,7 +35,11 @@ function compileServiceWorker({ config, paths, mode }) { // TODO: This could be cleaner if the production SW is built in the same // way instead of using the CRA webpack config, so both can more easily // share environment variables. - const env = getEnv({ name: config.title, ...getPWAEnvVars(config) }) + const env = getEnv({ + name: config.title, + version: config.version, + ...getPWAEnvVars(config), + }) const webpackConfig = { mode, // "production" or "development" diff --git a/package.json b/package.json index 64d4b697a..4dbe7a680 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "@dhis2/cli-style": "^10.4.3", "@dhis2/cli-utils-docsite": "^3.0.0", "concurrently": "^6.0.0", + "patch-package": "^8.0.0", + "postinstall-postinstall": "^2.1.0", "serve": "^12.0.0" }, "scripts": { @@ -37,7 +39,8 @@ "docs:build": "d2-utils-docsite build ./docs -o ./dist", "test:adapter": "yarn workspace @dhis2/app-adapter test", "test:cli": "yarn workspace @dhis2/cli-app-scripts test", - "test": "yarn test:adapter && yarn test:cli" + "test": "yarn test:adapter && yarn test:cli", + "postinstall": "patch-package" }, "d2": { "docsite": { diff --git a/patches/workbox-precaching+6.5.3.patch b/patches/workbox-precaching+6.5.3.patch new file mode 100644 index 000000000..13e198f5f --- /dev/null +++ b/patches/workbox-precaching+6.5.3.patch @@ -0,0 +1,57 @@ +diff --git a/node_modules/workbox-precaching/PrecacheController.js b/node_modules/workbox-precaching/PrecacheController.js +index e00975e..b728b6a 100644 +--- a/node_modules/workbox-precaching/PrecacheController.js ++++ b/node_modules/workbox-precaching/PrecacheController.js +@@ -150,6 +150,7 @@ class PrecacheController { + return waitUntil(event, async () => { + const installReportPlugin = new PrecacheInstallReportPlugin(); + this.strategy.plugins.push(installReportPlugin); ++ const failedUrls = [] + // Cache entries one at a time. + // See https://github.com/GoogleChrome/workbox/issues/2528 + for (const [url, cacheKey] of this._urlsToCacheKeys) { +@@ -164,12 +165,43 @@ class PrecacheController { + params: { cacheKey }, + request, + event, +- })); ++ })) ++ // DHIS2: Catch precaching errors to provide a more thorough ++ // error message ++ .catch(err => { ++ failedUrls.push(url) ++ console.error(`Error when attempting to fetch and cache URL ${url}:\n`, err) ++ }); + } + const { updatedURLs, notUpdatedURLs } = installReportPlugin; + if (process.env.NODE_ENV !== 'production') { + printInstallDetails(updatedURLs, notUpdatedURLs); + } ++ // DHIS2: Log failed requests to give clear feedback, and throw ++ // error to abort installation ++ if (failedUrls.length > 0) { ++ const appVersion = process.env.REACT_APP_DHIS2_APP_VERSION ++ const appName = process.env.REACT_APP_DHIS2_APP_NAME ++ const errorMessage = ++ `The following assets were unable to be precached when ` + ++ `attempting to install ${appName} version ${appVersion}, ` + ++ `so the installation has been aborted:\n` + ++ `${failedUrls.reduce((acc, curr) => acc + ` * ${curr}\n`, '')}\n` + ++ `If another version of the app is installed in the browser, ` + ++ `you will continue to see that version. If not, you will ` + ++ `see version ${appVersion}, but caching and offline ` + ++ `features will be unavailable, and the app may not be ` + ++ `fully functional due to the missing files. You can find ` + ++ `the app version that's currently active at the bottom ` + ++ `of the user profile menu.\n\n` + ++ `[FOR TECHNICAL USERS]: If another version is installed ` + ++ `and you would like to attempt to view version ` + ++ `${appVersion} (knowing the above caveats), you may use ` + ++ `your browser's developer tools to find the service worker ` + ++ `that's currently active for this app and unregister it.` ++ throw new Error(errorMessage) ++ } ++ + return { updatedURLs, notUpdatedURLs }; + }); + } diff --git a/pwa/src/service-worker/set-up-service-worker.js b/pwa/src/service-worker/set-up-service-worker.js index 4bc5bfad7..ac20c713d 100644 --- a/pwa/src/service-worker/set-up-service-worker.js +++ b/pwa/src/service-worker/set-up-service-worker.js @@ -1,4 +1,9 @@ -import { precacheAndRoute, matchPrecache, precache } from 'workbox-precaching' +import { + precacheAndRoute, + matchPrecache, + // PrecacheController, + // PrecacheRoute, +} from 'workbox-precaching' import { registerRoute, setDefaultHandler } from 'workbox-routing' import { NetworkFirst, @@ -42,7 +47,7 @@ export function setUpServiceWorker() { // Disable verbose logs // TODO: control with env var - self.__WB_DISABLE_DEV_LOGS = true + // self.__WB_DISABLE_DEV_LOGS = true // Globals (Note: global state resets each time SW goes idle) @@ -60,21 +65,6 @@ export function setUpServiceWorker() { // In development, static assets are handled by 'network first' strategy // and will be kept up-to-date. if (PRODUCTION_ENV) { - // Precache all of the assets generated by your build process. - // Their URLs are injected into the manifest variable below. - // This variable must be present somewhere in your service worker file, - // even if you decide not to use precaching. See https://cra.link/PWA. - // Includes all built assets and index.html - const precacheManifest = self.__WB_MANIFEST || [] - - // todo: also do this routing for plugin.html - // Extract index.html from the manifest to precache, then route - // in a custom way - const indexHtmlManifestEntry = precacheManifest.find(({ url }) => - url.endsWith('index.html') - ) - precache([indexHtmlManifestEntry]) - // Custom strategy for handling app navigation, specifically to allow // navigations to redirect to the login page while online if the // user is unauthenticated. Fixes showing the app shell login dialog @@ -125,13 +115,17 @@ export function setUpServiceWorker() { // NOTE: This route must come before any precacheAndRoute calls registerRoute(navigationRouteMatcher, navigationRouteHandler) - // Handle the rest of files in the manifest - filter out index.html, - // and all moment-locales, which bulk up the precache and slow down - // installation significantly. Handle them network-first in app shell - const restOfManifest = precacheManifest.filter((e) => { - if (e === indexHtmlManifestEntry) { - return false - } + // Precache all of the assets generated by your build process. + // Their URLs are injected into the manifest variable below. + // This variable must be present somewhere in your service worker file, + // even if you decide not to use precaching. See https://cra.link/PWA. + // Includes all built assets and index.html + const precacheManifest = self.__WB_MANIFEST || [] + + // This precache manifest needs to be filtered here instead of at build + // time because CRA manages the build process. The manifests below + // are managed by our own build tools, so we can filter at build time + const filteredPrecacheManifest = precacheManifest.filter((e) => { // Files from the precache manifest generated by CRA need to be // managed here, because we don't have access to their webpack // config @@ -140,11 +134,9 @@ export function setUpServiceWorker() { ) return !entryShouldBeExcluded }) - precacheAndRoute(restOfManifest) // Same thing for built plugin assets const pluginPrecacheManifest = self.__WB_PLUGIN_MANIFEST || [] - precacheAndRoute(pluginPrecacheManifest) // Similar to above; manifest injection from `workbox-build` // Precaches all assets in the shell's build folder except in `static` @@ -154,7 +146,86 @@ export function setUpServiceWorker() { // 'injectPrecacheManifest.js' in the CLI package. // '[]' fallback prevents an error when switching pwa enabled to disabled const sharedBuildManifest = self.__WB_BUILD_MANIFEST || [] - precacheAndRoute(sharedBuildManifest) + + // Add all these URLs to the precache list and instruct workbox to make + // a route for them + // NOTE: this includes index.html since it's on the precache list; it's + // important that the navigation route above gets registered first + precacheAndRoute([ + ...filteredPrecacheManifest, + ...pluginPrecacheManifest, + ...sharedBuildManifest, + ]) + } else { + // This will execute in dev environments; just included for testing purposes. + // Remove before merging + precacheAndRoute([ + // Let this one work + { url: './index.html', revision: '1' }, + // The following are expected to fail + { url: 'https://not-a-site.com/hey.jpg', revision: '1' }, + { url: 'https://not-a-site.com/bogus1.jpg', revision: '1' }, + { url: 'https://not-a-site.com/derp2.jpg', revision: '1' }, + { url: 'https://not-a-site.com/diddlibap3.jpg', revision: '1' }, + { url: 'https://not-a-site.com/squip4.jpg', revision: '1' }, + ]) + + /* An alternative to patch package, where a custom precache controller is made and implemented + class CustomPrecacheController extends PrecacheController { + // Copied and modified slightly from the original `install` method + // in order to catch errors without scrapping the entire SW + // installation + install(event) { + const fetchAndCache = async () => { + // Cache entries one at a time. + // See https://github.com/GoogleChrome/workbox/issues/2528 + for (const [url, cacheKey] of this._urlsToCacheKeys) { + const integrity = + this._cacheKeysToIntegrities.get(cacheKey) + const cacheMode = this._urlsToCacheModes.get(url) + + const request = new Request(url, { + integrity, + cache: cacheMode, + credentials: 'same-origin', + }) + + await Promise.all( + this.strategy.handleAll({ + params: { cacheKey }, + request, + event, + }) + ).catch((err) => { + console.log('Whoops there was a precaching err!') + console.error(err) + }) + } + } + event.waitUntil(fetchAndCache()) + } + } + const precacheController = new CustomPrecacheController() + + precacheController.addToCacheList([ + { url: 'https://not-a-site.com/index.html', revision: '5' }, + { url: 'https://not-a-site.com/hey.jpg', revision: '1000' }, + { url: 'https://not-a-site.com/bogus1.jpg', revision: '1000' }, + { url: 'https://not-a-site.com/derp2.jpg', revision: '1000' }, + { url: 'https://not-a-site.com/diddlibap3.jpg', revision: '1000' }, + { url: 'https://not-a-site.com/squip4.jpg', revision: '1000' }, + ]) + + self.addEventListener('install', (event) => { + precacheController.install(event) + }) + + self.addEventListener('activate', (event) => { + precacheController.activate(event) + }) + + const precacheRoute = new PrecacheRoute(precacheController) + registerRoute(precacheRoute) */ } // Handling pings: only use the network, and don't update the connection diff --git a/yarn.lock b/yarn.lock index 4187d8c83..30d3968ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5052,6 +5052,11 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.2.tgz#6d2967ffa407466481c6c90b6e16b3098f080128" integrity sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg== +ci-info@^3.7.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" + integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== + cjs-module-lexer@^1.0.0: version "1.2.2" resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" @@ -7423,6 +7428,13 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" +find-yarn-workspace-root@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd" + integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ== + dependencies: + micromatch "^4.0.2" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -9584,6 +9596,13 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json-stable-stringify@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.2.tgz#e06f23128e0bbe342dc996ed5a19e28b57b580e0" + integrity sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g== + dependencies: + jsonify "^0.0.1" + json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -9617,6 +9636,11 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonify@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" + integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== + jsonparse@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" @@ -9676,6 +9700,13 @@ kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +klaw-sync@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" + integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== + dependencies: + graceful-fs "^4.1.11" + klaw@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/klaw/-/klaw-4.0.1.tgz#8dc6f5723f05894e8e931b516a8ff15c2976d368" @@ -10831,7 +10862,7 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -open@^7.3.1: +open@^7.3.1, open@^7.4.2: version "7.4.2" resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== @@ -11070,6 +11101,27 @@ pascalcase@^0.1.1: resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw== +patch-package@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.0.tgz#d191e2f1b6e06a4624a0116bcb88edd6714ede61" + integrity sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA== + dependencies: + "@yarnpkg/lockfile" "^1.1.0" + chalk "^4.1.2" + ci-info "^3.7.0" + cross-spawn "^7.0.3" + find-yarn-workspace-root "^2.0.0" + fs-extra "^9.0.0" + json-stable-stringify "^1.0.2" + klaw-sync "^6.0.0" + minimist "^1.2.6" + open "^7.4.2" + rimraf "^2.6.3" + semver "^7.5.3" + slash "^2.0.0" + tmp "^0.0.33" + yaml "^2.2.2" + path-dirname@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" @@ -11778,6 +11830,11 @@ postcss@^8.4.14, postcss@^8.4.4, postcss@^8.4.7: picocolors "^1.0.0" source-map-js "^1.0.2" +postinstall-postinstall@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" + integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -12696,6 +12753,13 @@ rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + rimraf@~2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" @@ -12903,6 +12967,13 @@ semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: dependencies: lru-cache "^6.0.0" +semver@^7.5.3: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + send@0.18.0, send@latest: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -13068,6 +13139,11 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -15315,6 +15391,11 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.2.2: + version "2.3.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b" + integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ== + yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"