diff --git a/cli/src/lib/pwa/injectPrecacheManifest.js b/cli/src/lib/pwa/injectPrecacheManifest.js index b7b42c42d..13e5e86e4 100644 --- a/cli/src/lib/pwa/injectPrecacheManifest.js +++ b/cli/src/lib/pwa/injectPrecacheManifest.js @@ -42,8 +42,9 @@ module.exports = function injectPrecacheManifest(paths, config) { swDest: paths.shellBuildServiceWorker, globDirectory: paths.shellBuildOutput, globPatterns: ['**/*'], - // Skip `static` directory; CRA's workbox-webpack-plugin handles it smartly - globIgnores: ['static/**/*'], + // Skip index.html and `static` directory; + // CRA's workbox-webpack-plugin handles it smartly + globIgnores: ['static/**/*', 'index.html'], additionalManifestEntries: config.pwa.caching.additionalManifestEntries, injectionPoint: 'self.__WB_BUILD_MANIFEST', // Skip revision hashing for files with hash or semver in name: diff --git a/examples/pwa-app/d2.config.js b/examples/pwa-app/d2.config.js index 731e4fd6a..d7f7c1bfa 100644 --- a/examples/pwa-app/d2.config.js +++ b/examples/pwa-app/d2.config.js @@ -1,8 +1,6 @@ const config = { type: 'app', - standalone: true, - pwa: { enabled: true, caching: { diff --git a/examples/pwa-app/package.json b/examples/pwa-app/package.json index b961a144d..ce2a1c5ea 100644 --- a/examples/pwa-app/package.json +++ b/examples/pwa-app/package.json @@ -10,7 +10,7 @@ "test": "d2-app-scripts test", "deploy": "d2-app-scripts deploy", "serve": "serve -l 5500 build/app", - "demo": "yarn build --force --debug && yarn serve" + "demo": "yarn build --force --standalone --debug && yarn serve" }, "peerDependencies": { "@dhis2/app-runtime": "*", diff --git a/pwa/src/service-worker/service-worker.js b/pwa/src/service-worker/service-worker.js index c84f5a64f..a69bf6697 100644 --- a/pwa/src/service-worker/service-worker.js +++ b/pwa/src/service-worker/service-worker.js @@ -1,5 +1,5 @@ import { clientsClaim } from 'workbox-core' -import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching' +import { precacheAndRoute, matchPrecache, precache } from 'workbox-precaching' import { registerRoute, setDefaultHandler } from 'workbox-routing' import { NetworkFirst, @@ -57,45 +57,79 @@ export function setUpServiceWorker() { // 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 - precacheAndRoute(self.__WB_MANIFEST || []) + // 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 || [] + + // 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 + // in production if a user is online and unauthenticated. + // Uses app-shell style routing to route navigations to index.html. + const navigationRouteMatcher = ({ request, url }) => { + // If this isn't a navigation, skip. + if (request.mode !== 'navigate') { + return false + } + + // If this is a URL that starts with /_, skip. + if (url.pathname.startsWith('/_')) { + return false + } + + // If this looks like a URL for a resource, because it contains + // a file extension, skip (unless it's index.html) + if ( + fileExtensionRegexp.test(url.pathname) && + !url.pathname.endsWith('index.html') + ) { + return false + } + + // Return true to signal that we want to use the handler. + return true + } + const indexUrl = process.env.PUBLIC_URL + '/index.html' + const navigationRouteHandler = ({ request }) => { + return fetch(request) + .then(response => { + if (response.type === 'opaqueredirect') { + // It's sending a redirect to the login page. Return + // that to the client + return response + } + + // Otherwise return precached index.html + return matchPrecache(indexUrl) + }) + .catch(() => { + // Request failed (maybe offline). Return cached response + return matchPrecache(indexUrl) + }) + } + registerRoute(navigationRouteMatcher, navigationRouteHandler) + + // Handle the rest of files in the manifest + const restOfManifest = precacheManifest.filter( + e => e !== indexHtmlManifestEntry + ) + precacheAndRoute(restOfManifest) // Similar to above; manifest injection from `workbox-build` // Precaches all assets in the shell's build folder except in `static` // (which CRA's workbox-webpack-plugin handle smartly). // Additional files to precache can be added using the - // `additionalManifestEntries` option in d2.config.js; see the docs + // `additionalManifestEntries` option in d2.config.js; see the docs and + // 'injectPrecacheManifest.js' in the CLI package. // '[]' fallback prevents an error when switching pwa enabled to disabled precacheAndRoute(self.__WB_BUILD_MANIFEST || []) - - // From CRA Boilerplate: - // Set up App Shell-style routing, so that all navigation requests - // are fulfilled with your index.html shell. Learn more at - // https://developers.google.com/web/fundamentals/architecture/app-shell - registerRoute( - // Return false to exempt requests from being fulfilled by index.html. - ({ request, url }) => { - // If this isn't a navigation, skip. - if (request.mode !== 'navigate') { - return false - } - - // If this is a URL that starts with /_, skip. - if (url.pathname.startsWith('/_')) { - return false - } - - // If this looks like a URL for a resource, because it contains - // a file extension, skip. - if (url.pathname.match(fileExtensionRegexp)) { - return false - } - - // Return true to signal that we want to use the handler. - return true - }, - createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html') - ) } // Request handler during recording mode: ALL requests are cached