diff --git a/.gitignore b/.gitignore index ec23192..e01e5a8 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,3 @@ cypress/screenshots # IntellJ .idea - -# Electron -/out diff --git a/package.json b/package.json index 1086394..5b6f6f2 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "react-dom": "^16.13.1", "react-redux": "^7.2.1", "react-router-dom": "^5.2.0", - "react-scripts": "4.0.0", + "react-scripts": "4.0.1", "react-test-renderer": "^16.13.1", "redux": "^4.0.5", "redux-logger": "^3.0.6", diff --git a/src/index.tsx b/src/index.tsx index 23bd104..48f116f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,6 +2,7 @@ import React from "react"; import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; +import * as serviceWorkerRegistration from "./serviceWorkerRegistration"; import reportWebVitals from "./reportWebVitals"; import { Provider } from "react-redux"; @@ -49,6 +50,11 @@ ReactDOM.render( document.getElementById("root") ); +// If you want your app to work offline and load faster, you can change +// unregister() to register() below. Note this comes with some pitfalls. +// Learn more about service workers: https://cra.link/PWA +serviceWorkerRegistration.register(); + // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals diff --git a/src/reportWebVitals.ts b/src/reportWebVitals.ts index 59d81aa..5fa3583 100644 --- a/src/reportWebVitals.ts +++ b/src/reportWebVitals.ts @@ -1,15 +1,15 @@ -import { ReportHandler } from 'web-vitals'; +import { ReportHandler } from "web-vitals"; const reportWebVitals = (onPerfEntry?: ReportHandler) => { if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); + import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); getFID(onPerfEntry); getFCP(onPerfEntry); getLCP(onPerfEntry); getTTFB(onPerfEntry); }); } -} +}; export default reportWebVitals; diff --git a/src/service-worker.ts b/src/service-worker.ts new file mode 100644 index 0000000..5be3706 --- /dev/null +++ b/src/service-worker.ts @@ -0,0 +1,81 @@ +/// +/* eslint-disable no-restricted-globals */ + +// This service worker can be customized! +// See https://developers.google.com/web/tools/workbox/modules +// for the list of available Workbox modules, or add any other +// code you'd like. +// You can also remove this file if you'd prefer not to use a +// service worker, and the Workbox build step will be skipped. + +import { clientsClaim } from "workbox-core"; +import { ExpirationPlugin } from "workbox-expiration"; +import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching"; +import { registerRoute } from "workbox-routing"; +import { StaleWhileRevalidate } from "workbox-strategies"; + +declare const self: ServiceWorkerGlobalScope; + +clientsClaim(); + +// 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); + +// 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 +const fileExtensionRegexp = new RegExp("/[^/?]+\\.[^/]+$"); +registerRoute( + // Return false to exempt requests from being fulfilled by index.html. + ({ request, url }: { request: Request; url: 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") +); + +// An example runtime caching route for requests that aren't handled by the +// precache, in this case same-origin .png requests like those from in public/ +registerRoute( + // Add in any other file extensions or routing criteria as needed. + ({ url }) => + url.origin === self.location.origin && url.pathname.endsWith(".png"), + // Customize this strategy as needed, e.g., by changing to CacheFirst. + new StaleWhileRevalidate({ + cacheName: "images", + plugins: [ + // Ensure that once this runtime cache reaches a maximum size the + // least-recently used images are removed. + new ExpirationPlugin({ maxEntries: 50 }), + ], + }) +); + +// This allows the web app to trigger skipWaiting via +// registration.waiting.postMessage({type: 'SKIP_WAITING'}) +self.addEventListener("message", (event) => { + if (event.data && event.data.type === "SKIP_WAITING") { + self.skipWaiting(); + } +}); + +// Any other custom service worker logic can go here. diff --git a/src/serviceWorkerRegistration.ts b/src/serviceWorkerRegistration.ts new file mode 100644 index 0000000..6d59991 --- /dev/null +++ b/src/serviceWorkerRegistration.ts @@ -0,0 +1,146 @@ +// This optional code is used to register a service worker. +// register() is not called by default. + +// This lets the app load faster on subsequent visits in production, and gives +// it offline capabilities. However, it also means that developers (and users) +// will only see deployed updates on subsequent visits to a page, after all the +// existing tabs open on the page have been closed, since previously cached +// resources are updated in the background. + +// To learn more about the benefits of this model and instructions on how to +// opt-in, read https://cra.link/PWA + +const isLocalhost = Boolean( + window.location.hostname === "localhost" || + // [::1] is the IPv6 localhost address. + window.location.hostname === "[::1]" || + // 127.0.0.0/8 are considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + ) +); + +type Config = { + onSuccess?: (registration: ServiceWorkerRegistration) => void; + onUpdate?: (registration: ServiceWorkerRegistration) => void; +}; + +export function register(config?: Config) { + if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebook/create-react-app/issues/2374 + return; + } + + window.addEventListener("load", () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + + if (isLocalhost) { + // This is running on localhost. Let's check if a service worker still exists or not. + checkValidServiceWorker(swUrl, config); + + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready.then(() => { + console.log( + "This web app is being served cache-first by a service " + + "worker. To learn more, visit https://cra.link/PWA" + ); + }); + } else { + // Is not localhost. Just register service worker + registerValidSW(swUrl, config); + } + }); + } +} + +function registerValidSW(swUrl: string, config?: Config) { + navigator.serviceWorker + .register(swUrl) + .then((registration) => { + registration.onupdatefound = () => { + const installingWorker = registration.installing; + if (installingWorker == null) { + return; + } + installingWorker.onstatechange = () => { + if (installingWorker.state === "installed") { + if (navigator.serviceWorker.controller) { + // At this point, the updated precached content has been fetched, + // but the previous service worker will still serve the older + // content until all client tabs are closed. + console.log( + "New content is available and will be used when all " + + "tabs for this page are closed. See https://cra.link/PWA." + ); + + // Execute callback + if (config && config.onUpdate) { + config.onUpdate(registration); + } + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log("Content is cached for offline use."); + + // Execute callback + if (config && config.onSuccess) { + config.onSuccess(registration); + } + } + } + }; + }; + }) + .catch((error) => { + console.error("Error during service worker registration:", error); + }); +} + +function checkValidServiceWorker(swUrl: string, config?: Config) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl, { + headers: { "Service-Worker": "script" }, + }) + .then((response) => { + // Ensure service worker exists, and that we really are getting a JS file. + const contentType = response.headers.get("content-type"); + if ( + response.status === 404 || + (contentType != null && contentType.indexOf("javascript") === -1) + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then((registration) => { + registration.unregister().then(() => { + window.location.reload(); + }); + }); + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl, config); + } + }) + .catch(() => { + console.log( + "No internet connection found. App is running in offline mode." + ); + }); +} + +export function unregister() { + if ("serviceWorker" in navigator) { + navigator.serviceWorker.ready + .then((registration) => { + registration.unregister(); + }) + .catch((error) => { + console.error(error.message); + }); + } +} diff --git a/yarn.lock b/yarn.lock index a0fcfc8..b4ca670 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9318,7 +9318,7 @@ inquirer@7.0.4: strip-ansi "^5.1.0" through "^2.3.6" -inquirer@7.3.3, inquirer@^7.0.0: +inquirer@^7.0.0: version "7.3.3" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== @@ -13674,7 +13674,7 @@ promise@^8.1.0: dependencies: asap "~2.0.6" -prompts@^2.0.1: +prompts@2.4.0, prompts@^2.0.1: version "2.4.0" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.0.tgz#4aa5de0723a231d1ee9121c40fdf663df73f61d7" integrity sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ== @@ -13966,10 +13966,10 @@ react-dev-utils@^10.0.0: strip-ansi "6.0.0" text-table "0.2.0" -react-dev-utils@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.0.tgz#9fdb1b173f4ffc1f23fcf98465d93b16a860b73e" - integrity sha512-uIZTUZXB5tbiM/0auUkLVjWhZGM7DSI304iGunyhA9m985iIDVXd9I4z6MkNa9jeLzeUJbU9A7TUNrcbXAahxw== +react-dev-utils@^11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.1.tgz#30106c2055acfd6b047d2dc478a85c356e66fe45" + integrity sha512-rlgpCupaW6qQqvu0hvv2FDv40QG427fjghV56XyPcP5aKtOAPzNAhQ7bHqk1YdS2vpW1W7aSV3JobedxuPlBAA== dependencies: "@babel/code-frame" "7.10.4" address "1.1.2" @@ -13985,11 +13985,11 @@ react-dev-utils@^11.0.0: globby "11.0.1" gzip-size "5.1.1" immer "7.0.9" - inquirer "7.3.3" is-root "2.1.0" loader-utils "2.0.0" open "^7.0.2" pkg-up "3.1.0" + prompts "2.4.0" react-error-overlay "^6.0.8" recursive-readdir "2.2.2" shell-quote "1.7.2" @@ -14209,10 +14209,10 @@ react-router@5.2.0: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-scripts@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-4.0.0.tgz#36f3d84ffff708ac0618fd61e71eaaea11c26417" - integrity sha512-icJ/ctwV5XwITUOupBP9TUVGdWOqqZ0H08tbJ1kVC5VpNWYzEZ3e/x8axhV15ZXRsixLo27snwQE7B6Zd9J2Tg== +react-scripts@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-4.0.1.tgz#34974c0f4cfdf1655906c95df6a04d80db8b88f0" + integrity sha512-NnniMSC/wjwhcJAyPJCWtxx6CWONqgvGgV9+QXj1bwoW/JI++YF1eEf3Upf/mQ9KmP57IBdjzWs1XvnPq7qMTQ== dependencies: "@babel/core" "7.12.3" "@pmmmwh/react-refresh-webpack-plugin" "0.4.2" @@ -14256,8 +14256,9 @@ react-scripts@4.0.0: postcss-normalize "8.0.1" postcss-preset-env "6.7.0" postcss-safe-parser "5.0.2" + prompts "2.4.0" react-app-polyfill "^2.0.0" - react-dev-utils "^11.0.0" + react-dev-utils "^11.0.1" react-refresh "^0.8.3" resolve "1.18.1" resolve-url-loader "^3.1.2"