├── .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 | [](https://opencollective.com/react-pwa) [](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 | 
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 |
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 | //
21 | // );
22 | // };
23 | export const SyncDataScript: FC<{ id: string; data: any }> = ({ id, data }) => (
24 |
31 | );
32 |
--------------------------------------------------------------------------------
/packages/core/src/defaults/server.ts:
--------------------------------------------------------------------------------
1 | export default undefined;
2 |
--------------------------------------------------------------------------------
/packages/core/src/defaults/webmanifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | short_name: 'ReactPWA',
3 | name: 'ReactPWA',
4 | id: '/?source=pwa',
5 | start_url: '/?source=pwa',
6 | background_color: '#FFFFFF',
7 | display: 'standalone',
8 | scope: '/',
9 | theme_color: '#FFFFFF',
10 | shortcuts: [
11 | // {
12 | // name: "How's weather today?",
13 | // short_name: 'Today',
14 | // description: 'View weather information for today',
15 | // url: '/today?source=pwa',
16 | // icons: [{ src: '/images/today.png', sizes: '192x192' }],
17 | // },
18 | // {
19 | // name: "How's weather tomorrow?",
20 | // short_name: 'Tomorrow',
21 | // description: 'View weather information for tomorrow',
22 | // url: '/tomorrow?source=pwa',
23 | // icons: [{ src: '/images/tomorrow.png', sizes: '192x192' }],
24 | // },
25 | ],
26 | description: 'React PWA - The simplest framework to create PWA with React',
27 | screenshots: [
28 | // {
29 | // src: '/images/screenshot1.png',
30 | // type: 'image/png',
31 | // sizes: '540x720',
32 | // },
33 | // {
34 | // src: '/images/screenshot2.jpg',
35 | // type: 'image/jpg',
36 | // sizes: '540x720',
37 | // },
38 | ],
39 | };
40 |
--------------------------------------------------------------------------------
/packages/core/src/express-server.tsx:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import { readFileSync } from 'node:fs';
3 | import express from 'express';
4 | import compression from 'compression';
5 | import { router, appServer } from './server.js';
6 | import { ChunksMap } from './utils/asset-extract.js';
7 |
8 | const app = express();
9 | app.set('trust proxy', true);
10 |
11 | let webChunksMap: ChunksMap = {
12 | chunks: [],
13 | };
14 | try {
15 | const jsonStatsContent = readFileSync(
16 | path.resolve(__dirname, 'chunks-map.json'),
17 | {
18 | encoding: 'utf-8',
19 | },
20 | );
21 | webChunksMap = JSON.parse(jsonStatsContent);
22 | app.locals.chunksMap = webChunksMap;
23 | } catch {
24 | // web-chunks's map not found
25 | }
26 |
27 | // Enable compression
28 | app.use(compression());
29 |
30 | const staticOptions = {
31 | immutable: true,
32 | maxAge: 31536000,
33 | setHeaders: (res: express.Response, pathname: string) => {
34 | if (pathname.indexOf('/build/') && pathname.indexOf('sw.js') === -1) {
35 | // We ask the build assets to be cached for 1 year as it would be immutable
36 | res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
37 | }
38 | },
39 | };
40 |
41 | app.use(express.static(path.resolve(__dirname, 'build'), staticOptions));
42 |
43 | const init = async () => {
44 | if (
45 | appServer
46 | && (Object.keys(appServer).length || typeof appServer === 'function')
47 | ) {
48 | app.use(appServer);
49 | }
50 | app.use(router);
51 | return app;
52 | };
53 |
54 | if (require.main === module) {
55 | const port = +(process?.env?.PORT ?? '0') || 3000;
56 | // Add host from env
57 | const host = (process?.env?.HOST ?? '0.0.0.0') || '0.0.0.0';
58 | const initedServer = await init();
59 | initedServer.listen(
60 | {
61 | port,
62 | host,
63 | },
64 | () => {
65 | // eslint-disable-next-line no-console
66 | console.info(`Server now listening on http://${host}:${port}`);
67 | },
68 | );
69 | }
70 |
71 | export default init;
72 |
--------------------------------------------------------------------------------
/packages/core/src/hooks/useData.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useMemo } from 'react';
2 | import { DataContext } from '../components/data.js';
3 |
4 | export function useData Promise>(
6 | id: string,
7 | promiseCallback: T,
8 | ) {
9 | const { createDataPromise, removeDataPromise } = useContext(DataContext);
10 | // On unmount remove the data promise
11 | useEffect(() => () => removeDataPromise(id), []);
12 | return useMemo(() => {
13 | const promise = createDataPromise(id, promiseCallback);
14 | return promise.read();
15 | }, [id, promiseCallback]);
16 | }
17 |
--------------------------------------------------------------------------------
/packages/core/src/hooks/useSyncData.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useMemo } from 'react';
2 | import { DataContext } from '../components/data.js';
3 | import { SyncDataScript } from '../components/sync-data-script.js';
4 |
5 | export function useSyncData Promise>(
7 | id: string,
8 | promiseCallback: T,
9 | ): { data: Awaited>; syncScript: React.ReactElement } {
10 | const { createDataPromise, removeDataPromise } = useContext(DataContext);
11 | // On unmount remove the data promise
12 | useEffect(() => () => removeDataPromise(id), []);
13 | return useMemo(() => {
14 | const promise = createDataPromise(id, promiseCallback);
15 | const data = promise.read();
16 | return {
17 | data,
18 | syncScript: ,
19 | };
20 | }, [id, promiseCallback]);
21 | }
22 |
--------------------------------------------------------------------------------
/packages/core/src/index.ts:
--------------------------------------------------------------------------------
1 | import { parse } from 'bowser';
2 | import { ReactElement } from 'react';
3 | import { RouteObject as RRRouteObject } from 'react-router-dom';
4 | import { IWebManifest } from './typedefs/webmanifest.js';
5 |
6 | export { DataContext } from './components/data.js';
7 | export { useData } from './hooks/useData.js';
8 | export { useSyncData } from './hooks/useSyncData.js';
9 | export { SyncDataScript as SyncData } from './components/sync-data-script.js';
10 | export {
11 | Outlet,
12 | Link,
13 | useLocation,
14 | useParams,
15 | useSearchParams,
16 | useHref,
17 | useMatch,
18 | useMatches,
19 | useNavigate,
20 | useNavigation,
21 | useNavigationType,
22 | useOutlet,
23 | useResolvedPath,
24 | useRoutes,
25 | useInRouterContext,
26 | useLinkClickHandler,
27 | } from 'react-router-dom';
28 | export { useCookies } from 'react-cookie';
29 | export * from 'react-error-boundary';
30 | export { Head } from './components/head/index.js';
31 | export { HttpStatus } from './components/http-status.js';
32 | export { Redirect } from './components/redirect.js';
33 |
34 | export type RoutesArgs = {
35 | getLocation: () => URL;
36 | browserDetect: () => Promise>;
37 | userAgent: string;
38 | isbot: () => Promise;
39 | getScoped: (
40 | key: string,
41 | callback: (() => any) | (() => Promise),
42 | ) => Promise;
43 | addToHeadPreStyles: (components: ReactElement | ReactElement[]) => void;
44 | addToFooter: (components: ReactElement | ReactElement[]) => void;
45 | addHeaders: (headers: Headers) => void;
46 | };
47 |
48 | export type Routes =
49 | | RouteObject[]
50 | | ((args: RoutesArgs) => Promise)
51 | | ((args: RoutesArgs) => RouteObject[]);
52 |
53 | export interface RouteObject
54 | extends Omit, 'children'> {
55 | element: () => Promise<{ default: React.ComponentType }>;
56 | children?: RouteObject[];
57 | webpack?: (string | number)[];
58 | module?: string[];
59 | skeleton?: React.ComponentType;
60 | error?: React.ComponentType;
61 | props?: Record;
62 | resolveHeadManually?: boolean;
63 | }
64 |
65 | export type WebManifest =
66 | | IWebManifest
67 | | ((args: RoutesArgs) => Promise)
68 | | ((args: RoutesArgs) => IWebManifest);
69 |
--------------------------------------------------------------------------------
/packages/core/src/node/build.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | import path from 'node:path';
3 | import { existsSync, writeFileSync } from 'node:fs';
4 | import fse from 'fs-extra';
5 | import { pathToFileURL } from 'node:url';
6 | import webpack from 'webpack';
7 | import { extractChunksMap } from '../utils/asset-extract.js';
8 | import { WebpackHandler } from '../webpack.js';
9 | import { RunOptions } from '../typedefs/server.js';
10 |
11 | const webpackStatsDisplayOptions: webpack.StatsOptions = {
12 | colors: true,
13 | performance: true,
14 | errors: true,
15 | warnings: true,
16 | assets: false,
17 | moduleAssets: false,
18 | modules: false,
19 | };
20 |
21 | export const run = async (options: RunOptions) => {
22 | const projectWebpack = path.resolve(options.projectRoot, 'webpack.js');
23 | let WHandler: typeof WebpackHandler = WebpackHandler;
24 | if (projectWebpack && existsSync(projectWebpack)) {
25 | const projectWebpackHandler = await import(
26 | pathToFileURL(projectWebpack).toString()
27 | );
28 | if (!projectWebpackHandler.default) {
29 | // eslint-disable-next-line no-console
30 | console.error(
31 | 'webpack.js should default export a class extending WebpackHandler.',
32 | );
33 | } else if (
34 | !(projectWebpackHandler.default.prototype instanceof WebpackHandler)
35 | ) {
36 | // eslint-disable-next-line no-console
37 | console.error(
38 | 'webpack.js should extends WebpackHandler from "@reactpwa/core/webpack"',
39 | );
40 | } else {
41 | // No issues at all, create an instance of project handler instead
42 | WHandler = projectWebpackHandler.default;
43 | }
44 | }
45 |
46 | const webWebpackHandler = new WHandler({
47 | mode: options.mode,
48 | target: 'web',
49 | projectRoot: options.projectRoot,
50 | buildWithHttpServer: false,
51 | envVars: options.envVars ?? {},
52 | config: options.config ?? {},
53 | serverSideRender: options.serverSideRender ?? true,
54 | });
55 |
56 | const nodeWebpackHandler = new WHandler({
57 | mode: options.mode,
58 | target: 'node',
59 | projectRoot: options.projectRoot,
60 | buildWithHttpServer: true,
61 | envVars: options.envVars ?? {},
62 | config: options.config ?? {},
63 | copyPublicFolder: true,
64 | serverSideRender: options.serverSideRender ?? true,
65 | });
66 |
67 | const WebConfig: webpack.Configuration = webWebpackHandler.getConfig();
68 | const ServerConfig: webpack.Configuration = nodeWebpackHandler.getConfig();
69 |
70 | const compiler = webpack([WebConfig, ServerConfig]);
71 |
72 | try {
73 | // Clean the dist folder
74 | if (
75 | ServerConfig?.output?.path
76 | && fse.existsSync(ServerConfig.output.path)
77 | ) {
78 | fse.removeSync(ServerConfig.output.path);
79 | }
80 | const compileStats: webpack.MultiStats | undefined = await new Promise(
81 | (resolve, reject) => {
82 | compiler.run((webErr, stats) => {
83 | if (webErr) {
84 | // eslint-disable-next-line no-console
85 | reject(webErr);
86 | return;
87 | }
88 | // eslint-disable-next-line no-console
89 | console.log(stats?.toString(webpackStatsDisplayOptions));
90 |
91 | if (ServerConfig.output?.path) {
92 | const webStats = stats?.stats.find(
93 | (s) => s.compilation.name === 'web',
94 | );
95 | const webChunksMap = extractChunksMap(webStats);
96 | const chunksMapFilePath = path.join(
97 | ServerConfig.output.path,
98 | 'chunks-map.json',
99 | );
100 | writeFileSync(chunksMapFilePath, JSON.stringify(webChunksMap), {
101 | encoding: 'utf-8',
102 | });
103 | }
104 | resolve(stats);
105 | });
106 | },
107 | );
108 | return compileStats;
109 | } catch (ex) {
110 | // eslint-disable-next-line no-console
111 | console.error(ex);
112 | process.exit(1);
113 | }
114 | return undefined;
115 | };
116 |
--------------------------------------------------------------------------------
/packages/core/src/node/start.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import { existsSync } from 'node:fs';
3 | import { Server } from 'http';
4 | import { pathToFileURL } from 'url';
5 | import express from 'express';
6 | import webpack from 'webpack';
7 | import webpackDevMiddleware from 'webpack-dev-middleware';
8 | import webpackHotMiddleware from 'webpack-hot-middleware';
9 | import { WebpackHandler } from '../webpack.js';
10 | import { requireFromString } from '../utils/require-from-string.js';
11 | import { extractChunksMap } from '../utils/asset-extract.js';
12 | import { RunOptions } from '../typedefs/server.js';
13 |
14 | let expressServer: ReturnType;
15 |
16 | const startServer = () => new Promise((resolve) => {
17 | const port = +(process?.env?.PORT ?? '0') || 3000;
18 | const httpServer = expressServer.listen(
19 | {
20 | port,
21 | },
22 | () => {
23 | // eslint-disable-next-line no-console
24 | console.info(`Server now listening on http://localhost:${port}`);
25 | resolve(httpServer);
26 | },
27 | );
28 | });
29 |
30 | export const run = async (options: RunOptions): Promise => {
31 | const projectWebpack = path.resolve(options.projectRoot, 'webpack.js');
32 | let WHandler: typeof WebpackHandler = WebpackHandler;
33 | if (projectWebpack && existsSync(projectWebpack)) {
34 | const projectWebpackHandler = await import(
35 | pathToFileURL(projectWebpack).toString()
36 | );
37 | if (!projectWebpackHandler.default) {
38 | // eslint-disable-next-line no-console
39 | console.error(
40 | 'webpack.js should default export a class extending WebpackHandler.',
41 | );
42 | } else if (
43 | !(projectWebpackHandler.default.prototype instanceof WebpackHandler)
44 | ) {
45 | // eslint-disable-next-line no-console
46 | console.error(
47 | 'webpack.js should extends WebpackHandler from "@reactpwa/core/webpack"',
48 | );
49 | } else {
50 | // No issues at all, create an instance of project handler instead
51 | WHandler = projectWebpackHandler.default;
52 | }
53 | }
54 |
55 | expressServer = express();
56 | expressServer.set('trust proxy', true);
57 |
58 | const webWebpackHandler = new WHandler({
59 | mode: options.mode,
60 | target: 'web',
61 | projectRoot: options.projectRoot,
62 | envVars: options.envVars,
63 | config: options.config,
64 | serverSideRender: options.serverSideRender ?? true,
65 | });
66 |
67 | const nodeWebpackHandler = new WHandler({
68 | mode: options.mode,
69 | target: 'node',
70 | projectRoot: options.projectRoot,
71 | envVars: options.envVars,
72 | config: options.config,
73 | serverSideRender: options.serverSideRender ?? true,
74 | });
75 |
76 | const WebConfig: webpack.Configuration = webWebpackHandler.getConfig();
77 | const ServerConfig: webpack.Configuration = nodeWebpackHandler.getConfig();
78 |
79 | const compiler = webpack([WebConfig, ServerConfig]);
80 | const devMiddleware = webpackDevMiddleware(compiler, {
81 | serverSideRender: true,
82 | writeToDisk: true,
83 | });
84 | expressServer.use(devMiddleware);
85 |
86 | const webCompiler = compiler.compilers.find((n) => n.name === 'web');
87 | if (webCompiler) {
88 | const hotMiddleware = webpackHotMiddleware(webCompiler);
89 | expressServer.use(hotMiddleware);
90 | }
91 |
92 | expressServer.use(
93 | express.static(path.join(options.projectRoot, 'src', 'public')),
94 | );
95 |
96 | compiler.hooks.done.tap('InformServerCompiled', async (compilation) => {
97 | const nodePath = process.env.NODE_PATH || '';
98 |
99 | const jsonWebpackStats = compilation.toJson();
100 | // Get webStats and nodeStats
101 | const webStats = jsonWebpackStats.children?.find?.((c) => c.name === 'web');
102 | const nodeStats = jsonWebpackStats.children?.find?.(
103 | (c) => c.name === 'node',
104 | );
105 |
106 | if (!webStats || !nodeStats) return;
107 | expressServer.locals.chunksMap = extractChunksMap(webStats);
108 |
109 | const { outputPath } = nodeStats;
110 | if (outputPath) {
111 | const serverFilePath = path.join(outputPath, 'server.cjs');
112 | const nodeCompiler = compiler.compilers.find(
113 | (c) => c.options.name === 'node',
114 | );
115 |
116 | // @ts-ignore
117 | const serverContent = nodeCompiler?.outputFileSystem?.readFileSync?.(
118 | serverFilePath,
119 | 'utf-8',
120 | );
121 |
122 | const imported = await requireFromString(serverContent, {
123 | appendPaths: nodePath.split(path.delimiter),
124 | });
125 |
126 | /**
127 | * Remove the old RPWA Router attached to the express app
128 | * and assign the new router to it.
129 | */
130 | // @ts-ignore
131 | const rpwaRouterIndex = (expressServer.router?.stack ?? []).findIndex(
132 | (r: any) => r?.name === 'RPWA_Router',
133 | );
134 | if (rpwaRouterIndex !== -1) {
135 | // @ts-ignore
136 | expressServer.router.stack.splice(rpwaRouterIndex, 1);
137 | }
138 | // @ts-ignore
139 | const stack = expressServer.router?.stack ?? [];
140 | const rpwaClientRouterIndex = stack.findIndex(
141 | (r: any) => r?.name === 'RPWA_App_Server',
142 | );
143 | if (rpwaClientRouterIndex !== -1) {
144 | // @ts-ignore
145 | expressServer.router.stack.splice(rpwaClientRouterIndex, 1);
146 | }
147 |
148 | if (
149 | imported.appServer
150 | && (Object.keys(imported.appServer).length
151 | || typeof imported.appServer === 'function')
152 | ) {
153 | expressServer.use(imported.appServer);
154 | }
155 | expressServer.use(imported.router);
156 | }
157 | });
158 |
159 | const httpServer = await startServer();
160 | return httpServer;
161 | };
162 |
--------------------------------------------------------------------------------
/packages/core/src/root.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'node:path';
2 | import { fileURLToPath } from 'node:url';
3 |
4 | const currentFileUrl = new URL(import.meta.url);
5 | const path = fileURLToPath(currentFileUrl);
6 | const libSrc = resolve(path, '..');
7 |
8 | export { libSrc };
9 |
--------------------------------------------------------------------------------
/packages/core/src/router.ts:
--------------------------------------------------------------------------------
1 | export { Router, Request, Response } from 'express';
2 |
--------------------------------------------------------------------------------
/packages/core/src/server.tsx:
--------------------------------------------------------------------------------
1 | import zlib from 'zlib';
2 | import { PassThrough } from 'node:stream';
3 | import Cookies from 'universal-cookie';
4 | import { renderToPipeableStream } from 'react-dom/server';
5 | import { StaticRouter } from 'react-router-dom/server.js';
6 | // @ts-ignore
7 | import appRoutes from '@currentProject/routes';
8 | // @ts-ignore
9 | import appServer from '@currentProject/server';
10 | // @ts-ignore
11 | import appWebmanifest from '@currentProject/webmanifest';
12 | import { Request, Response, Router } from 'express';
13 | import { matchRoutes } from 'react-router-dom';
14 | import { CookiesProvider } from 'react-cookie';
15 | import {
16 | extractMainScripts,
17 | extractMainStyles,
18 | getCssFileContent,
19 | LazyRouteMatch,
20 | } from './utils/asset-extract.js';
21 | import { App } from './components/app.js';
22 | import { DataProvider } from './components/data.js';
23 | import { HeadProvider } from './components/head/provider.js';
24 | import { ReactStrictMode } from './components/strict.js';
25 | import { ReactPWAContext } from './components/reactpwa.js';
26 | import { getInternalVar, setInternalVar } from './utils/request-internals.js';
27 | import { statusCodeWithLocations } from './utils/redirect.js';
28 | import {
29 | getHttpStatusCode,
30 | getIsBot,
31 | getRedirectUrl,
32 | getRequestArgs,
33 | } from './utils/server.js';
34 | import { WebManifest } from './index.js';
35 | import { IWebManifest } from './typedefs/webmanifest.js';
36 | import { cookieChangeHandler } from './utils/cookie.js';
37 | import { notBoolean } from './utils/not-boolean.js';
38 |
39 | const getCompression = (request: Request) => {
40 | const acceptEncoding = request.headers['accept-encoding'] ?? '';
41 | if (acceptEncoding.indexOf('br') !== -1) {
42 | return {
43 | compressionStream: zlib.createBrotliCompress(),
44 | encoding: 'br',
45 | };
46 | }
47 | if (acceptEncoding.indexOf('gzip') !== -1) {
48 | return {
49 | compressionStream: zlib.createGzip(),
50 | encoding: 'gzip',
51 | };
52 | }
53 | if (acceptEncoding.indexOf('deflate') !== -1) {
54 | return {
55 | compressionStream: zlib.createDeflate(),
56 | encoding: 'deflate',
57 | };
58 | }
59 | return {
60 | compressionStream: new PassThrough(),
61 | encoding: false,
62 | };
63 | };
64 |
65 | const initWebmanifest = async (request: Request) => {
66 | const computedWebmanifest = getInternalVar(request, 'Webmanifest', null);
67 | if (computedWebmanifest !== null) {
68 | return;
69 | }
70 | if (typeof appWebmanifest === 'function') {
71 | const webmanifest: WebManifest = await appWebmanifest(
72 | getRequestArgs(request),
73 | );
74 | setInternalVar(request, 'Webmanifest', webmanifest);
75 | return;
76 | }
77 | setInternalVar(request, 'Webmanifest', appWebmanifest ?? {});
78 | };
79 |
80 | const webmanifestHandler = async (request: Request, response: Response) => {
81 | await initWebmanifest(request);
82 | response.json(getInternalVar(request, 'Webmanifest', {}));
83 | };
84 |
85 | const handler = async (request: Request, response: Response) => {
86 | /**
87 | * Manually reject *.map as they should be directly served via static
88 | */
89 | const requestUrl = new URL(request.url, `http://${request.get('host')}`);
90 | if (requestUrl.pathname.endsWith('.map')) {
91 | response.status(404);
92 | response.send('Not found');
93 | return;
94 | }
95 |
96 | const { chunksMap } = request.app.locals;
97 |
98 | let routes = appRoutes;
99 | const userAgent = request.headers['user-agent'] ?? '';
100 | // get isbot instance
101 | const isBot = getIsBot()(userAgent);
102 |
103 | // Init web manifest for the request
104 | await initWebmanifest(request);
105 | const { lang: webLang, charSet: webCharSet } = getInternalVar(
106 | request,
107 | 'Webmanifest',
108 | {},
109 | );
110 | const lang = webLang ?? 'en';
111 | const charSet = webCharSet ?? 'utf-8';
112 | /**
113 | * Set header to text/html with charset as per webmanifest
114 | * or default to utf-8
115 | */
116 | response.set({
117 | 'content-type': `text/html; charset=${charSet}`,
118 | });
119 |
120 | const { compressionStream, encoding } = getCompression(request);
121 | if (typeof encoding === 'string') {
122 | response.set({ 'content-encoding': encoding });
123 | }
124 |
125 | const initialHtml = ``;
126 | compressionStream.pipe(response);
127 |
128 | if (!isBot) {
129 | compressionStream.write(initialHtml);
130 | }
131 | if (typeof appRoutes === 'function') {
132 | routes = await appRoutes(getRequestArgs(request));
133 | }
134 | const matchedRoutes = matchRoutes(routes, request.url) as LazyRouteMatch[];
135 | let stylesWithContent: { href: string; content: string }[] = [];
136 | let styles: string[] = [];
137 |
138 | const mainStyles = extractMainStyles(chunksMap);
139 | if (mainStyles?.length) {
140 | stylesWithContent = (
141 | await Promise.all(
142 | mainStyles.map(async (mainStyle) => {
143 | if (mainStyle.startsWith('http')) {
144 | return false;
145 | }
146 | return {
147 | content: await getCssFileContent(mainStyle),
148 | href: mainStyle,
149 | };
150 | }),
151 | )
152 | ).filter(notBoolean);
153 |
154 | styles = mainStyles
155 | .map((mainStyle) => {
156 | if (mainStyle.startsWith('http')) {
157 | return mainStyle;
158 | }
159 | return false;
160 | })
161 | .filter(notBoolean);
162 | }
163 | const mainScripts = extractMainScripts(chunksMap);
164 |
165 | // Initialize Cookies
166 | let universalCookies: Cookies | null = new Cookies(request.headers.cookie);
167 | const onCookieChange = cookieChangeHandler(response);
168 | universalCookies.addChangeListener(onCookieChange);
169 | const clearCookieListener = () => {
170 | if (universalCookies) {
171 | universalCookies.removeChangeListener(onCookieChange);
172 | universalCookies = null;
173 | }
174 | };
175 |
176 | // release universal cookies
177 | response.once('close', () => {
178 | clearCookieListener();
179 | });
180 |
181 | // Initiate the router to get manage the data
182 | const setRequestValue = (key: string, val: any) => {
183 | setInternalVar(request, key, val);
184 | };
185 | const getRequestValue = (key: string, defaultValue: any = null) => getInternalVar(request, key, defaultValue);
186 | // @ts-ignore
187 | const app = EnableServerSideRender ? : <>>;
188 | const stream = renderToPipeableStream(
189 |
190 |
193 |
194 |
195 |
196 | >)}
200 | >
201 | {app}
202 | {getRequestValue('footerScripts', <>>)}
203 |
204 |
205 |
206 |
207 |
208 | ,
209 | {
210 | bootstrapModules: mainScripts,
211 | onShellReady() {
212 | if (isBot) return;
213 | stream.pipe(compressionStream);
214 | },
215 | onShellError(error) {
216 | setInternalVar(request, 'hasExecutionError', true);
217 | // eslint-disable-next-line no-console
218 | console.log('A Shell error occurred:\n', error);
219 | // eslint-disable-next-line no-console
220 | console.log(
221 | 'A shell error may also occur if wrong react components are injected in head or footer.'
222 | + 'Please check you are using addToHeadPreStyles & addToFooter wisely.',
223 | );
224 | // Something errored before we could complete the shell so we emit an alternative shell.
225 | if (!isBot) {
226 | response.status(500);
227 | }
228 | /**
229 | * @todo do not add script on shell error. After adding the scripts the
230 | * frontend may work fine, thus the error is not directly visible to developer
231 | */
232 | compressionStream.write(
233 | '',
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 |
--------------------------------------------------------------------------------