├── .gitignore ├── BLOG.md ├── LICENSE ├── README.md ├── netlify-auth.png ├── netlify-deploy-settings.png ├── netlify-deployed.png ├── netlify-repo-permissions.png ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── pwa-audit.png ├── src ├── About.tsx ├── App.css ├── App.test.tsx ├── App.tsx ├── Home.tsx ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts ├── serviceWorker.ts └── setupTests.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | build 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | -------------------------------------------------------------------------------- /BLOG.md: -------------------------------------------------------------------------------- 1 | # From `create-react-app` to PWA 2 | 3 | Progressive Web Apps are a (terribly named) wonderful idea. You can build an app *once* using web technologies which serves all devices and form factors. It can be accessible over the web, but also surface on the home screen of your Android / iOS device. That app can work offline, have a splash screen when it launches and have notifications too. 4 | 5 | PWAs can be a money saver for your business. The alternative, should you want an app experience for your users, is building the same application using three different technologies (one for web, one for Android and one for iOS). When you take this path it's hard to avoid a multiplication of cost and complexity. It often leads to dividing up the team as each works on a different stack. It's common to lose a certain amount of focus as a consequence. PWAs can help here; they are a compelling alternative, not just from a developers standpoint, but from a resourcing one too. 6 | 7 | However, the downside of PWAs is that they are more complicated than normal web apps. Writing one from scratch is just less straightforward than a classic web app. There are easy onramps to building a PWA that help you fall into the pit of success. This post will highlight one of these. How you can travel from zero to a PWA of your very own using React and TypeScript. 8 | 9 | This post presumes knowledge of: 10 | 11 | - React 12 | - TypeScript 13 | - Node 14 | 15 | #### From console to web app 16 | 17 | To create our PWA we're going to use [`create-react-app`](https://create-react-app.dev/). This excellent project has long had inbuilt support for making PWAs. In recent months that support has matured to a very satisfactory level. To create ourselves a TypeScript React app using `create-react-app` enter this `npx` command at the console: 18 | 19 | ```shell 20 | npx create-react-app pwa-react-typescript --template typescript 21 | ``` 22 | 23 | This builds you a react web app built with TypeScript; it can be tested locally with: 24 | 25 | ```shell 26 | cd pwa-react-typescript 27 | yarn start 28 | ``` 29 | 30 | #### From web app to PWA 31 | 32 | From web app to PWA is incredibly simple; it’s just a question of opting in to offline behaviour. If you open up the `index.tsx` file in your newly created project you'll find this code: 33 | 34 | 35 | ```ts 36 | // If you want your app to work offline and load faster, you can change 37 | // unregister() to register() below. Note this comes with some pitfalls. 38 | // Learn more about service workers: https://bit.ly/CRA-PWA 39 | serviceWorker.unregister(); 40 | ``` 41 | 42 | As the hint suggests, swap `serviceWorker.unregister()` for `serviceWorker.register()` and you now have a PWA. Amazing! What does this mean? Well to [quote the docs](https://create-react-app.dev/docs/making-a-progressive-web-app/#why-opt-in): 43 | 44 | > - 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. 45 | > - 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. 46 | > 47 | > ... it will take care of generating a service worker file that will automatically 48 | precache all of your local assets and keep them up to date as you deploy updates. 49 | 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) 50 | for handling all requests for local assets, including 51 | [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 52 | or unreliable network. 53 | 54 | Under the bonnet, `create-react-app` is achieving this through the use of technology called ["Workbox"](https://developers.google.com/web/tools/workbox). Workbox describes itself as: 55 | 56 | > a set of libraries and Node modules that make it easy to cache assets and take full advantage of features used to build [Progressive Web Apps](https://developers.google.com/web/progressive-web-apps/). 57 | 58 | The good folks of Google are aware that writing your own PWA can be tricky. There's much new behaviour to configure and be aware of; it's easy to make mistakes. Workbox is there to help ease the way forward by implementing default strategies for caching / offline behaviour which can be controlled through configuration. 59 | 60 | A downside of the usage of `Workbox` in `create-react-app` is that (as with most things `create-react-app`) there's little scope for configuration of your own if the defaults don't serve your purpose. This may change in the future, indeed [there's an open PR that adds this support](https://github.com/facebook/create-react-app/pull/5369). 61 | 62 | #### Icons and splash screens and A2HS, oh my! 63 | 64 | But it's not just an offline experience that makes this a PWA. Other important factors are: 65 | 66 | - That the app can be added to your home screen (A2HS AKA "installed"). 67 | - That the app has a name and an icon which can be customised. 68 | - That there's a splash screen displayed to the user as the app starts up. 69 | 70 | All of the above is "in the box" with `create-react-app`. Let's start customizing these. 71 | 72 | First of all, we'll give our app a name. Fire up `index.html` and replace `React App` with `My PWA`. (Feel free to concoct a more imaginative name than the one I've suggested.) Next open up `manifest.json` and replace: 73 | 74 | ```json 75 | "short_name": "React App", 76 | "name": "Create React App Sample", 77 | ``` 78 | 79 | with: 80 | 81 | ```json 82 | "short_name": "My PWA", 83 | "name": "My PWA", 84 | ``` 85 | 86 | Your app now has a name. The question you might be asking is: what is this `manifest.json` file? Well to [quote the good folks of Google](https://developers.google.com/web/fundamentals/web-app-manifest): 87 | 88 | > The [web app manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) is a simple JSON file that tells the browser about your web application and how it should behave when 'installed' on the user's mobile device or desktop. Having a manifest is required by Chrome to show the [Add to Home Screen prompt](https://developers.google.com/web/fundamentals/app-install-banners/). 89 | > 90 | > A typical manifest file includes information about the app name, icons it should use, the start_url it should start at when launched, and more. 91 | 92 | So the `manifest.json` is essentially metadata about your app. Here's what it should look like right now: 93 | 94 | ```json 95 | { 96 | "short_name": "My PWA", 97 | "name": "My PWA", 98 | "icons": [ 99 | { 100 | "src": "favicon.ico", 101 | "sizes": "64x64 32x32 24x24 16x16", 102 | "type": "image/x-icon" 103 | }, 104 | { 105 | "src": "logo192.png", 106 | "type": "image/png", 107 | "sizes": "192x192" 108 | }, 109 | { 110 | "src": "logo512.png", 111 | "type": "image/png", 112 | "sizes": "512x512" 113 | } 114 | ], 115 | "start_url": ".", 116 | "display": "standalone", 117 | "theme_color": "#000000", 118 | "background_color": "#ffffff" 119 | } 120 | ``` 121 | 122 | You can use the above properties (and others not yet configured) to control how your app behaves. For instance, if you want to replace icons your app uses then it's a simple matter of: 123 | 124 | - placing new logo files in the `public` folder 125 | - updating references to them in the `manifest.json` 126 | - finally, for older Apple devices, updating the `` in the `index.html`. 127 | 128 | #### Where are we? 129 | 130 | So far, we have a basic PWA in place. It's installable. You can run it locally and develop it with `yarn start`. You can build it for deployment with `yarn build`. 131 | 132 | What this isn't, is recognisably a web app. In the sense that it doesn't have support for different pages / URLs. We're typically going to want to break up our application this way. Let's do that now. We're going to use [`react-router`](https://github.com/ReactTraining/react-router); the de facto routing solution for React. To add it to our project (and the required type definitions for TypeScript) we use: 133 | 134 | ``` 135 | yarn add react-router-dom @types/react-router-dom 136 | ``` 137 | 138 | Now let's split up our app into a couple of pages. We'll replace the existing `App.tsx` with this: 139 | 140 | ```tsx 141 | import React from "react"; 142 | import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"; 143 | import About from "./About"; 144 | import Home from "./Home"; 145 | 146 | const App: React.FC = () => ( 147 | 148 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | ); 168 | 169 | export default App; 170 | ``` 171 | 172 | This will be our root page. It has the responsiblity of using `react-router` to render the pages we want to serve, and also to provide the links that allow users to navigate to those pages. In making our changes we'll have broken our test (which checked for a link we've now deleted), so we'll fix it like so: 173 | 174 | Replace the `App.test.tsx` with this: 175 | 176 | ```tsx 177 | import React from 'react'; 178 | import { render } from '@testing-library/react'; 179 | import App from './App'; 180 | 181 | test('renders about link', () => { 182 | const { getByText } = render(); 183 | const linkElement = getByText(/about/i); 184 | expect(linkElement).toBeInTheDocument(); 185 | }); 186 | ``` 187 | 188 | You'll have noticed that in our new `App.tsx` we import two new components (or pages); `About` and `Home`. Let's create those. First `About.tsx`: 189 | 190 | ```tsx 191 | import React from "react"; 192 | 193 | const About: React.FC = () => ( 194 |

This is a PWA

195 | ); 196 | 197 | export default About; 198 | ``` 199 | 200 | Then `Home.tsx`: 201 | 202 | ```tsx 203 | import React from "react"; 204 | 205 | const Home: React.FC = () => ( 206 |

Welcome to your PWA!

207 | ); 208 | 209 | export default Home; 210 | ``` 211 | 212 | #### Code splitting 213 | 214 | Now we've split up our app into multiple sections, we're going to split the code too. A good way to improve loading times for PWAs is to ensure that the code is not built into big files. At the moment our app builds a `single-file.js`. If you run `yarn build` you'll see what this looks like: 215 | 216 | ``` 217 | 47.88 KB build/static/js/2.89bc6648.chunk.js 218 | 784 B build/static/js/runtime-main.9c116153.js 219 | 555 B build/static/js/main.bc740179.chunk.js 220 | 269 B build/static/css/main.5ecd60fb.chunk.css 221 | ``` 222 | 223 | Notice the `build/static/js/main.bc740179.chunk.js` file. This is our `single-file.js`. It represents the compiled output of building the TypeScript files that make up our app. It will grow and grow as our app grows, eventually becoming problematic from a user loading speed perspective. 224 | 225 | `create-react-app` is built upon webpack. There is excellent support for code splitting in webpack and hence [create-react-app supports it by default](https://reactjs.org/docs/code-splitting.html#code-splitting). Let's apply it to our app. Again we're going to change `App.tsx`. 226 | 227 | Where we previously had: 228 | 229 | ```tsx 230 | import About from "./About"; 231 | import Home from "./Home"; 232 | ``` 233 | 234 | Let's replace with: 235 | 236 | ```tsx 237 | const About = lazy(() => import('./About')); 238 | const Home = lazy(() => import('./Home')); 239 | ``` 240 | 241 | This is the syntax to lazily load components in React. You'll note that it internally uses the [dynamic `import()` syntax](https://github.com/tc39/proposal-dynamic-import) which webpack uses as a "split point". 242 | 243 | Let's also give React something to render whilst it waits for the dynamic imports to be resolved. Just inside our `` component we'll add a `` component too: 244 | 245 | ```tsx 246 | 247 | Loading...}> 248 | {/*...*/} 249 | 250 | 251 | ``` 252 | 253 | The `` component will render the `
Loading...
` whilst it waits for a routes code to be dynamically loaded. So our final `App.tsx` component ends up looking like this: 254 | 255 | ```tsx 256 | import React, { lazy, Suspense } from "react"; 257 | import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"; 258 | const About = lazy(() => import("./About")); 259 | const Home = lazy(() => import("./Home")); 260 | 261 | const App: React.FC = () => ( 262 | 263 | Loading...}> 264 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | ); 285 | 286 | export default App; 287 | ``` 288 | 289 | This is now a code split application. How can we tell? If we run `yarn build` again we'll see something like this: 290 | 291 | ``` 292 | 47.88 KB build/static/js/2.89bc6648.chunk.js 293 | 1.18 KB (+428 B) build/static/js/runtime-main.415ab5ea.js 294 | 596 B (+41 B) build/static/js/main.e60948bb.chunk.js 295 | 269 B build/static/css/main.5ecd60fb.chunk.css 296 | 233 B build/static/js/4.0c85e1cb.chunk.js 297 | 228 B build/static/js/3.eed49094.chunk.js 298 | ``` 299 | 300 | Note that we now have multiple `*.chunk.js` files. Our initial `main.*.chunk.js` and then `3.*.chunk.js` representing `Home.tsx` and `4.*.chunk.js` representing `About.tsx`. 301 | 302 | As we continue to build out our app from this point we'll have a great approach in place to ensure that users load files as they need to and that those files should not be too large. Great performance which will scale. 303 | 304 | #### Deploy your PWA 305 | 306 | Now that we have our basic PWA in place, let's deploy it so the outside world can appreciate it. We're going to use [Netlify](https://www.netlify.com/) for this. 307 | 308 | The source code of our PWA lives on GitHub here: https://github.com/johnnyreilly/pwa-react-typescript 309 | 310 | We're going to log into Netlify, click on the "Create a new site" option and select GitHub as the provider. We'll need to authorize Netlify to access our GitHub. 311 | 312 | ![netlify-auth](./netlify-auth.png) 313 | 314 | You may need to click the "Configure Netlify on GitHub" button to grant permissions for Netlify to access your repo like so: 315 | 316 | ![netlify-repo-permissions](./netlify-repo-permissions.png) 317 | 318 | Then you can select your repo from within Netlify. All of the default settings that Netlify provides should work for our use case: 319 | 320 | ![netlify-deploy-settings](./netlify-deploy-settings.png) 321 | 322 | Let's hit the magic "Deploy site" button! In a matter of minutes you'll find that Netlify has deployed your PWA. 323 | 324 | ![netlify-deployed](./netlify-deployed.png) 325 | 326 | If we browse to the URL provided by Netlify we'll be able to see the deployed PWA in action. (You also have the opportunity to set up a custom domain name that you would typically want outside of a simple demo such as this.) Importantly this will be served over HTTPS which will allow our Service Worker to operate. 327 | 328 | Now that we know it's there, let's see how what we've built holds up according to the professionals. We're going to run the Google Chrome Developer Tools Audit against our PWA: 329 | 330 | ![pwa-audit](./pwa-audit.png) 331 | 332 | That is a good start for our PWA! 333 | 334 | [This post was originally published on LogRocket.](https://blog.logrocket.com/from-create-react-app-to-pwa/) 335 | 336 | [The source code for this project can be found here.](https://github.com/johnnyreilly/pwa-react-typescript) 337 | 338 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 John Reilly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /netlify-auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnyreilly/pwa-react-typescript/b9de380b46fc2ca3a1665cb97307374f5db807f2/netlify-auth.png -------------------------------------------------------------------------------- /netlify-deploy-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnyreilly/pwa-react-typescript/b9de380b46fc2ca3a1665cb97307374f5db807f2/netlify-deploy-settings.png -------------------------------------------------------------------------------- /netlify-deployed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnyreilly/pwa-react-typescript/b9de380b46fc2ca3a1665cb97307374f5db807f2/netlify-deployed.png -------------------------------------------------------------------------------- /netlify-repo-permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnyreilly/pwa-react-typescript/b9de380b46fc2ca3a1665cb97307374f5db807f2/netlify-repo-permissions.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pwa-react-typescript", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "@types/jest": "^24.0.0", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^16.9.0", 12 | "@types/react-dom": "^16.9.0", 13 | "@types/react-router-dom": "^5.1.3", 14 | "react": "^16.12.0", 15 | "react-dom": "^16.12.0", 16 | "react-router-dom": "^5.1.2", 17 | "react-scripts": "3.3.0", 18 | "typescript": "~3.7.2" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": "react-app" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnyreilly/pwa-react-typescript/b9de380b46fc2ca3a1665cb97307374f5db807f2/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnyreilly/pwa-react-typescript/b9de380b46fc2ca3a1665cb97307374f5db807f2/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnyreilly/pwa-react-typescript/b9de380b46fc2ca3a1665cb97307374f5db807f2/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "My PWA", 3 | "name": "My PWA", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /pwa-audit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnyreilly/pwa-react-typescript/b9de380b46fc2ca3a1665cb97307374f5db807f2/pwa-audit.png -------------------------------------------------------------------------------- /src/About.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const About: React.FC = () => ( 4 |

This is a PWA

5 | ); 6 | 7 | export default About; -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders about link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/about/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense } from "react"; 2 | import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"; 3 | const About = lazy(() => import("./About")); 4 | const Home = lazy(() => import("./Home")); 5 | 6 | const App: React.FC = () => ( 7 | 8 | Loading...}> 9 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /src/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Home: React.FC = () => ( 4 |

Welcome to your PWA!

5 | ); 6 | 7 | export default Home; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.register(); 13 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL!, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready.then(registration => { 142 | registration.unregister(); 143 | }); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------