├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── assets ├── atyantik.svg └── reactpwa.svg ├── examples ├── basic │ ├── .editorconfig │ ├── .esdoc.json │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .gitignore │ ├── .npmignore │ ├── .prettierignore │ ├── .prettierrc │ ├── .vscode │ │ └── settings.json │ ├── LICENSE.md │ ├── package.json │ ├── reactpwa.config.json │ ├── src │ │ ├── @types │ │ │ ├── images.d.ts │ │ │ └── styles.d.ts │ │ ├── components │ │ │ ├── child │ │ │ │ └── index.tsx │ │ │ ├── errors │ │ │ │ └── 404.tsx │ │ │ ├── page-loader │ │ │ │ ├── index.tsx │ │ │ │ ├── react-pwa-logo.png │ │ │ │ └── styles.scss │ │ │ ├── parent │ │ │ │ ├── images │ │ │ │ │ └── Atyantik Logo White BG - Rectangle.jpg │ │ │ │ ├── index.tsx │ │ │ │ └── styles.scss │ │ │ ├── shell │ │ │ │ └── index.tsx │ │ │ └── skeleton │ │ │ │ └── index.tsx │ │ ├── pages │ │ │ ├── about.tsx │ │ │ ├── home.tsx │ │ │ └── styles.scss │ │ ├── public │ │ │ └── robot.txt │ │ ├── resources │ │ │ ├── route-styles.scss │ │ │ └── styles.scss │ │ ├── routes.tsx │ │ ├── server.tsx │ │ └── services │ │ │ └── data.ts │ ├── tsconfig.eslint.json │ └── tsconfig.json └── recoil │ ├── .editorconfig │ ├── .esdoc.json │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .gitignore │ ├── .npmignore │ ├── .prettierignore │ ├── .prettierrc │ ├── .vscode │ └── settings.json │ ├── LICENSE.md │ ├── package.json │ ├── reactpwa.config.json │ ├── src │ ├── @types │ │ ├── images.d.ts │ │ └── styles.d.ts │ ├── components │ │ ├── child │ │ │ └── index.tsx │ │ ├── errors │ │ │ └── 404.tsx │ │ ├── page-loader │ │ │ ├── index.tsx │ │ │ ├── react-pwa-logo.png │ │ │ └── styles.scss │ │ ├── parent │ │ │ ├── index.tsx │ │ │ └── styles.scss │ │ ├── shell │ │ │ └── index.tsx │ │ └── skeleton │ │ │ └── index.tsx │ ├── pages │ │ ├── about.tsx │ │ ├── home.tsx │ │ └── styles.scss │ ├── resources │ │ ├── route-styles.scss │ │ └── styles.scss │ ├── routes.tsx │ └── services │ │ └── data.ts │ ├── tsconfig.eslint.json │ └── tsconfig.json ├── lerna.json ├── package-lock.json ├── package.json └── packages ├── cli ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .vscode │ └── settings.json ├── LICENSE.md ├── package.json ├── reactpwa.js ├── src │ ├── index.ts │ ├── static-site-generator.ts │ └── util.ts ├── tsconfig.eslint.json ├── tsconfig.json └── tsup.config.ts ├── core ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .swcrc ├── .vscode │ └── settings.json ├── LICENSE.md ├── assets │ ├── reactpwa-192.png │ ├── reactpwa-512.png │ └── reactpwa-512.svg ├── index.js ├── package.json ├── reactpwa.js ├── router.d.ts ├── router.js ├── src │ ├── @types │ │ ├── assets.d.ts │ │ └── jsx.d.ts │ ├── babel │ │ └── lazy-routes.ts │ ├── client.tsx │ ├── components │ │ ├── app.tsx │ │ ├── data.tsx │ │ ├── error.tsx │ │ ├── head │ │ │ ├── context.ts │ │ │ ├── index.tsx │ │ │ ├── lazy.tsx │ │ │ └── provider.tsx │ │ ├── http-status.tsx │ │ ├── reactpwa.tsx │ │ ├── redirect.tsx │ │ ├── route.tsx │ │ ├── strict.tsx │ │ └── sync-data-script.tsx │ ├── defaults │ │ ├── server.ts │ │ └── webmanifest.ts │ ├── express-server.tsx │ ├── hooks │ │ ├── useData.tsx │ │ └── useSyncData.tsx │ ├── index.ts │ ├── node │ │ ├── build.ts │ │ └── start.ts │ ├── root.ts │ ├── router.ts │ ├── server.tsx │ ├── typedefs │ │ ├── head.ts │ │ ├── server.ts │ │ ├── webmanifest.ts │ │ └── webpack.ts │ ├── utils │ │ ├── asset-extract.ts │ │ ├── client.ts │ │ ├── cookie.ts │ │ ├── delay.ts │ │ ├── env.ts │ │ ├── event-emmiter.ts │ │ ├── express.ts │ │ ├── head.tsx │ │ ├── not-boolean.ts │ │ ├── promise.ts │ │ ├── redirect.ts │ │ ├── request-internals.ts │ │ ├── require-from-string.ts │ │ ├── resolver.ts │ │ └── server.ts │ ├── webpack.ts │ └── webpack │ │ ├── experiments.ts │ │ ├── externals.ts │ │ ├── generator-options.ts │ │ ├── image-assets-extensions.ts │ │ ├── loader-options │ │ ├── babel-loader-options.ts │ │ ├── css-loader-options.ts │ │ ├── post-css-loader-options.ts │ │ └── sass-loader-options.ts │ │ ├── optimization.ts │ │ ├── output.ts │ │ ├── plugins │ │ └── inject-sw.ts │ │ ├── resolver.ts │ │ ├── rules │ │ ├── assets-rule.ts │ │ ├── css-rule.ts │ │ ├── images-rule.ts │ │ ├── js-rule.ts │ │ ├── mjs-rule.ts │ │ └── raw-resource-rule.ts │ │ ├── service-worker.ts │ │ ├── static-assets-extensions.ts │ │ └── utils.ts ├── tsconfig.eslint.json ├── tsconfig.json ├── tsup.config.ts └── typings.d.ts └── eslint-config-reactpwa ├── .npmignore ├── LICENSE.md ├── empty.ts ├── index.js ├── package.json └── tsconfig.json /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | "packages/core", 4 | "packages/cli", 5 | "packages/eslint-config-reactpwa", 6 | "examples/basic", 7 | "examples/recoil", 8 | ], 9 | "editor.defaultFormatter": "esbenp.prettier-vscode", 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll.eslint": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Atyantik Technologies Private Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Backers on Open Collective](https://opencollective.com/react-pwa/backers/badge.svg)](https://opencollective.com/react-pwa) [![Sponsors on Open Collective](https://opencollective.com/react-pwa/sponsors/badge.svg)](https://opencollective.com/react-pwa) 2 | 3 | 4 |

5 | 6 |

7 |

ReactPWA - The simplest way to create PWAs with React

8 | 9 | 10 | A highly scalable, **Progressive Web Application *Framework*** with the best Developer Experience. 11 | This framework utilizes the power of React 18 with Suspense. 12 | 13 | 14 | ### Getting Started 15 | To install and get started with ReactPWA execute the following commands: 16 | 17 | ##### 1. Clone the repo to your local PC and go to the installation 18 | ```bash 19 | git clone https://github.com/Atyantik/react-pwa-boilerplate.git my-pwa && cd my-pwa 20 | ``` 21 | 22 | ##### 2. Install the dependencies 23 | ```bash 24 | npm install 25 | ``` 26 | 27 | 28 | ##### 3. Run in development mode 29 | ```bash 30 | npm run dev 31 | ``` 32 | 33 | ##### 4. To build the project 34 | ```bash 35 | npm run build 36 | ``` 37 | 38 | #### Features 39 | - ✅ Progressive Web Application 40 | - ✅ Server Side Rendering 41 | - ✅ CSS bundling 42 | - ✅ SEO First 43 | - ✅ Code splitting 44 | - ✅ Routing via React-Router 45 | - ✅ React Suspense Support with SSR 46 | 47 | ### Need contributors. 48 | 49 | This project exists thanks to all the people who contribute. [[Contribute]](CONTRIBUTING.md). 50 | 51 | 52 | 53 | We are actively looking for contributors for testing and documentation. 54 | Please contact us: admin [at] atyantik.com or contact [at] atyantik.com 55 | 56 | Visit us at [Atyantik Technologies Private Limited](https://www.atyantik.com) 57 | 58 | ### Backers 59 | 60 | Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/react-pwa#backer)] 61 | 62 | 63 | ### Sponsors 64 | 65 | Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/react-pwa#sponsor)] 66 | 67 | ### Supporters 68 | 69 | ##### Atyantik Technologies Private Limited 70 | 71 | Everyone at Atyantik Technologies is contributing their free time for contributing to the project and core discussions. 72 | 73 | 74 | ##### DigitalOcean 75 | 76 | DigitalOcean has been supporting the open-source project since the very start and has given a dedicated free server to host the website and host the demos. Their contribution is invaluable to the project. 77 | 78 | 79 | ##### Browser stack 80 | Thanks to the Browser stack we can test the PWA nature of applications on various mobiles and write automated test cases. 81 | 82 | 83 | ##### Navicat 84 | 85 | We are very thankful to Navicat for offering their support to the project and providing us with an open-source license for further project development. 86 | 87 | 88 | 89 | ### License & Copyright 90 | 91 | Creative Commons Licence
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License. 92 | 93 | Copyright (c) 2022 [Atyantik Technologies Private Limited](https://www.atyantik.com/) -------------------------------------------------------------------------------- /assets/atyantik.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/basic/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | # The JSON files contain newlines inconsistently 13 | [*.json] 14 | insert_final_newline = ignore 15 | 16 | # Minified JavaScript files shouldn't be changed 17 | [**.min.js] 18 | indent_style = ignore 19 | insert_final_newline = ignore 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | 24 | [*.{ts,tsx,js,jsx}] 25 | end_of_line = lf 26 | charset = utf-8 27 | trim_trailing_whitespace = true 28 | indent_style = space 29 | indent_size = 2 30 | -------------------------------------------------------------------------------- /examples/basic/.esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./docs", 4 | "plugins": [ 5 | { 6 | "name": "esdoc-standard-plugin" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /examples/basic/.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | lib 4 | src/@types 5 | src/__tests__ 6 | jest-transformer.js 7 | jest.config.js 8 | src/sw.js 9 | -------------------------------------------------------------------------------- /examples/basic/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "reactpwa" 5 | ], 6 | "parserOptions": { 7 | "project": "./tsconfig.eslint.json" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/basic/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # WebStorm configurations 62 | .idea 63 | 64 | # Ignore the etc folders created by 65 | etc/ 66 | 67 | # Do not add dist/lib to git 68 | dist 69 | lib 70 | 71 | # ignore the config assets as its generated runtime and thus should notbe included in git 72 | src/config/assets.js 73 | 74 | # Ignore sass cache 75 | .sass-cache 76 | 77 | # demo's package-lock.json 78 | demo/package-lock.json 79 | -------------------------------------------------------------------------------- /examples/basic/.npmignore: -------------------------------------------------------------------------------- 1 | demo 2 | packages 3 | # Logs 4 | logs 5 | 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | # WebStorm configurations 64 | .idea 65 | 66 | # Ignore the etc folders created by 67 | etc/ 68 | 69 | # Do not add dist to git 70 | dist 71 | 72 | # ignore the config assets as its generated runtime and thus should notbe included in git 73 | src/config/assets.js 74 | 75 | # Ignore sass cache 76 | .sass-cache -------------------------------------------------------------------------------- /examples/basic/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /examples/basic/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "trailingComma": "all", 8 | "endOfLine": "auto" 9 | } 10 | -------------------------------------------------------------------------------- /examples/basic/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.options": { 3 | "overrideConfigFile": ".eslintrc.json" 4 | }, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/basic/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Atyantik Technologies Private Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 10 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 11 | SOFTWARE. 12 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactpwa/examples-basic", 3 | "version": "1.0.50-alpha.14", 4 | "description": "Core of ReactPWA", 5 | "type": "module", 6 | "private": true, 7 | "scripts": { 8 | "dev": "reactpwa dev", 9 | "build": "reactpwa --mode=production build", 10 | "build:static": "reactpwa build --mode=production --static-site", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "keywords": [], 14 | "author": "Tirth Bodawala ", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@reactpwa/core": "^1.0.50-alpha.14", 18 | "bulma": "^0.9.4", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "^18.2.12", 24 | "@types/react-dom": "^18.2.5" 25 | }, 26 | "gitHead": "1b9ab957cac25218179b12f2ddc727fabb06f000" 27 | } 28 | -------------------------------------------------------------------------------- /examples/basic/reactpwa.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "react": { 3 | "strictMode": false 4 | }, 5 | "hotReload": true, 6 | "serviceWorker": "minimal", 7 | "alias": { 8 | "@components": "./src/components", 9 | "@pages": "./src/pages" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/basic/src/@types/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | 3 | declare module '*.jpg'; 4 | 5 | declare module '*.jpeg'; 6 | 7 | declare module '*.ico'; 8 | 9 | declare module '*.svg'; 10 | 11 | declare module '*.gif'; 12 | -------------------------------------------------------------------------------- /examples/basic/src/@types/styles.d.ts: -------------------------------------------------------------------------------- 1 | // declare module '*.scss' { 2 | // const styles: { [className: string]: string }; 3 | // export default styles; 4 | // } 5 | declare module '*.scss'; 6 | -------------------------------------------------------------------------------- /examples/basic/src/components/child/index.tsx: -------------------------------------------------------------------------------- 1 | import { useSyncData, Head } from '@reactpwa/core'; 2 | import { FC, useState, useEffect } from 'react'; 3 | import { ChildData } from '../../services/data'; 4 | import '../../resources/styles.scss'; 5 | 6 | export const ChildComponent: FC = () => { 7 | const { 8 | data: childDetails, 9 | syncScript, 10 | } = useSyncData('child.data', ChildData); 11 | const titleText = `Welcome, ${childDetails.name}`; 12 | const [title, setTitle] = useState(titleText); 13 | useEffect(() => { 14 | setTimeout(() => { 15 | setTitle(`${titleText} - 1`); 16 | }, 4000); 17 | }, []); 18 | return ( 19 | <> 20 | 21 | {title} | Atyantik Technology 22 | 29 | 30 |

Welcome, { childDetails.name }

31 | {syncScript} 32 | 33 | ); 34 | }; 35 | 36 | export default ChildComponent; 37 | -------------------------------------------------------------------------------- /examples/basic/src/components/errors/404.tsx: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '@reactpwa/core'; 2 | 3 | const NotFoundComponent: React.FC = () => ( 4 | 5 |

404 Not found.

6 |

The page you are looking for, does not exists

7 |
8 | ); 9 | 10 | export default NotFoundComponent; 11 | -------------------------------------------------------------------------------- /examples/basic/src/components/page-loader/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactPWALogo from './react-pwa-logo.png'; 2 | import styles from './styles.scss'; 3 | 4 | export const PageLoader: React.FC = () => ( 5 |
6 | 10 |
11 |
12 | ); 13 | -------------------------------------------------------------------------------- /examples/basic/src/components/page-loader/react-pwa-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atyantik/react-pwa/b498cafe5dd7bab8005fa8ad89e2f6c03eb3cc8a/examples/basic/src/components/page-loader/react-pwa-logo.png -------------------------------------------------------------------------------- /examples/basic/src/components/page-loader/styles.scss: -------------------------------------------------------------------------------- 1 | $loaderColor: #00ABEB; 2 | 3 | .box { 4 | position: absolute; 5 | width: 100vw; 6 | height: 50vh; 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | justify-content: center; 11 | } 12 | 13 | .img { 14 | width: 200px; 15 | } 16 | 17 | .loader { 18 | width: 10px; 19 | height: 10px; 20 | border-radius: 50%; 21 | display: block; 22 | margin:15px auto; 23 | margin-top: 30px; 24 | position: relative; 25 | color: $loaderColor; 26 | left: -100px; 27 | box-sizing: border-box; 28 | opacity: 0.6; 29 | animation: shadowRolling 2s linear infinite; 30 | } 31 | 32 | @keyframes shadowRolling { 33 | 0% { 34 | box-shadow: 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0); 35 | } 36 | 12% { 37 | box-shadow: 100px 0 $loaderColor, 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0); 38 | } 39 | 25% { 40 | box-shadow: 110px 0 $loaderColor, 100px 0 $loaderColor, 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0); 41 | } 42 | 36% { 43 | box-shadow: 120px 0 $loaderColor, 110px 0 $loaderColor, 100px 0 $loaderColor, 0px 0 rgba(255, 255, 255, 0); 44 | } 45 | 50% { 46 | box-shadow: 130px 0 $loaderColor, 120px 0 $loaderColor, 110px 0 $loaderColor, 100px 0 $loaderColor; 47 | } 48 | 62% { 49 | box-shadow: 200px 0 rgba(255, 255, 255, 0), 130px 0 $loaderColor, 120px 0 $loaderColor, 110px 0 $loaderColor; 50 | } 51 | 75% { 52 | box-shadow: 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0), 130px 0 $loaderColor, 120px 0 $loaderColor; 53 | } 54 | 87% { 55 | box-shadow: 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0), 130px 0 $loaderColor; 56 | } 57 | 100% { 58 | box-shadow: 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/basic/src/components/parent/images/Atyantik Logo White BG - Rectangle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atyantik/react-pwa/b498cafe5dd7bab8005fa8ad89e2f6c03eb3cc8a/examples/basic/src/components/parent/images/Atyantik Logo White BG - Rectangle.jpg -------------------------------------------------------------------------------- /examples/basic/src/components/parent/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useSyncData, ErrorBoundary, Head, 3 | } from '@reactpwa/core'; 4 | import { Suspense, FC, lazy } from 'react'; 5 | import { ParentData } from '../../services/data'; 6 | import { Skeleton } from '../skeleton'; 7 | import styles from './styles.scss'; 8 | 9 | const LazyChild = lazy(() => import('@components/child')); 10 | 11 | export const ParentComponent: FC = () => { 12 | const { 13 | data: parentDetails, 14 | syncScript, 15 | } = useSyncData('parent.data', ParentData); 16 | return ( 17 | <> 18 | 19 | Welcome, {parentDetails.name} | Atyantik 20 | 24 | 25 | 26 |
27 |

Welcome, { parentDetails.name }

28 |
29 |
30 | <>Error}> 31 | }> 32 | 33 | 34 | 35 | {syncScript} 36 | 37 | ); 38 | }; 39 | 40 | export default ParentComponent; 41 | -------------------------------------------------------------------------------- /examples/basic/src/components/parent/styles.scss: -------------------------------------------------------------------------------- 1 | .red { 2 | color: red; 3 | } 4 | 5 | .logo { 6 | width: 300px; 7 | height: 300px; 8 | background-image: url('./images/Atyantik\ Logo\ White\ BG\ -\ Rectangle.jpg'); 9 | } 10 | -------------------------------------------------------------------------------- /examples/basic/src/components/shell/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Outlet, 3 | Link, 4 | } from '@reactpwa/core'; 5 | 6 | /** 7 | * @todo: Check for header re-render, do not touch script if not updated 8 | */ 9 | const Shell: React.FC = () => ( 10 | <> 11 |
12 | 17 |
18 |
19 | 20 |
21 | 22 | ); 23 | 24 | export default Shell; 25 | -------------------------------------------------------------------------------- /examples/basic/src/components/skeleton/index.tsx: -------------------------------------------------------------------------------- 1 | export const Skeleton: React.FC = () => ( 2 |
3 | ...Loading.. 4 |
5 | ); 6 | -------------------------------------------------------------------------------- /examples/basic/src/pages/about.tsx: -------------------------------------------------------------------------------- 1 | import { FC, lazy, Suspense } from 'react'; 2 | import { Skeleton } from '@components/skeleton'; 3 | 4 | const LazyParent = lazy(() => import('@components/parent')); 5 | 6 | const HomePage: FC = () => ( 7 |
8 | }> 9 | 10 | 11 |
12 | ); 13 | 14 | export default HomePage; 15 | -------------------------------------------------------------------------------- /examples/basic/src/pages/home.tsx: -------------------------------------------------------------------------------- 1 | import { useSyncData, Head } from '@reactpwa/core'; 2 | import { FC } from 'react'; 3 | import { HomeData } from '../services/data'; 4 | import styles from './styles.scss'; 5 | 6 | const HomePage: FC<{ name?: string }> = ({ name }) => { 7 | const { data: homeData, syncScript } = useSyncData('home.data', HomeData); 8 | return ( 9 |
10 | 11 | 12 | This is home page 13 | {name ? ` for ${name}` : ''} 14 | 15 | 16 |

Home Page

17 |

{homeData.content}

18 | {syncScript} 19 |
20 | ); 21 | }; 22 | 23 | export default HomePage; 24 | -------------------------------------------------------------------------------- /examples/basic/src/pages/styles.scss: -------------------------------------------------------------------------------- 1 | .italic { 2 | font-style: italic; 3 | } 4 | -------------------------------------------------------------------------------- /examples/basic/src/public/robot.txt: -------------------------------------------------------------------------------- 1 | Tirth Bodawala 2 | -------------------------------------------------------------------------------- /examples/basic/src/resources/route-styles.scss: -------------------------------------------------------------------------------- 1 | .black { 2 | color: purple; 3 | } 4 | -------------------------------------------------------------------------------- /examples/basic/src/resources/styles.scss: -------------------------------------------------------------------------------- 1 | .redBg { 2 | background-color: red; 3 | } 4 | -------------------------------------------------------------------------------- /examples/basic/src/routes.tsx: -------------------------------------------------------------------------------- 1 | import { Routes } from '@reactpwa/core'; 2 | import { PageLoader } from '@components/page-loader'; 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '/', 7 | element: () => import('@components/shell'), 8 | children: [ 9 | { 10 | path: '/', 11 | element: () => import('@pages/home'), 12 | skeleton: PageLoader, 13 | props: { 14 | name: 'John', 15 | }, 16 | }, 17 | { 18 | path: '/about', 19 | element: () => import('@pages/about'), 20 | skeleton: PageLoader, 21 | }, 22 | ], 23 | }, 24 | { 25 | path: '*', 26 | element: () => import('@components/errors/404'), 27 | }, 28 | ]; 29 | 30 | export default routes; 31 | -------------------------------------------------------------------------------- /examples/basic/src/server.tsx: -------------------------------------------------------------------------------- 1 | // import fastify from 'fastify'; 2 | 3 | // const instance = fastify(); 4 | 5 | // instance.get('/something2', (request, reply) => { 6 | // reply.send({ 7 | // name: 'Tirth', 8 | // }); 9 | // }); 10 | 11 | // export default instance; 12 | -------------------------------------------------------------------------------- /examples/basic/src/services/data.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Promisify timeout, time in miliseconds 3 | * @param time miliseconds 4 | * @returns Promise 5 | */ 6 | const delay = (time: number) => new Promise((resolve) => { setTimeout(resolve, time); }); 7 | 8 | export const HomeData = async () => { 9 | // delay of 2 seconds 10 | await delay(2000); 11 | return { 12 | content: 'Develop Progressive Web Applications with Ease.', 13 | }; 14 | }; 15 | 16 | export const ChildData = async () => { 17 | await delay(2000); 18 | return { 19 | name: 'Stewie', 20 | }; 21 | }; 22 | 23 | export const ParentData = async () => { 24 | await delay(2000); 25 | return { 26 | name: 'Peter', 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /examples/basic/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | // extend your base config so you don't have to redefine your compilerOptions 3 | "extends": "./tsconfig.json", 4 | "include": [ 5 | "**/**/*.*" 6 | ], 7 | "exclude": [ 8 | "node_modules", 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /examples/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-reactpwa/tsconfig.json", 3 | "compilerOptions": { 4 | // "noEmit": true, 5 | "declarationDir": "./dist/types", 6 | "rootDir": "./src/", 7 | "esModuleInterop": true, 8 | "paths": { 9 | "@components/*": ["./src/components/*"], 10 | "@pages/*": ["./src/pages/*"], 11 | } 12 | }, 13 | "exclude": ["node_modules"], 14 | "include": ["./src/**/*"] 15 | } 16 | -------------------------------------------------------------------------------- /examples/recoil/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | # The JSON files contain newlines inconsistently 13 | [*.json] 14 | insert_final_newline = ignore 15 | 16 | # Minified JavaScript files shouldn't be changed 17 | [**.min.js] 18 | indent_style = ignore 19 | insert_final_newline = ignore 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | 24 | [*.{ts,tsx,js,jsx}] 25 | end_of_line = lf 26 | charset = utf-8 27 | trim_trailing_whitespace = true 28 | indent_style = space 29 | indent_size = 2 30 | -------------------------------------------------------------------------------- /examples/recoil/.esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./docs", 4 | "plugins": [ 5 | { 6 | "name": "esdoc-standard-plugin" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /examples/recoil/.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | lib 4 | src/@types 5 | src/__tests__ 6 | jest-transformer.js 7 | jest.config.js 8 | src/sw.js 9 | -------------------------------------------------------------------------------- /examples/recoil/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "reactpwa" 5 | ], 6 | "parserOptions": { 7 | "project": "./tsconfig.eslint.json" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/recoil/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # WebStorm configurations 62 | .idea 63 | 64 | # Ignore the etc folders created by 65 | etc/ 66 | 67 | # Do not add dist/lib to git 68 | dist 69 | lib 70 | 71 | # ignore the config assets as its generated runtime and thus should notbe included in git 72 | src/config/assets.js 73 | 74 | # Ignore sass cache 75 | .sass-cache 76 | 77 | # demo's package-lock.json 78 | demo/package-lock.json 79 | -------------------------------------------------------------------------------- /examples/recoil/.npmignore: -------------------------------------------------------------------------------- 1 | demo 2 | packages 3 | # Logs 4 | logs 5 | 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | # WebStorm configurations 64 | .idea 65 | 66 | # Ignore the etc folders created by 67 | etc/ 68 | 69 | # Do not add dist to git 70 | dist 71 | 72 | # ignore the config assets as its generated runtime and thus should notbe included in git 73 | src/config/assets.js 74 | 75 | # Ignore sass cache 76 | .sass-cache -------------------------------------------------------------------------------- /examples/recoil/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /examples/recoil/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "trailingComma": "all", 8 | "endOfLine": "auto" 9 | } 10 | -------------------------------------------------------------------------------- /examples/recoil/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.options": { 3 | "overrideConfigFile": ".eslintrc.json" 4 | }, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/recoil/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Atyantik Technologies Private Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 10 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 11 | SOFTWARE. 12 | -------------------------------------------------------------------------------- /examples/recoil/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactpwa/examples-recoil", 3 | "version": "1.0.50-alpha.14", 4 | "description": "Integrating Recoil with ReactPWA", 5 | "type": "module", 6 | "private": true, 7 | "scripts": { 8 | "dev": "reactpwa dev", 9 | "build": "reactpwa build", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [], 13 | "author": "Tirth Bodawala ", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@reactpwa/core": "^1.0.50-alpha.14", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "recoil": "^0.7.7" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.2.12", 23 | "@types/react-dom": "^18.2.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/recoil/reactpwa.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "react": { 3 | "strictMode": false 4 | }, 5 | "hotReload": true, 6 | "serviceWorker": "minimal", 7 | "alias": { 8 | "@components": "./src/components", 9 | "@pages": "./src/pages" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/recoil/src/@types/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | 3 | declare module '*.jpg'; 4 | 5 | declare module '*.jpeg'; 6 | 7 | declare module '*.ico'; 8 | 9 | declare module '*.svg'; 10 | 11 | declare module '*.gif'; 12 | -------------------------------------------------------------------------------- /examples/recoil/src/@types/styles.d.ts: -------------------------------------------------------------------------------- 1 | // declare module '*.scss' { 2 | // const styles: { [className: string]: string }; 3 | // export default styles; 4 | // } 5 | declare module '*.scss'; 6 | -------------------------------------------------------------------------------- /examples/recoil/src/components/child/index.tsx: -------------------------------------------------------------------------------- 1 | import { useSyncData, Head } from '@reactpwa/core'; 2 | import { FC, useState, useEffect } from 'react'; 3 | import { ChildData } from '../../services/data'; 4 | import '../../resources/styles.scss'; 5 | 6 | export const ChildComponent: FC = () => { 7 | const { 8 | data: childDetails, 9 | syncScript, 10 | } = useSyncData('child.data', ChildData); 11 | const titleText = `Welcome, ${childDetails.name}`; 12 | const [title, setTitle] = useState(titleText); 13 | useEffect(() => { 14 | setTimeout(() => { 15 | setTitle(`${titleText} - 1`); 16 | }, 4000); 17 | }, []); 18 | return ( 19 | <> 20 | 21 | {title} | Atyantik 22 | 26 | 27 |

Welcome, { childDetails.name }

28 | {syncScript} 29 | 30 | ); 31 | }; 32 | 33 | export default ChildComponent; 34 | -------------------------------------------------------------------------------- /examples/recoil/src/components/errors/404.tsx: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '@reactpwa/core'; 2 | 3 | const NotFoundComponent: React.FC = () => ( 4 | 5 |

404 Not found.

6 |

The page you are looking for, does not exists

7 |
8 | ); 9 | 10 | export default NotFoundComponent; 11 | -------------------------------------------------------------------------------- /examples/recoil/src/components/page-loader/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactPWALogo from './react-pwa-logo.png'; 2 | import styles from './styles.scss'; 3 | 4 | export const PageLoader: React.FC = () => ( 5 |
6 | 10 |
11 |
12 | ); 13 | -------------------------------------------------------------------------------- /examples/recoil/src/components/page-loader/react-pwa-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atyantik/react-pwa/b498cafe5dd7bab8005fa8ad89e2f6c03eb3cc8a/examples/recoil/src/components/page-loader/react-pwa-logo.png -------------------------------------------------------------------------------- /examples/recoil/src/components/page-loader/styles.scss: -------------------------------------------------------------------------------- 1 | $loaderColor: #00ABEB; 2 | 3 | .box { 4 | position: absolute; 5 | width: 100vw; 6 | height: 50vh; 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | justify-content: center; 11 | } 12 | 13 | .img { 14 | width: 200px; 15 | } 16 | 17 | .loader { 18 | width: 10px; 19 | height: 10px; 20 | border-radius: 50%; 21 | display: block; 22 | margin:15px auto; 23 | margin-top: 30px; 24 | position: relative; 25 | color: $loaderColor; 26 | left: -100px; 27 | box-sizing: border-box; 28 | opacity: 0.6; 29 | animation: shadowRolling 2s linear infinite; 30 | } 31 | 32 | @keyframes shadowRolling { 33 | 0% { 34 | box-shadow: 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0); 35 | } 36 | 12% { 37 | box-shadow: 100px 0 $loaderColor, 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0); 38 | } 39 | 25% { 40 | box-shadow: 110px 0 $loaderColor, 100px 0 $loaderColor, 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0); 41 | } 42 | 36% { 43 | box-shadow: 120px 0 $loaderColor, 110px 0 $loaderColor, 100px 0 $loaderColor, 0px 0 rgba(255, 255, 255, 0); 44 | } 45 | 50% { 46 | box-shadow: 130px 0 $loaderColor, 120px 0 $loaderColor, 110px 0 $loaderColor, 100px 0 $loaderColor; 47 | } 48 | 62% { 49 | box-shadow: 200px 0 rgba(255, 255, 255, 0), 130px 0 $loaderColor, 120px 0 $loaderColor, 110px 0 $loaderColor; 50 | } 51 | 75% { 52 | box-shadow: 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0), 130px 0 $loaderColor, 120px 0 $loaderColor; 53 | } 54 | 87% { 55 | box-shadow: 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0), 130px 0 $loaderColor; 56 | } 57 | 100% { 58 | box-shadow: 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/recoil/src/components/parent/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useSyncData, ErrorBoundary, Head, 3 | } from '@reactpwa/core'; 4 | import { Suspense, FC, lazy } from 'react'; 5 | import { ParentData } from '../../services/data'; 6 | import { Skeleton } from '../skeleton'; 7 | import styles from './styles.scss'; 8 | 9 | const LazyChild = lazy(() => import('@components/child')); 10 | 11 | export const ParentComponent: FC = () => { 12 | const { 13 | data: parentDetails, 14 | syncScript, 15 | } = useSyncData('parent.data', ParentData); 16 | return ( 17 | <> 18 | 19 | Welcome, {parentDetails.name} | Atyantik 20 | 24 | 25 | 26 |

Welcome, { parentDetails.name }

27 | <>Error}> 28 | }> 29 | 30 | 31 | 32 | {syncScript} 33 | 34 | ); 35 | }; 36 | 37 | export default ParentComponent; 38 | -------------------------------------------------------------------------------- /examples/recoil/src/components/parent/styles.scss: -------------------------------------------------------------------------------- 1 | .red { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /examples/recoil/src/components/shell/index.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet, Link, Head } from '@reactpwa/core'; 2 | import { RecoilRoot } from 'recoil'; 3 | 4 | /** 5 | * @todo: Check for header re-render, do not touch script if not updated 6 | */ 7 | const Shell: React.FC = () => ( 8 | 9 | 10 | Example Site 11 | 12 |
13 | 18 |
19 |
20 | 21 |
22 |
23 | ); 24 | 25 | export default Shell; 26 | -------------------------------------------------------------------------------- /examples/recoil/src/components/skeleton/index.tsx: -------------------------------------------------------------------------------- 1 | export const Skeleton: React.FC = () => ( 2 |
3 | ...Loading.. 4 |
5 | ); 6 | -------------------------------------------------------------------------------- /examples/recoil/src/pages/about.tsx: -------------------------------------------------------------------------------- 1 | import { FC, lazy, Suspense } from 'react'; 2 | import { Skeleton } from '@components/skeleton'; 3 | 4 | const LazyParent = lazy(() => import('@components/parent')); 5 | 6 | const HomePage: FC = () => ( 7 |
8 | }> 9 | 10 | 11 |
12 | ); 13 | 14 | export default HomePage; 15 | -------------------------------------------------------------------------------- /examples/recoil/src/pages/home.tsx: -------------------------------------------------------------------------------- 1 | import { useData, Head } from '@reactpwa/core'; 2 | import { FC } from 'react'; 3 | import { HomeData } from '../services/data'; 4 | import styles from './styles.scss'; 5 | 6 | const HomePage: FC<{ name?: string }> = ({ name }) => { 7 | const homeData = useData('home.data', HomeData); 8 | return ( 9 |
10 | 11 | 12 | This is home page 13 | {name ? `for ${name}` : ''} 14 | 15 | 16 |

Home Page

17 |

{homeData.content}

18 |
19 | ); 20 | }; 21 | 22 | export default HomePage; 23 | -------------------------------------------------------------------------------- /examples/recoil/src/pages/styles.scss: -------------------------------------------------------------------------------- 1 | .italic { 2 | font-style: italic; 3 | } 4 | -------------------------------------------------------------------------------- /examples/recoil/src/resources/route-styles.scss: -------------------------------------------------------------------------------- 1 | .black { 2 | color: purple; 3 | } 4 | -------------------------------------------------------------------------------- /examples/recoil/src/resources/styles.scss: -------------------------------------------------------------------------------- 1 | .redBg { 2 | background-color: red; 3 | } 4 | -------------------------------------------------------------------------------- /examples/recoil/src/routes.tsx: -------------------------------------------------------------------------------- 1 | import { Routes } from '@reactpwa/core'; 2 | import { PageLoader } from '@components/page-loader'; 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '/', 7 | element: () => import('@components/shell'), 8 | children: [ 9 | { 10 | path: '/', 11 | element: () => import('@pages/home'), 12 | skeleton: PageLoader, 13 | props: { 14 | name: 'John', 15 | }, 16 | }, 17 | { 18 | path: '/about', 19 | element: () => import('@pages/about'), 20 | skeleton: PageLoader, 21 | }, 22 | ], 23 | }, 24 | { 25 | path: '*', 26 | element: () => import('@components/errors/404'), 27 | }, 28 | ]; 29 | 30 | export default routes; 31 | -------------------------------------------------------------------------------- /examples/recoil/src/services/data.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Promisify timeout, time in miliseconds 3 | * @param time miliseconds 4 | * @returns Promise 5 | */ 6 | const delay = (time: number) => new Promise((resolve) => { setTimeout(resolve, time); }); 7 | 8 | export const HomeData = async () => { 9 | // delay of 2 seconds 10 | await delay(2000); 11 | return { 12 | content: 'Develop Progressive Web Applications with Ease.', 13 | }; 14 | }; 15 | 16 | export const ChildData = async () => { 17 | await delay(2000); 18 | return { 19 | name: 'Stewie', 20 | }; 21 | }; 22 | 23 | export const ParentData = async () => { 24 | await delay(2000); 25 | return { 26 | name: 'Peter', 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /examples/recoil/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | // extend your base config so you don't have to redefine your compilerOptions 3 | "extends": "./tsconfig.json", 4 | "include": [ 5 | "**/**/*.*" 6 | ], 7 | "exclude": [ 8 | "node_modules", 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /examples/recoil/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-reactpwa/tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "rootDir": "./src/", 6 | "esModuleInterop": true, 7 | "paths": { 8 | "@components/*": ["./src/components/*"], 9 | "@pages/*": ["./src/pages/*"], 10 | } 11 | }, 12 | "exclude": ["node_modules"], 13 | "include": ["./src/**/*"] 14 | } 15 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "useNx": true, 4 | "useWorkspaces": true, 5 | "version": "1.0.50-alpha.14" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactpwa", 3 | "version": "1.0.0", 4 | "description": "ReactPWA", 5 | "scripts": { 6 | "dev": "concurrently \"npm:dev:*\"", 7 | "dev:core": "npm run dev -w @reactpwa/core", 8 | "dev:cli": "npm run dev -w @reactpwa/cli", 9 | "build": "npm run build:core && npm run build:cli", 10 | "build:core": "npm run build -w @reactpwa/core", 11 | "build:cli": "npm run build -w @reactpwa/cli", 12 | "lint": "concurrently \"npm:lint:*\"", 13 | "lint:core": "npm run lint -w @reactpwa/core", 14 | "lint:cli": "npm run lint -w @reactpwa/cli", 15 | "publish": "lerna publish", 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "author": "Tirth Bodawala ", 19 | "license": "by-nc-nd", 20 | "workspaces": [ 21 | "packages/*", 22 | "examples/*" 23 | ], 24 | "devDependencies": { 25 | "concurrently": "^8.2.0", 26 | "lerna": "^6.4.1", 27 | "pre-commit": "^1.2.2" 28 | }, 29 | "pre-commit": { 30 | "run": [ 31 | "lint", 32 | "build" 33 | ] 34 | }, 35 | "dependencies": { 36 | "speed-measure-webpack-plugin": "^1.5.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/cli/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | max_line_length=120 12 | 13 | # The JSON files contain newlines inconsistently 14 | [*.json] 15 | insert_final_newline = ignore 16 | 17 | # Minified JavaScript files shouldn't be changed 18 | [**.min.js] 19 | indent_style = ignore 20 | insert_final_newline = ignore 21 | 22 | [*.md] 23 | trim_trailing_whitespace = false 24 | 25 | [*.{ts,tsx,js,jsx}] 26 | end_of_line = lf 27 | charset = utf-8 28 | trim_trailing_whitespace = true 29 | indent_style = space 30 | indent_size = 2 31 | -------------------------------------------------------------------------------- /packages/cli/.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | lib 3 | reactpwa.js 4 | node_modules 5 | -------------------------------------------------------------------------------- /packages/cli/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/eslintrc", 3 | "extends": "reactpwa", 4 | "root": true, 5 | "parserOptions": { 6 | "project": "./tsconfig.eslint.json" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/cli/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # WebStorm configurations 62 | .idea 63 | 64 | # Ignore the etc folders created by 65 | etc/ 66 | 67 | # Do not add dist/lib to git 68 | dist 69 | lib 70 | 71 | # ignore the config assets as its generated runtime and thus should notbe included in git 72 | src/config/assets.js 73 | 74 | # Ignore sass cache 75 | .sass-cache 76 | 77 | # demo's package-lock.json 78 | demo/package-lock.json 79 | 80 | 81 | # docs for now 82 | docs 83 | .external-ecmascript.js 84 | .esdoc.json 85 | -------------------------------------------------------------------------------- /packages/cli/.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # WebStorm configurations 62 | .idea 63 | 64 | # Ignore the etc folders created by 65 | etc/ 66 | 67 | # Do not add dist to git 68 | dist 69 | 70 | # Ignore sass cache 71 | .sass-cache 72 | 73 | src 74 | -------------------------------------------------------------------------------- /packages/cli/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/cli/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/prettierrc", 3 | "printWidth": 120, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "semi": true, 7 | "singleQuote": true, 8 | "trailingComma": "all", 9 | "endOfLine": "lf" 10 | } 11 | -------------------------------------------------------------------------------- /packages/cli/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.options": { 3 | "overrideConfigFile": ".eslintrc.json" 4 | }, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/cli/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Atyantik Technologies Private Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 10 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 11 | SOFTWARE. 12 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactpwa/cli", 3 | "version": "1.0.50-alpha.14", 4 | "description": "Command line interface for ReactPWA", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "tsup --watch", 8 | "build": "tsup", 9 | "lint": "prettier --write . && eslint . --fix", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "typings": "./lib", 13 | "main": "./reactpwa.js", 14 | "bin": { 15 | "reactpwa": "./reactpwa.js" 16 | }, 17 | "keywords": [], 18 | "author": "Tirth Bodawala ", 19 | "license": "ISC", 20 | "devDependencies": { 21 | "@reactpwa/core": "^1.0.50-alpha.14", 22 | "eslint": "^8.45.0", 23 | "eslint-config-reactpwa": "^1.0.50-alpha.14", 24 | "prettier": "^3.0.0", 25 | "tsup": "^7.1.0" 26 | }, 27 | "dependencies": { 28 | "chokidar": "^3.5.3", 29 | "commander": "^11.0.0", 30 | "dotenv": "^16.3.1", 31 | "dotenv-expand": "^10.0.0", 32 | "fs-extra": "^11.1.1", 33 | "node-fetch": "^3.3.1", 34 | "webpack": "^5.88.2" 35 | }, 36 | "gitHead": "1b9ab957cac25218179b12f2ddc727fabb06f000" 37 | } 38 | -------------------------------------------------------------------------------- /packages/cli/reactpwa.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import './lib/index.js'; 4 | -------------------------------------------------------------------------------- /packages/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import { Command, Option } from 'commander'; 3 | import chokidar from 'chokidar'; 4 | import { Server } from 'http'; 5 | import { getEnvFilePath, getReactpwaConfigFilePath, getRunOptions } from './util.js'; 6 | import { generateStaticSite } from './static-site-generator.js'; 7 | 8 | const program = new Command(); 9 | const projectRoot = process.cwd(); 10 | 11 | program.name('reactpwa').description('Create & run ReactPWA seamlessly').version('1.0.0'); 12 | 13 | const modeOption = new Option('-m, --mode ', 'provide the mode to which the code can compile'); 14 | modeOption.choices(['development', 'production']); 15 | 16 | program.option('-ecf, --env-config-file ', 'relative path to .env file', '.env'); 17 | program.option('-cdn, --cdn-path ', 'CDN Path for the output resources', ''); 18 | program.option('-rcf, --reactpwa-config-file ', 'relative path reactpwa.config.js'); 19 | program.addOption(modeOption); 20 | 21 | program 22 | .command('dev') 23 | .description('Start the current project in development mode') 24 | .action(async () => { 25 | // Something to do here now. 26 | const reactpwaCore = await import('@reactpwa/core/start'); 27 | let server: Server; 28 | let restartingServer = false; 29 | const stopServer = (cb: () => void = () => {}) => { 30 | server.close(cb); 31 | }; 32 | // ... 33 | const startServer = async () => { 34 | server = await reactpwaCore.run(getRunOptions(program, { serverSideRender: true }, 'development')); 35 | restartingServer = false; 36 | }; 37 | const restartServer = async () => { 38 | if (restartingServer) { 39 | return; 40 | } 41 | restartingServer = true; 42 | stopServer(async () => { 43 | await startServer(); 44 | }); 45 | }; 46 | await startServer(); 47 | 48 | const watchPaths = [ 49 | getReactpwaConfigFilePath(program)(), 50 | getEnvFilePath(program)(), 51 | `${resolve(projectRoot, 'src', 'public')}`, 52 | // `${resolve(projectRoot, 'src', 'server')}`, 53 | // `${join(projectRoot, 'src', 'server')}.*`, 54 | ].filter((n) => typeof n === 'string') as string[]; 55 | const watcher = chokidar.watch(watchPaths, { ignoreInitial: true }); 56 | watcher.on('all', (_event, path) => { 57 | if (path.indexOf('/server/') !== -1) { 58 | // eslint-disable-next-line no-console 59 | console.info('Changes observed in server or server folder. Restarting Server...'); 60 | } 61 | if (path.indexOf('/public/') !== -1) { 62 | // eslint-disable-next-line no-console 63 | console.info('Changes observed in public folder. Restarting Server...'); 64 | } else { 65 | // eslint-disable-next-line no-console 66 | console.info('Changes observed in configurations (env/config). Restarting Server...'); 67 | } 68 | restartServer(); 69 | }); 70 | }); 71 | 72 | const staticSiteOption = new Option('-ss, --static-site', 'Create a static file with index.html and manifest.json'); 73 | program 74 | .command('build') 75 | .addOption(staticSiteOption) 76 | .description('Build the current project') 77 | .action(async ({ staticSite }) => { 78 | // Something to do here now. 79 | const reactpwaCore = await import('@reactpwa/core/build'); 80 | const stats = await reactpwaCore.run(getRunOptions(program, { serverSideRender: true }, 'production')); 81 | if (staticSite && stats) { 82 | // Generate index.html & manifest.json files 83 | generateStaticSite(stats); 84 | } 85 | }); 86 | 87 | program.parse(); 88 | -------------------------------------------------------------------------------- /packages/cli/src/static-site-generator.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import fse from 'fs-extra'; 3 | import path from 'node:path'; 4 | import { MultiStats } from 'webpack'; 5 | import fetch from 'node-fetch'; 6 | import { Server } from 'http'; 7 | import { Application } from 'express'; 8 | 9 | let localServer: Server; 10 | 11 | const startLocalServer = async (app: Application) => new Promise((resolve, reject) => { 12 | localServer = app.listen(() => { 13 | const address = localServer.address(); 14 | if (!address || typeof address === 'string') { 15 | localServer.close(); 16 | reject(new Error('Error with express server. Cannot resolve address for static file generation')); 17 | return; 18 | } 19 | resolve(`http://localhost:${address.port}`); 20 | }); 21 | }); 22 | 23 | const stopLocalServer = async () => localServer.close(); 24 | 25 | export const generateStaticSite = async (stats: MultiStats | undefined) => { 26 | if (!stats) { 27 | return; 28 | } 29 | const serverStats = stats.stats.find((s) => s.compilation.name === 'node'); 30 | const webStats = stats.stats.find((s) => s.compilation.name === 'web'); 31 | 32 | if (!serverStats || !webStats) { 33 | return; 34 | } 35 | const { outputOptions: serverOutputOptions } = serverStats.compilation; 36 | const { outputOptions: webOutputOptions } = webStats.compilation; 37 | 38 | const webPath = webOutputOptions.path; 39 | if ( 40 | serverOutputOptions.path 41 | && webPath 42 | && serverOutputOptions.filename 43 | && fs.existsSync(path.resolve(serverOutputOptions.path, serverOutputOptions.filename.toString())) 44 | ) { 45 | const serverCjs = await import(path.resolve(serverOutputOptions.path, serverOutputOptions.filename.toString())); 46 | const fastifyServer = await (await serverCjs?.default)?.default?.(); 47 | if (fastifyServer) { 48 | const indexFile = 'index.html'; 49 | const manifestFile = 'manifest.webmanifest'; 50 | // eslint-disable-next-line no-console 51 | console.info('\nGenerating static files..'); 52 | const localServerBase = await startLocalServer(fastifyServer); 53 | 54 | const [indexResponse, manifestResponse] = await Promise.all([ 55 | fetch(new URL(localServerBase).toString()).then((r) => r.text()), 56 | fetch(new URL(manifestFile, localServerBase).toString()).then((r) => r.text()), 57 | ]); 58 | await stopLocalServer(); 59 | 60 | const indexPath = path.join(webPath, indexFile); 61 | const manifestPath = path.join(webPath, manifestFile); 62 | fs.writeFileSync(indexPath, indexResponse, 'utf-8'); 63 | fs.writeFileSync(manifestPath, manifestResponse, 'utf-8'); 64 | const currentDir = process.cwd(); 65 | 66 | // eslint-disable-next-line no-console 67 | console.info(`\nStatic files created: 68 | - index: ${indexPath.replace(currentDir, '')} 69 | - manifest: ${manifestPath.replace(currentDir, '')}`); 70 | // eslint-disable-next-line no-console 71 | console.info('\nRe-organizing static files:\n - Moving dist/build as dist\n'); 72 | try { 73 | const tempBuildPath = path.join(currentDir, '.reactpwa'); 74 | if (fse.existsSync(tempBuildPath)) { 75 | fse.removeSync(tempBuildPath); 76 | } 77 | 78 | fse.moveSync(webPath, tempBuildPath); 79 | fse.removeSync(serverOutputOptions.path); 80 | fse.moveSync(tempBuildPath, serverOutputOptions.path); 81 | 82 | // eslint-disable-next-line no-console 83 | console.info(`Static site generated successfully at: ${serverOutputOptions.path}`); 84 | process.exit(0); 85 | } catch (ex) { 86 | // eslint-disable-next-line 87 | console.log(ex); 88 | } 89 | } 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /packages/cli/src/util.ts: -------------------------------------------------------------------------------- 1 | import { isAbsolute, resolve, extname } from 'node:path'; 2 | import { existsSync, readFileSync } from 'node:fs'; 3 | import { Command } from 'commander'; 4 | import { expand } from 'dotenv-expand'; 5 | import { parse } from 'dotenv'; 6 | 7 | /** 8 | * Return closure that works with program 9 | * @param program Command 10 | * @returns false | string 11 | */ 12 | export const getEnvFilePath = (program: Command) => (): false | string => { 13 | const { envConfigFile } = program.opts(); 14 | if (!envConfigFile) { 15 | return false; 16 | } 17 | let absolutePath = envConfigFile; 18 | if (!isAbsolute(envConfigFile)) { 19 | absolutePath = resolve(process.cwd(), envConfigFile); 20 | } 21 | if (existsSync(absolutePath)) { 22 | return absolutePath; 23 | } 24 | return false; 25 | }; 26 | 27 | /** 28 | * Return closure to get envVars for the project 29 | * @param program Command 30 | * @returns Record 31 | */ 32 | export const getEnvVars = (program: Command) => (): Record => { 33 | const { envConfigFile } = program.opts(); 34 | const envFilePath = getEnvFilePath(program)(); 35 | if (!envFilePath) { 36 | // eslint-disable-next-line no-console 37 | console.warn(`WARNING: Unable to resolve path ${envConfigFile}`); 38 | return {}; 39 | } 40 | try { 41 | const envData = readFileSync(envFilePath, { encoding: 'utf-8' }); 42 | return expand(parse(envData)); 43 | } catch (ex) { 44 | // eslint-disable-next-line no-console 45 | console.error(ex); 46 | } 47 | return {}; 48 | }; 49 | 50 | /** 51 | * @param program Command 52 | * @returns false | string 53 | */ 54 | export const getReactpwaConfigFilePath = (program: Command) => (): string | false => { 55 | const { reactpwaConfigFile } = program.opts(); 56 | 57 | let configPath = resolve(process.cwd(), 'reactpwa.config.json'); 58 | const hasDefaultReactpwaConfig = existsSync(configPath); 59 | 60 | // if no path is specified and default does not exists, then 61 | // return false 62 | if (!reactpwaConfigFile && !hasDefaultReactpwaConfig) { 63 | return false; 64 | } 65 | 66 | if (reactpwaConfigFile) { 67 | configPath = reactpwaConfigFile; 68 | } 69 | 70 | if (!isAbsolute(configPath)) { 71 | configPath = resolve(process.cwd(), reactpwaConfigFile); 72 | } 73 | 74 | if (existsSync(configPath)) { 75 | if (extname(configPath) !== '.json') { 76 | // eslint-disable-next-line no-console 77 | console.error('ERROR: Reactpwa config file should be a valid json file like reactpwa.config.json'); 78 | process.exit(1); 79 | } 80 | return configPath; 81 | } 82 | return false; 83 | }; 84 | 85 | export const getReactpwaConfig = (program: Command) => (): Record => { 86 | const absolutePath = getReactpwaConfigFilePath(program)(); 87 | if (!absolutePath) { 88 | // eslint-disable-next-line no-console 89 | console.warn(`WARNING: Unable to resolve path ${absolutePath}`); 90 | return {}; 91 | } 92 | try { 93 | const reactpwaConfigData = readFileSync(absolutePath, { encoding: 'utf-8' }); 94 | return JSON.parse(reactpwaConfigData); 95 | } catch (ex) { 96 | // eslint-disable-next-line no-console 97 | console.error(ex); 98 | // return nothing 99 | } 100 | return {}; 101 | }; 102 | 103 | type RunOptions = { 104 | serverSideRender: boolean; 105 | }; 106 | 107 | export const getRunOptions = (program: Command, options: RunOptions, defaultMode = 'development') => { 108 | const { mode } = program.opts(); 109 | const { cdnPath } = program.opts(); 110 | const projectRoot = process.cwd(); 111 | return { 112 | projectRoot, 113 | envVars: getEnvVars(program)(), 114 | config: { ...getReactpwaConfig(program)(), cdnPath }, 115 | mode: mode ?? defaultMode, 116 | serverSideRender: options.serverSideRender, 117 | }; 118 | }; 119 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | // extend your base config so you don't have to redefine your compilerOptions 3 | "extends": "./tsconfig.json", 4 | "include": ["**/**/*.*"], 5 | "exclude": ["node_modules"] 6 | } 7 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-reactpwa/tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "Node16", 5 | "baseUrl": "./", 6 | "rootDir": "./src/", 7 | "outDir": "./lib/esm/" 8 | }, 9 | "include": ["./src/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/cli/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['./src'], 5 | outDir: './lib', 6 | splitting: true, 7 | sourcemap: true, 8 | clean: true, 9 | dts: true, 10 | platform: 'node', 11 | skipNodeModulesBundle: true, 12 | bundle: false, 13 | format: ['esm'], 14 | minify: true, 15 | }); 16 | -------------------------------------------------------------------------------- /packages/core/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | max_line_length=80 12 | 13 | # The JSON files contain newlines inconsistently 14 | [*.json] 15 | insert_final_newline = ignore 16 | 17 | # Minified JavaScript files shouldn't be changed 18 | [**.min.js] 19 | indent_style = ignore 20 | insert_final_newline = ignore 21 | 22 | [*.md] 23 | trim_trailing_whitespace = false 24 | 25 | [*.{ts,tsx,js,jsx}] 26 | end_of_line = lf 27 | charset = utf-8 28 | trim_trailing_whitespace = true 29 | indent_style = space 30 | indent_size = 2 31 | -------------------------------------------------------------------------------- /packages/core/.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | lib 4 | src/babel/lazy-routes.ts 5 | reactpwa.js 6 | node_modules 7 | src/__tests__ 8 | jest-transformer.js 9 | jest.config.js 10 | src/defaults/sw.js 11 | -------------------------------------------------------------------------------- /packages/core/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/eslintrc", 3 | "extends": "reactpwa", 4 | "parserOptions": { 5 | "project": "./tsconfig.eslint.json" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # WebStorm configurations 62 | .idea 63 | 64 | # Ignore the etc folders created by 65 | etc/ 66 | 67 | # Do not add dist/lib to git 68 | dist 69 | lib 70 | 71 | # ignore the config assets as its generated runtime and thus should notbe included in git 72 | src/config/assets.js 73 | 74 | # Ignore sass cache 75 | .sass-cache 76 | 77 | # demo's package-lock.json 78 | demo/package-lock.json 79 | 80 | 81 | # docs for now 82 | docs 83 | .external-ecmascript.js 84 | .esdoc.json 85 | -------------------------------------------------------------------------------- /packages/core/.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # WebStorm configurations 62 | .idea 63 | 64 | # Ignore the etc folders created by 65 | etc/ 66 | 67 | # Do not add dist to git 68 | dist 69 | 70 | # ignore the config assets as its generated runtime and thus should notbe included in git 71 | src 72 | 73 | # Ignore sass cache 74 | .sass-cache 75 | -------------------------------------------------------------------------------- /packages/core/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/core/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/prettierrc", 3 | "printWidth": 80, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "semi": true, 7 | "singleQuote": true, 8 | "trailingComma": "all", 9 | "endOfLine": "lf" 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/swcrc", 3 | "jsc": { 4 | "target": "es2022", 5 | "parser": { 6 | "syntax": "typescript", 7 | "decorators": false, 8 | "dynamicImport": true 9 | }, 10 | "transform": { 11 | "react": { 12 | "runtime": "automatic", 13 | "pragma": "React.createElement", 14 | "pragmaFrag": "React.Fragment", 15 | "throwIfNamespace": true, 16 | "development": false, 17 | "useBuiltins": false 18 | } 19 | }, 20 | "experimental": { 21 | "plugins": [["@swc/plugin-loadable-components", {}]] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.options": { 3 | "overrideConfigFile": ".eslintrc.json" 4 | }, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Atyantik Technologies Private Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 10 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 11 | SOFTWARE. 12 | -------------------------------------------------------------------------------- /packages/core/assets/reactpwa-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atyantik/react-pwa/b498cafe5dd7bab8005fa8ad89e2f6c03eb3cc8a/packages/core/assets/reactpwa-192.png -------------------------------------------------------------------------------- /packages/core/assets/reactpwa-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atyantik/react-pwa/b498cafe5dd7bab8005fa8ad89e2f6c03eb3cc8a/packages/core/assets/reactpwa-512.png -------------------------------------------------------------------------------- /packages/core/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/extensions 2 | export * from './lib/index.js'; 3 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactpwa/core", 3 | "version": "1.0.50-alpha.14", 4 | "description": "Core of ReactPWA", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "tsup --watch", 8 | "build": "tsup", 9 | "lint": "prettier --write . && eslint . --fix", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "typings": "typings.d.ts", 13 | "main": "./lib/index.cjs", 14 | "module": "./lib/index.js", 15 | "files": [ 16 | "lib/*", 17 | "assets/*", 18 | "index.js", 19 | "router.d.ts", 20 | "router.js", 21 | "typings.d.ts", 22 | "reactpwa.js" 23 | ], 24 | "exports": { 25 | ".": { 26 | "types": "./typings.d.ts", 27 | "import": "./lib/index.js", 28 | "require": "./lib/index.cjs" 29 | }, 30 | "./start": { 31 | "types": "./lib/node/start.d.ts", 32 | "import": "./lib/node/start.js", 33 | "require": "./lib/node/start.cjs" 34 | }, 35 | "./build": { 36 | "types": "./lib/node/build.d.ts", 37 | "import": "./lib/node/build.js", 38 | "require": "./lib/node/build.cjs" 39 | }, 40 | "./webpack": { 41 | "types": "./lib/webpack.d.ts", 42 | "import": "./lib/webpack.js", 43 | "require": "./lib/webpack.cjs" 44 | }, 45 | "./router": { 46 | "types": "./router.d.ts", 47 | "import": "./lib/router.js", 48 | "require": "./lib/router.cjs" 49 | }, 50 | "./assets/*": "./assets/*", 51 | "./package.json": "./package.json" 52 | }, 53 | "keywords": [ 54 | "react", 55 | "react-dom", 56 | "pwa", 57 | "progressive", 58 | "reactpwa" 59 | ], 60 | "author": "Tirth Bodawala ", 61 | "license": "ISC", 62 | "dependencies": { 63 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 64 | "@babel/preset-env": "^7.22.9", 65 | "@babel/preset-react": "^7.22.5", 66 | "@babel/preset-typescript": "^7.22.5", 67 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", 68 | "autoprefixer": "^10.4.14", 69 | "babel-loader": "^9.1.3", 70 | "bowser": "^2.11.0", 71 | "compression": "^1.7.4", 72 | "copy-webpack-plugin": "^11.0.0", 73 | "css-loader": "^6.8.1", 74 | "express": "^5.0.0-beta.1", 75 | "fs-extra": "^11.1.1", 76 | "is-object": "^1.0.2", 77 | "isbot": "^3.6.13", 78 | "mini-css-extract-plugin": "^2.7.6", 79 | "node-sass": "^9.0.0", 80 | "postcss-loader": "^7.3.3", 81 | "react-cookie": "^4.1.1", 82 | "react-error-boundary": "^4.0.10", 83 | "react-refresh": "^0.14.0", 84 | "react-router-dom": "^6.14.2", 85 | "sass": "^1.64.0", 86 | "sass-embedded": "^1.64.0", 87 | "sass-loader": "^13.3.2", 88 | "serialize-javascript": "^6.0.1", 89 | "style-loader": "^3.3.3", 90 | "universal-cookie": "^4.0.4", 91 | "webpack": "^5.88.2", 92 | "webpack-dev-middleware": "^6.1.1", 93 | "webpack-hot-middleware": "^2.25.4", 94 | "webpack-node-externals": "^3.0.0", 95 | "workbox-webpack-plugin": "^7.0.0" 96 | }, 97 | "peerDependencies": { 98 | "react": ">=18.2.0", 99 | "react-dom": ">=18.2.0" 100 | }, 101 | "devDependencies": { 102 | "@types/compression": "^1.7.2", 103 | "@types/fs-extra": "^11.0.1", 104 | "@types/is-object": "^1.0.2", 105 | "@types/node-sass": "^4.11.3", 106 | "@types/require-from-string": "^1.2.1", 107 | "@types/serialize-javascript": "^5.0.2", 108 | "@types/webpack-hot-middleware": "^2.25.6", 109 | "@types/webpack-node-externals": "^3.0.0", 110 | "eslint": "^8.45.0", 111 | "eslint-config-reactpwa": "^1.0.50-alpha.14", 112 | "prettier": "^3.0.0", 113 | "react": "^18.2.0", 114 | "react-dom": "^18.2.0", 115 | "tsup": "^7.1.0" 116 | }, 117 | "gitHead": "1b9ab957cac25218179b12f2ddc727fabb06f000" 118 | } 119 | -------------------------------------------------------------------------------- /packages/core/reactpwa.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { run } from './lib/node.js'; 4 | run(); 5 | -------------------------------------------------------------------------------- /packages/core/router.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/extensions 2 | export * from './lib/router.js'; 3 | -------------------------------------------------------------------------------- /packages/core/router.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/extensions 2 | export * from './lib/router.js'; 3 | -------------------------------------------------------------------------------- /packages/core/src/@types/assets.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@reactpwa/core/assets/*.svg' { 2 | // 3 | } 4 | declare module '@reactpwa/core/assets/*.png' { 5 | // 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/src/@types/jsx.d.ts: -------------------------------------------------------------------------------- 1 | declare module JSX { 2 | interface IntrinsicElements { 3 | 'app-content': Element; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/babel/lazy-routes.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import syntaxDynamicImport from '@babel/plugin-syntax-dynamic-import'; 3 | 4 | export default (api: any) => { 5 | const { types: t } = api; 6 | return { 7 | inherits: syntaxDynamicImport.default, 8 | visitor: { 9 | Program: { 10 | enter(programPath: any) { 11 | programPath.traverse({ 12 | CallExpression(path: any) { 13 | if (!path.isCallExpression()) { 14 | return; 15 | } 16 | const source = path.get('arguments.0')?.node?.value; 17 | if (!source) { 18 | return; 19 | } 20 | // const source = path.parentPath.node.arguments[0].value; 21 | if ( 22 | path.parent?.type !== 'ArrowFunctionExpression' && 23 | path.parentPath?.parent?.type !== 'ObjectProperty' && 24 | path.parentPath?.parent?.key?.name === 'component' 25 | ) { 26 | return; 27 | } 28 | const callPaths: any[] = []; 29 | path.traverse({ 30 | Import(importPath: any) { 31 | callPaths.push(importPath.parentPath); 32 | }, 33 | }); 34 | if (callPaths.length === 0) return; 35 | 36 | // Multiple imports call is not supported 37 | if (callPaths.length > 1) { 38 | throw new Error( 39 | 'ReactPWA: multiple import calls inside for routes function are not supported.', 40 | ); 41 | } 42 | 43 | try { 44 | const obj = path.parentPath.parentPath; 45 | const propertiesMap: any = {}; 46 | if (!obj.container || !obj.container.length) { 47 | return; 48 | } 49 | obj.container.forEach((property: any) => { 50 | propertiesMap[property.key.name] = property.value.value; 51 | }); 52 | 53 | const moduleObj = t.objectProperty( 54 | t.identifier('module'), 55 | t.arrayExpression([t.StringLiteral(source)]), 56 | ); 57 | const webpackObj = t.objectProperty( 58 | t.identifier('webpack'), 59 | t.arrayExpression([ 60 | t.callExpression( 61 | t.memberExpression( 62 | t.identifier('require'), 63 | t.identifier('resolveWeak'), 64 | ), 65 | [callPaths[0].get('arguments')[0].node], 66 | ), 67 | ]), 68 | ); 69 | 70 | obj.parentPath.pushContainer('properties', moduleObj); 71 | obj.parentPath.pushContainer('properties', webpackObj); 72 | } catch (ex) { 73 | // eslint-disable-next-line 74 | console.log(ex); 75 | } 76 | }, 77 | }); 78 | }, 79 | }, 80 | }, 81 | }; 82 | }; 83 | -------------------------------------------------------------------------------- /packages/core/src/client.tsx: -------------------------------------------------------------------------------- 1 | import { matchRoutes } from 'react-router-dom'; 2 | 3 | const rootElement = document.getElementsByTagName('app-content')?.[0]; 4 | // @ts-ignore 5 | const ssrEnabled = EnableServerSideRender; 6 | 7 | if (rootElement) { 8 | const init = async () => { 9 | const [ 10 | { createRoot, hydrateRoot }, 11 | { CookiesProvider }, 12 | { default: Routes }, 13 | { BrowserRouter }, 14 | { ReactStrictMode }, 15 | { App }, 16 | { DataProvider }, 17 | { HeadProvider }, 18 | { requestArgs }, 19 | ] = await Promise.all([ 20 | import('react-dom/client'), 21 | import('react-cookie'), 22 | // @ts-ignore 23 | import('@currentProject/routes'), 24 | // import('./components/browser-router.js') 25 | import('react-router-dom'), 26 | import('./components/strict.js'), 27 | import('./components/app.js'), 28 | import('./components/data.js'), 29 | import('./components/head/provider.js'), 30 | import('./utils/client.js'), 31 | ]); 32 | let routes = Routes; 33 | if (!Routes) { 34 | routes = []; 35 | } 36 | if (typeof Routes === 'function') { 37 | routes = await Routes(requestArgs); 38 | } 39 | const matched = matchRoutes(routes, window.location) ?? []; 40 | await Promise.all( 41 | // @ts-ignore 42 | matched.map((route) => route.route?.element?.()), 43 | ); 44 | const children = ( 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ); 57 | const render = async () => { 58 | if (ssrEnabled) { 59 | hydrateRoot(rootElement, children, { 60 | onRecoverableError: (error) => { 61 | /** 62 | * A Developer to a developer: 63 | * this is a known error, we are knowingly ignoring the error 64 | * of hydration. this can be improved with improvised implementation 65 | * of routes and router. Right now we are using ReactRouter and it only 66 | * happens when we doing selective hydrate on the same route. 67 | */ 68 | if (error instanceof Error) { 69 | const isHydratingError = error.message 70 | .toLowerCase() 71 | .indexOf( 72 | 'this suspense boundary received an update before it finished hydrating', 73 | ) !== -1; 74 | const isDocumentLoading = document.readyState === 'loading'; 75 | if (isHydratingError && isDocumentLoading) { 76 | // Do nothing ignore the error. 77 | } 78 | } else { 79 | throw error; 80 | } 81 | }, 82 | }); 83 | } else { 84 | const root = createRoot(rootElement); 85 | root.render(children); 86 | } 87 | }; 88 | if (document.readyState === 'complete') { 89 | render(); 90 | } 91 | document.addEventListener('readystatechange', () => { 92 | if (document.readyState === 'complete') { 93 | render(); 94 | } 95 | }); 96 | 97 | // @ts-ignore 98 | if (EnableServiceWorker) { 99 | // Register service worker 100 | if ('serviceWorker' in navigator) { 101 | window.addEventListener('load', () => { 102 | navigator.serviceWorker 103 | .register('/sw.js') 104 | .then((registration) => { 105 | // eslint-disable-next-line no-console 106 | console.log('SW registered: ', registration); 107 | }) 108 | .catch((registrationError) => { 109 | // eslint-disable-next-line no-console 110 | console.log('SW registration failed: ', registrationError); 111 | }); 112 | }); 113 | } 114 | } else if ('serviceWorker' in navigator) { 115 | navigator.serviceWorker.getRegistrations().then((registrations) => { 116 | registrations.forEach((registration) => registration.unregister()); 117 | }); 118 | } 119 | }; 120 | init(); 121 | } else { 122 | // eslint-disable-next-line no-console 123 | console.log('Cannot find root element'); 124 | } 125 | -------------------------------------------------------------------------------- /packages/core/src/components/app.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useRoutes, RouteObject as RRRouteObject } from 'react-router-dom'; 3 | import { lazy } from './route.js'; 4 | import { ErrorFallback } from './error.js'; 5 | import { ErrorBoundary, RouteObject } from '../index.js'; 6 | 7 | const lazifyRoutes = ( 8 | routes: RouteObject[], 9 | parentResolveHeadManually = false, 10 | ): RRRouteObject[] => routes.map((route) => { 11 | const shouldResolveHeadManually = parentResolveHeadManually || route.resolveHeadManually; 12 | // Remove webpack, module and extra keys from route 13 | const { 14 | webpack, 15 | module, 16 | element, 17 | children, 18 | index, 19 | props, 20 | resolveHeadManually, 21 | ...otherProps 22 | } = route; 23 | const Element = lazy(route); 24 | // we are ignoring index route as of now, as we are not supporting 25 | // post of data 26 | return { 27 | ...otherProps, 28 | element: , 29 | children: children?.length 30 | ? lazifyRoutes(children, shouldResolveHeadManually) 31 | : [], 32 | }; 33 | }); 34 | 35 | export const App: React.FC<{ 36 | children?: React.ReactNode; 37 | routes: RouteObject[]; 38 | }> = ({ routes }) => { 39 | const loadableRoutes = useMemo( 40 | () => lazifyRoutes(routes), 41 | [JSON.stringify(routes)], 42 | ); 43 | 44 | const routesEle = useRoutes(loadableRoutes); 45 | return ( 46 | {routesEle} 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /packages/core/src/components/data.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, ReactNode, useRef } from 'react'; 2 | import { delay } from '../utils/delay.js'; 3 | import { WrappedPromise, wrapPromise } from '../utils/promise.js'; 4 | import { DataEventEmitter } from '../utils/event-emmiter.js'; 5 | 6 | export type GetPendingCount = () => number; 7 | 8 | /** 9 | * List of all the wrapped promises 10 | */ 11 | type DataPromise = { 12 | id: string; 13 | promise: WrappedPromise; 14 | done: boolean; 15 | }; 16 | 17 | const initialContextValue = { 18 | createDataPromise: (() => ({ read: () => {} })) as < 19 | T extends () => Promise, 20 | >( 21 | id: string, 22 | cb: T, 23 | ) => { read: () => Awaited> }, 24 | awaitDataCompletion: (() => ({ read: () => {} })) as (id: string) => { 25 | read: () => void; 26 | }, 27 | removeDataPromise: (() => {}) as (id: string) => void, 28 | }; 29 | 30 | /** 31 | * The data context, we can use it route wise in the future, 32 | * but I am willing to keep it global as of now 33 | */ 34 | export const DataContext = createContext(initialContextValue); 35 | 36 | const getSyncData = (id: string) => { 37 | try { 38 | if (typeof window !== 'undefined') { 39 | const dataScript = document.querySelector(`[data-sync-id="${id}"]`); 40 | if (dataScript && dataScript.innerHTML) { 41 | // eslint-disable-next-line no-eval 42 | return eval(`(${dataScript.innerHTML})`); 43 | } 44 | } 45 | } catch { 46 | // Do nothing as of now 47 | } 48 | 49 | return undefined; 50 | }; 51 | 52 | /** 53 | * I would rather not prefer having any state for data provider. 54 | * We would be using state for making sure we do not create more than 55 | * one instance of eventEmitter 56 | * Thus avoiding any child components to re-render. 57 | */ 58 | export const DataProvider: React.FC<{ children: ReactNode }> = ({ 59 | children, 60 | }) => { 61 | const eventEmitter = useRef(new DataEventEmitter()); 62 | /** 63 | * All promises are recorded at single place 64 | * This way we can understand how many promises are being 65 | * created and executed 66 | */ 67 | const pendingPromisesRef = useRef([]); 68 | 69 | /** 70 | * Get length of pending promises 71 | * @returns number 72 | */ 73 | const hasPendingPromises = async () => { 74 | /** 75 | * At serverside there is no extra time for loading 76 | * split chunks, thus we can safely assume we need a timeout of approx 77 | * 10 miliseconds before the useData is called anywhere in components 78 | * 79 | * However on clientside it might take from 10 miliseconds to 1s to load the 80 | * component before the hook useData is called! 81 | */ 82 | const isClient = typeof window !== 'undefined'; 83 | if (pendingPromisesRef.current.length === 0) { 84 | await delay(isClient ? 1000 : 10); 85 | } 86 | return pendingPromisesRef.current.some((p) => p.done === false); 87 | }; 88 | 89 | const observerPromisesRef = useRef([]); 90 | function createDataPromise Promise>(id: string, cb: T) { 91 | const previousPromise = pendingPromisesRef.current.find( 92 | (dc) => dc.id === id, 93 | ); 94 | if (previousPromise) { 95 | return previousPromise.promise as { read: () => Awaited> }; 96 | } 97 | 98 | const syncData: Awaited> | undefined = getSyncData(id); 99 | 100 | /** 101 | * Create a wrapped promise on runtime 102 | * and give the appropriate callback to it. 103 | * We need to make sure we do not store the data of 104 | * the promise execution in the memory or else it can 105 | * clog up the memory and cause issues. 106 | */ 107 | const wrapped = wrapPromise(cb, { 108 | // On finalize of the promise, 109 | // i.e. on error or success. 110 | onFinalize: async () => { 111 | await delay(10); 112 | const promiseIndex = pendingPromisesRef.current.findIndex( 113 | (dc) => dc.id === id, 114 | ); 115 | if (promiseIndex !== -1) { 116 | pendingPromisesRef.current[promiseIndex].done = true; 117 | } 118 | // Remove promise reference from array on done 119 | // pendingPromisesRef.current.splice(promiseIndex, 1); 120 | eventEmitter.current.emit('DataPromiseFinalise'); 121 | }, 122 | syncData, 123 | }); 124 | 125 | pendingPromisesRef.current.push({ 126 | id, 127 | promise: wrapped, 128 | done: false, 129 | }); 130 | return wrapped as { read: () => Awaited> }; 131 | } 132 | 133 | function removeDataPromise(id: string) { 134 | const promiseIndex = pendingPromisesRef.current.findIndex( 135 | (dc) => dc.id === id, 136 | ); 137 | if (promiseIndex !== -1) { 138 | pendingPromisesRef.current.splice(promiseIndex, 1); 139 | } 140 | } 141 | 142 | function awaitDataCompletion(id: string) { 143 | const previousPromise = observerPromisesRef.current.find( 144 | (dc) => dc.id === id, 145 | ); 146 | if (previousPromise) { 147 | return previousPromise.promise as { read: () => Awaited }; 148 | } 149 | 150 | /** 151 | * Create a wrapped promise on runtime 152 | * and give the appropriate callback to it. 153 | * We need to make sure we do not store the data of 154 | * the promise execution in the memory or else it can 155 | * clog up the memory and cause issues. 156 | */ 157 | const wrapped = wrapPromise( 158 | () => new Promise((r) => { 159 | (async () => { 160 | if (await hasPendingPromises()) { 161 | eventEmitter.current.on('DataPromiseFinalise', async () => { 162 | if (!(await hasPendingPromises())) { 163 | r(null); 164 | } 165 | }); 166 | } else { 167 | r(null); 168 | } 169 | })(); 170 | }), 171 | { 172 | // On finalize of the promise, 173 | // i.e. on error or success. 174 | onFinalize: () => { 175 | /** 176 | * CAUTION 177 | * @todo: Any way to get rid of timeout? 178 | * I am stuck in look if we do not give timeout. New promises are created again. 179 | * Causing memory leak and infinite looping 180 | * Thus making it super difficult to garbage collect observerPromisesRef 181 | * .Check if manual garbage collect is even required 182 | */ 183 | setTimeout(() => { 184 | const promiseIndex = observerPromisesRef.current.findIndex( 185 | (dc) => dc.id === id, 186 | ); 187 | // Remove promise reference from array on done 188 | observerPromisesRef.current.splice(promiseIndex, 1); 189 | }, 10); 190 | }, 191 | }, 192 | ); 193 | 194 | observerPromisesRef.current.push({ 195 | id, 196 | promise: wrapped, 197 | done: false, 198 | }); 199 | return wrapped as { read: () => Awaited }; 200 | } 201 | 202 | return ( 203 | 206 | {children} 207 | 208 | ); 209 | }; 210 | -------------------------------------------------------------------------------- /packages/core/src/components/error.tsx: -------------------------------------------------------------------------------- 1 | export const ErrorFallback: React.FC = ({ error, resetErrorBoundary }: any) => ( 2 |
3 |

Something went wrong:

4 |
{error.message}
5 | 6 |
7 | ); 8 | -------------------------------------------------------------------------------- /packages/core/src/components/head/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { HeadElement, PromiseResolver } from '../../typedefs/head'; 3 | 4 | type SetDataPromiseResolver = (options: { current: PromiseResolver }) => void; 5 | const initialContextValue = { 6 | addChildren: (() => {}) as (children: HeadElement, id: string) => void, 7 | removeChildren: (() => {}) as (id: string) => void, 8 | elements: { current: [] } as React.MutableRefObject, 9 | setDataPromiseResolver: (() => {}) as SetDataPromiseResolver, 10 | resolveDataPromiseResolver: (() => {}) as PromiseResolver, 11 | }; 12 | 13 | export const HeadContext = createContext(initialContextValue); 14 | -------------------------------------------------------------------------------- /packages/core/src/components/head/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | memo, useId, useContext, useEffect, 3 | } from 'react'; 4 | import { HeadElement } from '../../typedefs/head.js'; 5 | import { HeadContext } from './context.js'; 6 | 7 | export const Head = memo<{ children: HeadElement; resolve?: boolean }>( 8 | ({ children, resolve }) => { 9 | const { addChildren, removeChildren, resolveDataPromiseResolver } = useContext(HeadContext); 10 | const id = useId(); 11 | useEffect(() => { 12 | addChildren(children, id); 13 | return () => { 14 | removeChildren(id); 15 | }; 16 | }, [children]); 17 | if (typeof window === 'undefined') { 18 | addChildren(children, id); 19 | if (resolve) { 20 | resolveDataPromiseResolver(); 21 | } 22 | } 23 | return null; 24 | }, 25 | ); 26 | -------------------------------------------------------------------------------- /packages/core/src/components/head/lazy.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { DataContext } from '../data.js'; 3 | import { HeadContext } from './context.js'; 4 | 5 | export const LazyHead: React.FC = () => { 6 | const { awaitDataCompletion } = useContext(DataContext); 7 | awaitDataCompletion('@reactpwa/core.head').read(); 8 | 9 | const { elements } = useContext(HeadContext); 10 | return <>{elements.current}; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/core/src/components/http-status.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactElement, useContext } from 'react'; 2 | import { NavigateOptions, To, useHref } from 'react-router-dom'; 3 | import { statusCodeWithLocations } from '../utils/redirect.js'; 4 | import { ReactPWAContext } from './reactpwa.js'; 5 | 6 | export const HttpStatus: FC<{ 7 | children?: ReactElement | ReactElement[]; 8 | statusCode: number; 9 | location?: URL | To; 10 | relative?: NavigateOptions['relative']; 11 | }> = ({ 12 | children, statusCode, location, relative, 13 | }) => { 14 | let locationStr = location; 15 | if (typeof location === 'string') { 16 | // Do nothing we already assigned it to locationStr 17 | } else if (location instanceof URL) { 18 | locationStr = location.toString(); 19 | } else if (location) { 20 | locationStr = useHref(location, { relative }); 21 | } 22 | 23 | if (statusCodeWithLocations.includes(statusCode) && !location) { 24 | throw new Error(`Error: Status code: ${statusCode} requires location`); 25 | } 26 | const { setValue } = useContext(ReactPWAContext); 27 | setValue('httpStatusCode', statusCode); 28 | if (locationStr) { 29 | setValue('httpLocationHeader', locationStr); 30 | } 31 | 32 | if (!children) { 33 | return null; 34 | } 35 | if (Array.isArray(children)) { 36 | return <>{children}; 37 | } 38 | return children; 39 | }; 40 | -------------------------------------------------------------------------------- /packages/core/src/components/reactpwa.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const initialContextValue = { 4 | setValue: (() => {}) as (key: string, value: any) => void, 5 | getValue: (() => null) as (key: string, defaultValue: T) => T, 6 | }; 7 | 8 | export const ReactPWAContext = createContext(initialContextValue); 9 | -------------------------------------------------------------------------------- /packages/core/src/components/redirect.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactElement, useEffect } from 'react'; 2 | import { 3 | To, useNavigate, NavigateOptions, useHref, 4 | } from 'react-router-dom'; 5 | import { RedirectStatusCode } from '../utils/redirect.js'; 6 | import { HttpStatus } from './http-status.js'; 7 | 8 | /** 9 | * Redirect to the provided location, default statusCode 307 (Temporary redirect) 10 | * @param Redirect Props { 11 | * children?: ReactElement | ReactElement[], 12 | * to: To | URL, 13 | * statusCode: RedirectStatusCode, 14 | * } & NavigateOptions 15 | * @returns HttpStatus 16 | */ 17 | export const Redirect: FC< 18 | { 19 | children?: ReactElement | ReactElement[]; 20 | to: To | URL; 21 | statusCode?: RedirectStatusCode; 22 | } & NavigateOptions 23 | > = ({ 24 | children, 25 | to, 26 | statusCode = 307, 27 | replace, 28 | state, 29 | preventScrollReset, 30 | relative, 31 | }) => { 32 | const navigate = useNavigate(); 33 | const href = useHref(to, { relative }); 34 | useEffect(() => { 35 | navigate(to, { 36 | replace, 37 | state, 38 | preventScrollReset, 39 | relative, 40 | }); 41 | }, []); 42 | return ( 43 | 44 | {children} 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /packages/core/src/components/route.tsx: -------------------------------------------------------------------------------- 1 | import React, { lazy as reactLazy, Suspense } from 'react'; 2 | import { ErrorBoundary } from 'react-error-boundary'; 3 | import { DataContext } from './data.js'; 4 | import { RouteObject } from '../index.js'; 5 | import { ErrorFallback as DefaultErrorFallback } from './error.js'; 6 | import { HeadContext } from './head/context.js'; 7 | 8 | const DefaultFallbackComponent: React.FC<{}> = () => null; 9 | 10 | export function lazy(props: RouteObject) { 11 | const LazyComponent = reactLazy(props.element); 12 | const ErrorFallback = props.error || DefaultErrorFallback; 13 | const FallbackComponent = props.skeleton || DefaultFallbackComponent; 14 | return class extends React.Component { 15 | resolver = { 16 | current: () => {}, 17 | }; 18 | 19 | // eslint-disable-next-line class-methods-use-this 20 | render() { 21 | if (props.resolveHeadManually && typeof window === 'undefined') { 22 | return ( 23 | 24 | {({ createDataPromise }) => ( 25 | 26 | {({ setDataPromiseResolver }) => { 27 | setDataPromiseResolver(this.resolver); 28 | // Create data promise 29 | createDataPromise( 30 | 'CustomHeadResolver', 31 | () => new Promise((resolve) => { 32 | // @ts-ignore 33 | const headResolveTimeout = HeadResolveTimeout || 10000; 34 | const timeout = setTimeout(() => { 35 | // eslint-disable-next-line no-console 36 | console.warn( 37 | `WARN:: Forcefully resolving Head after waiting for ${headResolveTimeout}ms.`, 38 | ); 39 | resolve(null); 40 | }, headResolveTimeout); 41 | // 42 | this.resolver.current = () => { 43 | clearTimeout(timeout); 44 | resolve(null); 45 | }; 46 | }), 47 | ); 48 | return ( 49 | 50 | }> 51 | 52 | 53 | 54 | ); 55 | }} 56 | 57 | )} 58 | 59 | ); 60 | } 61 | // Wraps the input component in a container, without mutating it. Good! 62 | return ( 63 | 64 | }> 65 | 66 | 67 | 68 | ); 69 | } 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /packages/core/src/components/strict.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode, FC } from 'react'; 2 | 3 | export const ReactStrictMode: FC<{ children: React.ReactElement }> = ({ 4 | children, 5 | }) => { 6 | // @ts-ignore 7 | if (EnableReactStrictMode) { 8 | return {children}; 9 | } 10 | return children; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/core/src/components/sync-data-script.tsx: -------------------------------------------------------------------------------- 1 | import serialize from 'serialize-javascript'; 2 | import { FC } from 'react'; 3 | 4 | // export const SyncDataScript: FC<{ id: string; data: any }> = ({ id, data }) => { 5 | // const disableSyncScript = typeof document !== 'undefined' && document.readyState === 'complete'; 6 | // const [disableRenderScript, setDisableRenderScript] = useState(disableSyncScript); 7 | // useEffect(() => { 8 | // setDisableRenderScript(true); 9 | // }, []); 10 | // if (disableRenderScript) { 11 | // return null; 12 | // } 13 | // return ( 14 | // ', 234 | ); 235 | compressionStream.end(); 236 | }, 237 | onAllReady() { 238 | if (!isBot) return; 239 | // If you don't want streaming, use this instead of onShellReady. 240 | // This will fire after the entire page content is ready. 241 | // You can use this for crawlers or static generation. 242 | const statusCode = getHttpStatusCode(request, matchedRoutes); 243 | response.status(statusCode); 244 | if (statusCodeWithLocations.includes(statusCode)) { 245 | const redirectUrl = getRedirectUrl(request); 246 | response.redirect(statusCode, redirectUrl); 247 | return; 248 | } 249 | compressionStream.write(initialHtml); 250 | stream.pipe(compressionStream); 251 | }, 252 | onError(err: unknown) { 253 | setInternalVar(request, 'hasExecutionError', true); 254 | if ( 255 | err instanceof Error 256 | && err.message.indexOf('closed early') === -1 257 | ) { 258 | // eslint-disable-next-line no-console 259 | console.error('An error occurred: ', err); 260 | } else if (err?.toString?.().indexOf('closed early') === -1) { 261 | // eslint-disable-next-line no-console 262 | console.error('An error occurred: ', err); 263 | } 264 | }, 265 | }, 266 | ); 267 | }; 268 | 269 | const router = Router(); 270 | Object.defineProperty(router, 'name', { value: 'RPWA_Router' }); 271 | 272 | if ( 273 | appServer 274 | && (Object.keys(appServer).length || typeof appServer === 'function') 275 | ) { 276 | Object.defineProperty(router, 'name', { value: 'RPWA_App_Server' }); 277 | } 278 | 279 | // Add app server as priority 280 | 281 | // Use /manifest.webmanifest as webmanifestHandler 282 | router.get('/manifest.webmanifest', webmanifestHandler); 283 | 284 | // At end use * for default handler 285 | router.use(handler); 286 | 287 | export { router, appServer }; 288 | -------------------------------------------------------------------------------- /packages/core/src/typedefs/head.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement, ReactFragment } from 'react'; 2 | 3 | /** 4 | * Acceptable types of Head Elements 5 | * 6 | * // .. Acceptable Element Types, we are accepting Fragment till level 1 7 | * 8 | */ 9 | export type HeadElement = ReactElement | ReactElement[] | ReactFragment; 10 | 11 | export type PromiseResolver = () => void; 12 | -------------------------------------------------------------------------------- /packages/core/src/typedefs/server.ts: -------------------------------------------------------------------------------- 1 | export type RunOptions = { 2 | projectRoot: string; 3 | mode: 'production' | 'development'; 4 | envVars: Record; 5 | config: Record; 6 | serverSideRender: boolean; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/core/src/typedefs/webmanifest.ts: -------------------------------------------------------------------------------- 1 | export interface IWebManifest { 2 | /** 3 | * The background_color member describes the expected background color of the web application. 4 | */ 5 | background_color?: string; 6 | /** 7 | * The base direction of the manifest. 8 | */ 9 | dir?: 'ltr' | 'rtl' | 'auto'; 10 | /** 11 | * The item represents the developer's preferred display mode for the web application. 12 | */ 13 | display?: 'fullscreen' | 'standalone' | 'minimal-ui' | 'browser'; 14 | /** 15 | * The icons member is an array of icon objects that can serve as iconic 16 | * representations of the web application in various contexts. 17 | */ 18 | icons?: ManifestImageResource[]; 19 | /** 20 | * The primary language for the values of the manifest. 21 | */ 22 | lang?: string; 23 | /** 24 | * The name of the web application. 25 | */ 26 | name?: string; 27 | 28 | charSet?: string; 29 | 30 | /** 31 | * Description of the web application 32 | */ 33 | description?: string; 34 | /** 35 | * The orientation member is a string that serves as the 36 | * default orientation for all top-level browsing contexts of 37 | * the web application. 38 | */ 39 | orientation?: 40 | | 'any' 41 | | 'natural' 42 | | 'landscape' 43 | | 'portrait' 44 | | 'portrait-primary' 45 | | 'portrait-secondary' 46 | | 'landscape-primary' 47 | | 'landscape-secondary'; 48 | /** 49 | * Boolean value that is used as a hint for the user agent 50 | * to say that related applications should be preferred over 51 | * the web application. 52 | */ 53 | prefer_related_applications?: boolean; 54 | /** 55 | * Array of application accessible to the underlying application 56 | * platform that has a relationship with the web application. 57 | */ 58 | related_applications?: ExternalApplicationResource[]; 59 | /** 60 | * A string that represents the navigation scope of this 61 | * web application's application context. 62 | */ 63 | scope?: string; 64 | /** 65 | * A string that represents a short version of the name of 66 | * the web application. 67 | */ 68 | short_name?: string; 69 | /** 70 | * Array of shortcut items that provide access to key tasks within a web application. 71 | */ 72 | shortcuts?: ShortcutItem[]; 73 | /** 74 | * Represents the URL that the developer would prefer the 75 | * user agent load when the user launches the web application. 76 | */ 77 | start_url?: string; 78 | /** 79 | * The theme_color member serves as the default theme color for an application context. 80 | */ 81 | theme_color?: string; 82 | /** 83 | * A string that represents the id of the web application. 84 | */ 85 | id?: string; 86 | [k: string]: unknown; 87 | } 88 | export interface ManifestImageResource { 89 | /** 90 | * The sizes member is a string consisting of an 91 | * unordered set of unique space-separated tokens 92 | * which are ASCII case-insensitive that represents 93 | * the dimensions of an image for visual media. 94 | */ 95 | sizes?: string | 'any'; 96 | /** 97 | * The src member of an image is a URL from which a 98 | * user agent can fetch the icon's data. 99 | */ 100 | src: string; 101 | /** 102 | * The type member of an image is a hint as to the 103 | * media type of the image. 104 | */ 105 | type?: string; 106 | purpose?: 107 | | 'monochrome' 108 | | 'maskable' 109 | | 'any' 110 | | 'monochrome maskable' 111 | | 'monochrome any' 112 | | 'maskable monochrome' 113 | | 'maskable any' 114 | | 'any monochrome' 115 | | 'any maskable' 116 | | 'monochrome maskable any' 117 | | 'monochrome any maskable' 118 | | 'maskable monochrome any' 119 | | 'maskable any monochrome' 120 | | 'any monochrome maskable' 121 | | 'any maskable monochrome'; 122 | [k: string]: unknown; 123 | } 124 | export interface ExternalApplicationResource { 125 | /** 126 | * The platform it is associated to. 127 | */ 128 | platform: 'chrome_web_store' | 'play' | 'itunes' | 'windows'; 129 | /** 130 | * The URL where the application can be found. 131 | */ 132 | url?: string; 133 | /** 134 | * Information additional to the URL or instead 135 | * of the URL, depending on the platform. 136 | */ 137 | id?: string; 138 | /** 139 | * Information about the minimum version of an 140 | * application related to this web app. 141 | */ 142 | min_version?: string; 143 | /** 144 | * An array of fingerprint objects used for 145 | * verifying the application. 146 | */ 147 | fingerprints?: { 148 | type?: string; 149 | value?: string; 150 | [k: string]: unknown; 151 | }[]; 152 | [k: string]: unknown; 153 | } 154 | /** 155 | * A shortcut item represents a link to a key 156 | * task or page within a web app. A user agent 157 | * can use these values to assemble a context menu 158 | * to be displayed by the operating system when a 159 | * user engages with the web app's icon. 160 | */ 161 | export interface ShortcutItem { 162 | /** 163 | * The name member of a shortcut item is a string 164 | * that represents the name of the shortcut as it 165 | * is usually displayed to the user in a context menu. 166 | */ 167 | name: string; 168 | /** 169 | * The short_name member of a shortcut item is 170 | * a string that represents a short version of 171 | * the name of the shortcut. It is intended to 172 | * be used where there is insufficient space to 173 | * display the full name of the shortcut. 174 | */ 175 | short_name?: string; 176 | /** 177 | * The description member of a shortcut item is 178 | * a string that allows the developer to describe 179 | * the purpose of the shortcut. 180 | */ 181 | description?: string; 182 | /** 183 | * The url member of a shortcut item is a URL within 184 | * scope of a processed manifest that opens when 185 | * the associated shortcut is activated. 186 | */ 187 | url: string; 188 | /** 189 | * The icons member of a shortcut item serves as 190 | * iconic representations of the shortcut in various contexts. 191 | */ 192 | icons?: ManifestImageResource[]; 193 | [k: string]: unknown; 194 | } 195 | -------------------------------------------------------------------------------- /packages/core/src/typedefs/webpack.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from 'webpack'; 2 | 3 | export type WebpackHandlerConstructorOptions = { 4 | mode: Configuration['mode']; 5 | target: Configuration['target']; 6 | projectRoot: string; 7 | buildWithHttpServer: boolean; 8 | serverSideRender: boolean; 9 | envVars: Record; 10 | config: Record; 11 | copyPublicFolder: Boolean; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/core/src/utils/asset-extract.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'node:url'; 2 | import { join } from 'node:path'; 3 | import { existsSync, readFileSync } from 'node:fs'; 4 | import * as webpack from 'webpack'; 5 | import { RouteMatch, RouteObject } from 'react-router-dom'; 6 | 7 | export type LazyRouteMatch = RouteMatch & { 8 | route: RouteObject & { 9 | webpack?: (string | number)[]; 10 | module?: string[]; 11 | }; 12 | }; 13 | 14 | export type ChunksMap = { 15 | assetsByChunkName?: Record; 16 | chunks: { 17 | // position: number; 18 | names?: string[]; 19 | id?: string | number; 20 | files?: string[]; 21 | parents?: (string | number)[]; 22 | children?: (string | number)[]; 23 | reasons?: string[]; 24 | reasonsStr?: string; 25 | reasonModules?: (string | number)[]; 26 | }[]; 27 | }; 28 | 29 | const getStats = (webpackStats: any) => { 30 | if (webpackStats && 'toJson' in webpackStats) { 31 | return webpackStats?.toJson?.() ?? {}; 32 | } 33 | return webpackStats; 34 | }; 35 | 36 | const getPublicPathUrl = (publicPath: string) => { 37 | let isExternalCdn = false; 38 | let publicPathUrl = new URL('https://www.reactpwa.com'); 39 | try { 40 | publicPathUrl = new URL(publicPath); 41 | isExternalCdn = true; 42 | } catch (ex) { 43 | console.log('invalid public path url'); 44 | } 45 | return { publicPathUrl, isExternalCdn }; 46 | }; 47 | 48 | const processMainAssets = ( 49 | mainAssets: string[] = [], 50 | isExternalCdn: boolean = false, 51 | publicPathUrl: URL = new URL('https://www.reactpwa.com'), 52 | ) => mainAssets.map((asset: string) => { 53 | if (isExternalCdn) { 54 | return new URL(asset, publicPathUrl).toString(); 55 | } 56 | return asset; 57 | }); 58 | 59 | export const extractChunksMap = ( 60 | webpackStats: 61 | | webpack.Stats 62 | | webpack.MultiStats 63 | | webpack.StatsCompilation 64 | | undefined, 65 | ): ChunksMap => { 66 | const stats = getStats(webpackStats); 67 | if (!stats) { 68 | return { 69 | assetsByChunkName: {}, 70 | chunks: [], 71 | }; 72 | } 73 | 74 | const { publicPathUrl, isExternalCdn } = getPublicPathUrl(stats.publicPath); 75 | // Extract from id and children 76 | // const chunks = (stats.chunks ?? []).map((asset: any) => ({ 77 | // id: asset?.id, 78 | // idHints: asset?.idHints, 79 | // names: asset?.names, 80 | // files: asset?.files, 81 | // children: asset?.children, 82 | // })); 83 | const main = processMainAssets( 84 | stats.assetsByChunkName?.main, 85 | isExternalCdn, 86 | publicPathUrl, 87 | ); 88 | 89 | return { 90 | assetsByChunkName: { 91 | main, 92 | }, 93 | chunks: [], 94 | }; 95 | }; 96 | 97 | const hasExtension = (url: string, ext: string) => { 98 | try { 99 | const parsed = parse(url); 100 | return parsed.pathname?.endsWith?.(ext); 101 | } catch { 102 | // Do nothing 103 | } 104 | return false; 105 | }; 106 | 107 | const prependForwardSlash = (file: string) => { 108 | if (file.startsWith('http')) { 109 | return file; 110 | } 111 | return file.startsWith('/') ? file : `/${file}`; 112 | }; 113 | 114 | export const getCssFileContent = async (cssFile: string) => { 115 | const cssFileResolve = join(__dirname, 'build', cssFile); 116 | let cssContent = ''; 117 | if (existsSync(cssFileResolve)) { 118 | cssContent = readFileSync(cssFileResolve, { encoding: 'utf-8' }); 119 | } else { 120 | throw new Error('CSS file not found!'); 121 | } 122 | return cssContent; 123 | }; 124 | 125 | export const extractMainScripts = (chunksMap: ChunksMap) => (chunksMap?.assetsByChunkName?.main ?? []) 126 | .filter( 127 | (file) => hasExtension(file, '.mjs') && file.indexOf('hot-update') === -1, 128 | ) 129 | .map(prependForwardSlash); 130 | 131 | export const extractMainStyles = (chunksMap: ChunksMap) => (chunksMap?.assetsByChunkName?.main ?? []) 132 | .filter((file) => hasExtension(file, '.css')) 133 | .map(prependForwardSlash); 134 | -------------------------------------------------------------------------------- /packages/core/src/utils/client.ts: -------------------------------------------------------------------------------- 1 | import { RoutesArgs } from '../index.js'; 2 | 3 | const scopedCache = new WeakMap(); 4 | 5 | const setScopedVar = (key: string, value: any) => { 6 | const scopedVals = scopedCache.get(window) ?? {}; 7 | scopedCache.set(window, { 8 | ...scopedVals, 9 | [key]: value, 10 | }); 11 | }; 12 | 13 | const hasScopedVar = (key: string) => !!(scopedCache.get(window)?.[key] ?? false); 14 | 15 | const getScopedVar = (key: string, defaultValue?: T) => { 16 | const scopedValue = scopedCache.get(window)?.[key]; 17 | return scopedValue ?? defaultValue ?? null; 18 | }; 19 | 20 | const getScoped = async ( 21 | key: string, 22 | cb: (() => any) | (() => Promise), 23 | ) => { 24 | if (hasScopedVar(key)) { 25 | return getScopedVar(key); 26 | } 27 | const computedScopeCB = await cb(); 28 | setScopedVar(key, computedScopeCB); 29 | return computedScopeCB; 30 | }; 31 | 32 | const { userAgent } = window.navigator; 33 | export const requestArgs: RoutesArgs = { 34 | getLocation: () => new URL(window.location.href), 35 | browserDetect: async () => { 36 | const { parse } = await import('bowser'); 37 | try { 38 | return parse(userAgent); 39 | } catch { 40 | // Cannot parse useragent 41 | } 42 | return { 43 | browser: { name: '', version: '' }, 44 | os: { name: '', version: '', versionName: '' }, 45 | platform: { type: '' }, 46 | engine: { name: '', version: '' }, 47 | }; 48 | }, 49 | userAgent, 50 | isbot: async () => { 51 | const { default: isBot } = await import('isbot'); 52 | isBot.exclude(['chrome-lighthouse']); 53 | return isBot(userAgent); 54 | }, 55 | getScoped, 56 | addToHeadPreStyles: () => { 57 | // when called on client side do nothing 58 | }, 59 | addToFooter: () => { 60 | // when called on client side do nothing 61 | }, 62 | addHeaders: () => { 63 | // When called on client side do nothing 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /packages/core/src/utils/cookie.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express'; 2 | import { CookieChangeOptions } from 'universal-cookie'; 3 | 4 | export const cookieChangeHandler = (response: Response) => (change: CookieChangeOptions) => { 5 | if (response.headersSent) return; 6 | if (change.value === undefined) { 7 | response.clearCookie(change.name, change.options); 8 | } else { 9 | const cookieOpt = { ...change.options }; 10 | if (cookieOpt.maxAge && change.options && change.options.maxAge) { 11 | // the standard for maxAge is seconds but npm cookie uses milliseconds 12 | cookieOpt.maxAge = change.options.maxAge * 1000; 13 | } 14 | try { 15 | response.cookie(change.name, change.value, cookieOpt); 16 | } catch (ex) { 17 | // eslint-disable-next-line no-console 18 | console.log(ex); 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /packages/core/src/utils/delay.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param time miliseconds 3 | * @returns Promise 4 | */ 5 | export const delay = (time: number) => new Promise((resolve) => { 6 | setTimeout(resolve, time); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/core/src/utils/env.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Check for env variable in current running 3 | * environment available, or use the default value otherwise 4 | * @param key 5 | * @param defaultValue 6 | */ 7 | export const getEnv = (key: string, defaultValue: any = '') => { 8 | if (typeof process === 'undefined') { 9 | return defaultValue; 10 | } 11 | const envVal = process?.env?.[key]; 12 | if (!envVal) { 13 | return defaultValue; 14 | } 15 | return envVal; 16 | }; 17 | 18 | const APP_URL = getEnv('APP_URL', process.env.APP_URL ?? ''); 19 | 20 | export const getAppUrl = (url: string = '') => { 21 | let windowUrl; 22 | if (typeof window !== 'undefined') { 23 | windowUrl = window.location.href; 24 | } 25 | 26 | if (!(url || windowUrl)) { 27 | return APP_URL; 28 | } 29 | const u = `${url || windowUrl || ''}/`; 30 | if (u.indexOf('http') === -1) { 31 | return APP_URL; 32 | } 33 | let appUrl; 34 | if (u.indexOf(APP_URL) !== -1) { 35 | appUrl = APP_URL; 36 | } 37 | 38 | if (!appUrl) { 39 | return APP_URL; 40 | } 41 | return appUrl; 42 | }; 43 | -------------------------------------------------------------------------------- /packages/core/src/utils/event-emmiter.ts: -------------------------------------------------------------------------------- 1 | type Callback = (data?: any) => void; 2 | 3 | export class DataEventEmitter { 4 | callbacks: Record = {}; 5 | 6 | on(event: string, cb: Callback) { 7 | if (!this.callbacks[event]) this.callbacks[event] = []; 8 | this.callbacks[event].push(cb); 9 | } 10 | 11 | emit(event: string, data?: any) { 12 | const cbs = this.callbacks[event]; 13 | if (cbs) { 14 | cbs.forEach((cb) => cb(data)); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/utils/express.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | /** 4 | * Get full Url of the request, respect x-host, 5 | * x-forwarded-host, host header and finally the hostname by fastify 6 | * @todo: Not sure if we really need to add case sensitivity here for headers! 7 | * @param req FastifyRequest 8 | * @returns baseUrl without pathname 9 | */ 10 | export const getBaseUrl = (req: Request): URL => { 11 | const { protocol } = req; 12 | return new URL('/', `${protocol}://${req.get('host')}`); 13 | }; 14 | 15 | export const getUrl = (req: Request): URL => { 16 | const baseUrl = getBaseUrl(req); 17 | try { 18 | return new URL(req.url, baseUrl); 19 | } catch { 20 | // Some error with parsing of url 21 | } 22 | return baseUrl; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/core/src/utils/head.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | cloneElement, 3 | ReactElement, 4 | Fragment, 5 | Children, 6 | DetailedReactHTMLElement, 7 | } from 'react'; 8 | import { IWebManifest } from '../typedefs/webmanifest.js'; 9 | import { HeadElement } from '../typedefs/head.js'; 10 | 11 | // eslint-disable-next-line no-bitwise 12 | export const fastHashStr = (str: string) => str.split('').reduce((s, c) => (Math.imul(31, s) + c.charCodeAt(0)) | 0, 0); 13 | 14 | export function insertAfter(newNode: Node, existingNode: Node) { 15 | if (!existingNode?.parentNode) { 16 | return; 17 | } 18 | existingNode.parentNode.insertBefore(newNode, existingNode.nextSibling); 19 | } 20 | 21 | const METATYPES = [ 22 | 'name', 23 | 'rel', 24 | 'httpEquiv', 25 | 'charSet', 26 | 'itemProp', 27 | 'theme-color', 28 | ]; 29 | /** 30 | * The unique function is taken from next.js 31 | * @returns get set of unique 32 | */ 33 | export function unique() { 34 | const keys = new Set(); 35 | const tags = new Set(); 36 | const metaTypes = new Set(); 37 | const metaCategories: { [metatype: string]: Set } = {}; 38 | const hashList: number[] = []; 39 | 40 | return (h: React.ReactElement) => { 41 | let isUnique = true; 42 | let hasKey = false; 43 | 44 | if (h.key && typeof h.key !== 'number' && h.key.indexOf('$') > 0) { 45 | hasKey = true; 46 | const key = h.key.slice(h.key.indexOf('$') + 1); 47 | if (keys.has(key)) { 48 | isUnique = false; 49 | } else { 50 | keys.add(key); 51 | } 52 | } 53 | 54 | switch (h.type) { 55 | case 'title': 56 | case 'base': 57 | if (tags.has(h.type)) { 58 | isUnique = false; 59 | } else { 60 | tags.add(h.type); 61 | } 62 | break; 63 | case 'link': 64 | if (h.props?.rel === 'manifest') { 65 | if (tags.has('manifest')) { 66 | isUnique = false; 67 | } else { 68 | tags.add('manifest'); 69 | } 70 | } 71 | break; 72 | case 'meta': 73 | for (let i = 0, len = METATYPES.length; i < len; i += 1) { 74 | const metatype = METATYPES[i]; 75 | if (h.props.hasOwnProperty(metatype)) { 76 | if (metatype === 'charSet') { 77 | if (metaTypes.has(metatype)) { 78 | isUnique = false; 79 | } else { 80 | metaTypes.add(metatype); 81 | } 82 | } else { 83 | const category = h.props[metatype]; 84 | const categories = metaCategories[metatype] || new Set(); 85 | if ( 86 | (metatype !== 'name' || !hasKey) 87 | && categories.has(category) 88 | ) { 89 | isUnique = false; 90 | } else { 91 | categories.add(category); 92 | metaCategories[metatype] = categories; 93 | } 94 | } 95 | } 96 | } 97 | if (isUnique) { 98 | const propsHash = fastHashStr(JSON.stringify(h.props)); 99 | isUnique = !hashList.includes(propsHash); 100 | if (isUnique) { 101 | hashList.push(propsHash); 102 | } 103 | } 104 | if ( 105 | isUnique 106 | && typeof h.key === 'string' 107 | && h.key.indexOf('$') === -1 108 | && h.props?.property === 'og:image' 109 | ) { 110 | // this is a classic case when we want to avoid the default share image in og:image 111 | if (keys.has(h.key)) { 112 | isUnique = false; 113 | } else { 114 | keys.add(h.key); 115 | } 116 | } 117 | break; 118 | default: 119 | break; 120 | } 121 | return isUnique; 122 | }; 123 | } 124 | 125 | /** 126 | * Get Elements presented by user in the 127 | * tag as children and convert it to ReactElement 128 | * @param list HeadElement 129 | * @returns ReactElement [] 130 | */ 131 | export function convertToReactElement(list: HeadElement): ReactElement[] { 132 | let headNodes: any[] = []; 133 | const allNodes = Array.isArray(list) ? list : [list]; 134 | for (let i = 0; i < allNodes.length; i += 1) { 135 | const node = allNodes[i]; 136 | if ( 137 | typeof node !== 'string' 138 | && typeof node !== 'number' 139 | && typeof node !== 'boolean' 140 | && node !== null 141 | && typeof node !== 'undefined' 142 | ) { 143 | if ( 144 | // @ts-ignore 145 | node.type === Fragment 146 | // @ts-ignore 147 | && node.props?.children 148 | ) { 149 | const childElements = convertToReactElement( 150 | // @ts-ignore 151 | Children.toArray(node.props.children), 152 | ); 153 | headNodes = headNodes.concat(childElements); 154 | } else if ( 155 | // @ts-ignore 156 | node.type === 'title' 157 | // @ts-ignore 158 | && Array.isArray(node.props?.children) 159 | ) { 160 | const titleNode = node as DetailedReactHTMLElement; 161 | headNodes.push( 162 | cloneElement(titleNode, { 163 | children: titleNode.props.children.join(''), 164 | }), 165 | ); 166 | } else { 167 | headNodes.push(node); 168 | } 169 | 170 | // @ts-ignore 171 | const rel = node.props?.rel; 172 | // @ts-ignore 173 | const href = node.props?.href; 174 | 175 | if ( 176 | // @ts-ignore 177 | node.type === 'link' 178 | && rel === 'stylesheet' 179 | ) { 180 | const hrefText = href ? `${href} detected in your tag. ` : ''; 181 | // eslint-disable-next-line no-console 182 | console.warn( 183 | `WARNING:: ${hrefText} Avoid stylesheets in element.\n 184 | Read more here: https://www.reactpwa.com/blog/no-link-in-head.html`, 185 | ); 186 | } 187 | } 188 | } 189 | return headNodes; 190 | } 191 | 192 | /** 193 | * Add key to ReactElement if missing, 194 | * helps us solve the problem when we render components of head 195 | * as an array 196 | */ 197 | export const addKeyToElement = () => { 198 | let keyIndex = 1; 199 | const existingKeys: (string | number)[] = []; 200 | return (element: ReactElement) => { 201 | if (!element.key || existingKeys.includes(element.key)) { 202 | let key = `h.${keyIndex}`; 203 | while (existingKeys.includes(key)) { 204 | keyIndex += 1; 205 | key = `h.${keyIndex}`; 206 | } 207 | const clonedElement = cloneElement(element, { 208 | key, 209 | }); 210 | keyIndex += 1; 211 | existingKeys.push(key); 212 | return clonedElement; 213 | } 214 | existingKeys.push(element.key); 215 | return element; 216 | }; 217 | }; 218 | 219 | /** 220 | * Below two functions are for sorting of the meta tags in the head 221 | */ 222 | const priorities: Record = { 223 | default: 100, 224 | charSet: 1, 225 | httpEquiv: 2, 226 | viewport: 3, 227 | title: 4, 228 | description: 5, 229 | keywords: 6, 230 | author: 7, 231 | robots: 8, 232 | }; 233 | 234 | const getPriority = (element: React.ReactElement) => { 235 | if ( 236 | element.type === 'meta' 237 | && (element.props?.charSet || element.props?.charset) 238 | ) { 239 | return priorities.charSet; 240 | } 241 | if ( 242 | element.type === 'meta' 243 | && (element.props?.httpEquiv || element.props?.['http-equiv']) 244 | ) { 245 | return priorities.httpEquiv; 246 | } 247 | 248 | if (element.type === 'title') { 249 | return priorities.title; 250 | } 251 | if (element.type === 'meta' && typeof element.props?.name === 'string') { 252 | return priorities[element.props.name.toLowerCase()] ?? priorities.default; 253 | } 254 | return priorities.default; 255 | }; 256 | 257 | export const sortHeadElements = ( 258 | a: React.ReactElement, 259 | b: React.ReactElement, 260 | ) => getPriority(a) - getPriority(b); 261 | 262 | export const defaultHead = convertToReactElement( 263 | <> 264 | 265 | 266 | , 267 | ); 268 | 269 | export const getAppleIcon = (webmanifest: IWebManifest) => (webmanifest?.icons ?? []).find( 270 | (i: { sizes?: string; src: string }) => (i?.sizes?.indexOf('192') !== -1 || i?.sizes?.indexOf('180') !== -1) 271 | && i?.src?.indexOf?.('.svg') === -1, 272 | ); 273 | 274 | export const sanitizeElements = (elements: ReactElement[]) => elements 275 | .reverse() 276 | .filter(unique()) 277 | .reverse() 278 | .map(addKeyToElement()) 279 | .sort(sortHeadElements); 280 | 281 | const historyWeakMap = new WeakMap(); 282 | export const proxyHistoryPushReplace = (callback: Function) => { 283 | if (historyWeakMap.has(window)) { 284 | return; 285 | } 286 | (function proxyHistory(history) { 287 | const { pushState, replaceState } = history; 288 | historyWeakMap.set(window, { pushState, replaceState }); 289 | 290 | const pushOverride: typeof pushState = (...args) => { 291 | callback(); 292 | pushState.apply(history, args); 293 | }; 294 | const replaceOverride: typeof replaceState = (...args) => { 295 | callback(); 296 | pushState.apply(history, args); 297 | }; 298 | // Patch the history for custom monitoring of events 299 | // eslint-disable-next-line no-param-reassign 300 | history.pushState = pushOverride; 301 | // eslint-disable-next-line no-param-reassign 302 | history.replaceState = replaceOverride; 303 | }(window.history)); 304 | }; 305 | 306 | export const restoreHistoryPushReplace = () => { 307 | (function proxyHistory(history) { 308 | const { pushState, replaceState } = historyWeakMap.get(window); 309 | // Patch the history for custom monitoring of events 310 | // eslint-disable-next-line no-param-reassign 311 | history.pushState = pushState; 312 | // eslint-disable-next-line no-param-reassign 313 | history.replaceState = replaceState; 314 | }(window.history)); 315 | }; 316 | -------------------------------------------------------------------------------- /packages/core/src/utils/not-boolean.ts: -------------------------------------------------------------------------------- 1 | export function notBoolean(i: T): i is Exclude { 2 | return typeof i !== 'boolean'; 3 | } 4 | -------------------------------------------------------------------------------- /packages/core/src/utils/promise.ts: -------------------------------------------------------------------------------- 1 | export type PromiseCallback = () => Promise; 2 | 3 | export const wrapPromise = ( 4 | promise: T, 5 | options?: { 6 | onFinalize?: () => void; 7 | syncData?: Awaited>; 8 | }, 9 | ) => { 10 | let status = 'pending'; 11 | let result: Awaited>; 12 | let suspend = Promise.resolve(); 13 | if (options?.syncData) { 14 | status = 'success'; 15 | result = options.syncData; 16 | suspend = Promise.resolve(options.syncData); 17 | } else { 18 | suspend = promise() 19 | .then( 20 | (res) => { 21 | status = 'success'; 22 | result = res; 23 | }, 24 | (err) => { 25 | status = 'error'; 26 | result = err; 27 | }, 28 | ) 29 | .finally(() => { 30 | options?.onFinalize?.(); 31 | }); 32 | } 33 | return { 34 | read(): Awaited> { 35 | if (status === 'pending') { 36 | // eslint-disable-next-line @typescript-eslint/no-throw-literal 37 | throw suspend; 38 | } else if (status === 'error') { 39 | // eslint-disable-next-line @typescript-eslint/no-throw-literal 40 | throw result; 41 | } 42 | return result; 43 | }, 44 | }; 45 | }; 46 | 47 | export type WrappedPromise = ReturnType; 48 | -------------------------------------------------------------------------------- /packages/core/src/utils/redirect.ts: -------------------------------------------------------------------------------- 1 | export type RedirectStatusCode = 300 | 301 | 302 | 307 | 308; 2 | 3 | export const statusCodeWithLocations = [300, 301, 302, 307, 308]; 4 | -------------------------------------------------------------------------------- /packages/core/src/utils/request-internals.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | const requestInternals = new WeakMap(); 4 | 5 | export const setInternalVar = (request: Request, key: string, value: any) => { 6 | const requestVals = requestInternals.get(request) ?? {}; 7 | requestInternals.set(request, { 8 | ...requestVals, 9 | [key]: value, 10 | }); 11 | }; 12 | 13 | export const getInternalVar = ( 14 | request: Request, 15 | key: string, 16 | defaultValue?: T, 17 | ) => requestInternals.get(request)?.[key] ?? defaultValue ?? null; 18 | -------------------------------------------------------------------------------- /packages/core/src/utils/require-from-string.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import Module from 'module'; 3 | 4 | export const requireFromString = (code: string, fname: any, options?: any) => { 5 | let opts = options; 6 | let filename = fname; 7 | if (typeof filename === 'object') { 8 | opts = filename; 9 | filename = undefined; 10 | } 11 | 12 | opts = opts || {}; 13 | filename = filename || ''; 14 | 15 | opts.appendPaths = opts.appendPaths || []; 16 | opts.prependPaths = opts.prependPaths || []; 17 | 18 | if (typeof code !== 'string') { 19 | throw new Error(`code must be a string, not ${typeof code}`); 20 | } 21 | // @ts-ignore 22 | const paths = Module._nodeModulePaths(path.dirname(filename)); // eslint-disable-line 23 | 24 | // @ts-ignore 25 | const { parent } = Module; 26 | const m = new Module(filename, parent); 27 | m.filename = filename; 28 | m.paths = [].concat(opts.prependPaths).concat(paths).concat(opts.appendPaths); 29 | // @ts-ignore 30 | // eslint-disable-next-line no-underscore-dangle 31 | m._compile(code, filename); 32 | 33 | const { exports } = m; 34 | if (parent && parent.children) { 35 | parent.children.splice(parent.children.indexOf(m), 1); 36 | } 37 | return exports; 38 | }; 39 | -------------------------------------------------------------------------------- /packages/core/src/utils/resolver.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'node:fs'; 2 | 3 | const javascriptExtensions = [ 4 | '.mjs', 5 | '.mjsx', 6 | '.js', 7 | '.jsx', 8 | '.ts', 9 | '.tsx', 10 | '.json', 11 | ]; 12 | 13 | export const projectExistsSync = ( 14 | filePath: string, 15 | extension = javascriptExtensions, 16 | ) => { 17 | if (existsSync(filePath)) return filePath; 18 | let resolvedFilePath = ''; 19 | extension.forEach((jsExt) => { 20 | if (resolvedFilePath) { 21 | return; 22 | } 23 | if (existsSync(filePath + jsExt)) { 24 | resolvedFilePath = filePath + jsExt; 25 | } 26 | }); 27 | return resolvedFilePath; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/core/src/utils/server.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import bowser from 'bowser'; 3 | import { Request } from 'express'; 4 | import isbot from 'isbot'; 5 | import { RoutesArgs } from '../index.js'; 6 | import { LazyRouteMatch } from './asset-extract.js'; 7 | import { getBaseUrl, getUrl } from './express.js'; 8 | import { getInternalVar, setInternalVar } from './request-internals.js'; 9 | 10 | /** 11 | * get Http Status code for a request 12 | * @param request Fastify Request 13 | * @param matchedRoutes array of routes 14 | * @returns code number 15 | */ 16 | export const getHttpStatusCode = ( 17 | request: Request, 18 | matchedRoutes?: LazyRouteMatch[], 19 | ) => { 20 | let code = 200; 21 | if (!matchedRoutes) { 22 | code = 404; 23 | } 24 | const hasError = getInternalVar(request, 'hasExecutionError', false); 25 | if (hasError) { 26 | code = 500; 27 | } 28 | return getInternalVar(request, 'httpStatusCode', code); 29 | }; 30 | 31 | /** 32 | * Get redirect url 33 | * @param request FastifyRequest 34 | * @returns string 35 | */ 36 | export const getRedirectUrl = (request: Request) => { 37 | const location = getInternalVar(request, 'httpLocationHeader', '/'); 38 | const baseUrl = getBaseUrl(request); 39 | const baseUrlStr = baseUrl.toString(); 40 | const url = new URL(location, baseUrl); 41 | let urlString = url.toString(); 42 | if (urlString.indexOf(baseUrlStr) !== -1) { 43 | urlString = urlString.replace(baseUrlStr, ''); 44 | } 45 | if (!urlString) { 46 | urlString = '/'; 47 | } 48 | return urlString; 49 | }; 50 | 51 | /** 52 | * Get instance of isBot 53 | * @returns isbot 54 | */ 55 | export const getIsBot = () => { 56 | isbot.exclude(['chrome-lighthouse']); 57 | return isbot; 58 | }; 59 | 60 | const scopedCache = new WeakMap(); 61 | 62 | const setScopedVar = (request: Request, key: string, value: any) => { 63 | const scopedVals = scopedCache.get(request) ?? {}; 64 | scopedCache.set(request, { 65 | ...scopedVals, 66 | [key]: value, 67 | }); 68 | }; 69 | 70 | const hasScopedVar = (request: Request, key: string) => !!(scopedCache.get(request)?.[key] ?? false); 71 | 72 | const getScopedVar = ( 73 | request: Request, 74 | key: string, 75 | defaultValue?: T, 76 | ) => scopedCache.get(request)?.[key] ?? defaultValue ?? null; 77 | 78 | /** 79 | * Get request args for 80 | * @param request FastifyRequest 81 | */ 82 | export const getRequestArgs = (request: Request): RoutesArgs => { 83 | const userAgent = request.headers['user-agent'] ?? ''; 84 | const isBot = isbot(userAgent); 85 | const getScoped = async ( 86 | key: string, 87 | cb: (() => any) | (() => Promise), 88 | ) => { 89 | if (hasScopedVar(request, key)) { 90 | return getScopedVar(request, key); 91 | } 92 | const computedScopeCB = await cb(); 93 | setScopedVar(request, key, computedScopeCB); 94 | return computedScopeCB; 95 | }; 96 | 97 | return { 98 | getLocation: () => getUrl(request), 99 | browserDetect: async () => { 100 | try { 101 | return bowser.parse(userAgent); 102 | } catch { 103 | // Cannot parse useragent 104 | } 105 | return { 106 | browser: { name: '', version: '' }, 107 | os: { name: '', version: '', versionName: '' }, 108 | platform: { type: '' }, 109 | engine: { name: '', version: '' }, 110 | }; 111 | }, 112 | userAgent, 113 | isbot: async () => isBot, 114 | getScoped, 115 | addToHeadPreStyles: (components: ReactElement | ReactElement[]) => { 116 | const previousComponents = getInternalVar(request, 'headPreStyles', []); 117 | setInternalVar(request, 'headPreStyles', [ 118 | ...previousComponents, 119 | components, 120 | ]); 121 | }, 122 | addToFooter: (components: ReactElement | ReactElement[]) => { 123 | const previousComponents = getInternalVar(request, 'footerScripts', []); 124 | setInternalVar(request, 'footerScripts', [ 125 | ...previousComponents, 126 | components, 127 | ]); 128 | }, 129 | addHeaders: (headers: Headers) => { 130 | const requestHeaders = getInternalVar(request, 'headers', new Headers()); 131 | Array.from(headers.entries()).forEach(([key, value]) => { 132 | requestHeaders.set(key, value); 133 | }); 134 | setInternalVar(request, 'headers', requestHeaders); 135 | }, 136 | }; 137 | }; 138 | -------------------------------------------------------------------------------- /packages/core/src/webpack.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import fs from 'node:fs'; 3 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 4 | import webpack, { RuleSetRule } from 'webpack'; 5 | import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; 6 | import CopyPlugin from 'copy-webpack-plugin'; 7 | import { notBoolean } from './utils/not-boolean.js'; 8 | import { getServiceWorker } from './webpack/service-worker.js'; 9 | import { getResolve, getResolveLoader } from './webpack/resolver.js'; 10 | import { getCssRule } from './webpack/rules/css-rule.js'; 11 | import { 12 | getServerOptimization, 13 | getWebOptimization, 14 | } from './webpack/optimization.js'; 15 | import { getExperiments } from './webpack/experiments.js'; 16 | import { getJsRule } from './webpack/rules/js-rule.js'; 17 | import { getRawResourceRule } from './webpack/rules/raw-resource-rule.js'; 18 | import { getImagesRule } from './webpack/rules/images-rule.js'; 19 | import { getAssetsRule } from './webpack/rules/assets-rule.js'; 20 | import { libSrc } from './root.js'; 21 | import { WebpackHandlerConstructorOptions } from './typedefs/webpack.js'; 22 | import { getNodeExternals } from './webpack/externals.js'; 23 | import { getServerOutput, getWebOutput } from './webpack/output.js'; 24 | 25 | export const extensionRegex = (assetsList: string[]) => new RegExp(`\\.(${assetsList.join('|')})$`); 26 | 27 | const defaultConfig = { 28 | hotReload: true, 29 | react: { 30 | strictMode: true, 31 | }, 32 | sassCompiler: 'sass-embedded', 33 | cacheType: 'memory', 34 | serviceWorker: true, 35 | esmodules: [] as string[], 36 | cdnPath: '', 37 | }; 38 | 39 | type Optional = Pick, K> & Omit; 40 | export class WebpackHandler { 41 | protected configOptions: Record = defaultConfig; 42 | 43 | protected options: WebpackHandlerConstructorOptions; 44 | 45 | constructor( 46 | options: Optional< 47 | WebpackHandlerConstructorOptions, 48 | | 'buildWithHttpServer' 49 | | 'envVars' 50 | | 'config' 51 | | 'copyPublicFolder' 52 | | 'serverSideRender' 53 | >, 54 | ) { 55 | this.options = { 56 | buildWithHttpServer: false, 57 | envVars: {}, 58 | config: {}, 59 | copyPublicFolder: false, 60 | serverSideRender: true, 61 | ...options, 62 | }; 63 | 64 | const { react, serviceWorker, ...otherOptions } = options?.config ?? {}; 65 | this.configOptions = { 66 | react: { 67 | StrictMode: true, 68 | HeadResolveTimeout: 10000, 69 | ...(react ?? {}), 70 | }, 71 | serviceWorker: serviceWorker ?? !this.isDevelopment, 72 | ...otherOptions, 73 | }; 74 | } 75 | 76 | get isDevelopment() { 77 | return this.options.mode === 'development'; 78 | } 79 | 80 | get isTargetWeb() { 81 | return this.options.target === 'web'; 82 | } 83 | 84 | get isTargetServer() { 85 | return this.options.target === 'node'; 86 | } 87 | 88 | get shouldHotReload() { 89 | return ( 90 | this.isDevelopment && this.isTargetWeb && this.configOptions.hotReload 91 | ); 92 | } 93 | 94 | get shouldOutputCss() { 95 | return !this.isDevelopment && this.isTargetWeb; 96 | } 97 | 98 | getEntry(): webpack.Configuration['entry'] { 99 | if (this.isTargetWeb) { 100 | return [ 101 | this.shouldHotReload && 'webpack-hot-middleware/client?reload=true', 102 | this.shouldHotReload && 'react-refresh/runtime', 103 | path.resolve(libSrc, 'client.js'), 104 | ].filter(notBoolean); 105 | } 106 | if (this.isTargetServer) { 107 | if (this.options.buildWithHttpServer) { 108 | return [path.resolve(libSrc, 'express-server.js')].filter(notBoolean); 109 | } 110 | return [path.resolve(libSrc, 'server.js')].filter(notBoolean); 111 | } 112 | return []; 113 | } 114 | 115 | getOptimization(): webpack.Configuration['optimization'] { 116 | if (this.isTargetWeb) { 117 | return getWebOptimization({ minimize: !this.isDevelopment }); 118 | } 119 | if (this.isTargetServer) { 120 | return getServerOptimization({ minimize: !this.isDevelopment }); 121 | } 122 | return undefined; 123 | } 124 | 125 | getOutput(): webpack.Configuration['output'] { 126 | const outputOptions = { 127 | projectRoot: this.options.projectRoot, 128 | isDevelopment: this.isDevelopment, 129 | publicPath: this.configOptions.cdnPath, 130 | } 131 | if (this.isTargetWeb) { 132 | return getWebOutput(outputOptions); 133 | } 134 | if (this.isTargetServer) { 135 | return getServerOutput(outputOptions); 136 | } 137 | return undefined; 138 | } 139 | 140 | // eslint-disable-next-line class-methods-use-this 141 | getDevtool(): webpack.Configuration['devtool'] { 142 | return this.isDevelopment ? 'inline-source-map' : false; 143 | } 144 | 145 | getContext(): webpack.Configuration['context'] { 146 | return this.options.projectRoot; 147 | } 148 | 149 | getFilteredEnvVars() { 150 | if (this.isTargetServer) { 151 | return this.options.envVars; 152 | } 153 | const envVars: Record = {}; 154 | const envKeys = Object.keys(this.options.envVars); 155 | for (let i = 0; i < envKeys.length; i += 1) { 156 | if (!envKeys[i].startsWith('_PRIVATE_')) { 157 | envVars[envKeys[i]] = this.options.envVars[envKeys[i]]; 158 | } 159 | } 160 | return envVars; 161 | } 162 | 163 | canCopyPublicFolder(): Boolean { 164 | if (!this.options.copyPublicFolder) { 165 | return false; 166 | } 167 | if (!this.getOutput()?.path) { 168 | return false; 169 | } 170 | const pathToPublicFolder = path.resolve( 171 | this.options.projectRoot, 172 | 'src', 173 | 'public', 174 | ); 175 | try { 176 | return fs.statSync(pathToPublicFolder).isDirectory(); 177 | } catch { 178 | // do nothing 179 | } 180 | return false; 181 | } 182 | 183 | getServiceWorkerPlugin() { 184 | /** 185 | * Only emit service worker for target web and if the 186 | * user has not specified serviceWorker as false in the 187 | * config options 188 | */ 189 | if (!this.isTargetWeb || this.configOptions.serviceWorker === false) { 190 | return false; 191 | } 192 | return getServiceWorker( 193 | this.options.projectRoot, 194 | this.configOptions.serviceWorker, 195 | ); 196 | } 197 | 198 | getPlugins(): webpack.Configuration['plugins'] { 199 | return [ 200 | new webpack.DefinePlugin({ 201 | ...(this.isTargetWeb ? { 'process.env': {} } : {}), 202 | EnableServerSideRender: this.options.serverSideRender, 203 | EnableReactStrictMode: 204 | this.configOptions.react.StrictMode && this.isDevelopment, 205 | HeadResolveTimeout: this.configOptions.react.HeadResolveTimeout, 206 | EnableServiceWorker: this.configOptions.serviceWorker !== false, 207 | }), 208 | new webpack.EnvironmentPlugin({ 209 | ...this.getFilteredEnvVars(), 210 | }), 211 | this.shouldHotReload && new webpack.HotModuleReplacementPlugin(), 212 | this.shouldHotReload 213 | && new ReactRefreshWebpackPlugin({ 214 | esModule: true, 215 | overlay: { sockProtocol: 'ws' }, 216 | }), 217 | this.shouldOutputCss 218 | && new MiniCssExtractPlugin({ 219 | filename: '_rpwa/css/[name]-[contenthash].css', 220 | ignoreOrder: true, 221 | }), 222 | this.isTargetServer 223 | && new webpack.optimize.LimitChunkCountPlugin({ 224 | maxChunks: 1, 225 | }), 226 | this.isTargetServer 227 | && this.canCopyPublicFolder() 228 | && new CopyPlugin({ 229 | patterns: [ 230 | { 231 | from: path.resolve(this.options.projectRoot, 'src', 'public'), 232 | to: path.join(this.getOutput()?.path ?? '', 'build'), 233 | }, 234 | ], 235 | }), 236 | this.getServiceWorkerPlugin(), 237 | ].filter(notBoolean); 238 | } 239 | 240 | getRules(): RuleSetRule[] { 241 | return [ 242 | getAssetsRule({ 243 | emit: this.isTargetWeb, 244 | }), 245 | getImagesRule({ 246 | emit: this.isTargetWeb, 247 | }), 248 | getRawResourceRule({ emit: this.isTargetWeb }), 249 | getJsRule({ 250 | isTargetServer: this.isTargetServer, 251 | hotReload: this.shouldHotReload, 252 | projectRoot: this.options.projectRoot, 253 | useCache: this.isDevelopment, 254 | }), 255 | getCssRule({ 256 | useCache: this.isDevelopment, 257 | outputCss: this.shouldOutputCss, 258 | emit: this.isTargetWeb, 259 | sourceMap: this.isDevelopment, 260 | detailedIdentName: this.isDevelopment, 261 | sassCompiler: this.configOptions.sassCompiler, 262 | context: path.resolve(this.options.projectRoot, 'src'), 263 | }), 264 | ]; 265 | } 266 | 267 | getExternals(): webpack.Configuration['externals'] { 268 | if (this.isTargetServer) { 269 | return [ 270 | getNodeExternals({ 271 | projectRoot: this.options.projectRoot, 272 | allowList: this.configOptions.esmodules ?? [], 273 | }), 274 | ]; 275 | } 276 | return undefined; 277 | } 278 | 279 | getConfig(): webpack.Configuration { 280 | let cache: webpack.Configuration['cache']; 281 | if (this.isDevelopment) { 282 | if (this.configOptions.cacheType === 'memory') { 283 | cache = true; 284 | } else if (this.configOptions.cacheType === 'filesystem') { 285 | cache = { 286 | type: 'filesystem', 287 | allowCollectingMemory: true, 288 | memoryCacheUnaffected: true, 289 | name: 'RPWA_Cache', 290 | }; 291 | } 292 | } 293 | return { 294 | cache, 295 | name: this.isTargetWeb ? 'web' : 'node', 296 | mode: this.options.mode, 297 | entry: this.getEntry(), 298 | optimization: this.getOptimization(), 299 | experiments: getExperiments({ 300 | outputModule: this.isTargetWeb, 301 | cacheUnaffected: this.isDevelopment, 302 | }), 303 | output: this.getOutput(), 304 | externalsPresets: this.isTargetServer ? { node: true } : undefined, 305 | externals: this.getExternals(), 306 | module: { 307 | rules: this.getRules(), 308 | }, 309 | resolve: getResolve({ 310 | projectRoot: this.options.projectRoot, 311 | alias: this.configOptions.alias, 312 | }), 313 | resolveLoader: getResolveLoader(), 314 | devtool: this.getDevtool(), 315 | context: this.getContext(), 316 | target: this.options.target, 317 | plugins: this.getPlugins(), 318 | }; 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /packages/core/src/webpack/experiments.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from 'webpack'; 2 | 3 | export const getExperiments = (options: { 4 | outputModule: boolean; 5 | cacheUnaffected: boolean; 6 | }): Configuration['experiments'] => ({ 7 | cacheUnaffected: options.cacheUnaffected, 8 | // We need output as module to be included client side 9 | outputModule: options.outputModule, 10 | // outputModule: true, 11 | // Enable top level await for simplicity 12 | topLevelAwait: true, 13 | backCompat: false, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/core/src/webpack/externals.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import webpackNodeExternals from 'webpack-node-externals'; 3 | import { libSrc } from '../root.js'; 4 | 5 | export const getNodeExternals = (options: { 6 | projectRoot: string; 7 | allowList: string[]; 8 | }): ReturnType => webpackNodeExternals({ 9 | allowlist: [ 10 | /@reactpwa/, 11 | /\.(css|s[ac]ss)$/i, 12 | ...options.allowList.map((al) => new RegExp(al)), 13 | ], 14 | additionalModuleDirs: [ 15 | resolve(options.projectRoot, 'node_modules'), 16 | resolve(libSrc, 'node_modules'), 17 | resolve(libSrc, '..', 'node_modules'), 18 | resolve(libSrc, '..', '..', 'node_modules'), 19 | resolve(libSrc, '..', '..', '..', 'node_modules'), 20 | ], 21 | }); 22 | -------------------------------------------------------------------------------- /packages/core/src/webpack/generator-options.ts: -------------------------------------------------------------------------------- 1 | export const getGeneratorOptions = (options: { 2 | prefix: string; 3 | emit: boolean; 4 | }) => ({ 5 | // We will use content hash for long term caching of asset 6 | filename: `${options.prefix}/[contenthash]-[name][ext][query]`, 7 | emit: options.emit, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/core/src/webpack/image-assets-extensions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extensions of all assets that can be served via url 3 | */ 4 | export const imageAssetsExtensions = [ 5 | 'bmp', 6 | 'gif', 7 | 'ico', 8 | 'jpeg', 9 | 'jpg', 10 | 'png', 11 | 'svg', 12 | 'tif', 13 | 'tiff', 14 | 'webp', 15 | 'avif', 16 | ]; 17 | -------------------------------------------------------------------------------- /packages/core/src/webpack/loader-options/babel-loader-options.ts: -------------------------------------------------------------------------------- 1 | import { notBoolean } from '../../utils/not-boolean.js'; 2 | 3 | const getPresetEnvOptions = (options: { isTargetServer: boolean }) => { 4 | if (options.isTargetServer) { 5 | return [ 6 | { 7 | targets: { 8 | node: 'current', 9 | esmodules: true, 10 | }, 11 | }, 12 | ]; 13 | } 14 | 15 | return [ 16 | { 17 | useBuiltIns: 'entry', 18 | corejs: 'core-js@3', 19 | targets: { esmodules: true }, 20 | }, 21 | ]; 22 | }; 23 | 24 | export const getBabelLoaderOptions = (options: { 25 | isTargetServer: boolean; 26 | hotReload: boolean; 27 | useCache: boolean; 28 | }) => ({ 29 | cacheDirectory: options.useCache, 30 | // Disabling cache-compression, as it will occupy more space on disk, however 31 | // it is faster to read rather than decompress it. 32 | cacheCompression: false, 33 | presets: [ 34 | [ 35 | '@babel/preset-env', 36 | ...getPresetEnvOptions({ isTargetServer: options.isTargetServer }), 37 | ], 38 | [ 39 | '@babel/preset-react', 40 | { 41 | runtime: 'automatic', 42 | }, 43 | ], 44 | [ 45 | '@babel/preset-typescript', 46 | { 47 | allowDeclareFields: true, 48 | }, 49 | ], 50 | ], 51 | plugins: [options.hotReload && 'react-refresh/babel'].filter(notBoolean), 52 | }); 53 | -------------------------------------------------------------------------------- /packages/core/src/webpack/loader-options/css-loader-options.ts: -------------------------------------------------------------------------------- 1 | const getLocalIdentName = (options: { detailed: boolean }) => (options.detailed 2 | ? '[name]__[local]--[hash:base64:5]' 3 | : '[contenthash:base64:5]'); 4 | 5 | export const getCssLoaderOptions = (options: { 6 | sourceMap: boolean; 7 | detailedIdentName: boolean; 8 | context: string; 9 | }) => ({ 10 | sourceMap: options.sourceMap, 11 | modules: { 12 | localIdentName: getLocalIdentName({ detailed: options.detailedIdentName }), 13 | localIdentContext: options.context, 14 | mode: (resourcePath: string) => { 15 | if (/pure\.(css|s[ac]ss)$/i.test(resourcePath)) { 16 | return 'pure'; 17 | } 18 | if ( 19 | /global\.(css|s[ac]ss)$/i.test(resourcePath) 20 | || /(node_modules|src\/resources|src\\resources)/i.test(resourcePath) 21 | ) { 22 | return 'global'; 23 | } 24 | return 'local'; 25 | }, 26 | }, 27 | importLoaders: 2, 28 | }); 29 | -------------------------------------------------------------------------------- /packages/core/src/webpack/loader-options/post-css-loader-options.ts: -------------------------------------------------------------------------------- 1 | import autoprefixer from 'autoprefixer'; 2 | 3 | export const getPostcssLoaderOptions = () => ({ 4 | postcssOptions: { 5 | ident: 'postcss', 6 | plugins: [[autoprefixer]], 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/core/src/webpack/loader-options/sass-loader-options.ts: -------------------------------------------------------------------------------- 1 | import SassEmbedded from 'sass-embedded'; 2 | import * as Sass from 'sass'; 3 | import NodeSass from 'node-sass'; 4 | 5 | export const getSassLoaderOptions = (options: { 6 | compiler: 'sass' | 'sass-embedded' | 'node-sass'; 7 | }) => { 8 | let compiler: any = SassEmbedded; 9 | if (options.compiler === 'node-sass') { 10 | compiler = NodeSass; 11 | } else if (options.compiler === 'sass') { 12 | compiler = Sass; 13 | } 14 | return { 15 | // Prefer `dart-sass` or sass-embedded, 16 | // dart-sass with nodejs api is very very slow 17 | implementation: compiler, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/core/src/webpack/optimization.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from 'webpack'; 2 | 3 | export const getWebOptimization = (options: { 4 | minimize: boolean; 5 | }): Configuration['optimization'] => ({ 6 | minimize: options.minimize, 7 | splitChunks: { 8 | minSize: 2000, 9 | maxAsyncRequests: 20, 10 | maxInitialRequests: 20, 11 | cacheGroups: { 12 | vendor: { 13 | test: /[\\/]node_modules[\\/]/, 14 | reuseExistingChunk: true, 15 | name: (modulePath: any) => { 16 | if (modulePath && typeof modulePath.context === 'string') { 17 | const reactPackageMatch = modulePath.context.match( 18 | /[\\/]node_modules[\\/](react|react-dom|scheduler)([\\/]|$)/, 19 | ); 20 | if (reactPackageMatch) { 21 | return 'react'; 22 | } 23 | const packageName = modulePath.context.match( 24 | /[\\/]node_modules[\\/](.*?)([\\/]|$)/, 25 | ); 26 | // npm package names are URL-safe, but some servers don't like @ symbols 27 | return `${(packageName?.[1] ?? 'default').replace('@', '')}`; 28 | } 29 | return undefined; 30 | }, 31 | }, 32 | main: { 33 | name: 'main', 34 | type: 'css/mini-extract', 35 | chunks: 'all', 36 | minChunks: 1, 37 | reuseExistingChunk: true, 38 | enforce: true, 39 | }, 40 | }, 41 | }, 42 | }); 43 | 44 | export const getServerOptimization = (options: { 45 | minimize: boolean; 46 | }): Configuration['optimization'] => ({ 47 | splitChunks: false, 48 | minimize: options.minimize, 49 | }); 50 | -------------------------------------------------------------------------------- /packages/core/src/webpack/output.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import { Configuration } from 'webpack'; 3 | 4 | export const getWebOutput = (options: { 5 | projectRoot: string; 6 | isDevelopment: boolean; 7 | publicPath: string; 8 | }): Configuration['output'] => ({ 9 | module: true, 10 | asyncChunks: true, 11 | path: resolve(options.projectRoot, 'dist', 'build'), 12 | assetModuleFilename: '_rpwa/assets/[name]-[contenthash][ext]', 13 | filename: '_rpwa/js/[name]-[contenthash].mjs', 14 | cssFilename: '_rpwa/css/[name]-[contenthash].css', 15 | publicPath: options.publicPath ? options.publicPath : '/', 16 | }); 17 | 18 | export const getServerOutput = (options: { 19 | projectRoot: string; 20 | isDevelopment: boolean; 21 | publicPath: string; 22 | }): Configuration['output'] => ({ 23 | module: false, 24 | asyncChunks: false, 25 | chunkFormat: 'commonjs', 26 | path: resolve(options.projectRoot, 'dist'), 27 | filename: 'server.cjs', 28 | assetModuleFilename: '_rpwa/assets/[name]-[contenthash][ext]', 29 | publicPath: options.publicPath ? options.publicPath : '/', 30 | library: { 31 | type: 'commonjs-module', 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /packages/core/src/webpack/plugins/inject-sw.ts: -------------------------------------------------------------------------------- 1 | import { Compiler } from 'webpack'; 2 | import path from 'node:path'; 3 | import fs from 'node:fs'; 4 | import { 5 | extractChunksMap, 6 | extractMainScripts, 7 | extractMainStyles, 8 | } from '../../utils/asset-extract.js'; 9 | 10 | export class InjectSW { 11 | static defaultOptions = { 12 | srcFile: '', 13 | withScripts: false, 14 | }; 15 | 16 | options = InjectSW.defaultOptions; 17 | 18 | destFile = 'sw.js'; 19 | 20 | srcFileContent = "self.addEventListener('fetch', () => { return; });"; 21 | 22 | constructor(options: Partial = {}) { 23 | this.options = { ...InjectSW.defaultOptions, ...options }; 24 | try { 25 | const { srcFile } = this.options; 26 | if (srcFile && fs.existsSync(path.resolve(srcFile))) { 27 | this.srcFileContent = fs.readFileSync(srcFile, { encoding: 'utf-8' }); 28 | } 29 | } catch { 30 | this.srcFileContent = "self.addEventListener('fetch', () => { return; });"; 31 | } 32 | } 33 | 34 | apply(compiler: Compiler) { 35 | const pluginName = InjectSW.name; 36 | 37 | // webpack module instance can be accessed from the compiler object, 38 | // this ensures that correct version of the module is used 39 | // (do not require/import the webpack or any symbols from it directly). 40 | const { webpack } = compiler; 41 | 42 | // Compilation object gives us reference to some useful constants. 43 | const { Compilation } = webpack; 44 | 45 | // RawSource is one of the "sources" classes that should be used 46 | // to represent asset sources in compilation. 47 | const { RawSource } = webpack.sources; 48 | 49 | // Tapping to the "thisCompilation" hook in order to further tap 50 | // to the compilation process on an earlier stage. 51 | compiler.hooks.thisCompilation.tap(pluginName, (compilation) => { 52 | // Tapping to the assets processing pipeline on a specific stage. 53 | compilation.hooks.processAssets.tap( 54 | { 55 | name: pluginName, 56 | 57 | // Using one of the later asset processing stages to ensure 58 | // that all assets were already added to the compilation by other plugins. 59 | // stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE, 60 | stage: Compilation.PROCESS_ASSETS_STAGE_REPORT, 61 | }, 62 | () => { 63 | if (this.options.withScripts) { 64 | const chunksMap = extractChunksMap(compilation.getStats()); 65 | const filesToCache = chunksMap.chunks.map((c) => c.files).flat(2); 66 | const styles = extractMainStyles(chunksMap); 67 | const scripts = extractMainScripts(chunksMap); 68 | this.srcFileContent = `const __FILES=${JSON.stringify( 69 | filesToCache, 70 | )}; 71 | const __STYLES=${JSON.stringify(styles)}; 72 | const __SCRIPTS=${JSON.stringify(scripts)}; 73 | ${this.srcFileContent}`; 74 | } 75 | // Adding new asset to the compilation, so it would be automatically 76 | // generated by the webpack in the output directory. 77 | compilation.emitAsset( 78 | this.destFile, 79 | new RawSource(this.srcFileContent), 80 | ); 81 | }, 82 | ); 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/core/src/webpack/resolver.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from 'webpack'; 2 | import { resolve } from 'node:path'; 3 | import fs from 'node:fs'; 4 | import { projectExistsSync } from '../utils/resolver.js'; 5 | import { libSrc } from '../root.js'; 6 | 7 | export const getResolveExtensions = (): string[] => [ 8 | '.js', 9 | '.jsx', 10 | '.ts', 11 | '.tsx', 12 | '.mjs', 13 | '.cjs', 14 | '.json', 15 | ]; 16 | 17 | export const getResolveLoader = (): Configuration['resolveLoader'] => { 18 | const libraryNodeModules = resolve(libSrc, 'node_modules'); 19 | const libraryHasNodeModules = libraryNodeModules 20 | ? fs.existsSync(libraryNodeModules) 21 | : false; 22 | 23 | const workspaceNodeModules = resolve( 24 | libSrc, 25 | '..', 26 | '..', 27 | '..', 28 | 'node_modules', 29 | ); 30 | const workspaceHasNodeModules = workspaceNodeModules 31 | ? fs.existsSync(workspaceNodeModules) 32 | : false; 33 | return { 34 | modules: [ 35 | 'node_modules', 36 | ...(libraryHasNodeModules ? [libraryNodeModules] : []), 37 | ...(workspaceHasNodeModules ? [workspaceNodeModules] : []), 38 | ], 39 | }; 40 | }; 41 | 42 | export const getResolve = (options: { 43 | projectRoot: string; 44 | alias?: Record; 45 | }): Configuration['resolve'] => { 46 | const resolveConfig: Configuration['resolve'] = { 47 | alias: { 48 | '@currentProject/webmanifest': 49 | projectExistsSync(resolve(options.projectRoot, 'src', 'webmanifest')) 50 | || resolve(libSrc, 'defaults', 'webmanifest'), 51 | '@currentProject/server': 52 | projectExistsSync(resolve(options.projectRoot, 'src', 'server')) 53 | || resolve(libSrc, 'defaults', 'server'), 54 | '@currentProject': resolve(options.projectRoot, 'src'), 55 | }, 56 | extensions: getResolveExtensions(), 57 | }; 58 | 59 | const aliasKeys = Object.keys(options.alias ?? {}); 60 | if (options.alias && aliasKeys.length) { 61 | // 62 | for (let i = 0; i < aliasKeys.length; i += 1) { 63 | const key: string = aliasKeys[i]; 64 | const path = resolve(options.projectRoot, options.alias[key]); 65 | if (!resolveConfig.alias) { 66 | resolveConfig.alias = {}; 67 | } 68 | if (path) { 69 | // @ts-ignore 70 | resolveConfig.alias[key] = path; 71 | } 72 | } 73 | } 74 | return resolveConfig; 75 | }; 76 | -------------------------------------------------------------------------------- /packages/core/src/webpack/rules/assets-rule.ts: -------------------------------------------------------------------------------- 1 | import { staticAssetsExtensions } from '../static-assets-extensions.js'; 2 | import { extensionRegex } from '../utils.js'; 3 | 4 | export const getAssetsRule = (options: { emit: boolean }) => ({ 5 | test: extensionRegex(staticAssetsExtensions), 6 | type: 'asset', 7 | generator: { 8 | emit: options.emit, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/core/src/webpack/rules/css-rule.ts: -------------------------------------------------------------------------------- 1 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 2 | import { RuleSetRule } from 'webpack'; 3 | import { notBoolean } from '../../utils/not-boolean.js'; 4 | import { getCssLoaderOptions } from '../loader-options/css-loader-options.js'; 5 | import { getPostcssLoaderOptions } from '../loader-options/post-css-loader-options.js'; 6 | import { getSassLoaderOptions } from '../loader-options/sass-loader-options.js'; 7 | 8 | export const getCssRule = (options: { 9 | outputCss: boolean; 10 | emit: boolean; 11 | sourceMap: boolean; 12 | detailedIdentName: boolean; 13 | context: string; 14 | useCache: boolean; 15 | sassCompiler: 'sass' | 'node-sass' | 'sass-embedded'; 16 | }): RuleSetRule => ({ 17 | test: /\.(css|s[ac]ss)$/i, 18 | use: [ 19 | options.outputCss || { loader: 'style-loader' }, 20 | options.outputCss && { 21 | loader: MiniCssExtractPlugin.loader, 22 | options: { emit: options.emit }, 23 | }, 24 | { 25 | loader: 'css-loader', 26 | options: getCssLoaderOptions({ 27 | sourceMap: options.sourceMap, 28 | detailedIdentName: options.detailedIdentName, 29 | context: options.context, 30 | }), 31 | }, 32 | { 33 | loader: 'postcss-loader', 34 | options: getPostcssLoaderOptions(), 35 | }, 36 | // Compiles Sass to CSS 37 | { 38 | loader: 'sass-loader', 39 | options: getSassLoaderOptions({ 40 | compiler: options.sassCompiler, 41 | }), 42 | }, 43 | ].filter(notBoolean), 44 | }); 45 | -------------------------------------------------------------------------------- /packages/core/src/webpack/rules/images-rule.ts: -------------------------------------------------------------------------------- 1 | import { RuleSetRule } from 'webpack'; 2 | import { imageAssetsExtensions } from '../image-assets-extensions.js'; 3 | import { extensionRegex } from '../utils.js'; 4 | 5 | export const getImagesRule = (options: { emit: boolean }): RuleSetRule => ({ 6 | test: extensionRegex(imageAssetsExtensions), 7 | type: 'asset', 8 | parser: { 9 | dataUrlCondition: { 10 | maxSize: 0, // You can make it 4kb by 4 * 1024 11 | }, 12 | }, 13 | generator: { 14 | emit: options.emit, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /packages/core/src/webpack/rules/js-rule.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { RuleSetRule } from 'webpack'; 3 | import { notBoolean } from '../../utils/not-boolean.js'; 4 | import { getBabelLoaderOptions } from '../loader-options/babel-loader-options.js'; 5 | 6 | export const getJsRule = (options: { 7 | projectRoot: string; 8 | isTargetServer: boolean; 9 | hotReload: boolean; 10 | useCache: boolean; 11 | }): RuleSetRule => ({ 12 | test: /\.(j|t)sx?$/, 13 | exclude: [/node_modules/], 14 | include: [resolve(options.projectRoot, 'src')], 15 | use: [ 16 | { 17 | loader: 'babel-loader', 18 | options: getBabelLoaderOptions({ 19 | useCache: options.useCache, 20 | isTargetServer: options.isTargetServer, 21 | hotReload: options.hotReload, 22 | }), 23 | }, 24 | ].filter(notBoolean), 25 | }); 26 | -------------------------------------------------------------------------------- /packages/core/src/webpack/rules/mjs-rule.ts: -------------------------------------------------------------------------------- 1 | import { RuleSetRule } from 'webpack'; 2 | 3 | export const getMjsRule = (): RuleSetRule => ({ 4 | test: /\.mjs$/, 5 | include: /node_modules/, 6 | type: 'javascript/auto', 7 | }); 8 | -------------------------------------------------------------------------------- /packages/core/src/webpack/rules/raw-resource-rule.ts: -------------------------------------------------------------------------------- 1 | import { RuleSetRule } from 'webpack'; 2 | 3 | export const getRawResourceRule = (options: { 4 | emit: boolean; 5 | }): RuleSetRule => ({ 6 | resourceQuery: /raw/, 7 | type: 'asset/source', 8 | generator: { 9 | emit: options.emit, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/core/src/webpack/service-worker.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { readFileSync } from 'node:fs'; 3 | import WorkboxPlugin from 'workbox-webpack-plugin'; 4 | import { projectExistsSync } from '../utils/resolver.js'; 5 | import { InjectSW } from './plugins/inject-sw.js'; 6 | 7 | /** 8 | * @param projectSW Absolute path to service worker 9 | * @returns webpack.Plugin InjectSW | InjectManifest 10 | */ 11 | const injectSWManifest = (projectSW: string) => { 12 | const projectSWContent = readFileSync(projectSW, { encoding: 'utf-8' }); 13 | // If no manifest is needed, then simply inject the project sw.js 14 | if (projectSWContent.indexOf('self.__WB_MANIFEST') === -1) { 15 | return new InjectSW({ 16 | srcFile: projectSW, 17 | }); 18 | } 19 | // If __WB_MANIFEST exists then inject it via the InjectManifest Plugin 20 | return new WorkboxPlugin.InjectManifest({ 21 | swSrc: projectSW, 22 | swDest: 'sw.js', 23 | }); 24 | }; 25 | 26 | /** 27 | * 28 | * @param projectRoot string 29 | * @param serviceWorkerType boolean | 'minimal' | 'default' 30 | * @returns InjectSW | WorkboxPlugin.GenerateSW | InjectManifest 31 | */ 32 | export const getServiceWorker = ( 33 | projectRoot: string, 34 | serviceWorkerType: boolean | 'minimal' | 'default', 35 | ) => { 36 | // Check if custom sw already exists 37 | const projectSW = projectExistsSync(path.join(projectRoot, 'src', 'sw.js')); 38 | 39 | // if Project service worker exists, then 40 | // return and inject accordingly 41 | if (projectSW) { 42 | return injectSWManifest(projectSW); 43 | } 44 | 45 | // If the minimal option is selected, simply inject the minimum 46 | // service worker for PWA 47 | if (serviceWorkerType === 'minimal') { 48 | return new InjectSW(); 49 | } 50 | 51 | // If default option is selected then inject the offline supported 52 | // service worker 53 | return new WorkboxPlugin.GenerateSW({ 54 | clientsClaim: true, 55 | skipWaiting: true, 56 | swDest: 'sw.js', 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /packages/core/src/webpack/static-assets-extensions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extensions of all assets that can be served via url 3 | */ 4 | export const staticAssetsExtensions = [ 5 | '7z', 6 | 'arj', 7 | 'deb', 8 | 'pkg', 9 | 'rar', 10 | 'rpm', 11 | 'tar.gz', 12 | 'z', 13 | 'zip', 14 | 'bin', 15 | 'dmg', 16 | 'iso', 17 | 'toast', 18 | 'vcd', 19 | 'csv', 20 | 'dat', 21 | 'db', 22 | 'dbf', 23 | 'log', 24 | 'mdb', 25 | 'sav', 26 | 'sql', 27 | 'tar', 28 | 'xml', 29 | 'email', 30 | 'eml', 31 | 'emlx', 32 | 'msg', 33 | 'oft', 34 | 'ost', 35 | 'pst', 36 | 'vcf', 37 | 'apk', 38 | 'bat', 39 | 'cgi', 40 | 'pl', 41 | 'com', 42 | 'exe', 43 | 'gadget', 44 | 'jar', 45 | 'msi', 46 | 'py', 47 | 'wsf', 48 | 'fnt', 49 | 'fon', 50 | 'otf', 51 | 'ttf', 52 | 'eot', 53 | 'woff', 54 | 'woff2', 55 | 'ai', 56 | 'ps', 57 | 'psd', 58 | 'asp', 59 | 'aspx', 60 | 'cer', 61 | 'cfm', 62 | 'htm', 63 | 'html', 64 | 'jsp', 65 | 'part', 66 | 'php', 67 | 'rss', 68 | 'xhtml', 69 | 'key', 70 | 'odp', 71 | 'pps', 72 | 'ppt', 73 | 'pptx', 74 | 'c', 75 | 'class', 76 | 'cpp', 77 | 'cs', 78 | 'h', 79 | 'java', 80 | 'sh', 81 | 'swift', 82 | 'vb', 83 | 'ods', 84 | 'xls', 85 | 'xlsm', 86 | 'xlsx', 87 | 'bak', 88 | 'cab', 89 | 'cfg', 90 | 'cpl', 91 | 'cur', 92 | 'dll', 93 | 'dmp', 94 | 'drv', 95 | 'icns', 96 | 'ini', 97 | 'lnk', 98 | 'sys', 99 | 'tmp', 100 | '3g2', 101 | '3gp', 102 | 'avi', 103 | 'flv', 104 | 'h264', 105 | 'm4v', 106 | 'mkv', 107 | 'mov', 108 | 'mp4', 109 | 'mpg', 110 | 'mpeg', 111 | 'rm', 112 | 'swf', 113 | 'vob', 114 | 'wmv', 115 | 'doc', 116 | 'docx', 117 | 'odt', 118 | 'pdf', 119 | 'rtf', 120 | 'tex', 121 | 'txt', 122 | 'wpd', 123 | ]; 124 | -------------------------------------------------------------------------------- /packages/core/src/webpack/utils.ts: -------------------------------------------------------------------------------- 1 | export const extensionRegex = (assetsList: string[]) => new RegExp(`\\.(${assetsList.join('|')})$`); 2 | -------------------------------------------------------------------------------- /packages/core/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["**/**/*.*"], 4 | "exclude": ["node_modules", "lib"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-reactpwa/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es2022", 5 | "moduleResolution": "Node", 6 | "baseUrl": "./", 7 | "rootDir": "./src/", 8 | "outDir": "./lib/esm/" 9 | }, 10 | "include": ["./src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['./src'], 5 | outDir: './lib', 6 | splitting: true, 7 | sourcemap: true, 8 | clean: true, 9 | dts: true, 10 | platform: 'node', 11 | skipNodeModulesBundle: true, 12 | bundle: false, 13 | format: ['esm', 'cjs'], 14 | minify: true, 15 | }); 16 | -------------------------------------------------------------------------------- /packages/core/typings.d.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/index'; 2 | -------------------------------------------------------------------------------- /packages/eslint-config-reactpwa/.npmignore: -------------------------------------------------------------------------------- 1 | empty.ts -------------------------------------------------------------------------------- /packages/eslint-config-reactpwa/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Atyantik Technologies Private Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 10 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 11 | SOFTWARE. 12 | -------------------------------------------------------------------------------- /packages/eslint-config-reactpwa/empty.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atyantik/react-pwa/b498cafe5dd7bab8005fa8ad89e2f6c03eb3cc8a/packages/eslint-config-reactpwa/empty.ts -------------------------------------------------------------------------------- /packages/eslint-config-reactpwa/index.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:import/recommended", 6 | "plugin:import/typescript", 7 | "prettier", 8 | "airbnb-base", 9 | "airbnb-typescript/base", 10 | ], 11 | "plugins": [ 12 | "@typescript-eslint", 13 | "import", 14 | ], 15 | "parserOptions": { 16 | "ecmaFeatures": { "jsx": true }, 17 | "sourceType": "module", 18 | "ecmaVersion": 12, 19 | }, 20 | "env": { 21 | "browser": true, // Enables browser globals like window and document 22 | "amd": true, // Enables require() and define() as global variables as per the amd spec. 23 | "node": true, // Enables Node.js global variables and Node.js scoping. 24 | "es2021": true 25 | }, 26 | "rules": { 27 | "max-len": ["error", {"code": 120}], 28 | "@typescript-eslint/explicit-function-return-type": "off", 29 | "@typescript-eslint/no-use-before-define": [ 30 | "error", 31 | { 32 | "classes": true, 33 | "functions": false, 34 | "typedefs": true, 35 | "variables": true 36 | } 37 | ], 38 | "import/no-default-export": "off", 39 | "import/no-extraneous-dependencies": "off", 40 | "import/prefer-default-export": "off", 41 | "no-prototype-builtins": "off", 42 | "no-use-before-define": "off", 43 | "react/destructuring-assignment": "off", 44 | "react/jsx-filename-extension": "off", 45 | "react/jsx-uses-react": "off", 46 | "react/react-in-jsx-scope": "off", 47 | "unicorn/no-array-for-each": "off", 48 | "unicorn/prevent-abbreviations": "off", 49 | }, 50 | "overrides": [ 51 | { 52 | "files": ["webpack.js"], 53 | "rules": { 54 | "import/no-unresolved": 0 55 | } 56 | } 57 | ], 58 | "settings": { 59 | "import/core-modules": [ 60 | "react", 61 | "react-dom", 62 | "react-router", 63 | "react-router-dom", 64 | "express" 65 | ], 66 | "import/resolver": { 67 | "node": { 68 | "extensions": [ 69 | ".js", 70 | ".jsx", 71 | ".ts", 72 | ".tsx", 73 | ] 74 | } 75 | }, 76 | }, 77 | "ignorePatterns": [ 78 | "scaffold/**/*.(ts|js|tsx|jsx)" 79 | ] 80 | }; 81 | 82 | module.exports = config; 83 | -------------------------------------------------------------------------------- /packages/eslint-config-reactpwa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-reactpwa", 3 | "version": "1.0.50-alpha.14", 4 | "description": "Linting of ReactPWA Apps", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "main": "index.js", 9 | "keywords": [ 10 | "eslint", 11 | "typescript", 12 | "reactpwa", 13 | "linting", 14 | "prettier", 15 | "typescript-eslint", 16 | "eslint-config-airbnb-base", 17 | "eslint-config-airbnb-typescript", 18 | "@typescript-eslint/eslint-plugin", 19 | "prettier-eslint" 20 | ], 21 | "author": "Tirth Bodawala ", 22 | "license": "ISC", 23 | "dependencies": { 24 | "@typescript-eslint/eslint-plugin": "^6.1.0", 25 | "@typescript-eslint/parser": "^6.1.0", 26 | "eslint-config-airbnb-base": "^15.0.0", 27 | "eslint-config-airbnb-typescript": "^17.1.0", 28 | "eslint-config-prettier": "^8.8.0", 29 | "eslint-plugin-import": "^2.27.5" 30 | }, 31 | "peerDependencies": { 32 | "eslint": "^8.45.0", 33 | "prettier": "^3.0.0" 34 | }, 35 | "gitHead": "1b9ab957cac25218179b12f2ddc727fabb06f000" 36 | } 37 | -------------------------------------------------------------------------------- /packages/eslint-config-reactpwa/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "declaration": true, 6 | "declarationMap": true, 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "jsx": "react-jsx", 10 | "module": "ES2022", 11 | "moduleResolution": "Node", 12 | "resolveJsonModule": true, 13 | "skipLibCheck": true, 14 | "sourceMap": true, 15 | "strict": true, 16 | "target": "ESNext", 17 | }, 18 | "include": [ 19 | "empty.ts" 20 | ], 21 | "exclude": [ 22 | "node_modules", 23 | "lib", 24 | "dist", 25 | "index.js" 26 | ], 27 | } 28 | --------------------------------------------------------------------------------