diff --git a/docusaurus/docs/making-a-progressive-web-app.md b/docusaurus/docs/making-a-progressive-web-app.md index ba9802e6088..a25c9c02ba2 100644 --- a/docusaurus/docs/making-a-progressive-web-app.md +++ b/docusaurus/docs/making-a-progressive-web-app.md @@ -5,12 +5,42 @@ title: Making a Progressive Web App The production build has all the tools necessary to generate a first-class [Progressive Web App](https://developers.google.com/web/progressive-web-apps/), -but **the offline/cache-first behavior is opt-in only**. By default, -the build process will generate a service worker file, but it will not be -registered, so it will not take control of your production web app. +but **the offline/cache-first behavior is opt-in only**. + +Starting with Create React App 4, you can add a `src/service-worker.js` file to +your project to use the built-in support for +[Workbox](https://developers.google.com/web/tools/workbox/)'s +[`InjectManifest`](https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-webpack-plugin.InjectManifest) +plugin, which will +[compile](https://developers.google.com/web/tools/workbox/guides/using-bundlers) +your service worker and inject into it a list of URLs to +[precache](https://developers.google.com/web/tools/workbox/guides/precache-files). + +If you start a new project using one of the PWA [custom +templates](https://create-react-app.dev/docs/custom-templates/), you'll get a +`src/service-worker.js` file that serves as a good starting point for an +offline-first service worker: + +```sh +npx create-react-app my-app --template cra-template-pwa +``` + +The TypeScript equivalent is: + +```sh +npx create-react-app my-app --template cra-template-pwa-typescript +``` + +If you know that you won't be using service workers, or if you'd prefer to use a +different approach to creating your service worker, don't create a +`src/service-worker.js` file. The `InjectManifest` plugin won't be run in that +case. -In order to opt-in to the offline-first behavior, developers should look for the -following in their [`src/index.js`](https://github.com/facebook/create-react-app/blob/master/packages/cra-template/template/src/index.js) file: +In addition to creating your local `src/service-worker.js` file, it needs to be +registered before it will be used. In order to opt-in to the offline-first +behavior, developers should look for the following in their +[`src/index.js`](https://github.com/facebook/create-react-app/blob/master/packages/cra-template/template/src/index.js) +file: ```js // If you want your app to work offline and load faster, you can change @@ -24,23 +54,59 @@ As the comment states, switching `serviceWorker.unregister()` to ## Why Opt-in? -Offline-first Progressive Web Apps are faster and more reliable than traditional web pages, and provide an engaging mobile experience: +Offline-first Progressive Web Apps are faster and more reliable than traditional +web pages, and provide an engaging mobile experience: + +- All static site assets that are a part of your `webpack` build are cached so + that your page loads fast on subsequent visits, regardless of network + connectivity (such as 2G or 3G). Updates are downloaded in the background. +- Your app will work regardless of network state, even if offline. This means + your users will be able to use your app at 10,000 feet and on the subway. +- On mobile devices, your app can be added directly to the user's home screen, + app icon and all. This eliminates the need for the app store. + +However, they [can make debugging deployments more +challenging](https://github.com/facebook/create-react-app/issues/2398). + +The +[`workbox-webpack-plugin`](https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin) +is integrated into production configuration, and it will take care of compiling +a service worker file that will automatically precache all of your +`webpack`-generated assets and keep them up to date as you deploy updates. The +service worker will use a [cache-first +strategy](https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#cache-falling-back-to-network) +for handling all requests for `webpack`-generated assets, including [navigation +requests](https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading#first_what_are_navigation_requests) +for your HTML, ensuring that your web app is consistently fast, even on a slow +or unreliable network. -- All static site assets are cached so that your page loads fast on subsequent visits, regardless of network connectivity (such as 2G or 3G). Updates are downloaded in the background. -- Your app will work regardless of network state, even if offline. This means your users will be able to use your app at 10,000 feet and on the subway. -- On mobile devices, your app can be added directly to the user's home screen, app icon and all. This eliminates the need for the app store. +Note: Resources that are not generated by `webpack`, such as static files that are +copied over from your local +[`public/` directory](https://github.com/facebook/create-react-app/blob/master/packages/cra-template/template/public/) +or third-party resources, will not be precached. You can optionally set up Workbox +[routes](https://developers.google.com/web/tools/workbox/guides/route-requests) +to apply the runtime caching strategy of your choice to those resources. + +## Customization + +Starting with Create React App 4, you have full control over customizing the +logic in this service worker, by creating your own `src/service-worker.js` file, +or customizing the one added by the `cra-template-pwa` (or +`cra-template-pwa-typescript`) template. You can use [additional +modules](https://developers.google.com/web/tools/workbox/modules) from the +Workbox project, add in a push notification library, or remove some of the +default caching logic. The one requirement is that you keep `self.__WB_MANIFEST` +somewhere in your file, as the Workbox compilation plugin checks for this value +when generating a manifest of URLs to precache. If you would prefer not to use +precaching, you can just assign `self.__WB_MANIFEST` to a variable that will be +ignored, like: -However, they [can make debugging deployments more challenging](https://github.com/facebook/create-react-app/issues/2398) so, starting with Create React App 2, service workers are opt-in. +```js +// eslint-disable-next-line no-restricted-globals +const ignored = self.__WB_MANIFEST; -The [`workbox-webpack-plugin`](https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin) -is integrated into production configuration, -and it will take care of generating a service worker file that will automatically -precache all of your local assets and keep them up to date as you deploy updates. -The service worker will use a [cache-first strategy](https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#cache-falling-back-to-network) -for handling all requests for local assets, including -[navigation requests](https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading#first_what_are_navigation_requests) -for your HTML, ensuring that your web app is consistently fast, even on a slow -or unreliable network. +// Your custom service worker code goes here. +``` ## Offline-First Considerations @@ -88,7 +154,8 @@ following into account: 1. By default, the generated service worker file will not intercept or cache any cross-origin traffic, like HTTP [API requests](integrating-with-an-api-backend.md), - images, or embeds loaded from a different domain. + images, or embeds loaded from a different domain. Starting with Create + React App 4, this can be customized, as explained above. ## Progressive Web App Metadata diff --git a/package.json b/package.json index a97c57bcb78..4f3c9bee078 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@testing-library/react": "^10.2.1", "@testing-library/user-event": "^12.0.2", "alex": "^8.0.0", - "eslint": "^7.3.0", + "eslint": "^7.5.0", "execa": "1.0.0", "fs-extra": "^9.0.0", "get-port": "^5.1.1", diff --git a/packages/cra-template-typescript/template/src/index.tsx b/packages/cra-template-typescript/template/src/index.tsx index bdf2dd80541..ef2edf8ea3f 100644 --- a/packages/cra-template-typescript/template/src/index.tsx +++ b/packages/cra-template-typescript/template/src/index.tsx @@ -2,7 +2,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; -import * as serviceWorker from './serviceWorker'; import reportWebVitals from './reportWebVitals'; ReactDOM.render( @@ -12,11 +11,6 @@ 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 -serviceWorker.unregister(); - // 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/packages/cra-template-typescript/template/src/serviceWorker.ts b/packages/cra-template-typescript/template/src/serviceWorker.ts deleted file mode 100644 index 9fca0aa3bdf..00000000000 --- a/packages/cra-template-typescript/template/src/serviceWorker.ts +++ /dev/null @@ -1,149 +0,0 @@ -// 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/packages/cra-template/template/src/index.js b/packages/cra-template/template/src/index.js index bdf2dd80541..ef2edf8ea3f 100644 --- a/packages/cra-template/template/src/index.js +++ b/packages/cra-template/template/src/index.js @@ -2,7 +2,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; -import * as serviceWorker from './serviceWorker'; import reportWebVitals from './reportWebVitals'; ReactDOM.render( @@ -12,11 +11,6 @@ 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 -serviceWorker.unregister(); - // 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/packages/cra-template/template/src/serviceWorker.js b/packages/cra-template/template/src/serviceWorker.js deleted file mode 100644 index 02b0533647d..00000000000 --- a/packages/cra-template/template/src/serviceWorker.js +++ /dev/null @@ -1,141 +0,0 @@ -// 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}$/ - ) -); - -export function register(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, 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, 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/packages/eslint-config-react-app/package.json b/packages/eslint-config-react-app/package.json index b152abd36d9..573fece08be 100644 --- a/packages/eslint-config-react-app/package.json +++ b/packages/eslint-config-react-app/package.json @@ -19,11 +19,11 @@ "@typescript-eslint/parser": "^3.0.0", "babel-eslint": "^10.0.0", "eslint": "^7.0.0", - "eslint-plugin-flowtype": "^5.0.0", - "eslint-plugin-import": "^2.21.1", - "eslint-plugin-jsx-a11y": "^6.3.0", - "eslint-plugin-react": "^7.20.0", - "eslint-plugin-react-hooks": "^4.0.1" + "eslint-plugin-flowtype": "^5.2.0", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-jsx-a11y": "^6.3.1", + "eslint-plugin-react": "^7.20.3", + "eslint-plugin-react-hooks": "^4.0.8" }, "dependencies": { "confusing-browser-globals": "^1.0.9" diff --git a/packages/react-error-overlay/package.json b/packages/react-error-overlay/package.json index 9fcfe6ddd9f..14bf3f49b99 100644 --- a/packages/react-error-overlay/package.json +++ b/packages/react-error-overlay/package.json @@ -44,13 +44,13 @@ "chalk": "2.4.2", "chokidar": "^3.3.0", "cross-env": "7.0.2", - "eslint": "^7.3.0", + "eslint": "^7.5.0", "eslint-config-react-app": "^5.2.1", - "eslint-plugin-flowtype": "^5.1.3", - "eslint-plugin-import": "^2.21.2", + "eslint-plugin-flowtype": "^5.2.0", + "eslint-plugin-import": "^2.22.0", "eslint-plugin-jsx-a11y": "^6.3.1", - "eslint-plugin-react": "^7.20.0", - "eslint-plugin-react-hooks": "^4.0.4", + "eslint-plugin-react": "^7.20.3", + "eslint-plugin-react-hooks": "^4.0.8", "flow-bin": "^0.116.0", "html-entities": "1.3.1", "jest": "26.1.0", diff --git a/packages/react-scripts/config/paths.js b/packages/react-scripts/config/paths.js index 11d81b7f5a7..516513081b2 100644 --- a/packages/react-scripts/config/paths.js +++ b/packages/react-scripts/config/paths.js @@ -72,6 +72,7 @@ module.exports = { testsSetup: resolveModule(resolveApp, 'src/setupTests'), proxySetup: resolveApp('src/setupProxy.js'), appNodeModules: resolveApp('node_modules'), + swSrc: resolveModule(resolveApp, 'src/service-worker'), publicUrlOrPath, }; @@ -94,6 +95,7 @@ module.exports = { testsSetup: resolveModule(resolveApp, 'src/setupTests'), proxySetup: resolveApp('src/setupProxy.js'), appNodeModules: resolveApp('node_modules'), + swSrc: resolveModule(resolveApp, 'src/service-worker'), publicUrlOrPath, // These properties only exist before ejecting: ownPath: resolveOwn('.'), @@ -129,6 +131,7 @@ if ( testsSetup: resolveModule(resolveOwn, `${templatePath}/src/setupTests`), proxySetup: resolveOwn(`${templatePath}/src/setupProxy.js`), appNodeModules: resolveOwn('node_modules'), + swSrc: resolveModule(resolveOwn, `${templatePath}/src/service-worker`), publicUrlOrPath, // These properties only exist before ejecting: ownPath: resolveOwn('.'), diff --git a/packages/react-scripts/config/webpack.config.js b/packages/react-scripts/config/webpack.config.js index 2c32b66ac9a..1dba35cf903 100644 --- a/packages/react-scripts/config/webpack.config.js +++ b/packages/react-scripts/config/webpack.config.js @@ -60,6 +60,9 @@ const imageInlineSizeLimit = parseInt( // Check if TypeScript is setup const useTypeScript = fs.existsSync(paths.appTsConfig); +// Get the path to the uncompiled service worker (if it exists). +const swSrc = paths.swSrc; + // style files regexes const cssRegex = /\.css$/; const cssModuleRegex = /\.module\.css$/; @@ -692,20 +695,11 @@ module.exports = function (webpackEnv) { // Generate a service worker script that will precache, and keep up to date, // the HTML & assets that are part of the webpack build. isEnvProduction && - new WorkboxWebpackPlugin.GenerateSW({ - clientsClaim: true, - exclude: [/\.map$/, /asset-manifest\.json$/], - importWorkboxFrom: 'cdn', - navigateFallback: paths.publicUrlOrPath + 'index.html', - navigateFallbackBlacklist: [ - // Exclude URLs starting with /_, as they're likely an API call - new RegExp('^/_'), - // Exclude any URLs whose last part seems to be a file extension - // as they're likely a resource and not a SPA route. - // URLs containing a "?" character won't be blacklisted as they're likely - // a route with query params (e.g. auth callbacks). - new RegExp('/[^/?]+\\.[^/]+$'), - ], + fs.existsSync(swSrc) && + new WorkboxWebpackPlugin.InjectManifest({ + swSrc, + dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./, + exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/], }), // TypeScript type checking useTypeScript && diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 7b82028d742..12db4f1d957 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -29,7 +29,7 @@ "types": "./lib/react-app.d.ts", "dependencies": { "@babel/core": "7.10.5", - "@pmmmwh/react-refresh-webpack-plugin": "0.3.3", + "@pmmmwh/react-refresh-webpack-plugin": "0.4.0-beta.8", "@svgr/webpack": "5.4.0", "@typescript-eslint/eslint-plugin": "^3.3.0", "@typescript-eslint/parser": "^3.3.0", @@ -44,14 +44,14 @@ "css-loader": "3.6.0", "dotenv": "8.2.0", "dotenv-expand": "5.1.0", - "eslint": "^7.3.0", + "eslint": "^7.5.0", "eslint-config-react-app": "^5.2.1", "eslint-loader": "^4.0.2", - "eslint-plugin-flowtype": "^5.1.3", - "eslint-plugin-import": "^2.21.2", + "eslint-plugin-flowtype": "^5.2.0", + "eslint-plugin-import": "^2.22.0", "eslint-plugin-jsx-a11y": "^6.3.1", - "eslint-plugin-react": "^7.20.0", - "eslint-plugin-react-hooks": "^4.0.4", + "eslint-plugin-react": "^7.20.3", + "eslint-plugin-react-hooks": "^4.0.8", "file-loader": "6.0.0", "fs-extra": "^9.0.0", "html-webpack-plugin": "4.3.0", @@ -82,7 +82,7 @@ "webpack": "4.43.0", "webpack-dev-server": "3.11.0", "webpack-manifest-plugin": "2.2.0", - "workbox-webpack-plugin": "4.3.1" + "workbox-webpack-plugin": "5.1.3" }, "devDependencies": { "react": "^16.12.0",