├── .all-contributorsrc ├── .babel-plugin-macrosrc.js ├── .editorconfig ├── .env.local ├── .env.production ├── .eslintrc.js ├── .gitattributes ├── .gitbook.yaml ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build.yaml │ ├── commitlint.yaml │ ├── lint.yml │ ├── release.yml │ └── test.yaml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── prepare-commit-msg ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── .versionrc.js ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASE_PROCESS.md ├── commitlint.config.js ├── docs ├── README.md ├── SUMMARY.md ├── building-blocks │ ├── README.md │ ├── async-components.md │ ├── css.md │ ├── i18n.md │ ├── routing.md │ ├── slice │ │ ├── README.md │ │ ├── redux-injectors.md │ │ ├── redux-saga.md │ │ ├── redux-toolkit.md │ │ └── reselect.md │ └── testing.md ├── deployment │ ├── aws.md │ ├── azure.md │ ├── heroku.md │ └── netlify.md ├── misc │ └── faq.md ├── quick-start.md ├── tools │ ├── commands.md │ ├── editors.md │ └── package-managers.md └── understanding-react-boilerplate.md ├── internals ├── extractMessages │ ├── __tests__ │ │ └── extractingMessages.test.ts │ ├── i18next-scanner.config.js │ ├── jest.config.js │ └── stringfyTranslations.js ├── generators │ ├── component │ │ ├── index.test.tsx.hbs │ │ ├── index.ts │ │ ├── index.tsx.hbs │ │ ├── loadable.ts.hbs │ │ └── messages.ts.hbs │ ├── paths.ts │ ├── plopfile.ts │ ├── slice │ │ ├── appendRootState.hbs │ │ ├── importContainerState.hbs │ │ ├── index.ts │ │ ├── index.ts.hbs │ │ ├── saga.ts.hbs │ │ ├── selectors.ts.hbs │ │ └── types.ts.hbs │ └── utils │ │ └── index.ts ├── scripts │ ├── clean.ts │ ├── create-changelog.script.ts │ ├── create-cra-app.script.ts │ ├── create-npm-package.script.ts │ ├── create-npm-package.ts │ ├── create-template-folder.ts │ ├── utils.ts │ └── verify-startingTemplate-changes.ts ├── startingTemplate │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── app │ │ │ ├── __tests__ │ │ │ │ └── index.test.tsx │ │ │ ├── components │ │ │ │ └── NotFoundPage │ │ │ │ │ ├── Loadable.tsx │ │ │ │ │ ├── P.ts │ │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ └── pages │ │ │ │ └── HomePage │ │ │ │ ├── Loadable.tsx │ │ │ │ └── index.tsx │ │ ├── index.tsx │ │ ├── locales │ │ │ ├── __tests__ │ │ │ │ └── i18n.test.ts │ │ │ ├── en │ │ │ │ └── translation.json │ │ │ ├── i18n.ts │ │ │ ├── translations.ts │ │ │ └── types.ts │ │ ├── react-app-env.d.ts │ │ ├── reportWebVitals.ts │ │ ├── setupTests.ts │ │ ├── store │ │ │ ├── __tests__ │ │ │ │ ├── configureStore.test.ts │ │ │ │ └── reducer.test.ts │ │ │ ├── configureStore.ts │ │ │ └── reducers.ts │ │ ├── styles │ │ │ ├── __tests__ │ │ │ │ └── media.test.ts │ │ │ ├── global-styles.ts │ │ │ └── media.ts │ │ ├── types │ │ │ ├── RootState.ts │ │ │ └── index.ts │ │ └── utils │ │ │ ├── @reduxjs │ │ │ └── toolkit.tsx │ │ │ ├── loadable.tsx │ │ │ ├── messages.ts │ │ │ ├── redux-injectors.ts │ │ │ └── types │ │ │ └── injector-typings.ts │ └── tsconfig.json └── testing │ ├── generators │ ├── componentVariations.ts │ ├── sliceVariations.ts │ └── test-generators.ts │ └── loadable.mock.tsx ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── app │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── components │ │ ├── A │ │ │ ├── __tests__ │ │ │ │ └── index.test.tsx │ │ │ └── index.ts │ │ ├── FormLabel │ │ │ ├── __tests__ │ │ │ │ └── index.test.tsx │ │ │ └── index.ts │ │ ├── Link │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.test.tsx.snap │ │ │ │ └── index.test.tsx │ │ │ └── index.ts │ │ ├── LoadingIndicator │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.test.tsx.snap │ │ │ │ └── index.test.tsx │ │ │ └── index.tsx │ │ ├── NavBar │ │ │ ├── Logo.tsx │ │ │ ├── Nav.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Logo.test.tsx │ │ │ │ ├── Nav.test.tsx │ │ │ │ ├── __snapshots__ │ │ │ │ │ ├── Logo.test.tsx.snap │ │ │ │ │ ├── Nav.test.tsx.snap │ │ │ │ │ └── index.test.tsx.snap │ │ │ │ └── index.test.tsx │ │ │ ├── assets │ │ │ │ ├── documentation-icon.svg │ │ │ │ └── github-icon.svg │ │ │ └── index.tsx │ │ ├── PageWrapper │ │ │ └── index.ts │ │ └── Radio │ │ │ └── index.tsx │ ├── index.tsx │ └── pages │ │ ├── HomePage │ │ ├── Features │ │ │ ├── GithubRepoForm │ │ │ │ ├── RepoItem.tsx │ │ │ │ ├── __tests__ │ │ │ │ │ ├── RepoItem.test.tsx │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ └── RepoItem.test.tsx.snap │ │ │ │ │ └── index.test.tsx │ │ │ │ ├── assets │ │ │ │ │ ├── new-window.svg │ │ │ │ │ └── star.svg │ │ │ │ ├── components │ │ │ │ │ ├── Input.ts │ │ │ │ │ ├── TextButton.ts │ │ │ │ │ └── __tests__ │ │ │ │ │ │ ├── TextButton.test.tsx │ │ │ │ │ │ └── input.test.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── slice │ │ │ │ │ ├── __tests__ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ └── saga.test.ts.snap │ │ │ │ │ ├── saga.test.ts │ │ │ │ │ ├── selectors.test.ts │ │ │ │ │ └── slice.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── saga.ts │ │ │ │ │ ├── selectors.ts │ │ │ │ │ └── types.ts │ │ │ ├── LanguageSwitch │ │ │ │ ├── __tests__ │ │ │ │ │ └── index.test.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── messages.ts │ │ │ ├── ThemeSwitch │ │ │ │ ├── __tests__ │ │ │ │ │ └── index.test.tsx │ │ │ │ └── index.tsx │ │ │ ├── assets │ │ │ │ ├── code-analysis.svg │ │ │ │ ├── css.svg │ │ │ │ ├── instant-feedback.svg │ │ │ │ ├── intl.svg │ │ │ │ ├── route.svg │ │ │ │ ├── scaffolding.svg │ │ │ │ ├── seo.svg │ │ │ │ ├── state.svg │ │ │ │ └── ts.svg │ │ │ └── index.tsx │ │ ├── Loadable.tsx │ │ ├── Logos.tsx │ │ ├── Masthead.tsx │ │ ├── __tests__ │ │ │ ├── Features.test.tsx │ │ │ ├── Logos.test.tsx │ │ │ ├── Masthead.test.tsx │ │ │ ├── __snapshots__ │ │ │ │ ├── Features.test.tsx.snap │ │ │ │ ├── Logos.test.tsx.snap │ │ │ │ ├── Masthead.test.tsx.snap │ │ │ │ └── index.test.tsx.snap │ │ │ └── index.test.tsx │ │ ├── assets │ │ │ ├── cra-logo.svg │ │ │ ├── plus-sign.svg │ │ │ └── rp-logo.svg │ │ ├── components │ │ │ ├── Lead.ts │ │ │ ├── P.ts │ │ │ ├── SubTitle.ts │ │ │ ├── Title.ts │ │ │ └── __tests__ │ │ │ │ ├── Lead.test.tsx │ │ │ │ ├── P.test.tsx │ │ │ │ ├── Subtitle.test.tsx │ │ │ │ ├── Title.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ ├── Lead.test.tsx.snap │ │ │ │ ├── P.test.tsx.snap │ │ │ │ ├── Subtitle.test.tsx.snap │ │ │ │ └── Title.test.tsx.snap │ │ ├── index.tsx │ │ └── messages.ts │ │ └── NotFoundPage │ │ ├── Loadable.tsx │ │ ├── P.ts │ │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ │ └── index.tsx ├── index.tsx ├── locales │ ├── __tests__ │ │ └── i18n.test.ts │ ├── de │ │ └── translation.json │ ├── en │ │ └── translation.json │ ├── i18n.ts │ ├── translations.ts │ └── types.ts ├── react-app-env.d.ts ├── reportWebVitals.ts ├── setupTests.ts ├── store │ ├── __tests__ │ │ ├── configureStore.test.ts │ │ └── reducer.test.ts │ ├── configureStore.ts │ └── reducers.ts ├── styles │ ├── StyleConstants.ts │ ├── __tests__ │ │ └── media.test.ts │ ├── global-styles.ts │ ├── media.ts │ └── theme │ │ ├── ThemeProvider.tsx │ │ ├── __tests__ │ │ ├── ThemeProvider.test.tsx │ │ └── utils.test.ts │ │ ├── slice │ │ ├── __tests__ │ │ │ └── slice.test.ts │ │ ├── index.ts │ │ ├── selectors.ts │ │ └── types.ts │ │ ├── styled.d.ts │ │ ├── themes.ts │ │ └── utils.ts ├── types │ ├── Repo.d.ts │ ├── RootState.ts │ └── index.ts └── utils │ ├── @reduxjs │ └── toolkit.tsx │ ├── __tests__ │ ├── __snapshots__ │ │ └── loadable.test.tsx.snap │ ├── loadable.test.tsx │ └── request.test.ts │ ├── loadable.tsx │ ├── messages.ts │ ├── redux-injectors.ts │ ├── request.ts │ └── types │ └── injector-typings.ts ├── template.json ├── tsconfig.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "react-boilerplate-cra-template", 3 | "projectOwner": "react-boilerplate", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 80, 10 | "commit": true, 11 | "contributors": [ 12 | { 13 | "login": "Can-Sahin", 14 | "name": "Can Sahin", 15 | "avatar_url": "https://avatars2.githubusercontent.com/u/33245689", 16 | "profile": "https://github.com/Can-Sahin", 17 | "contributions": [ 18 | "code", 19 | "doc", 20 | "ideas", 21 | "review", 22 | "test" 23 | ] 24 | }, 25 | { 26 | "login": "receptiryaki", 27 | "name": "Recep Tiryaki", 28 | "avatar_url": "https://avatars0.githubusercontent.com/u/3495307", 29 | "profile": "https://github.com/receptiryaki", 30 | "contributions": [ 31 | "code", 32 | "ideas", 33 | "design" 34 | ] 35 | }, 36 | { 37 | "login": "mogsdad", 38 | "name": "David Bingham", 39 | "avatar_url": "https://avatars3.githubusercontent.com/u/1707731", 40 | "profile": "https://github.com/mogsdad", 41 | "contributions": [ 42 | "doc" 43 | ] 44 | }, 45 | { 46 | "login": "lourensdev", 47 | "name": "Lourens de Villiers", 48 | "avatar_url": "https://avatars.githubusercontent.com/u/5746141", 49 | "profile": "https://github.com/lourensdev", 50 | "contributions": [ 51 | "doc" 52 | ] 53 | }, 54 | { 55 | "login": "rejochandran", 56 | "name": "Rejo Chandran", 57 | "avatar_url": "https://avatars.githubusercontent.com/u/4696985", 58 | "profile": "https://github.com/rejochandran", 59 | "contributions": [ 60 | "code", 61 | "doc", 62 | "test" 63 | ] 64 | }, 65 | { 66 | "login": "qeleb", 67 | "name": "Caleb Hoff", 68 | "avatar_url": "https://avatars.githubusercontent.com/u/15345696", 69 | "profile": "https://github.com/qeleb", 70 | "contributions": [ 71 | "code", 72 | "doc", 73 | "ideas", 74 | "test" 75 | ] 76 | } 77 | ], 78 | "contributorsPerLine": 8, 79 | "commitConvention": "none" 80 | } 81 | -------------------------------------------------------------------------------- /.babel-plugin-macrosrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | styledComponents: { 3 | displayName: process.env.NODE_ENV !== 'production', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [{package,bower}.json] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [{.eslintrc,.scss-lint.yml}] 19 | indent_style = space 20 | indent_size = 2 21 | 22 | [*.{scss,sass}] 23 | indent_style = space 24 | indent_size = 2 25 | -------------------------------------------------------------------------------- /.env.local: -------------------------------------------------------------------------------- 1 | BROWSER=none 2 | EXTEND_ESLINT=true 3 | FAST_REFRESH=true -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | GENERATE_SOURCEMAP=false -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const prettierOptions = JSON.parse( 5 | fs.readFileSync(path.resolve(__dirname, '.prettierrc'), 'utf8'), 6 | ); 7 | 8 | module.exports = { 9 | extends: ['react-app', 'prettier'], 10 | plugins: ['prettier'], 11 | rules: { 12 | 'prettier/prettier': ['error', prettierOptions], 13 | }, 14 | overrides: [ 15 | { 16 | files: ['**/*.ts?(x)'], 17 | rules: { 'prettier/prettier': ['warn', prettierOptions] }, 18 | }, 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./docs 2 | 3 | ​structure: 4 | readme: README.md 5 | summary: SUMMARY.md​ 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Global owners 2 | * @can-sahin @receptiryaki 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | Before opening a new issue, please take a moment to review our [**community guidelines**](https://github.com/react-boilerplate/react-boilerplate-cra-template/blob/master/CONTRIBUTING.md) to make the contribution process easy and effective for everyone involved. 7 | 8 | ## Description 9 | 10 | A clear and concise description of what the bug is. 11 | 12 | ## Steps to reproduce 13 | 14 | Steps to reproduce the behavior: 15 | 16 | (Add link to a demo on https://jsfiddle.net or similar if possible) 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Screenshots** 22 | If applicable, add screenshots to help explain your problem. 23 | 24 | ## Versions 25 | 26 | - react-boilerplate-cra-template: 27 | - Node/NPM: 28 | - Browser: 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | **Is your feature request related to a problem? Please describe.** 7 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 8 | 9 | **Describe the solution you'd like** 10 | A clear and concise description of what you want to happen. 11 | 12 | **Describe alternatives you've considered** 13 | A clear and concise description of any alternative solutions or features you've considered. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## React Boilerplate CRA Template 2 | 3 | ### ⚠️ Clear this template before you submit (after you read the things below) 4 | 5 | Thank you for contributing! Please take a moment to review our [**contributing guidelines**](https://github.com/react-boilerplate/react-boilerplate/blob/master/CONTRIBUTING.md) 6 | to make the process easy and effective for everyone involved. 7 | 8 | **Please open an issue** before embarking on any significant pull request, especially those that 9 | add a new library or change existing tests, otherwise you risk spending a lot of time working 10 | on something that might not end up being merged into the project. 11 | 12 | Before opening a pull request, please ensure: 13 | 14 | - [ ] You have followed our [**contributing guidelines**](https://github.com/react-boilerplate/react-boilerplate/blob/master/CONTRIBUTING.md) 15 | - [ ] Double-check your branch is based on `dev` and targets `dev` 16 | - [ ] Pull request has tests (we are going for 100% coverage!) 17 | - [ ] Code is well-commented, linted and follows project conventions 18 | - [ ] Documentation is updated (if necessary) 19 | - [ ] Internal code generators and templates are updated (if necessary) 20 | - [ ] Description explains the issue/use-case resolved and auto-closes related issues 21 | 22 | Be kind to code reviewers, please try to keep pull requests as small and focused as possible :) 23 | 24 | **IMPORTANT**: By submitting a patch, you agree to allow the project 25 | owners to license your work under the terms of the [MIT License](https://github.com/react-boilerplate/react-boilerplate/blob/master/LICENSE.md). 26 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yaml: -------------------------------------------------------------------------------- 1 | # Run commitlint on Pull Requests and commits 2 | name: commitlint 3 | on: 4 | pull_request: 5 | types: ['opened', 'edited', 'reopened', 'synchronize'] 6 | push: 7 | branches: 8 | - master 9 | - dev 10 | 11 | jobs: 12 | lint-pull-request-name: 13 | # Only on pull requests 14 | if: github.event_name == 'pull_request' 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v1 18 | - run: yarn add @commitlint/config-conventional 19 | - uses: JulienKode/pull-request-name-linter-action@v0.1.2 20 | lint-commits: 21 | # Only if we are pushing or merging PR to the master 22 | if: (github.event_name == 'pull_request' && github.base_ref == 'refs/heads/master') || github.event_name == 'push' 23 | runs-on: ubuntu-latest 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | steps: 27 | - uses: actions/checkout@v2 28 | with: 29 | fetch-depth: 30 # Its fine to lint last 30 commits only 30 | - run: yarn add @commitlint/config-conventional 31 | - uses: wagoid/commitlint-github-action@v1 32 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - run: yarn --frozen-lockfile 11 | - run: yarn run lint 12 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | workflow_dispatch: 8 | 9 | jobs: 10 | createAndTestCRAFromNpm: 11 | strategy: 12 | matrix: 13 | node-version: [16.x, 18.x] 14 | 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - name: Create CRA from npm template 22 | run: yarn create react-app --template cra-template-rb . 23 | - run: yarn run build 24 | - run: yarn run test:generators 25 | - run: yarn run lint 26 | - run: yarn run checkTs 27 | - run: yarn run cleanAndSetup 28 | - run: yarn run build 29 | - run: yarn run test:generators 30 | - run: yarn run lint 31 | - run: yarn run checkTs 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | - push 4 | - pull_request 5 | - workflow_dispatch 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Use Node.js 16.x 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: 16.x 16 | - run: yarn --frozen-lockfile 17 | - run: yarn run test:coverage 18 | - name: Upload to Coveralls 19 | uses: coverallsapp/github-action@master 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | testInternals: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Use Node.js 16.x 27 | uses: actions/setup-node@v3 28 | with: 29 | node-version: 16.x 30 | - run: yarn --frozen-lockfile 31 | - run: yarn run test:internals 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't check auto-generated stuff into git 2 | coverage 3 | build 4 | node_modules 5 | stats.json 6 | .pnp 7 | .pnp.js 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log* 12 | 13 | # yarn 14 | yarn-debug.log* 15 | yarn-error.log* 16 | .pnp.* 17 | .yarn/* 18 | !.yarn/patches 19 | !.yarn/plugins 20 | !.yarn/releases 21 | !.yarn/sdks 22 | !.yarn/versions 23 | 24 | # env 25 | .env.development.local 26 | .env.test.local 27 | .env.production.local 28 | 29 | # boilerplate internals 30 | generated-cra-app 31 | .cra-template-rb 32 | template 33 | .eslintcache 34 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | if yarn git-branch-is dev; 5 | then yarn commitlint --edit $1; 6 | fi 7 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn checkTs 5 | yarn lint-staged 6 | yarn verify-startingTemplate-changes 7 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn devmoji -e -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/fermium 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | package-lock.json 4 | yarn.lock -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "arrowParens": "avoid" 9 | } 10 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "processors": ["stylelint-processor-styled-components"], 3 | "extends": [ 4 | "stylelint-config-recommended", 5 | "stylelint-config-styled-components" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.versionrc.js: -------------------------------------------------------------------------------- 1 | const internalSection = `Internals`; 2 | /* 3 | * Used for creating CHANGELOG.md automatically. 4 | * Anything under the internalSection should be boilerplate internals 5 | * and shouldn't interest the end users, meaning that the template shouldn't be effected. 6 | */ 7 | 8 | // Check the descriptions of the types -> https://github.com/commitizen/conventional-commit-types/blob/master/index.json 9 | module.exports = { 10 | types: [ 11 | { type: 'feat', section: 'Features', hidden: false }, 12 | { type: 'fix', section: 'Bug Fixes', hidden: false }, 13 | { type: 'docs', section: 'Documentation', hidden: false }, 14 | { type: 'perf', section: 'Performance Updates', hidden: false }, 15 | 16 | // Other changes that don't modify src or test files 17 | { type: 'chore', section: internalSection, hidden: false }, 18 | 19 | // Adding missing tests or correcting existing tests 20 | { type: 'test', section: internalSection, hidden: false }, 21 | 22 | // Changes to our CI configuration files and scripts 23 | { type: 'ci', section: internalSection, hidden: false }, 24 | 25 | // A code change that neither fixes a bug nor adds a feature 26 | { type: 'refactor', section: internalSection, hidden: false }, 27 | 28 | // Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 29 | { type: 'style', section: internalSection, hidden: false }, 30 | ], 31 | skip: { 32 | changelog: true, 33 | }, 34 | commitAll: true, 35 | }; 36 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "styled-components.vscode-styled-components", 6 | "orta.vscode-jest" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Chrome", 6 | "type": "chrome", 7 | "request": "launch", 8 | "url": "http://localhost:3000", 9 | "webRoot": "${workspaceFolder}/src", 10 | "sourceMapPathOverrides": { 11 | "webpack:///src/*": "${webRoot}/*" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "[typescript]": { 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "editor.tabSize": 2 6 | }, 7 | "[typescriptreact]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "[javascript]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "[json]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "[html]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode" 18 | }, 19 | "editor.codeActionsOnSave": { 20 | "source.fixAll": true, 21 | "source.fixAll.eslint": true 22 | }, 23 | "editor.formatOnSave": true, 24 | "javascript.format.enable": false 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Maximilian Stoiber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /RELEASE_PROCESS.md: -------------------------------------------------------------------------------- 1 | # RELEASE PROCESS 2 | 3 | The release process is **semi-automated**. The generated changelog requires editing to keep it visually appealing and clear for everyone. 4 | 5 | ## Step by step 6 | 7 | 1. All the development goes into `dev` branch. There are only squash merges allowed there so that its not flooded with everyones commits. 8 | 2. Make a PR to `master` from `dev` and if all checks are good then merge with the title `chore: 🔧 releasing x.x.x`. 9 | 3. Generate the changelog 10 | - `yarn run changelog` 11 | 4. Take a look at the previous changelogs and modify the generated changelog accordingly. Delete and organize the commits and move them under internals section if needed. 12 | 5. Create the release 13 | - `yarn run release` 14 | 6. Publish to npm 15 | - `yarn run publish:npm` 16 | 7. Push the changes to git. 17 | 8. Create release in github by copy pasting the related section from the CHANGELOG.md 18 | 9. There is a `release CI workflow`. Wait for it to be succeeded to see if there any problems with the released version. 19 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | // Use types from .versionrc.js so that when generating CHANGELOG there are no inconsistencies 2 | const standardVersionTypes = require('./.versionrc').types; 3 | const typeEnums = standardVersionTypes.map(t => t.type); 4 | 5 | module.exports = { 6 | extends: ['@commitlint/config-conventional'], 7 | rules: { 8 | 'type-enum': [2, 'always', typeEnums], 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # React Boilerplate CRA Template 2 | 3 | ## What is React Boilerplate CRA Template 4 | 5 | {% hint style="info" %} 6 | 7 | 💁‍♂️ **TL;DR:** **`CRA`** handles the bootstrapping and **`React Boilerplate`** gets you started with the best tools and practices. 8 | 9 | {% endhint %} 10 | 11 | This is a custom [`create-react-app`] template of [`react-boilerplate`]. React Boilerplate has been developing the ultimate React starter kit for many years now. On the other hand **`CRA`** (`create-react-app`) is currently the people's favorite choice. This template has been created to join their strengths together. **`CRA`** provides the necessary bootstrapping to start your projects but does not provide a guide on how to build it. That is where **`react-boilerplate`** comes in and prepares the bases with the battle-tested techniques and tools to guide you into creating state-of-the-art web applications. 12 | 13 | {% hint style="warning" %} 14 | 15 | This documentation assumes that you are familiar with the [`create-react-app`]. The template is built on top of it. ;) 16 | 17 | {% endhint %} 18 | 19 | ## Let's get started with the documentation 20 | 21 | In the following sections, we will briefly introduce this boilerplate to you and then start diving into details with the [Building Blocks](building-blocks/overview) section. 22 | 23 | {% hint style="info" %} 24 | 25 | Source Code & Repo: [Github](https://github.com/react-boilerplate/react-boilerplate-cra-template) 26 | 27 | NPM Package: [npm](https://www.npmjs.com/package/cra-template-rb) 28 | 29 | {% endhint %} 30 | 31 | [`create-react-app`]: https://github.com/facebook/create-react-app 32 | [`react-boilerplate`]: https://github.com/react-boilerplate/react-boilerplate 33 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | ‌# Summary​ 2 | 3 | - [Quick Start](quick-start.md) 4 | - [Understanding `react-boilerplate`](understanding-react-boilerplate.md) 5 | 6 | ## Tools 7 | 8 | - [CLI & Scaffolding](tools/commands.md) 9 | - [Editor Configuration](tools/editors.md) 10 | - [Package Managers](tools/package-managers.md) 11 | 12 | ## Building Blocks 13 | 14 | - [Building Blocks](building-blocks/README.md) 15 | - [The `Slice`](building-blocks/slice/README.md) 16 | - [Redux & Toolkit](building-blocks/slice/redux-toolkit.md) 17 | - [Reselect](building-blocks/slice/reselect.md) 18 | - [Redux-Saga](building-blocks/slice/redux-saga.md) 19 | - [Redux Injectors](building-blocks/slice/redux-injectors.md) 20 | - [Async Components](building-blocks/async-components.md) 21 | - [Routing](building-blocks/routing.md) 22 | - [i18n Internationalization & Pluralization](building-blocks/i18n.md) 23 | - [Styling (CSS)](building-blocks/css.md) 24 | - [Testing](building-blocks/testing.md) 25 | 26 | ## Deployment 27 | 28 | - [AWS](deployment/aws.md) 29 | - [Azure](deployment/azure.md) 30 | - [Heroku](deployment/heroku.md) 31 | - [Netlify](deployment/netlify.md) 32 | 33 | ## Misc 34 | 35 | - [FAQ](misc/faq.md) 36 | -------------------------------------------------------------------------------- /docs/building-blocks/async-components.md: -------------------------------------------------------------------------------- 1 | # Async Components 2 | 3 | To load a component asynchronously, create a `Loadable` file by hand or via component generator with the 'Do you want to load resources asynchronously?' option activated. 4 | 5 | This is the content of the file by default: 6 | 7 | #### `Loadable.tsx` 8 | 9 | ```ts 10 | import { lazyLoad } from 'utils/loadable'; 11 | 12 | export const HomePage = lazyLoad( 13 | () => import('./index'), 14 | module => module.HomePage, // Select your exported HomePage function for lazy loading 15 | ); 16 | ``` 17 | 18 | In this case, the app won't show anything while loading your component. You can, however, make it display a custom loader with: 19 | 20 | ```ts 21 | import React from 'react'; 22 | import { lazyLoad } from 'utils/loadable'; 23 | 24 | export const HomePage = lazyLoad( 25 | () => import('./index'), 26 | module => module.HomePage, 27 | { 28 | fallback:
Loading...
, 29 | } 30 | ); 31 | ``` 32 | 33 | Make sure to rename your `Loadable.ts` file to `Loadable.tsx`. 34 | This feature is built into the boilerplate using React's `lazy` and `Suspense` features. 35 | -------------------------------------------------------------------------------- /docs/building-blocks/routing.md: -------------------------------------------------------------------------------- 1 | # Routing 2 | 3 | `react-router` is the de-facto standard routing solution for React applications. 4 | 5 | ## Why not use [connected-react-router](https://github.com/supasate/connected-react-router)? 6 | 7 | There is a detailed explanation for this decision [here](https://reacttraining.com/react-router/web/guides/deep-redux-integration). In short, the recommendation is to forego keeping routes in the Redux store, simply because it shouldn't be needed. There are other ways of navigating, as explained there. 8 | 9 | ## Usage 10 | 11 | To add a new route, simply import the `Route` component and use it standalone or inside the `Routes` component (all part of [RR6 API](https://reactrouter.com/docs/en/v6/getting-started/overview)): 12 | 13 | ```ts 14 | } /> 15 | ``` 16 | 17 | Top level routes are located in `src/app/index.tsx`. 18 | 19 | If you want your route component (or any component for that matter) to be loaded asynchronously, use the component generator with 'Do you want to load resources asynchronously?' option activated. 20 | 21 | ## Child Routes 22 | 23 | For example, if you have a route called `about` at `/about`, and want to make a child route called `team` at `/about/our-team`, follow the example in `src/app/index.tsx` to create a `Routes` within the parent component. 24 | 25 | #### `AboutPage/index.tsx` 26 | 27 | ```ts 28 | import { Routes, Route } from 'react-router-dom'; 29 | 30 | export function AboutPage() { 31 | return ( 32 | 33 | 34 | 35 | ); 36 | } 37 | ``` 38 | 39 | ## Routing programmatically 40 | 41 | You can use the `react-router hooks`, such as [useNavigate](https://reactrouter.com/docs/en/v6/hooks/use-navigate) or [useParams](https://reactrouter.com/docs/en/v6/hooks/use-params), to change the route, get params, and more. 42 | 43 | ```ts 44 | import { useNavigate } from 'react-router-dom'; 45 | 46 | function HomeButton() { 47 | let navigate = useNavigate(); 48 | 49 | function handleClick() { 50 | navigate('/home'); 51 | } 52 | 53 | return ( 54 | 57 | ); 58 | } 59 | ``` 60 | 61 | {% hint style="info" %} 62 | 63 | You can read more in [`react-router`'s documentation](https://reactrouter.com/docs/en/v6). 64 | 65 | {% endhint %} 66 | -------------------------------------------------------------------------------- /docs/building-blocks/slice/README.md: -------------------------------------------------------------------------------- 1 | # What is a Slice? 2 | 3 | If you have read the redux-toolkit documentation you are familiar with the `slice` concept now. Here, we are taking it another step further by enriching it with `reselect` and `redux-saga`. 4 | 5 | Slice manages, encapsulates, and operates a `portion` of your application's data. For example, if you have a page that displays a user list, then you can have a slice called 'UsersPageSlice' that contains all the users in its state, also the functions to read it from the store and the functions to update the users in the list. So, in short, a slice is a redux-toolkit slice also containing the relative `reselect` and `redux-saga` operations within its folder. After all, they are all related to managing the same portion of the data. 6 | 7 | A `slice` is independent of the UI component. It can contain any kind of logic and it can be located in any folder. To follow the `folder-by-feature` pattern it is recommended to keep your `slices` closer to your component using it. But, this doesn't mean that it only belongs to that component. You can import and use that slice in whichever component you want. 8 | 9 | The next steps in the documentation describes how to use the slices with some examples. 10 | 11 | Example folder view: 12 | 13 | ``` 14 | project 15 | | 16 | ├── app 17 | │ └── src 18 | │ ├── app 19 | │ │ ├── Homepage 20 | │ │ │ ├── index.tsx 21 | │ │ │ ├── slice => Contains the relevant stuff for Homepage data 22 | │ │ │ │ ├── index.ts 23 | │ │ │ │ ├── saga.ts 24 | │ │ │ │ ├── selectors.ts 25 | │ │ │ │ └── types.ts 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/building-blocks/slice/redux-injectors.md: -------------------------------------------------------------------------------- 1 | # Redux Injectors 2 | 3 | [`redux-injectors`](https://github.com/react-boilerplate/redux-injectors) is an official `react-boilerplate` companion library. We built it so that it can be used and maintained independently from `react-boilerplate`. It allows you to dynamically load reducers and sagas as needed, instead of loading them all upfront. This has some nice benefits, such as avoiding having to manage a big global list of reducers and sagas. It also facilitates more effective use of [code-splitting](https://webpack.js.org/guides/code-splitting/). 4 | 5 | You can find the main repo for the library [here](https://github.com/react-boilerplate/redux-injectors) and read the docs [here](https://github.com/react-boilerplate/redux-injectors/blob/master/docs/api.md). 6 | 7 | ## Usage 8 | 9 | ```ts 10 | import { 11 | useInjectSaga, 12 | useInjectReducer, 13 | SagaInjectionModes, 14 | } from 'utils/redux-injectors'; 15 | import { saga } from './saga'; 16 | import { reducer } from '.'; 17 | 18 | export function SomeComponent() { 19 | useInjectReducer({ key: 'SomeComponent', reducer }); 20 | useInjectSaga({ 21 | key: 'SomeComponent', 22 | saga, 23 | mode: SagaInjectionModes.DAEMON, 24 | }); 25 | // ... 26 | } 27 | ``` 28 | 29 | {% hint style="info" %} 30 | 31 | **Note:** Importing `redux-injectors` from `utils/redux-injectors` will add extra type-safety. 32 | 33 | {% endhint %} 34 | -------------------------------------------------------------------------------- /docs/building-blocks/slice/reselect.md: -------------------------------------------------------------------------------- 1 | # Reselect 2 | 3 | `reselect` memoizes ("caches") previous state trees and calculations based upon the said tree. This means repeated changes and calculations are fast and efficient, providing us with a performance boost over standard `useSelector` implementations. 4 | 5 | The [official documentation](https://github.com/reactjs/reselect) offers a good starting point! 6 | 7 | ## Usage 8 | 9 | There are two different kinds of selectors, simple and complex ones. 10 | 11 | ### Simple selectors 12 | 13 | Simple selectors are just that: they take the application state and select a part of it. 14 | 15 | ```ts 16 | export const mySelector = (state: MyRootState) => state.someState; 17 | ``` 18 | 19 | ### Complex selectors 20 | 21 | If we need to, we can combine simple selectors to build more complex ones which get nested state parts with `reselect`'s `createSelector` function. We import other selectors and pass them to the `createSelector` call: 22 | 23 | #### `.../slice/selectors.ts` 24 | 25 | ```ts 26 | import { createSelector } from '@reduxjs/toolkit'; 27 | 28 | export const mySelector = (state: MyRootState) => state.someState; 29 | 30 | // Here type of `someState` will be inferred ✅ 31 | const myComplexSelector = createSelector( 32 | mySelector, 33 | someState => someState.someNestedState, 34 | ); 35 | 36 | export { myComplexSelector }; 37 | ``` 38 | 39 | ### Using your selectors in components 40 | 41 | #### `index.tsx` 42 | 43 | ```ts 44 | import React, { useEffect } from 'react'; 45 | import { useSelector } from 'react-redux'; 46 | import { selectUsername } from './slice/selectors'; 47 | 48 | export function HomePage() { 49 | // Type of the `username` will be inferred ✅ 50 | const username = useSelector(selectUsername); 51 | // ... 52 | } 53 | ``` 54 | 55 | {% hint style="info" %} 56 | 57 | 🎉 **Good News:** You don't need to write this boilerplate code by hand, the `slice` generator will generate it for you. ✓ 58 | 59 | {% endhint %} 60 | -------------------------------------------------------------------------------- /docs/deployment/azure.md: -------------------------------------------------------------------------------- 1 | # Deploy to Azure 2 | 3 | ### Easy 3-Step Deployment Process 4 | 5 | _Step 1:_ Within Azure Portal, add a 'Web App' resource to your resource group. Select the appropriate version of Node (i.e. 10.14) and verify that the operating system is set to Linux to ensure that Node is being run natively and not via IIS (iisnode). 6 | 7 | {% hint style="info" %} 8 | 9 | Note that following the steps in several of the quick start guides (such as [the Azure QuickStart](https://docs.microsoft.com/en-us/azure/app-service/app-service-web-get-started-nodejs)) may result in a Windows + IIS Node configuration that is incompatible with `react-boilerplate`. 10 | 11 | {% endhint %} 12 | 13 | _Step 2:_ When the resource has finished deploying, go to its deployment center and select Local Git (other methods will work as well, but the following steps assume this approach) and 'App Service' for the build provider. Note the Git Clone Uri that is presented when the wizard is finished. 14 | 15 | _Step 3:_ Within the root of your `react-boilerplate` source folder, execute the following commands to publish to Azure: 16 | 17 | 1. `git remote add azure https://YOUR_RESOURCE_NAME.scm.azurewebsites.net:443/YOUR_RESOURCE_NAME.git` 18 | 2. `git add .` 19 | 3. `git commit -m 'Made some epic changes as per usual'` 20 | 4. `git push azure master` 21 | -------------------------------------------------------------------------------- /docs/deployment/heroku.md: -------------------------------------------------------------------------------- 1 | # Deploy to Heroku 2 | 3 | This doc is still awaiting help. 😢 If you want to contribute to this documentation, please contact us. (Preferably via a pull request!) 4 | -------------------------------------------------------------------------------- /docs/deployment/netlify.md: -------------------------------------------------------------------------------- 1 | # Deploy to Netlify 2 | 3 | ### Easy 5-Step Deployment Process 4 | 5 | _Step 1:_ Create a `netlify.toml` file in the root directory of your project and copy this code below. Edit these settings if you did not follow the boilerplate structure. More settings available here (https://docs.netlify.com/configure-builds/file-based-configuration/#sample-file) 6 | 7 | ``` 8 | [build] 9 | # Directory to change to before starting a build. 10 | # This is where we will look for package.json/.nvmrc/etc. 11 | base = "/" 12 | 13 | # Directory (relative to root of your repo) that contains the deploy-ready 14 | # HTML files and assets generated by the build. If a base directory has 15 | # been specified, include it in the publish directory path. 16 | publish = "./build" 17 | 18 | # Default build command. 19 | command = "yarn run build" 20 | 21 | # The following redirect is intended for use with most SPAs that handle routing internally. 22 | [[redirects]] 23 | from = "/*" 24 | to = "/index.html" 25 | status = 200 26 | ``` 27 | 28 | _Step 2:_ Commit your code and push your latest updates to a GitHub repository. 29 | 30 | _Step 3:_ Register or Login in at [Netlify](https://app.netlify.com/). 31 | 32 | _Step 4:_ In your account | team page click `New site from git` then chose your repository. 33 | 34 | _Step 5:_ Click deploy. 35 | 36 | {% hint style="info" %} 37 | 38 | Note: No need to change any setting in the last step as `netlify.toml` overwrites these settings. 39 | 40 | {% endhint %} 41 | 42 | Now your code will be deployed automatically to Netlify on every push to the default branch of your repository.🥳🥳 43 | -------------------------------------------------------------------------------- /docs/misc/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | - [FAQ](#faq) 4 | - [Using reducers optimistically](#using-reducers-optimistically) 5 | - [Keeping up-to-date with the template](#keeping-up-to-date-with-the-template) 6 | 7 | ## Using reducers optimistically 8 | 9 | If you have components that should be available throughout the app, like a `NavigationBar` (i.e., they aren't route-specific), you need to add their respective reducers to the root reducer with the help of `combineReducers`. 10 | 11 | ```ts 12 | // In src/store/reducers.ts 13 | 14 | ... 15 | import { combineReducers } from '@reduxjs/toolkit'; 16 | ... 17 | 18 | import { reducer as navigationBarReducer } from 'components/NavigationBar/slice'; 19 | 20 | export function createReducer(injectedReducers: InjectedReducersType = {}) { 21 | const rootReducer = combineReducers({ 22 | navigationBar: navigationBarReducer, 23 | ...injectedReducers, 24 | }); 25 | 26 | return rootReducer; 27 | } 28 | ``` 29 | 30 | ## Keeping up-to-date with the template 31 | 32 | Even though the template is an npm package, it's not possible for you to **just update** the package as you would for a dependency, since you start CRA with this template initially. Instead, it is recommended to keep an eye on the [CHANGELOG](../../CHANGELOG.md) file. All the changes that **concern** the template user will be displayed there, like bug fixes, documentation updates, new features, and so on. You can check each change's commits and file changes to see what has been changed. Then, the decision is yours if you want to apply those to your code. 33 | -------------------------------------------------------------------------------- /docs/quick-start.md: -------------------------------------------------------------------------------- 1 | # QuickStart 2 | 3 | You have just **3** easy-peasy steps to do :) 4 | 5 | ⚠️ Using [Yarn Package Manager](https://yarnpkg.com) is recommended over `npm`. 6 | 7 | **1)** Create **CRA** app with the custom template 8 | 9 | ```shell 10 | yarn create react-app --template cra-template-rb my-app 11 | ``` 12 | 13 | **2)** Start the example application and checkout the features made ready for you. 14 | 15 | ```shell 16 | cd my-app 17 | yarn start 18 | ``` 19 | 20 | **3)** When you are done examining the sample application. Clean it and start your own app!! 21 | 22 | ```shell 23 | yarn run cleanAndSetup 24 | ``` 25 | 26 | {% hint style="success" %} 27 | 28 | That's it. As easy as it can be. Happy coding! 🎉 29 | 30 | {% endhint %} 31 | -------------------------------------------------------------------------------- /docs/tools/commands.md: -------------------------------------------------------------------------------- 1 | # CLI & Scaffolding 2 | 3 | ## Cleaning 4 | 5 | ```Shell 6 | yarn cleanAndSetup 7 | ``` 8 | 9 | Removes the example app, replacing it with the smallest amount of boilerplate code necessary to start writing your app! Also, it makes some essential changes to your setup to give you a clean and working start. 10 | 11 | {% hint style="warning" %} 12 | 13 | **Note:** This command is self-destructive; once you've run it, it disables itself. This action is for your safety, so you can't irreversibly delete portions of your project by accident. 14 | 15 | {% endhint %} 16 | 17 | ## Generators 18 | 19 | ```Shell 20 | yarn generate 21 | ``` 22 | 23 | Allows you to auto-generate boilerplate code for common parts of your application, specifically `component`s, and `redux-toolkit slice`s. You can also run `yarn generate ` to skip the first selection (e.g., `yarn generate component`). 24 | 25 | ```Shell 26 | yarn test:generators 27 | ``` 28 | 29 | Test whether the generators are working fine. It generates components and slices with a variety of settings. This command is helpful if you decide to customize generators for your needs. 30 | 31 | ## Production 32 | 33 | ```Shell 34 | yarn start:prod 35 | ``` 36 | 37 | - Builds your app (see `yarn run build`) 38 | - Serves the `build` folder locally 39 | 40 | The app is built for optimal performance; assets are minified and served `gzip`-ed. 41 | 42 | ## Unit testing 43 | 44 | ```Shell 45 | yarn test 46 | ``` 47 | 48 | Unit tests specified in the `**/__tests__/*.ts` files throughout the application are run. 49 | 50 | All the `test` commands allow an optional `-- [string]` argument to filter the tests run by Jest, useful if you need to run a specific test only. 51 | 52 | ```Shell 53 | # Run only the Button component tests 54 | yarn test -- Button 55 | ``` 56 | 57 | ## Linting 58 | 59 | ```Shell 60 | yarn lint 61 | ``` 62 | 63 | Lints your Typescript and your CSS. 64 | 65 | ```Shell 66 | yarn lint:fix 67 | ``` 68 | 69 | Lints your code and tries to fix any errors it finds. 70 | 71 | ## Extracting translation JSON Files 72 | 73 | ```Shell 74 | yarn extract-messages 75 | ``` 76 | 77 | ## Typescript 78 | 79 | ```Shell 80 | yarn checkTs 81 | ``` 82 | 83 | Checks for TypeScript errors. 84 | -------------------------------------------------------------------------------- /docs/tools/editors.md: -------------------------------------------------------------------------------- 1 | # Setting Up Your Editor 2 | 3 | You can edit React Boilerplate using any editor or IDE, but there are a few extra steps that you can take to make sure your coding experience is as good as it can be. 4 | 5 | ## VS Code 6 | 7 | We provide a `.vscode` folder out-of-the-box which includes the **recommended extensions**, **debugger configuration**, and **settings** 8 | 9 | They are highly suggested for the best Developer Experience. Extensions are responsible for: 10 | 11 | - [Eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 12 | - [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 13 | - [Styled Components](https://marketplace.visualstudio.com/items?itemName=styled-components.vscode-styled-components) 14 | - [Jest](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest) 15 | 16 | These are the basic building blocks in the boilerplate. 17 | 18 | ## Chrome Extensions 19 | 20 | For a better debugging and development experience we suggest [Redux DevTools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en). In the boilerplate, Redux is configured with this extension in mind so that you can debug and monitor your state changes very easily. 21 | -------------------------------------------------------------------------------- /docs/tools/package-managers.md: -------------------------------------------------------------------------------- 1 | # Using a Package Manager 2 | 3 | ## Switching from NPM to Yarn 4 | 5 | While you may be familiar with `npm`, or even use it as your package manager of choice, it is not the recommended package manager for this project. This is because `Yarn` is faster, more reliable, and more extensible than `npm`. If you are not familiar with `Yarn`, you can read more about it [here](https://yarnpkg.com/en). 6 | 7 | To download `Yarn` using `npm`, run the following command: 8 | 9 | ```bash 10 | npm install -g yarn 11 | ``` 12 | 13 | This will install `Yarn Classic/v1` globally on your machine. 14 | 15 | ## Upgrading to Yarn 3 16 | 17 | ### Why Yarn 3 18 | 19 | `Yarn 3` includes a host of benefits over `Yarn Classic/v1`, including: 20 | 21 | 1. Even **faster** installs 22 | 1. More verbose & readable output 23 | 1. `Yarn version` is stored directly in the repo to keep everyone on the same version 24 | 1. `Yarn plugins` can be added to extend the functionality of `Yarn` 25 | 1. `PnP` (Plug and Play) mode can be used to improve performance and stability (if supported by your project) 26 | 1. Helpful, new commands like `yarn dedupe`, `yarn info`, & more 27 | 28 | > Yarn does not use `PnP` by default. If you would like to use `PnP`, you can read more about it [here](https://yarnpkg.com/features/pnp). 29 | 30 | > Read more about the differences between `Yarn Classic/v1` and `Yarn 3` [here](https://yarnpkg.com/getting-started/migration). 31 | 32 | ### Upgrading 33 | 34 | To upgrade to `Yarn 3` from `Yarn Classic/v1`, run the following command: 35 | 36 | ```bash 37 | yarn set version berry 38 | yarn install 39 | ``` 40 | 41 | ## Yarn Plugins 42 | 43 | Now, you can start getting plugins to extend the functionality of `Yarn 3`. 44 | 45 | ### Yarn-Typescript Plugin 46 | 47 | ```bash 48 | yarn plugin import typescript 49 | ``` 50 | 51 | This will install the `Yarn-TypeScript` plugin, which automatically adds `@types/` packages into your dependencies when you add a package that doesn't include its own types 52 | 53 | ### Interactive-Tools 54 | 55 | ```bash 56 | yarn plugin import interactive-tools 57 | ``` 58 | 59 | This will install the `Interactive-Tools` plugin, which includes the `yarn dedupe` & `yarn upgrade-interactive` commands. These commands will help you to remove duplicate packages from your `node_modules` folder, and upgrade your dependencies to the latest versions. 60 | -------------------------------------------------------------------------------- /internals/extractMessages/i18next-scanner.config.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | const path = require('path'); 3 | const typescript = require('typescript'); 4 | const compilerOptions = require('../../tsconfig.json').compilerOptions; 5 | 6 | const stringfyTranslationObjects = require('./stringfyTranslations.js'); 7 | 8 | module.exports = { 9 | input: [ 10 | 'src/app/**/**.{ts,tsx}', 11 | '!**/node_modules/**', 12 | '!src/app/**/*.test.{ts,tsx}', 13 | ], 14 | output: './', 15 | options: { 16 | debug: false, 17 | removeUnusedKeys: false, 18 | func: { 19 | list: ['t'], 20 | extensions: [''], // We dont want this extension because we manually check on transform function below 21 | }, 22 | lngs: ['en', 'de'], 23 | defaultLng: 'en', 24 | defaultNs: 'translation', 25 | resource: { 26 | loadPath: 'src/locales/{{lng}}/{{ns}}.json', 27 | savePath: 'src/locales/{{lng}}/{{ns}}.json', 28 | jsonIndent: 2, 29 | lineEnding: '\n', 30 | }, 31 | keySeparator: '.', // char to separate keys 32 | nsSeparator: ':', // char to split namespace from key 33 | interpolation: { 34 | prefix: '{{', 35 | suffix: '}}', 36 | }, 37 | }, 38 | transform: function transform(file, enc, done) { 39 | const extensions = ['.ts', '.tsx']; 40 | 41 | const { base, ext } = path.parse(file.path); 42 | if (extensions.includes(ext) && !base.includes('.d.ts')) { 43 | const content = fs.readFileSync(file.path, enc); 44 | const shouldStringfyObjects = base === 'messages.ts'; 45 | parseContent(content, this.parser, shouldStringfyObjects); 46 | } 47 | 48 | done(); 49 | }, 50 | }; 51 | function parseContent(content, parser, shouldStringfyObjects = true) { 52 | const { outputText } = typescript.transpileModule(content, { 53 | compilerOptions: compilerOptions, 54 | }); 55 | let cleanedContent = outputText; 56 | if (shouldStringfyObjects) { 57 | cleanedContent = stringfyTranslationObjects(outputText); 58 | } 59 | parser.parseFuncFromString(cleanedContent); 60 | } 61 | 62 | module.exports.parseContent = parseContent; 63 | -------------------------------------------------------------------------------- /internals/extractMessages/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testMatch: [ 3 | '**/__tests__/**/*.+(ts|tsx|js)', 4 | '**/?(*.)+(spec|test).+(ts|tsx|js)', 5 | ], 6 | transform: { 7 | '^.+\\.(ts|tsx)$': 'ts-jest', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /internals/extractMessages/stringfyTranslations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is custom intermediate function to convert translations objects to i18next resource strings 3 | * `i18next-scanner expects strings such like: 4 | * 5 | * {t('a.b')} 6 | * 7 | * but translations object enables us to write the same thing as: 8 | * 9 | * {t(translations.a.b)} 10 | * 11 | * So, this function converts them into strings like the first one so that scanner recognizes 12 | */ 13 | 14 | function stringfyTranslationObjects(content) { 15 | let contentWithObjectsStringified = content; 16 | const pattern = /_t\((.+?)[),]/gim; 17 | const matches = content.matchAll(pattern); 18 | for (const match of matches) { 19 | if (match.length < 1) { 20 | continue; 21 | } 22 | const key = match[1]; 23 | let keyAsStringValue = ''; 24 | if (["'", '"', '`'].some(x => key.includes(x))) { 25 | keyAsStringValue = key; 26 | } else { 27 | keyAsStringValue = stringifyRecursively(content, key); 28 | keyAsStringValue = `'${keyAsStringValue}'`; 29 | } 30 | contentWithObjectsStringified = replaceTranslationObjectWithString( 31 | contentWithObjectsStringified, 32 | key, 33 | keyAsStringValue, 34 | ); 35 | } 36 | return contentWithObjectsStringified; 37 | } 38 | 39 | // Recursively concatenate all the `variables` until we hit the imported translations object 40 | function stringifyRecursively(content, key) { 41 | let [root, ...rest] = key.split('.'); 42 | const pattern = `${root} =(.+?);`; 43 | const regex = RegExp(pattern, 'gim'); 44 | let match = regex.exec(content); 45 | if (match && match.length > 1) { 46 | const key = match[1].trim(); 47 | root = stringifyRecursively(content, key); 48 | } else if (isImportedTranslationObject(content, root)) { 49 | root = null; 50 | } 51 | 52 | if (root != null) { 53 | return [root, ...rest].join('.'); 54 | } else { 55 | return [...rest].join('.'); 56 | } 57 | } 58 | 59 | function isImportedTranslationObject(content, key) { 60 | const pattern = `import {.*?${key}.*?} from.+locales/translations.*`; 61 | return RegExp(pattern, 'gim').test(content); 62 | } 63 | 64 | function replaceTranslationObjectWithString(content, key, keyAsStringValue) { 65 | return content.replace(`_t(${key}`, `t(${keyAsStringValue}`); 66 | } 67 | 68 | module.exports = stringfyTranslationObjects; 69 | -------------------------------------------------------------------------------- /internals/generators/component/index.test.tsx.hbs: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | 4 | import { {{ properCase componentName }} } from '..'; 5 | 6 | {{#if wantTranslations}} 7 | jest.mock('react-i18next', () => ({ 8 | useTranslation: () => { 9 | return { 10 | t: str => str, 11 | i18n: { 12 | changeLanguage: () => new Promise(() => {}), 13 | }, 14 | }; 15 | }, 16 | })); 17 | {{/if}} 18 | 19 | describe('<{{ properCase componentName }} />', () => { 20 | it('should match snapshot', () => { 21 | const loadingIndicator = render(<{{ properCase componentName }} />); 22 | expect(loadingIndicator.container.firstChild).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /internals/generators/component/index.tsx.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase componentName }} 4 | * 5 | */ 6 | {{#if wantMemo}} 7 | import React, { memo } from 'react'; 8 | {{else}} 9 | import * as React from 'react'; 10 | {{/if}} 11 | {{#if wantStyledComponents}} 12 | import styled from 'styled-components/macro'; 13 | {{/if}} 14 | {{#if wantTranslations}} 15 | import { useTranslation } from 'react-i18next'; 16 | import { messages } from './messages'; 17 | {{/if}} 18 | 19 | interface Props {} 20 | 21 | {{#if wantMemo}} 22 | export const {{ properCase componentName }} = memo((props: Props) => { 23 | {{else}} 24 | export function {{ properCase componentName }}(props: Props) { 25 | {{/if}} 26 | {{#if wantTranslations}} 27 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 28 | const { t, i18n } = useTranslation(); 29 | {{/if}} 30 | 31 | return ( 32 | {{#if wantStyledComponents}} 33 |
34 | {{else}} 35 |
36 | {{/if}} 37 | {{#if wantTranslations}} 38 | {t('')} 39 | {/* {t(...messages.someThing())} */} 40 | {{/if}} 41 | {{#if wantStyledComponents}} 42 |
43 | {{else}} 44 |
45 | {{/if}} 46 | ); 47 | 48 | {{#if wantMemo}} 49 | }); 50 | {{else}} 51 | }; 52 | {{/if}} 53 | 54 | {{#if wantStyledComponents}} 55 | const Div = styled.div``; 56 | {{/if}} 57 | -------------------------------------------------------------------------------- /internals/generators/component/loadable.ts.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for {{ properCase componentName }} 4 | * 5 | */ 6 | 7 | import { lazyLoad } from 'utils/loadable'; 8 | 9 | export const {{ properCase componentName }} = lazyLoad(() => import('./index'), module => module.{{ properCase componentName }}); -------------------------------------------------------------------------------- /internals/generators/component/messages.ts.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is only need if you want to extract messages into JSON files in locales folder 3 | * AND if you are also using the object syntax instead of string syntax. \ 4 | * Check the documentation section i18n for details 5 | */ 6 | import { translations } from 'locales/translations'; 7 | import { _t } from 'utils/messages'; 8 | 9 | export const messages = { 10 | // someThing: () => _t(translations.someThing,'default value'), 11 | }; 12 | -------------------------------------------------------------------------------- /internals/generators/paths.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | export const baseGeneratorPath = path.join(__dirname, '../../src/app'); 4 | -------------------------------------------------------------------------------- /internals/generators/plopfile.ts: -------------------------------------------------------------------------------- 1 | import { NodePlopAPI } from 'node-plop'; 2 | import { componentGenerator } from './component'; 3 | import shell from 'shelljs'; 4 | import { sliceGenerator } from './slice'; 5 | interface PrettifyCustomActionData { 6 | path: string; 7 | } 8 | 9 | export default function plop(plop: NodePlopAPI) { 10 | plop.setGenerator('component', componentGenerator); 11 | plop.setGenerator('slice', sliceGenerator); 12 | 13 | plop.setActionType('prettify', (answers, config) => { 14 | const data = config!.data as PrettifyCustomActionData; 15 | shell.exec(`yarn run prettify -- "${data.path}"`, { silent: true }); 16 | return ''; 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /internals/generators/slice/appendRootState.hbs: -------------------------------------------------------------------------------- 1 | {{ camelCase sliceName }}?: {{ properCase sliceName }}State; 2 | // [INSERT NEW REDUCER KEY ABOVE] < Needed for generating containers seamlessly 3 | -------------------------------------------------------------------------------- /internals/generators/slice/importContainerState.hbs: -------------------------------------------------------------------------------- 1 | import { {{ properCase sliceName }}State } from 'app/{{ path }}/slice/types'; 2 | // [IMPORT NEW CONTAINERSTATE ABOVE] < Needed for generating containers seamlessly 3 | -------------------------------------------------------------------------------- /internals/generators/slice/index.ts.hbs: -------------------------------------------------------------------------------- 1 | import { PayloadAction } from '@reduxjs/toolkit'; 2 | import { createSlice } from 'utils/@reduxjs/toolkit'; 3 | {{#if wantSaga}} 4 | import { useInjectReducer, useInjectSaga } from 'utils/redux-injectors'; 5 | import { {{ camelCase sliceName }}Saga } from './saga'; 6 | {{else}} 7 | import { useInjectReducer } from 'utils/redux-injectors'; 8 | {{/if}} 9 | import { {{ properCase sliceName }}State } from './types'; 10 | 11 | export const initialState: {{ properCase sliceName }}State = {}; 12 | 13 | const slice = createSlice({ 14 | name: '{{ camelCase sliceName }}', 15 | initialState, 16 | reducers: { 17 | someAction(state, action: PayloadAction) {}, 18 | }, 19 | }); 20 | 21 | export const { actions: {{ camelCase sliceName }}Actions } = slice; 22 | 23 | export const use{{ properCase sliceName }}Slice = () => { 24 | useInjectReducer({ key: slice.name, reducer: slice.reducer }); 25 | {{#if wantSaga}} 26 | useInjectSaga({ key: slice.name, saga: {{ camelCase sliceName }}Saga }); 27 | {{/if}} 28 | return { actions: slice.actions }; 29 | }; 30 | 31 | /** 32 | * Example Usage: 33 | * 34 | * export function MyComponentNeedingThisSlice() { 35 | * const { actions } = use{{ properCase sliceName }}Slice(); 36 | * 37 | * const onButtonClick = (evt) => { 38 | * dispatch(actions.someAction()); 39 | * }; 40 | * } 41 | */ -------------------------------------------------------------------------------- /internals/generators/slice/saga.ts.hbs: -------------------------------------------------------------------------------- 1 | // import { take, call, put, select, takeLatest } from 'redux-saga/effects'; 2 | // import { {{ camelCase sliceName }}Actions as actions } from '.'; 3 | 4 | // function* doSomething() {} 5 | 6 | export function* {{ camelCase sliceName }}Saga() { 7 | // yield takeLatest(actions.someAction.type, doSomething); 8 | } 9 | -------------------------------------------------------------------------------- /internals/generators/slice/selectors.ts.hbs: -------------------------------------------------------------------------------- 1 | import { createSelector } from '@reduxjs/toolkit'; 2 | 3 | import { RootState } from 'types'; 4 | import { initialState } from '.'; 5 | 6 | const selectSlice = (state: RootState) => state.{{ camelCase sliceName }} || initialState; 7 | 8 | export const select{{ properCase sliceName }} = createSelector( 9 | [selectSlice], 10 | state => state, 11 | ); 12 | -------------------------------------------------------------------------------- /internals/generators/slice/types.ts.hbs: -------------------------------------------------------------------------------- 1 | /* --- STATE --- */ 2 | export interface {{ properCase sliceName }}State {} 3 | -------------------------------------------------------------------------------- /internals/generators/utils/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | export function pathExists(path: string) { 4 | return fs.existsSync(path); 5 | } 6 | -------------------------------------------------------------------------------- /internals/scripts/clean.ts: -------------------------------------------------------------------------------- 1 | import shell from 'shelljs'; 2 | import path from 'path'; 3 | import chalk from 'chalk'; 4 | import fs from 'fs'; 5 | const packageJson = require('../../package.json'); 6 | 7 | interface Options {} 8 | 9 | process.chdir(path.join(__dirname, '../..')); 10 | 11 | export function cleanAndSetup(opts: Options = {}) { 12 | if (!shell.test('-e', 'internals/startingTemplate')) { 13 | shell.echo('The example app has already deleted.'); 14 | shell.exit(1); 15 | } 16 | shell.echo(chalk.blue('Cleaning the example app...')); 17 | 18 | shell.rm('-rf', 'public/*'); 19 | shell.rm('-rf', 'src/*'); 20 | 21 | shell.cp('-r', 'internals/startingTemplate/public/*', 'public'); 22 | shell.cp('-r', 'internals/startingTemplate/src/*', 'src'); 23 | shell.cp('internals/startingTemplate/tsconfig.json', 'tsconfig.json'); 24 | 25 | shell.rm('-rf', 'internals/startingTemplate'); 26 | shell.rm('-rf', 'internals/scripts'); 27 | 28 | shell.exec('yarn run prettify -- src/*', { silent: true }); 29 | 30 | modifyPackageJsonFile(); 31 | 32 | shell.echo( 33 | chalk.green('Example app removed and setup completed. Happy Coding!!!'), 34 | ); 35 | } 36 | 37 | function modifyPackageJsonFile() { 38 | delete packageJson['eslintConfig']; 39 | delete packageJson['scripts']['cleanAndSetup']; 40 | 41 | packageJson['scripts']['prepare'] = 'husky install'; 42 | 43 | fs.writeFileSync('./package.json', JSON.stringify(packageJson)); 44 | shell.exec('yarn run prettify -- package.json', { silent: true }); 45 | 46 | shell.exec('yarn install', { silent: false }); 47 | } 48 | 49 | (function () { 50 | cleanAndSetup(); 51 | })(); 52 | -------------------------------------------------------------------------------- /internals/scripts/create-changelog.script.ts: -------------------------------------------------------------------------------- 1 | import shell from 'shelljs'; 2 | 3 | interface Options {} 4 | 5 | export function createChangeLog(opts: Options = {}) { 6 | const changes1 = shell.exec(`git diff package.json`, { silent: true }); 7 | const changes2 = shell.exec(`git diff package-lock.json`, { silent: true }); 8 | if (changes1.stdout.length > 0 || changes2.stdout.length > 0) { 9 | console.error('Error: Unstaged files'); 10 | process.exit(1); 11 | } 12 | shell.exec( 13 | `yarn standard-version --skip.commit --skip.tag --skip.changelog=0`, 14 | { 15 | silent: false, 16 | }, 17 | ); 18 | 19 | // Revert the bumbped version 20 | shell.exec(`git checkout -- package-lock.json`, { silent: true }); 21 | shell.exec(`git checkout -- package.json`, { silent: true }); 22 | } 23 | 24 | createChangeLog(); 25 | -------------------------------------------------------------------------------- /internals/scripts/create-cra-app.script.ts: -------------------------------------------------------------------------------- 1 | import shell from 'shelljs'; 2 | import { createNpmPackage, removeNpmPackage } from './create-npm-package'; 3 | 4 | interface Options {} 5 | 6 | export function createCRA(opts: Options = {}) { 7 | const app_name = 'generated-cra-app'; 8 | shell.rm('-rf', app_name); 9 | 10 | const template = createNpmPackage(); 11 | shell.exec(`yarn create react-app ${app_name} --template file:${template}`, { 12 | silent: false, 13 | fatal: true, 14 | }); 15 | 16 | removeNpmPackage(); 17 | } 18 | 19 | createCRA(); 20 | -------------------------------------------------------------------------------- /internals/scripts/create-npm-package.script.ts: -------------------------------------------------------------------------------- 1 | import { createNpmPackage } from './create-npm-package'; 2 | 3 | createNpmPackage({}); 4 | -------------------------------------------------------------------------------- /internals/scripts/create-npm-package.ts: -------------------------------------------------------------------------------- 1 | import shell from 'shelljs'; 2 | import { 3 | createTemplateFolder, 4 | removeTemplateFolder, 5 | } from './create-template-folder'; 6 | import { shellEnableAbortOnFail, shellDisableAbortOnFail } from './utils'; 7 | 8 | const packageFolder = '.cra-template-rb'; 9 | interface Options {} 10 | 11 | export function createNpmPackage(opts: Options = {}) { 12 | const abortOnFailEnabled = shellEnableAbortOnFail(); 13 | 14 | shell.rm('-rf', packageFolder); 15 | 16 | createTemplateFolder(opts); 17 | 18 | // Create a tarball archive and get filename of generated archive from stdout 19 | const archiveFilename = shell 20 | .exec(`npm pack`, { silent: true }) 21 | .stdout.trim(); 22 | 23 | shell.exec(`tar -xvf ${archiveFilename}`, { silent: true }); 24 | shell.mv('package', packageFolder); 25 | shell.rm(archiveFilename); 26 | 27 | removeTemplateFolder(); 28 | 29 | // Rename the files that NPM has special conditions back 30 | shell.mv( 31 | `${packageFolder}/template/npmrc`, 32 | `${packageFolder}/template/.npmrc`, 33 | ); 34 | 35 | if (abortOnFailEnabled) shellDisableAbortOnFail(); 36 | return packageFolder; 37 | } 38 | 39 | export function removeNpmPackage() { 40 | shell.rm('-rf', packageFolder); 41 | } 42 | -------------------------------------------------------------------------------- /internals/scripts/utils.ts: -------------------------------------------------------------------------------- 1 | import shell from 'shelljs'; 2 | 3 | export function shellEnableAbortOnFail() { 4 | if (!shell.config.fatal) { 5 | shell.config.fatal = true; 6 | return true; 7 | } 8 | return false; 9 | } 10 | 11 | export function shellDisableAbortOnFail() { 12 | if (shell.config.fatal) { 13 | shell.config.fatal = false; 14 | } 15 | } 16 | 17 | export function parseArgv(argv: string[], key: string, existsOnly?: boolean) { 18 | const index = argv.indexOf(`--${key}`); 19 | if (index > 0) { 20 | if (existsOnly) { 21 | return true; 22 | } 23 | return argv[index + 1]; 24 | } 25 | return undefined; 26 | } 27 | -------------------------------------------------------------------------------- /internals/scripts/verify-startingTemplate-changes.ts: -------------------------------------------------------------------------------- 1 | import shell from 'shelljs'; 2 | import fs from 'fs'; 3 | 4 | interface Options {} 5 | 6 | /* 7 | * Check if the changed files are also updated if they are in the startingTemplate 8 | */ 9 | export function verifyStartingTemplateChanges(opts: Options = {}) { 10 | const gitDiff = shell.exec(`git diff --staged --name-only`, { silent: true }); 11 | const changedFiles = gitDiff.stdout.split('\n'); 12 | for (const file of changedFiles) { 13 | if (file.startsWith(pathInTemplate(''))) { 14 | continue; 15 | } 16 | if (fileIsInStartingTemplate(file)) { 17 | const fileNotChangedInStartingTemplate = 18 | changedFiles.find(f => f === pathInTemplate(file)) === undefined; 19 | if (fileNotChangedInStartingTemplate) { 20 | console.error(`File: ${file} must be updated in the startingTemplate`); 21 | process.exit(1); 22 | } 23 | } 24 | } 25 | } 26 | 27 | function fileIsInStartingTemplate(file: string) { 28 | const path = pathInTemplate(file); 29 | return fs.existsSync(path) && !fs.statSync(path).isDirectory(); 30 | } 31 | 32 | const pathInTemplate = (file: string) => `internals/startingTemplate/${file}`; 33 | 34 | verifyStartingTemplateChanges(); 35 | -------------------------------------------------------------------------------- /internals/startingTemplate/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-boilerplate/react-boilerplate-cra-template/535c85448e4fba15a3099dcd079fabf554050329/internals/startingTemplate/public/favicon.ico -------------------------------------------------------------------------------- /internals/startingTemplate/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /internals/startingTemplate/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-boilerplate/react-boilerplate-cra-template/535c85448e4fba15a3099dcd079fabf554050329/internals/startingTemplate/public/logo192.png -------------------------------------------------------------------------------- /internals/startingTemplate/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-boilerplate/react-boilerplate-cra-template/535c85448e4fba15a3099dcd079fabf554050329/internals/startingTemplate/public/logo512.png -------------------------------------------------------------------------------- /internals/startingTemplate/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /internals/startingTemplate/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/app/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { createRenderer } from 'react-test-renderer/shallow'; 3 | 4 | import { App } from '../index'; 5 | 6 | const renderer = createRenderer(); 7 | 8 | describe('', () => { 9 | it('should render and match the snapshot', () => { 10 | renderer.render(); 11 | const renderedOutput = renderer.getRenderOutput(); 12 | expect(renderedOutput).toMatchSnapshot(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/app/components/NotFoundPage/Loadable.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Asynchronously loads the component for NotFoundPage 3 | */ 4 | 5 | import { lazyLoad } from 'utils/loadable'; 6 | 7 | export const NotFoundPage = lazyLoad( 8 | () => import('./index'), 9 | module => module.NotFoundPage, 10 | ); 11 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/app/components/NotFoundPage/P.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro'; 2 | 3 | export const P = styled.p` 4 | font-size: 1rem; 5 | line-height: 1.5; 6 | color: black; 7 | margin: 0.625rem 0 1.5rem 0; 8 | `; 9 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/app/components/NotFoundPage/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components/macro'; 3 | import { P } from './P'; 4 | import { Helmet } from 'react-helmet-async'; 5 | 6 | export function NotFoundPage() { 7 | return ( 8 | <> 9 | 10 | 404 Page Not Found 11 | 12 | 13 | 14 | 15 | 4 16 | <span role="img" aria-label="Crying Face"> 17 | 😢 18 | </span> 19 | 4 20 | 21 |

Page not found.

22 |
23 | 24 | ); 25 | } 26 | 27 | const Wrapper = styled.div` 28 | height: 100vh; 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | flex-direction: column; 33 | min-height: 320px; 34 | `; 35 | 36 | const Title = styled.div` 37 | margin-top: -8vh; 38 | font-weight: bold; 39 | color: black; 40 | font-size: 3.375rem; 41 | 42 | span { 43 | font-size: 3.125rem; 44 | } 45 | `; 46 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/app/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * App 4 | * 5 | * This component is the skeleton around the actual pages, and should only 6 | * contain code that should be seen on all pages. (e.g. navigation bar) 7 | */ 8 | 9 | import * as React from 'react'; 10 | import { Helmet } from 'react-helmet-async'; 11 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 12 | 13 | import { GlobalStyle } from 'styles/global-styles'; 14 | 15 | import { HomePage } from './pages/HomePage/Loadable'; 16 | import { NotFoundPage } from './components/NotFoundPage/Loadable'; 17 | import { useTranslation } from 'react-i18next'; 18 | 19 | export function App() { 20 | const { i18n } = useTranslation(); 21 | return ( 22 | 23 | 28 | 29 | 30 | 31 | 32 | } /> 33 | } /> 34 | 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/app/pages/HomePage/Loadable.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Asynchronously loads the component for HomePage 3 | */ 4 | 5 | import { lazyLoad } from 'utils/loadable'; 6 | 7 | export const HomePage = lazyLoad( 8 | () => import('./index'), 9 | module => module.HomePage, 10 | ); 11 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/app/pages/HomePage/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Helmet } from 'react-helmet-async'; 3 | 4 | export function HomePage() { 5 | return ( 6 | <> 7 | 8 | HomePage 9 | 10 | 11 | My HomePage 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * index.tsx 3 | * 4 | * This is the entry file for the application, only setup and boilerplate 5 | * code. 6 | */ 7 | 8 | import 'react-app-polyfill/ie11'; 9 | import 'react-app-polyfill/stable'; 10 | 11 | import * as React from 'react'; 12 | import ReactDOM from 'react-dom/client'; 13 | import { Provider } from 'react-redux'; 14 | 15 | // Use consistent styling 16 | import 'sanitize.css/sanitize.css'; 17 | 18 | // Import root app 19 | import { App } from 'app'; 20 | 21 | import { HelmetProvider } from 'react-helmet-async'; 22 | 23 | import { configureAppStore } from 'store/configureStore'; 24 | 25 | import reportWebVitals from 'reportWebVitals'; 26 | 27 | // Initialize languages 28 | import './locales/i18n'; 29 | 30 | const store = configureAppStore(); 31 | const root = ReactDOM.createRoot( 32 | document.getElementById('root') as HTMLElement, 33 | ); 34 | 35 | root.render( 36 | 37 | 38 | 39 | 40 | 41 | 42 | , 43 | ); 44 | 45 | // Hot reloadable translation json files 46 | if (module.hot) { 47 | module.hot.accept(['./locales/i18n'], () => { 48 | // No need to render the App again because i18next works with the hooks 49 | }); 50 | } 51 | 52 | // If you want to start measuring performance in your app, pass a function 53 | // to log results (for example: reportWebVitals(console.log)) 54 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 55 | reportWebVitals(); 56 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/locales/__tests__/i18n.test.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from '../i18n'; 2 | 3 | describe('i18n', () => { 4 | it('should initiate i18n', async () => { 5 | const t = await i18n; 6 | expect(t).toBeDefined(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "welcome" 3 | } 4 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/locales/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | import LanguageDetector from 'i18next-browser-languagedetector'; 4 | 5 | import en from './en/translation.json'; 6 | import { convertLanguageJsonToObject } from './translations'; 7 | 8 | export const translationsJson = { 9 | en: { 10 | translation: en, 11 | }, 12 | }; 13 | 14 | // Create the 'translations' object to provide full intellisense support for the static json files. 15 | convertLanguageJsonToObject(en); 16 | 17 | export const i18n = i18next 18 | // pass the i18n instance to react-i18next. 19 | .use(initReactI18next) 20 | // detect user language 21 | // learn more: https://github.com/i18next/i18next-browser-languageDetector 22 | .use(LanguageDetector) 23 | // init i18next 24 | // for all options read: https://www.i18next.com/overview/configuration-options 25 | .init({ 26 | resources: translationsJson, 27 | fallbackLng: 'en', 28 | debug: 29 | process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test', 30 | 31 | interpolation: { 32 | escapeValue: false, // not needed for react as it escapes by default 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/locales/translations.ts: -------------------------------------------------------------------------------- 1 | import { ConvertedToObjectType, TranslationJsonType } from './types'; 2 | 3 | /** 4 | * This file is seperate from the './i18n.ts' simply to make the Hot Module Replacement work seamlessly. 5 | * Your components can import this file in 'messages.ts' files which would ruin the HMR if this isn't a separate module 6 | */ 7 | export const translations: ConvertedToObjectType = 8 | {} as any; 9 | 10 | /* 11 | * Converts the static JSON file into an object where keys are identical 12 | * but values are strings concatenated according to syntax. 13 | * This is helpful when using the JSON file keys and still having the intellisense support 14 | * along with type-safety 15 | */ 16 | export const convertLanguageJsonToObject = ( 17 | json: any, 18 | objToConvertTo = translations, 19 | current?: string, 20 | ) => { 21 | Object.keys(json).forEach(key => { 22 | const currentLookupKey = current ? `${current}.${key}` : key; 23 | if (typeof json[key] === 'object') { 24 | objToConvertTo[key] = {}; 25 | convertLanguageJsonToObject( 26 | json[key], 27 | objToConvertTo[key], 28 | currentLookupKey, 29 | ); 30 | } else { 31 | objToConvertTo[key] = currentLookupKey; 32 | } 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/locales/types.ts: -------------------------------------------------------------------------------- 1 | export type ConvertedToObjectType = { 2 | [P in keyof T]: T[P] extends string ? string : ConvertedToObjectType; 3 | }; 4 | 5 | /** 6 | 7 | If you don't want non-existing keys to throw ts error you can simply do(also keeping the intellisense) 8 | 9 | export type ConvertedToObjectType = { 10 | [P in keyof T]: T[P] extends string ? string : ConvertedToObjectType; 11 | } & { 12 | [P: string]: any; 13 | }; 14 | 15 | */ 16 | 17 | // Selecting the json file that our intellisense would pick from 18 | export type TranslationJsonType = typeof import('./en/translation.json'); 19 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // To solve the issue: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31245 4 | /// 5 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | 7 | import 'react-app-polyfill/ie11'; 8 | import 'react-app-polyfill/stable'; 9 | 10 | import 'jest-styled-components'; 11 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/store/__tests__/configureStore.test.ts: -------------------------------------------------------------------------------- 1 | import { configureAppStore } from '../configureStore'; 2 | 3 | describe('configureStore', () => { 4 | it('should return a store with injected enhancers', () => { 5 | const store = configureAppStore(); 6 | expect(store).toEqual( 7 | expect.objectContaining({ 8 | runSaga: expect.any(Function), 9 | injectedReducers: expect.any(Object), 10 | injectedSagas: expect.any(Object), 11 | }), 12 | ); 13 | }); 14 | 15 | it('should return an empty store', () => { 16 | const store = configureAppStore(); 17 | expect(store.getState()).toBeUndefined(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/store/__tests__/reducer.test.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from '../reducers'; 2 | import { Reducer } from '@reduxjs/toolkit'; 3 | 4 | describe('reducer', () => { 5 | it('should inject reducers', () => { 6 | const dummyReducer = (s = {}, a) => 'dummyResult'; 7 | const reducer = createReducer({ test: dummyReducer } as any) as Reducer< 8 | any, 9 | any 10 | >; 11 | const state = reducer({}, ''); 12 | expect(state.test).toBe('dummyResult'); 13 | }); 14 | 15 | it('should return identity reducers when empty', () => { 16 | const reducer = createReducer() as Reducer; 17 | const state = { a: 1 }; 18 | const newState = reducer(state, ''); 19 | expect(newState).toBe(state); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/store/configureStore.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create the store with dynamic reducers 3 | */ 4 | 5 | import { 6 | configureStore, 7 | getDefaultMiddleware, 8 | StoreEnhancer, 9 | } from '@reduxjs/toolkit'; 10 | import { createInjectorsEnhancer } from 'redux-injectors'; 11 | import createSagaMiddleware from 'redux-saga'; 12 | 13 | import { createReducer } from './reducers'; 14 | 15 | export function configureAppStore() { 16 | const reduxSagaMonitorOptions = {}; 17 | const sagaMiddleware = createSagaMiddleware(reduxSagaMonitorOptions); 18 | const { run: runSaga } = sagaMiddleware; 19 | 20 | // Create the store with saga middleware 21 | const middlewares = [sagaMiddleware]; 22 | 23 | const enhancers = [ 24 | createInjectorsEnhancer({ 25 | createReducer, 26 | runSaga, 27 | }), 28 | ] as StoreEnhancer[]; 29 | 30 | const store = configureStore({ 31 | reducer: createReducer(), 32 | middleware: [...getDefaultMiddleware(), ...middlewares], 33 | devTools: process.env.NODE_ENV !== 'production', 34 | enhancers, 35 | }); 36 | 37 | return store; 38 | } 39 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/store/reducers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Combine all reducers in this file and export the combined reducers. 3 | */ 4 | 5 | import { combineReducers } from '@reduxjs/toolkit'; 6 | 7 | import { InjectedReducersType } from 'utils/types/injector-typings'; 8 | 9 | /** 10 | * Merges the main reducer with the router state and dynamically injected reducers 11 | */ 12 | export function createReducer(injectedReducers: InjectedReducersType = {}) { 13 | // Initially we don't have any injectedReducers, so returning identity function to avoid the error 14 | if (Object.keys(injectedReducers).length === 0) { 15 | return state => state; 16 | } else { 17 | return combineReducers({ 18 | ...injectedReducers, 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/styles/__tests__/media.test.ts: -------------------------------------------------------------------------------- 1 | import { media, sizes } from '../media'; 2 | import { css } from 'styled-components/macro'; 3 | 4 | describe('media', () => { 5 | it('should return media query in css', () => { 6 | const mediaQuery = `${media.small()}{color:red;}`; 7 | const cssVersion = css` 8 | @media (min-width: ${sizes.small}px) { 9 | color: red; 10 | } 11 | `.join(''); 12 | expect(mediaQuery).toMatch(cssVersion); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/styles/global-styles.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | export const GlobalStyle = createGlobalStyle` 4 | html, 5 | body { 6 | height: 100%; 7 | width: 100%; 8 | } 9 | 10 | body { 11 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 12 | } 13 | 14 | #root { 15 | min-height: 100%; 16 | min-width: 100%; 17 | } 18 | 19 | p, 20 | label { 21 | font-family: Georgia, Times, 'Times New Roman', serif; 22 | line-height: 1.5em; 23 | } 24 | 25 | input, select { 26 | font-family: inherit; 27 | font-size: inherit; 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/styles/media.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Media queries utility 3 | */ 4 | 5 | /* 6 | * Inspired by https://github.com/DefinitelyTyped/DefinitelyTyped/issues/32914 7 | */ 8 | 9 | // Update your breakpoints if you want 10 | export const sizes = { 11 | small: 600, 12 | medium: 1024, 13 | large: 1440, 14 | xlarge: 1920, 15 | }; 16 | 17 | // Iterate through the sizes and create min-width media queries 18 | export const media = (Object.keys(sizes) as Array).reduce( 19 | (acc, size) => { 20 | acc[size] = () => `@media (min-width:${sizes[size]}px)`; 21 | return acc; 22 | }, 23 | {} as { [key in keyof typeof sizes]: () => string }, 24 | ); 25 | 26 | /* Example 27 | const SomeDiv = styled.div` 28 | display: flex; 29 | .... 30 | ${media.medium} { 31 | display: block 32 | } 33 | `; 34 | */ 35 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/types/RootState.ts: -------------------------------------------------------------------------------- 1 | // [IMPORT NEW CONTAINERSTATE ABOVE] < Needed for generating containers seamlessly 2 | 3 | /* 4 | Because the redux-injectors injects your reducers asynchronously somewhere in your code 5 | You have to declare them here manually 6 | */ 7 | export interface RootState { 8 | // [INSERT NEW REDUCER KEY ABOVE] < Needed for generating containers seamlessly 9 | } 10 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from './RootState'; 2 | 3 | export type { RootState }; 4 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/utils/@reduxjs/toolkit.tsx: -------------------------------------------------------------------------------- 1 | import { RootStateKeyType } from '../types/injector-typings'; 2 | import { 3 | createSlice as createSliceOriginal, 4 | SliceCaseReducers, 5 | CreateSliceOptions, 6 | } from '@reduxjs/toolkit'; 7 | 8 | /* Wrap createSlice with stricter Name options */ 9 | 10 | /* istanbul ignore next */ 11 | export const createSlice = < 12 | State, 13 | CaseReducers extends SliceCaseReducers, 14 | Name extends RootStateKeyType, 15 | >( 16 | options: CreateSliceOptions, 17 | ) => { 18 | return createSliceOriginal(options); 19 | }; 20 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/utils/loadable.tsx: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense } from 'react'; 2 | 3 | interface Opts { 4 | fallback: React.ReactNode; 5 | } 6 | type Unpromisify = T extends Promise ? P : never; 7 | 8 | export const lazyLoad = < 9 | T extends Promise, 10 | U extends React.ComponentType, 11 | >( 12 | importFunc: () => T, 13 | selectorFunc?: (s: Unpromisify) => U, 14 | opts: Opts = { fallback: null }, 15 | ) => { 16 | let lazyFactory: () => Promise<{ default: U }> = importFunc; 17 | 18 | if (selectorFunc) { 19 | lazyFactory = () => 20 | importFunc().then(module => ({ default: selectorFunc(module) })); 21 | } 22 | 23 | const LazyComponent = lazy(lazyFactory); 24 | 25 | return (props: React.ComponentProps): JSX.Element => ( 26 | 27 | 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/utils/messages.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This function has two roles: 3 | * 1) If the `id` is empty it assings something so does i18next doesn't throw error. Typescript should prevent this anyway 4 | * 2) It has a hand-picked name `_t` (to be short) and should only be used while using objects instead of strings for translation keys 5 | * `internals/extractMessages/stringfyTranslations.js` script converts this to `t('a.b.c')` style before `i18next-scanner` scans the file contents 6 | * so that our json objects can also be recognized by the scanner. 7 | */ 8 | export const _t = (id: string, ...rest: any[]): [string, ...any[]] => { 9 | if (!id) { 10 | id = '_NOT_TRANSLATED_'; 11 | } 12 | return [id, ...rest]; 13 | }; 14 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/utils/redux-injectors.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useInjectReducer as useReducer, 3 | useInjectSaga as useSaga, 4 | } from 'redux-injectors'; 5 | import { 6 | InjectReducerParams, 7 | InjectSagaParams, 8 | RootStateKeyType, 9 | } from './types/injector-typings'; 10 | 11 | /* Wrap redux-injectors with stricter types */ 12 | 13 | export function useInjectReducer( 14 | params: InjectReducerParams, 15 | ) { 16 | return useReducer(params); 17 | } 18 | 19 | export function useInjectSaga(params: InjectSagaParams) { 20 | return useSaga(params); 21 | } 22 | -------------------------------------------------------------------------------- /internals/startingTemplate/src/utils/types/injector-typings.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from 'types'; 2 | import { Saga } from 'redux-saga'; 3 | import { SagaInjectionModes } from 'redux-injectors'; 4 | import { Reducer, AnyAction } from '@reduxjs/toolkit'; 5 | 6 | type RequiredRootState = Required; 7 | 8 | export type RootStateKeyType = keyof RootState; 9 | 10 | export type InjectedReducersType = { 11 | [P in RootStateKeyType]?: Reducer; 12 | }; 13 | export interface InjectReducerParams { 14 | key: Key; 15 | reducer: Reducer; 16 | } 17 | 18 | export interface InjectSagaParams { 19 | key: RootStateKeyType | string; 20 | saga: Saga; 21 | mode?: SagaInjectionModes; 22 | } 23 | -------------------------------------------------------------------------------- /internals/startingTemplate/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": false, 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "jsx": "react", 19 | "baseUrl": "./src" 20 | }, 21 | "include": ["src"], 22 | "ts-node": { 23 | "compilerOptions": { 24 | "esModuleInterop": true, 25 | "module": "commonjs", 26 | "moduleResolution": "node", 27 | "noEmit": true, 28 | "allowSyntheticDefaultImports": true 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /internals/testing/generators/componentVariations.ts: -------------------------------------------------------------------------------- 1 | import { ComponentProptNames } from '../../generators/component'; 2 | 3 | type ComponentVariationType = { [P in ComponentProptNames]: any }[]; 4 | 5 | const containerNameBase = `GeneratorTestingComponent`; 6 | 7 | export const componentVariations = (): ComponentVariationType => { 8 | const variations: ComponentVariationType = []; 9 | 10 | // Test all the component generator options against each other 11 | const allCombinations = permuatateBooleans(5); 12 | for (let i = 0; i < allCombinations.length; i++) { 13 | const values = allCombinations[i]; 14 | variations.push({ 15 | componentName: `${containerNameBase}${i}`, 16 | path: ``, 17 | wantLoadable: values[0], 18 | wantMemo: values[1], 19 | wantStyledComponents: values[2], 20 | wantTests: values[3], 21 | wantTranslations: values[4], 22 | }); 23 | } 24 | 25 | // Test some paths 26 | const paths = [ 27 | '/components', 28 | '/pages/HomePage/Features', 29 | '/pages/HomePage/Features/GithubRepoForm', 30 | ]; 31 | for (let i = 0; i < paths.length; i++) { 32 | const path = paths[i]; 33 | variations.push({ 34 | componentName: `${containerNameBase}${i}`, 35 | path: `${path}`, 36 | wantLoadable: true, 37 | wantMemo: true, 38 | wantStyledComponents: true, 39 | wantTests: true, 40 | wantTranslations: true, 41 | }); 42 | } 43 | return variations; 44 | }; 45 | 46 | // Create true, false permutation of a length in an array form 47 | export function permuatateBooleans(length: number) { 48 | const array: boolean[][] = []; 49 | for (let i = 0; i < 1 << length; i++) { 50 | const items: boolean[] = []; 51 | for (let j = length - 1; j > 0; j--) { 52 | items.push(!!(i & (1 << j))); 53 | } 54 | array.push([...items, !!(i & 1)]); 55 | } 56 | return array; 57 | } 58 | -------------------------------------------------------------------------------- /internals/testing/generators/sliceVariations.ts: -------------------------------------------------------------------------------- 1 | import { SliceProptNames } from '../../generators/slice'; 2 | 3 | type SliceVariationType = { [P in SliceProptNames]: any }[]; 4 | 5 | const sliceNameBase = `generatorTestingSlice`; 6 | 7 | export const sliceVariations = (): SliceVariationType => { 8 | const variations: SliceVariationType = [ 9 | { 10 | sliceName: `${sliceNameBase}1`, 11 | path: ``, 12 | wantSaga: true, 13 | }, 14 | { 15 | sliceName: `${sliceNameBase}2`, 16 | path: `/pages/HomePage`, 17 | wantSaga: false, 18 | }, 19 | { 20 | sliceName: `${sliceNameBase}3`, 21 | path: `/pages/HomePage/Features`, 22 | wantSaga: true, 23 | }, 24 | ]; 25 | 26 | return variations; 27 | }; 28 | -------------------------------------------------------------------------------- /internals/testing/loadable.mock.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export function ExportedFunc() { 4 | return
My lazy-loaded component
; 5 | } 6 | export default ExportedFunc; 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-boilerplate/react-boilerplate-cra-template/535c85448e4fba15a3099dcd079fabf554050329/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 24 | 25 | 26 | 30 | React Boilerplate Example App 31 | 35 | 36 | 37 | 38 |
39 | 40 | 41 | 45 | 46 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-boilerplate/react-boilerplate-cra-template/535c85448e4fba15a3099dcd079fabf554050329/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-boilerplate/react-boilerplate-cra-template/535c85448e4fba15a3099dcd079fabf554050329/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/app/__tests__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` should render and match the snapshot 1`] = ` 4 | 5 | 17 | 21 | 22 | 23 | } 25 | path="/" 26 | /> 27 | } 29 | path="*" 30 | /> 31 | 32 | 33 | 34 | `; 35 | -------------------------------------------------------------------------------- /src/app/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { createRenderer } from 'react-test-renderer/shallow'; 3 | 4 | import { App } from '../index'; 5 | 6 | const renderer = createRenderer(); 7 | 8 | describe('', () => { 9 | it('should render and match the snapshot', () => { 10 | renderer.render(); 11 | const renderedOutput = renderer.getRenderOutput(); 12 | expect(renderedOutput).toMatchSnapshot(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/app/components/A/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | 4 | import { A } from '../index'; 5 | import { themes } from 'styles/theme/themes'; 6 | import { DefaultTheme } from 'styled-components'; 7 | 8 | const renderWithTheme = (theme?: DefaultTheme) => 9 | render(); 10 | 11 | describe('', () => { 12 | it('should render an tag', () => { 13 | const a = renderWithTheme(); 14 | expect(a.container.querySelector('a')).toBeInTheDocument(); 15 | }); 16 | 17 | it('should have theme', () => { 18 | const a = renderWithTheme(); 19 | expect(a.container.firstChild).toHaveStyle( 20 | `color: ${themes.light.primary}`, 21 | ); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/components/A/index.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro'; 2 | 3 | export const A = styled.a` 4 | color: ${p => p.theme.primary}; 5 | text-decoration: none; 6 | 7 | &:hover { 8 | text-decoration: underline; 9 | opacity: 0.8; 10 | } 11 | 12 | &:active { 13 | opacity: 0.4; 14 | } 15 | `; 16 | -------------------------------------------------------------------------------- /src/app/components/FormLabel/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | 4 | import { FormLabel } from '../index'; 5 | import { themes } from 'styles/theme/themes'; 6 | import { DefaultTheme } from 'styled-components'; 7 | 8 | const renderWithTheme = (theme?: DefaultTheme) => 9 | render(); 10 | 11 | describe('', () => { 12 | it('should render an 25 | HeaderLink 26 | 27 | `; 28 | -------------------------------------------------------------------------------- /src/app/components/Link/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | 4 | import { Link } from '../index'; 5 | import { themes } from 'styles/theme/themes'; 6 | import { DefaultTheme } from 'styled-components'; 7 | import { MemoryRouter } from 'react-router-dom'; 8 | 9 | const renderWithTheme = (theme?: DefaultTheme) => { 10 | return render( 11 | 12 | 13 | HeaderLink 14 | 15 | , 16 | ); 17 | }; 18 | 19 | describe('', () => { 20 | it('should match snapshot', () => { 21 | const link = renderWithTheme(); 22 | expect(link.container.firstChild).toMatchSnapshot(); 23 | }); 24 | 25 | it('should have theme', () => { 26 | const link = renderWithTheme(); 27 | expect(link.container.firstChild).toHaveStyle( 28 | `color: ${themes.light.primary}`, 29 | ); 30 | }); 31 | 32 | it('should have a class attribute', () => { 33 | const link = renderWithTheme(); 34 | expect(link.queryByText('HeaderLink')).toHaveAttribute('class'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/app/components/Link/index.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro'; 2 | import { Link as RouterLink } from 'react-router-dom'; 3 | 4 | export const Link = styled(RouterLink)` 5 | color: ${p => p.theme.primary}; 6 | text-decoration: none; 7 | 8 | &:hover { 9 | text-decoration: underline; 10 | opacity: 0.8; 11 | } 12 | 13 | &:active { 14 | opacity: 0.4; 15 | } 16 | `; 17 | -------------------------------------------------------------------------------- /src/app/components/LoadingIndicator/__tests__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` should match snapshot 1`] = ` 4 | .c0 { 5 | -webkit-animation: cqkDSr 2.625s linear infinite; 6 | animation: cqkDSr 2.625s linear infinite; 7 | height: 3rem; 8 | width: 3rem; 9 | -webkit-transform-origin: center; 10 | -ms-transform-origin: center; 11 | transform-origin: center; 12 | } 13 | 14 | .c1 { 15 | -webkit-animation: caVMmK 1.5s ease-in-out infinite; 16 | animation: caVMmK 1.5s ease-in-out infinite; 17 | stroke: rgba(215,113,88,1); 18 | stroke-linecap: round; 19 | } 20 | 21 | 25 | 33 | 34 | `; 35 | 36 | exports[` should match snapshot when props changed 1`] = ` 37 | .c0 { 38 | -webkit-animation: cqkDSr 2.625s linear infinite; 39 | animation: cqkDSr 2.625s linear infinite; 40 | height: 1.25rem; 41 | width: 1.25rem; 42 | -webkit-transform-origin: center; 43 | -ms-transform-origin: center; 44 | transform-origin: center; 45 | } 46 | 47 | .c1 { 48 | -webkit-animation: caVMmK 1.5s ease-in-out infinite; 49 | animation: caVMmK 1.5s ease-in-out infinite; 50 | stroke: rgba(215,113,88,1); 51 | stroke-linecap: round; 52 | } 53 | 54 | 58 | 66 | 67 | `; 68 | -------------------------------------------------------------------------------- /src/app/components/LoadingIndicator/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | 4 | import { LoadingIndicator } from '../index'; 5 | import { themes } from 'styles/theme/themes'; 6 | import { DefaultTheme, ThemeProvider } from 'styled-components'; 7 | 8 | const renderWithTheme = ( 9 | props: Parameters[number] = {}, 10 | theme?: DefaultTheme, 11 | ) => 12 | render( 13 | 14 | 15 | , 16 | ); 17 | 18 | describe('', () => { 19 | it('should match snapshot', () => { 20 | const loadingIndicator = renderWithTheme(); 21 | expect(loadingIndicator.container.firstChild).toMatchSnapshot(); 22 | }); 23 | 24 | it('should match snapshot when props changed', () => { 25 | const loadingIndicator = renderWithTheme({ small: true }); 26 | expect(loadingIndicator.container.firstChild).toMatchSnapshot(); 27 | }); 28 | 29 | it('should have theme', () => { 30 | const loadingIndicator = renderWithTheme(); 31 | expect(loadingIndicator.container.querySelector('circle')).toHaveStyle( 32 | `stroke: ${themes.light.primary}`, 33 | ); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/components/LoadingIndicator/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled, { keyframes } from 'styled-components/macro'; 3 | 4 | interface Props extends SvgProps {} 5 | 6 | export const LoadingIndicator = (props: Props) => ( 7 | 8 | 9 | 10 | ); 11 | 12 | const speed = 1.5; 13 | 14 | const rotate = keyframes` 15 | 100% { 16 | transform: rotate(360deg); 17 | } 18 | `; 19 | 20 | const dash = keyframes` 21 | 0% { 22 | stroke-dasharray: 0, 150; 23 | stroke-dashoffset: 0; 24 | } 25 | 50% { 26 | stroke-dasharray: 100, 150; 27 | stroke-dashoffset: -24; 28 | } 29 | 100% { 30 | stroke-dasharray: 0, 150; 31 | stroke-dashoffset: -124; 32 | } 33 | `; 34 | 35 | interface SvgProps { 36 | small?: boolean; 37 | } 38 | 39 | const Svg = styled.svg` 40 | animation: ${rotate} ${speed * 1.75}s linear infinite; 41 | height: ${p => (p.small ? '1.25rem' : '3rem')}; 42 | width: ${p => (p.small ? '1.25rem' : '3rem')}; 43 | transform-origin: center; 44 | `; 45 | 46 | const Circle = styled.circle` 47 | animation: ${dash} ${speed}s ease-in-out infinite; 48 | stroke: ${p => p.theme.primary}; 49 | stroke-linecap: round; 50 | `; 51 | -------------------------------------------------------------------------------- /src/app/components/NavBar/Logo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components/macro'; 3 | 4 | export function Logo() { 5 | return ( 6 | 7 | React Boilerplate 8 | Create React App Template 9 | 10 | ); 11 | } 12 | 13 | const Wrapper = styled.div` 14 | display: flex; 15 | align-items: center; 16 | `; 17 | 18 | const Title = styled.div` 19 | font-size: 1.25rem; 20 | color: ${p => p.theme.text}; 21 | font-weight: bold; 22 | margin-right: 1rem; 23 | `; 24 | 25 | const Description = styled.div` 26 | font-size: 0.875rem; 27 | color: ${p => p.theme.textSecondary}; 28 | font-weight: normal; 29 | `; 30 | -------------------------------------------------------------------------------- /src/app/components/NavBar/Nav.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components/macro'; 3 | import { ReactComponent as DocumentationIcon } from './assets/documentation-icon.svg'; 4 | import { ReactComponent as GithubIcon } from './assets/github-icon.svg'; 5 | 6 | export function Nav() { 7 | return ( 8 | 9 | 15 | 16 | Documentation 17 | 18 | 24 | 25 | Github 26 | 27 | 28 | ); 29 | } 30 | 31 | const Wrapper = styled.nav` 32 | display: flex; 33 | margin-right: -1rem; 34 | `; 35 | 36 | const Item = styled.a` 37 | color: ${p => p.theme.primary}; 38 | cursor: pointer; 39 | text-decoration: none; 40 | display: flex; 41 | padding: 0.25rem 1rem; 42 | font-size: 0.875rem; 43 | font-weight: 500; 44 | align-items: center; 45 | 46 | &:hover { 47 | opacity: 0.8; 48 | } 49 | 50 | &:active { 51 | opacity: 0.4; 52 | } 53 | 54 | .icon { 55 | margin-right: 0.25rem; 56 | } 57 | `; 58 | -------------------------------------------------------------------------------- /src/app/components/NavBar/__tests__/Logo.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { Logo } from '../Logo'; 4 | 5 | describe('', () => { 6 | it('should match snapshot', () => { 7 | const logo = render(); 8 | expect(logo.container.firstChild).toMatchSnapshot(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/app/components/NavBar/__tests__/Nav.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { Nav } from '../Nav'; 4 | import { MemoryRouter } from 'react-router-dom'; 5 | 6 | describe('