├── .editorconfig ├── .gitattributes ├── .github ├── auto_assign.yml ├── dependabot.yml ├── stale.yml └── workflows │ └── ci.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc ├── .markdownlint.json ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .stylelintignore ├── .stylelintrc ├── .vscode ├── extensions.json ├── settings.json ├── typescript.code-snippets └── typescriptreact.code-snippets ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bun.lock ├── bunfig.toml ├── commitlint.config.ts ├── env └── .env.example ├── eslint.config.js ├── index.html ├── package.json ├── public ├── icons │ └── favicon.svg └── mockServiceWorker.js ├── src ├── App │ ├── App.css │ ├── App.spec.tsx │ ├── App.tsx │ ├── index.ts │ └── react.svg ├── __mocks__ │ ├── README.md │ ├── browser.ts │ ├── handlers.ts │ └── server.ts ├── index.css ├── main.tsx ├── reportWebVitals.ts ├── setup-test.ts └── vite-env.d.ts ├── tsconfig.json ├── vite.config.ts └── vitest.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.lockb binary diff=lockb 2 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | # Set to true to add reviewers to pull requests 2 | addReviewers: true 3 | 4 | # Set to true to add assignees to pull requests 5 | addAssignees: author 6 | 7 | # A list of reviewers to be added to pull requests (GitHub user name) 8 | #reviewers: 9 | 10 | 11 | # A number of reviewers added to the pull request 12 | # Set 0 to add all the reviewers (default: 0) 13 | numberOfReviewers: 0 14 | 15 | # A list of assignees, overrides reviewers if set 16 | # assignees: 17 | # - assigneeA 18 | 19 | # A number of assignees to add to the pull request 20 | # Set to 0 to add all of the assignees. 21 | # Uses numberOfReviewers if unset. 22 | # numberOfAssignees: 2 23 | 24 | # A list of keywords to be skipped the process that add reviewers if pull requests include it 25 | # skipKeywords: 26 | # - wip 27 | 28 | filterLabels: 29 | exclude: 30 | - dependencies 31 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an Issue or Pull Request becomes stale 2 | daysUntilStale: 90 3 | # Number of days of inactivity before a stale Issue or Pull Request is closed 4 | daysUntilClose: 30 5 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking as stale 10 | staleLabel: stale 11 | # Comment to post when marking as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when removing the stale label. Set to `false` to disable 17 | unmarkComment: false 18 | # Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable 19 | closeComment: true 20 | # Limit to only `issues` or `pulls` 21 | only: issues 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs on every push to the staging and main branches 2 | # and on pull requests targeting those branches. 3 | # It installs dependencies, runs linting, tests, and builds the project using Bun. 4 | name: 'CI: Pre-Merge Validations' 5 | 6 | on: 7 | push: 8 | branches: 9 | - staging 10 | - main 11 | pull_request: 12 | branches: 13 | - staging 14 | - main 15 | 16 | # cancel in-progress runs on new commits to same PR (gitub.event.number) 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.event.number || github.sha }} 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | run: 23 | name: Install, lint, test, and build on Bun ${{ matrix.os }} 24 | 25 | runs-on: ${{ matrix.os }} 26 | strategy: 27 | matrix: 28 | os: [ubuntu-latest] 29 | 30 | steps: 31 | - name: Checkout repo 32 | uses: actions/checkout@v3 33 | 34 | - name: Setup Bun and install dependencies 35 | uses: oven-sh/setup-bun@v1 36 | with: 37 | bun-version: latest 38 | 39 | - name: Install 40 | run: bun install 41 | 42 | - name: Lint 43 | run: bun run lint-staged 44 | 45 | - name: Test 46 | run: bun run test 47 | 48 | - name: Build 49 | run: bun run build 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | # project files 11 | node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | .eslintcache 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | !.vscode/settings.json 21 | !.vscode/typescript.code-snippets 22 | !.vscode/typescriptreact.code-snippets 23 | .idea 24 | .DS_Store 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | 31 | # test reports 32 | coverage 33 | /coverage/ 34 | **/coverage/* 35 | 36 | # ambient vars 37 | .env 38 | .env.local 39 | .env.production 40 | .env.staging 41 | 42 | # PWA dev files 43 | /dev-dist 44 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | bun --no -- commitlint --edit "$1" 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | bun run test && bun run build 2 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{html,md,json,jsonc}": ["bun run prettier:fix"], 3 | "*.{ts,tsx}": ["bun run prettier:fix", "bun run lint:fix"], 4 | "*.css": ["bun run lint:css"] 5 | } 6 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "md025": false, 3 | "md013": false, 4 | "md036": false 5 | } 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | .eslintcache 5 | yarn.lock 6 | package-lock.json 7 | pnpm-lock.yaml 8 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | import { default as eruptionPrettierConfig } from '@eruptionjs/config/prettier' 2 | 3 | export default { ...eruptionPrettierConfig } 4 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | # test reports 2 | coverage 3 | /coverage/ 4 | **/coverage/* 5 | 6 | # project files 7 | node_modules 8 | dist 9 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard"], 3 | "fix": true, 4 | "rules": { 5 | "selector-class-pattern": "^([a-z][a-z0-9]*)((--?|__?|_)?[a-z0-9]+)*$" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "EditorConfig.EditorConfig", 8 | "mikestead.dotenv", 9 | "dbaeumer.vscode-eslint", 10 | "christian-kohler.npm-intellisense", 11 | "esbenp.prettier-vscode", 12 | "natqe.reload", 13 | "mattpocock.ts-error-translator", 14 | "antfu.vite", 15 | "stylelint.vscode-stylelint", 16 | "ms-azuretools.vscode-docker" 17 | ], 18 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 19 | "unwantedRecommendations": [] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // update on import files 3 | "javascript.updateImportsOnFileMove.enabled": "always", 4 | "typescript.updateImportsOnFileMove.enabled": "always", 5 | "typescript.preferences.importModuleSpecifier": "non-relative", 6 | // auto format code 7 | "editor.formatOnPaste": true, 8 | "editor.formatOnType": true, 9 | "editor.formatOnSave": true, 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll.eslint": "explicit", 12 | "source.fixAll.tslint": "explicit", 13 | "source.fixAll.stylelint": "explicit", 14 | "source.organizeImports": "never" 15 | }, 16 | // Standard settings 17 | "javascript.validate.enable": false, 18 | "html.format.indentHandlebars": true, 19 | "html.format.indentInnerHtml": true, 20 | "html.format.templating": true, 21 | // prettier settings 22 | "prettier.requireConfig": true, 23 | "prettier.prettierPath": "./node_modules/prettier", 24 | // doc formatters 25 | "[html]": { 26 | "editor.defaultFormatter": "vscode.html-language-features" 27 | }, 28 | "[css]": { 29 | "editor.defaultFormatter": "vscode.css-language-features" 30 | }, 31 | "[javascript]": { 32 | "editor.defaultFormatter": "esbenp.prettier-vscode" 33 | }, 34 | "[json]": { 35 | "editor.defaultFormatter": "esbenp.prettier-vscode" 36 | }, 37 | "[typescript]": { 38 | "editor.defaultFormatter": "vscode.typescript-language-features" 39 | }, 40 | "[typescriptreact]": { 41 | "editor.defaultFormatter": "esbenp.prettier-vscode" 42 | }, 43 | "[javascriptreact]": { 44 | "editor.defaultFormatter": "vscode.typescript-language-features" 45 | }, 46 | "[jsonc]": { 47 | "editor.defaultFormatter": "esbenp.prettier-vscode" 48 | }, 49 | 50 | "eslint.format.enable": true, 51 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"] 52 | } 53 | -------------------------------------------------------------------------------- /.vscode/typescript.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "import": { 3 | "prefix": ["imp"], 4 | "body": "import ${2:module} from '${1}'$0" 5 | }, 6 | "describe": { 7 | "prefix": ["desc"], 8 | "body": ["describe('$1', () => {", " it('should $2', () => {", " $3", " })", "})$0"] 9 | }, 10 | "it test closure": { 11 | "prefix": ["it"], 12 | "body": ["it('should $1', () => {", " $2", "})$0"] 13 | }, 14 | "function": { 15 | "prefix": ["fc"], 16 | "body": ["function ${1:name}() {", "$2", " return $3", "}", "$0"] 17 | }, 18 | "functionProps": { 19 | "prefix": ["fcp"], 20 | "body": ["function ${1:name}(${2:props}) {", "$3", " return $4", "}", "$0"] 21 | }, 22 | "functionPropsTyped": { 23 | "prefix": ["fcpt"], 24 | "body": [ 25 | "function ${1:Component}(${2:props}): ${3:returnValue} {", 26 | " $4", 27 | " return $5", 28 | "}", 29 | "$0" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.vscode/typescriptreact.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "importReact": { 3 | "prefix": ["imr"], 4 | "body": ["import { $1 } from 'react'$0"] 5 | }, 6 | "importReactComponent": { 7 | "prefix": ["imrc"], 8 | "body": ["import { $1, Component } from 'react'$0"] 9 | }, 10 | "importReactState": { 11 | "prefix": ["imrs"], 12 | "body": ["import { $1, useState } from 'react'$0"] 13 | }, 14 | "importReactEffect": { 15 | "prefix": ["imre"], 16 | "body": ["import { $1, useEffect } from 'react'$0"] 17 | }, 18 | "importReactStateEffect": { 19 | "prefix": ["imrse"], 20 | "body": ["import { $1, useEffect, useState } from 'react'$0"] 21 | }, 22 | "useState": { 23 | "prefix": ["usf", "usestate"], 24 | "body": [ 25 | "const [${1}, set${1/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}] = useState(${2:initialState})$0" 26 | ] 27 | }, 28 | "useStateProps": { 29 | "prefix": ["uspf", "usestateprops"], 30 | "body": [ 31 | "const [${1}, set${1/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}] = useState<${3:Props}>(${2:initialState})$0" 32 | ] 33 | }, 34 | "useEffect": { 35 | "prefix": ["uef", "useeffect"], 36 | "body": ["useEffect(() => {", " $2", "}, [${1:deps}])$0"] 37 | }, 38 | "useReducer": { 39 | "prefix": ["urf", "usereducer"], 40 | "body": ["const [state, dispatch] = useReducer(${1:reducer}, ${2:initialState})$0"] 41 | }, 42 | "useRef": { 43 | "prefix": ["uref", "useref"], 44 | "body": ["const ${1:ref} = useRef($2)$0"] 45 | }, 46 | "useCallback": { 47 | "prefix": ["ucf", "usecallback"], 48 | "body": ["const ${1:callback} = useCallback(() => {", " $3", "}, [${2:deps}])$0"] 49 | }, 50 | "useMemo": { 51 | "prefix": ["umf", "umemo", "usememo"], 52 | "body": ["const ${1:value} = useMemo(() => $3, [${2:deps}])$0"] 53 | }, 54 | "classComponentRender": { 55 | "prefix": ["ccr"], 56 | "body": [ 57 | "class $1 extends Component {", 58 | " render() {", 59 | " return ($2)", 60 | " }", 61 | "}", 62 | "", 63 | "export default $1", 64 | "$0" 65 | ] 66 | }, 67 | "classComponentRenderConstructor": { 68 | "prefix": ["ccc"], 69 | "body": [ 70 | "class $1 extends Component {", 71 | " constructor(props) {", 72 | " super(props)", 73 | " this.state = { $2 }", 74 | " }", 75 | " render() {", 76 | " return ($3)", 77 | " }", 78 | "}", 79 | "", 80 | "export default $1", 81 | "$0" 82 | ] 83 | }, 84 | "statelessFunctionalComponent": { 85 | "prefix": ["sfc"], 86 | "body": [ 87 | "const ${1:Component}: FC = (${2:props}) => {", 88 | "", 89 | " $4", 90 | "", 91 | " return ($3)", 92 | "}", 93 | "", 94 | "export default ${1:Component}", 95 | "$0" 96 | ] 97 | }, 98 | "FunctionalComponentProps": { 99 | "prefix": ["fcp"], 100 | "body": [ 101 | "import { FC, Children } from 'react'", 102 | "", 103 | "type ${1:Component}Props = {", 104 | " children?: typeof Children", 105 | "}", 106 | "", 107 | "const ${1:Component}: FC<${1:Component}Props> = ({ children }): JSX.Element => {", 108 | "", 109 | " $2", 110 | "", 111 | " return (", 112 | " <>", 113 | " {children}", 114 | " ", 115 | " )", 116 | "}", 117 | "", 118 | "export { ${1:Component} }", 119 | "$0" 120 | ] 121 | }, 122 | "statelessFunctionalComponentProps": { 123 | "prefix": ["sfcp"], 124 | "body": [ 125 | "const ${1:Component}: FC<${2:Props}> = (${3:props}) => {", 126 | "", 127 | " $5", 128 | "", 129 | " return ($4)", 130 | "}", 131 | "", 132 | "export default ${1:Component}", 133 | "$0" 134 | ] 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.11.0 4 | 5 | - uses bun as default on project config 6 | 7 | ## v1.10.0 8 | 9 | - chore(rc/1.10): upgrade deps to latest by @devmozao in #61 10 | 11 | ## v1.9.0 12 | 13 | Bump on dependencies to latest. Only major change that's worth noting: husky v9. 14 | 15 | ## v1.8.0 16 | 17 | - feat(dx): add qr-code on the dev server by @raisiqueira in #55 18 | - chore(packages): update msw version by @raisiqueira in #54 19 | - chore(ci): remove ci cache by @raisiqueira in #57 20 | - chore(rc-v1.8): first rc with dep bumps, everything working as - - expected on dev by @devmozao in #56 21 | 22 | ## v1.7.0 23 | 24 | - upgrade vite to v5 25 | - improved bundle optimization splitting chunks that are common and doesn't change much, like react, react-dom and react/jsx 26 | - add treemap bundle analyzer 27 | - improved linters 28 | - enforced linters before build: it must succeed before commit and deploying with actions 29 | - removed dep `@fvilers/disable-react-devtools` and implemented the snippet directly on `index.html` at root, in order to optimize bundle size 30 | - improved some script commands to improve DX 31 | - minor change on `.eslintrc` in order to prepare to the v9 changes (it will be huge) 32 | - upgrade to node v20 (🥳🎉) 33 | 34 | ### to the future 35 | 36 | - improve eslint rules by visiting each rule and each plugin and enforce them in a friendly way 37 | - implement strict typescript lint and check with tsc itself 38 | - apply optimizations for assets compression (brotli, gzip) 39 | - apply optimizations for other assets like images and fonts (specially fonts injected by third parties) 40 | - implement qr-code to open application on mobile and facilitate dev testing for responsives 41 | - check msw workflow with vitest/testing-lib 42 | - check if css modules will work in chunks/modules as desired 43 | 44 | ## v1.6.0 45 | 46 | - Updated dependencies to support Node v20 with NPM v10. 47 | - Upgraded to Typescript v5. 48 | - Upgraded several libs to the latest versions (no major changes). 49 | - The dependency "@vitest/coverage-c8" was moved to "@vitest/coverage-v8", but without breaking changes. 50 | - Prettier now requires that "@trivago/prettier-plugin-sort-imports" must be explicit configured as a plugin (wasn't before). 51 | - Added a new script command called "host", to support vite host mode. 52 | - Added a new script called "format:check", to check if the code is formatted on lintstaged. 53 | - Upgraded lintstaged, there was a typo on it for css formatting. 54 | - Added a new code snippet for function components autocomplete. 55 | - Increased rules for a better approach with commintlint usage. 56 | - Increased eslint rules to ensure some good practices. (still a working in progress) 57 | 58 | ### To the future 59 | 60 | - A better and greater eslint ruleset- We aim to have a great DX, but also a great code quality, and that means a more incisive and codebase quality control over time. 61 | - As we learn how to deal with postcss on mantine 7, those learns will also reflect here, not only for css-in-js cases. 62 | - We still need to add a better support for msw, but we are still learning how to use it properly. 63 | - Also we need to add a better support for testing, it's still a pain to work with it in general. 64 | - Our goal to have an opinionated and full swiss army knife version of Eruption ready to use, is still a work in progress. We don't have ATM the time to work on it, but we will get there eventually. 65 | 66 | ## v1.5.0 67 | 68 | - Upgraded several libs to the latest version (Hello Vite 4.3). 69 | - Update the main README to use the create-eruption CLI. 70 | - Added GitHub Actions. 71 | 72 | ## v1.4.0 73 | 74 | - Upgraded several libs to the latest version. 75 | - Stylelint v15 removed all style rules, making `stylelint-config-prettier` obsolete. 76 | - Several new `eslint-import` rules added for DX QoL. 77 | 78 | ## v1.3.0 79 | 80 | - Upgraded Node to latest LTS version, v18 (hydrogen). 81 | - Upgraded Vite to v4. 82 | - Changed vite-react plugin to vite-react-swc. 83 | - Bumped dev dependencies to latest. 84 | 85 | ## v1.2.0 86 | 87 | - Added msw as a dev dep, but still missing default config. 88 | - Changed minor configs to match vite@latest with focus on DX. 89 | 90 | ## v1.1.0 91 | 92 | - Vite depedency updated to from v2 to v3. 93 | - Fixed missing .sh files for husky pre-commit and commit-msg hook. 94 | - Added missing config for promp-cli at commitlint. 95 | 96 | ## v1.0.0 97 | 98 | - Eruption First Release. 99 | ༼ つ ◕_◕ ༽つ🌋 100 | 101 | ## Know issue 102 | 103 | 1. on vitest/globals conflicting with testing-lib/jest-dom when running for build/preview scripts. Solutions provided were not sufficient to keep both globals working properly. As a fix, `"skipLibCheck": true,` from `tsconfig.json` was changed to `false`, in order to keep running those scripts. 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2022-present, Diogo "Mozão" Fonseca and further contributors 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 | 23 | June 20th, 2022. 24 | Osasco/SP - Brazil 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eruption 🌋 2 | 3 | > Boilerplate for React/Typescript, built on top of Vite ⚡️ 4 | 5 | _It's fast! Even the tests are fast, thanks to Vite with Vitest ⚡️_ 6 | 7 | # What's in the boilerplate 8 | 9 | - [Vite](https://vitejs.dev/) 10 | - [React](https://reactjs.org/) 11 | - [Typescript](https://www.typescriptlang.org/) 12 | - [Vitest](https://vitest.dev/) 13 | - [Testing Library](https://testing-library.com/) 14 | - Dev Tools 15 | - [ESLint](https://eslint.org/) 16 | - [Prettier](https://prettier.io/) 17 | - [CommitLint](https://commitlint.js.org/#/) 18 | - [Husky](https://typicode.github.io/husky/#/) 19 | - [Lint-Staged](https://github.com/okonet/lint-staged) 20 | 21 | # Installation 22 | 23 | There are two ways to install Eruption: using the template directly from GitHub (through the "Use Template" button), or using the [CLI](https://www.npmjs.com/package/create-eruption). 24 | 25 | If you want to use the CLI, run the following command on your terminal: 26 | 27 | ```Bash 28 | npm init eruption@latest 29 | ``` 30 | 31 | then, to start the project 32 | 33 | ```Bash 34 | cd your-project-name 35 | npm install 36 | npm run dev 37 | ``` 38 | 39 | ## Try it online 40 | 41 | Want to try Eruption without clone local? Try it on [StackBlitz](https://stackblitz.com/fork/github/eruptionjs/core) 42 | 43 | # Commits 44 | 45 | This project have commits configured to follow the Conventional Commits's best practice and it's configured with ESLint, Prettier and Stylelint. 46 | 47 | To commit, you must follow the convention `[optional scope]: `. In practice, it would be as follow: 48 | 49 | ```git 50 | git commit -m "feat: add button component" 51 | ``` 52 | 53 | Then, Husky will start the pre-commit hook and run lint-staged, who will run `prettier`, `lint` and `stylelint` to validate code format and code lint. If you fail to follow any of these validations, the commit will be aborted. 54 | 55 | After that, if everything is validated correctly, Husky will proceed with the commit-msg hook, where it will evaluate if your commit message is following the Conventional Commit's best practice and later run the tests of your project. If any of the tests are broken, the commit will be aborted. You must fix the tests before proceed. 56 | 57 | You can also commit your files with the help of the CLI. To do so, just run `npm run commit`. From there, the CLI will assist you in the process. As before: if your changes fails the validation, you must fix it before proceed. 58 | 59 | As a best practice, it is strongly recommended that you do not skip the validations. If you need to change the way your commit messages are written, just go to file `commitlint.config.ts` and you will find there the config needed. 60 | 61 | Check out [commitlint](https://commitlint.js.org/#/) docs to see further configurations that you can do. 62 | 63 | # Motivation 64 | 65 | Everything started because I was in need of a good, solid, reliable and fast boilerplate to work with React/Typescript projects. I was working with Create-React-App and Webpack but both of them wasn't that good at all, specially in performance. Later on I discovered that I could use Vite to replace Webpack, so here we are now. =) 66 | 67 | I believe that Eruption as it is right now, is an excellent starting point to any React/Typescript project, with enough dev tools to help you to write the best software possible and ship to production without any headaches. 68 | 69 | # License 70 | 71 | MIT 72 | -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eruptionjs/core/8d10047de63e357f21a6c1787425942fd4d4e8bc/bunfig.toml -------------------------------------------------------------------------------- /commitlint.config.ts: -------------------------------------------------------------------------------- 1 | import { type UserConfig } from '@commitlint/types' 2 | 3 | const types = [ 4 | 'build', 5 | 'chore', 6 | 'ci', 7 | 'docs', 8 | 'feat', 9 | 'fix', 10 | 'perf', 11 | 'refactor', 12 | 'revert', 13 | 'style', 14 | 'test', 15 | 'spike', 16 | ] 17 | 18 | const Configuration: UserConfig = { 19 | extends: ['@commitlint/config-conventional'], 20 | rules: { 21 | 'type-empty': [2, 'never'], 22 | 'type-case': [2, 'always', 'lower-case'], 23 | 'scope-empty': [2, 'never'], 24 | 'scope-case': [2, 'always', 'lower-case'], 25 | 'subject-empty': [2, 'never'], 26 | 'body-empty': [2, 'always'], 27 | 'footer-empty': [2, 'always'], 28 | 'type-enum': [2, 'always', types], 29 | }, 30 | } 31 | 32 | export default Configuration 33 | -------------------------------------------------------------------------------- /env/.env.example: -------------------------------------------------------------------------------- 1 | # in order to use the env vars, 2 | # you must remove the .example from the .env 3 | # https://vitejs.dev/guide/env-and-mode.html#env-files 4 | VITE_EXAMPLE="value1" 5 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { default as eruptionESLintConfig } from '@eruptionjs/config/eslint' 2 | 3 | export default [...eruptionESLintConfig] 4 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Vite + React/TS = EruptionJS 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eruption", 3 | "description": "Next Generation Boilerplate for React/Typescript, built on top of Vite ⚡️", 4 | "private": false, 5 | "version": "1.13", 6 | "type": "module", 7 | "main": "src/main.tsx", 8 | "license": "MIT", 9 | "author": "EruptionJS🌋", 10 | "homepage": "https://github.com/eruptionjs/core#readme", 11 | "repository": "github:eruptionjs/core.git", 12 | "bugs": "https://github.com/eruptionjs/core/issues", 13 | "scripts": { 14 | "dev": "vite --host", 15 | "start": "vite --host", 16 | "build": "tsc && vite build", 17 | "preview": "vite preview --host", 18 | "test": "vitest", 19 | "treemap": "vite-bundle-visualizer --open --template=treemap --output='./dist/treemap.html'", 20 | "prune": "rm -rf node_modules yarn.lock package-lock.json pnpm-lock.yaml bun.lockb dist coverage build dev-dist", 21 | "preupdate": "bun pm cache rm", 22 | "update": "ncu --interactive --format group --install never", 23 | "postupdate": "bun install", 24 | "upgrade": "bun run prune && bun run update", 25 | "test:watch": "vitest --watch", 26 | "test:coverage": "vitest --watch --coverage", 27 | "test:ui": "vitest --watch --coverage --ui", 28 | "lint": "eslint src --no-inline-config --report-unused-disable-directives --max-warnings 0", 29 | "lint:fix": "bun run lint -- --fix", 30 | "lint:css": "stylelint **/*.css --aei --color", 31 | "prettier": "prettier src --check", 32 | "prettier:fix": "bun run prettier -- --write", 33 | "format": "bun run prettier && bun run lint", 34 | "format:fix": "bun run prettier:fix && bun run lint:fix", 35 | "typecheck": "tsc", 36 | "lint-staged": "tsc & lint-staged", 37 | "prebuild": "bun run lint-staged", 38 | "prepare": "husky", 39 | "commit": "commit" 40 | }, 41 | "engines": { 42 | "node": "!^x", 43 | "npm": "!^x", 44 | "yarn": "!^x", 45 | "pnpm": "!^x", 46 | "bun": "^1.2.x" 47 | }, 48 | "dependencies": { 49 | "react": "^19.x", 50 | "react-dom": "^19.x" 51 | }, 52 | "devDependencies": { 53 | "@commitlint/cli": "^19.x", 54 | "@commitlint/config-conventional": "^19.x", 55 | "@commitlint/prompt-cli": "^19.x", 56 | "@commitlint/types": "^19.x", 57 | "@eruptionjs/config": "^0.6.1", 58 | "@testing-library/jest-dom": "^6.x", 59 | "@testing-library/react": "^16.x", 60 | "@testing-library/user-event": "^14.x", 61 | "@types/bun": "^1.x", 62 | "@types/react": "^19.x", 63 | "@types/react-dom": "^19.x", 64 | "@vitejs/plugin-react-swc": "^3.x", 65 | "@vitest/coverage-v8": "^3.x", 66 | "@vitest/ui": "^3.x", 67 | "c8": "^10.x", 68 | "eslint": "^9.x", 69 | "happy-dom": "^17.x", 70 | "husky": "^9.x", 71 | "lint-staged": "^15.x", 72 | "msw": "^2.x", 73 | "npm-check-updates": "^17.x", 74 | "prettier": "^3.x", 75 | "stylelint": "^16.x", 76 | "stylelint-config-standard": "^37.x", 77 | "typescript": "^5.x", 78 | "vite": "^6.x", 79 | "vite-bundle-visualizer": "^1.x", 80 | "vite-plugin-compression2": "^1.x", 81 | "vite-plugin-qrcode": "^0.x", 82 | "vitest": "^3.x", 83 | "web-vitals": "^4.x" 84 | }, 85 | "msw": { 86 | "workerDirectory": "public" 87 | }, 88 | "browserslist": { 89 | "production": [ 90 | ">0.2%", 91 | "not dead", 92 | "not op_mini all" 93 | ], 94 | "development": [ 95 | "last 1 chrome version", 96 | "last 1 firefox version", 97 | "last 1 safari version" 98 | ] 99 | }, 100 | "keywords": [ 101 | "react", 102 | "boilerplate", 103 | "vite", 104 | "bundle", 105 | "typescript", 106 | "starter", 107 | "template" 108 | ] 109 | } 110 | -------------------------------------------------------------------------------- /public/icons/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/mockServiceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | 4 | /** 5 | * Mock Service Worker. 6 | * @see https://github.com/mswjs/msw 7 | * - Please do NOT modify this file. 8 | * - Please do NOT serve this file on production. 9 | */ 10 | 11 | const PACKAGE_VERSION = '2.7.3' 12 | const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f' 13 | const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') 14 | const activeClientIds = new Set() 15 | 16 | self.addEventListener('install', function () { 17 | self.skipWaiting() 18 | }) 19 | 20 | self.addEventListener('activate', function (event) { 21 | event.waitUntil(self.clients.claim()) 22 | }) 23 | 24 | self.addEventListener('message', async function (event) { 25 | const clientId = event.source.id 26 | 27 | if (!clientId || !self.clients) { 28 | return 29 | } 30 | 31 | const client = await self.clients.get(clientId) 32 | 33 | if (!client) { 34 | return 35 | } 36 | 37 | const allClients = await self.clients.matchAll({ 38 | type: 'window', 39 | }) 40 | 41 | switch (event.data) { 42 | case 'KEEPALIVE_REQUEST': { 43 | sendToClient(client, { 44 | type: 'KEEPALIVE_RESPONSE', 45 | }) 46 | break 47 | } 48 | 49 | case 'INTEGRITY_CHECK_REQUEST': { 50 | sendToClient(client, { 51 | type: 'INTEGRITY_CHECK_RESPONSE', 52 | payload: { 53 | packageVersion: PACKAGE_VERSION, 54 | checksum: INTEGRITY_CHECKSUM, 55 | }, 56 | }) 57 | break 58 | } 59 | 60 | case 'MOCK_ACTIVATE': { 61 | activeClientIds.add(clientId) 62 | 63 | sendToClient(client, { 64 | type: 'MOCKING_ENABLED', 65 | payload: { 66 | client: { 67 | id: client.id, 68 | frameType: client.frameType, 69 | }, 70 | }, 71 | }) 72 | break 73 | } 74 | 75 | case 'MOCK_DEACTIVATE': { 76 | activeClientIds.delete(clientId) 77 | break 78 | } 79 | 80 | case 'CLIENT_CLOSED': { 81 | activeClientIds.delete(clientId) 82 | 83 | const remainingClients = allClients.filter((client) => { 84 | return client.id !== clientId 85 | }) 86 | 87 | // Unregister itself when there are no more clients 88 | if (remainingClients.length === 0) { 89 | self.registration.unregister() 90 | } 91 | 92 | break 93 | } 94 | } 95 | }) 96 | 97 | self.addEventListener('fetch', function (event) { 98 | const { request } = event 99 | 100 | // Bypass navigation requests. 101 | if (request.mode === 'navigate') { 102 | return 103 | } 104 | 105 | // Opening the DevTools triggers the "only-if-cached" request 106 | // that cannot be handled by the worker. Bypass such requests. 107 | if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { 108 | return 109 | } 110 | 111 | // Bypass all requests when there are no active clients. 112 | // Prevents the self-unregistered worked from handling requests 113 | // after it's been deleted (still remains active until the next reload). 114 | if (activeClientIds.size === 0) { 115 | return 116 | } 117 | 118 | // Generate unique request ID. 119 | const requestId = crypto.randomUUID() 120 | event.respondWith(handleRequest(event, requestId)) 121 | }) 122 | 123 | async function handleRequest(event, requestId) { 124 | const client = await resolveMainClient(event) 125 | const response = await getResponse(event, client, requestId) 126 | 127 | // Send back the response clone for the "response:*" life-cycle events. 128 | // Ensure MSW is active and ready to handle the message, otherwise 129 | // this message will pend indefinitely. 130 | if (client && activeClientIds.has(client.id)) { 131 | ;(async function () { 132 | const responseClone = response.clone() 133 | 134 | sendToClient( 135 | client, 136 | { 137 | type: 'RESPONSE', 138 | payload: { 139 | requestId, 140 | isMockedResponse: IS_MOCKED_RESPONSE in response, 141 | type: responseClone.type, 142 | status: responseClone.status, 143 | statusText: responseClone.statusText, 144 | body: responseClone.body, 145 | headers: Object.fromEntries(responseClone.headers.entries()), 146 | }, 147 | }, 148 | [responseClone.body], 149 | ) 150 | })() 151 | } 152 | 153 | return response 154 | } 155 | 156 | // Resolve the main client for the given event. 157 | // Client that issues a request doesn't necessarily equal the client 158 | // that registered the worker. It's with the latter the worker should 159 | // communicate with during the response resolving phase. 160 | async function resolveMainClient(event) { 161 | const client = await self.clients.get(event.clientId) 162 | 163 | if (activeClientIds.has(event.clientId)) { 164 | return client 165 | } 166 | 167 | if (client?.frameType === 'top-level') { 168 | return client 169 | } 170 | 171 | const allClients = await self.clients.matchAll({ 172 | type: 'window', 173 | }) 174 | 175 | return allClients 176 | .filter((client) => { 177 | // Get only those clients that are currently visible. 178 | return client.visibilityState === 'visible' 179 | }) 180 | .find((client) => { 181 | // Find the client ID that's recorded in the 182 | // set of clients that have registered the worker. 183 | return activeClientIds.has(client.id) 184 | }) 185 | } 186 | 187 | async function getResponse(event, client, requestId) { 188 | const { request } = event 189 | 190 | // Clone the request because it might've been already used 191 | // (i.e. its body has been read and sent to the client). 192 | const requestClone = request.clone() 193 | 194 | function passthrough() { 195 | // Cast the request headers to a new Headers instance 196 | // so the headers can be manipulated with. 197 | const headers = new Headers(requestClone.headers) 198 | 199 | // Remove the "accept" header value that marked this request as passthrough. 200 | // This prevents request alteration and also keeps it compliant with the 201 | // user-defined CORS policies. 202 | const acceptHeader = headers.get('accept') 203 | if (acceptHeader) { 204 | const values = acceptHeader.split(',').map((value) => value.trim()) 205 | const filteredValues = values.filter( 206 | (value) => value !== 'msw/passthrough', 207 | ) 208 | 209 | if (filteredValues.length > 0) { 210 | headers.set('accept', filteredValues.join(', ')) 211 | } else { 212 | headers.delete('accept') 213 | } 214 | } 215 | 216 | return fetch(requestClone, { headers }) 217 | } 218 | 219 | // Bypass mocking when the client is not active. 220 | if (!client) { 221 | return passthrough() 222 | } 223 | 224 | // Bypass initial page load requests (i.e. static assets). 225 | // The absence of the immediate/parent client in the map of the active clients 226 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet 227 | // and is not ready to handle requests. 228 | if (!activeClientIds.has(client.id)) { 229 | return passthrough() 230 | } 231 | 232 | // Notify the client that a request has been intercepted. 233 | const requestBuffer = await request.arrayBuffer() 234 | const clientMessage = await sendToClient( 235 | client, 236 | { 237 | type: 'REQUEST', 238 | payload: { 239 | id: requestId, 240 | url: request.url, 241 | mode: request.mode, 242 | method: request.method, 243 | headers: Object.fromEntries(request.headers.entries()), 244 | cache: request.cache, 245 | credentials: request.credentials, 246 | destination: request.destination, 247 | integrity: request.integrity, 248 | redirect: request.redirect, 249 | referrer: request.referrer, 250 | referrerPolicy: request.referrerPolicy, 251 | body: requestBuffer, 252 | keepalive: request.keepalive, 253 | }, 254 | }, 255 | [requestBuffer], 256 | ) 257 | 258 | switch (clientMessage.type) { 259 | case 'MOCK_RESPONSE': { 260 | return respondWithMock(clientMessage.data) 261 | } 262 | 263 | case 'PASSTHROUGH': { 264 | return passthrough() 265 | } 266 | } 267 | 268 | return passthrough() 269 | } 270 | 271 | function sendToClient(client, message, transferrables = []) { 272 | return new Promise((resolve, reject) => { 273 | const channel = new MessageChannel() 274 | 275 | channel.port1.onmessage = (event) => { 276 | if (event.data && event.data.error) { 277 | return reject(event.data.error) 278 | } 279 | 280 | resolve(event.data) 281 | } 282 | 283 | client.postMessage( 284 | message, 285 | [channel.port2].concat(transferrables.filter(Boolean)), 286 | ) 287 | }) 288 | } 289 | 290 | async function respondWithMock(response) { 291 | // Setting response status code to 0 is a no-op. 292 | // However, when responding with a "Response.error()", the produced Response 293 | // instance will have status code set to 0. Since it's not possible to create 294 | // a Response instance with status code 0, handle that use-case separately. 295 | if (response.status === 0) { 296 | return Response.error() 297 | } 298 | 299 | const mockedResponse = new Response(response.body, response) 300 | 301 | Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { 302 | value: true, 303 | enumerable: true, 304 | }) 305 | 306 | return mockedResponse 307 | } 308 | -------------------------------------------------------------------------------- /src/App/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .flex { 9 | display: flex; 10 | flex-direction: row; 11 | align-items: center; 12 | justify-content: center; 13 | } 14 | 15 | .logo { 16 | height: 6em; 17 | padding: 1.5em; 18 | will-change: filter; 19 | } 20 | 21 | .logo.eruption { 22 | font-size: 6em; 23 | padding: 0; 24 | } 25 | 26 | .logo:hover { 27 | filter: drop-shadow(0 0 2em #646cffaa); 28 | } 29 | 30 | .logo.react:hover { 31 | filter: drop-shadow(0 0 2em #61dafbaa); 32 | } 33 | 34 | .logo.eruption:hover { 35 | filter: drop-shadow(0 0 0.3em rgb(245 119 0)); 36 | } 37 | 38 | @keyframes logo-spin { 39 | from { 40 | transform: rotate(0deg); 41 | } 42 | 43 | to { 44 | transform: rotate(360deg); 45 | } 46 | } 47 | 48 | @media (prefers-reduced-motion: no-preference) { 49 | a:nth-of-type(2) .logo { 50 | animation: logo-spin infinite 20s linear; 51 | } 52 | } 53 | 54 | .card { 55 | padding: 2em; 56 | } 57 | 58 | .read-the-docs { 59 | color: #888; 60 | } 61 | -------------------------------------------------------------------------------- /src/App/App.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, waitFor } from '@testing-library/react' 2 | import userEvent from '@testing-library/user-event' 3 | 4 | import { App } from '.' 5 | 6 | describe('App', () => { 7 | test('should return the correct text', () => { 8 | render() 9 | 10 | expect(screen.getByText('Vite + React/TS = EruptionJS')).toBeInTheDocument() 11 | }) 12 | 13 | test('should return 1 when the user click one time at button', async () => { 14 | render() 15 | 16 | const buttonElement = screen.getByRole('button') 17 | expect(screen.queryByText('count is 0')).toBeInTheDocument() 18 | 19 | await userEvent.click(buttonElement) 20 | 21 | await waitFor(() => expect(screen.queryByText('count is 1')).toBeInTheDocument()) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/App/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | import './App.css' 4 | import reactLogo from './react.svg' 5 | 6 | const App = () => { 7 | const [count, setCount] = useState(0) 8 | 9 | useEffect(() => { 10 | console.log('count', count) 11 | }, [count]) 12 | 13 | return ( 14 |
15 | 26 |

Vite + React/TS = EruptionJS

27 |
28 | 29 |

30 | Edit src/App.tsx and save to test HMR 31 |

32 |
33 |

Click on the Vite, React and Eruption logos to learn more

34 |
35 | ) 36 | } 37 | 38 | export { App } 39 | -------------------------------------------------------------------------------- /src/App/index.ts: -------------------------------------------------------------------------------- 1 | export { App } from './App' 2 | -------------------------------------------------------------------------------- /src/App/react.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/__mocks__/README.md: -------------------------------------------------------------------------------- 1 | # Mocks 2 | 3 | Use this to mock any third party HTTP resources that you don't have running 4 | locally and want to have mocked for local development as well as tests. 5 | 6 | ## Usage 7 | 8 | Each feature must have your own folder with the same name as the feature. Inside, the file must return a `HttpHandler[]` array. 9 | 10 | Learn more about how to use this at [mswjs.io](https://mswjs.io/) 11 | -------------------------------------------------------------------------------- /src/__mocks__/browser.ts: -------------------------------------------------------------------------------- 1 | import { setupWorker } from 'msw/browser' 2 | 3 | import { handlers } from './handlers' 4 | 5 | export const worker = setupWorker(...handlers) 6 | -------------------------------------------------------------------------------- /src/__mocks__/handlers.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse, http } from 'msw' 2 | 3 | export const handlers = [ 4 | http.get('https://api.github.com/repos/eruptionjs/core', () => { 5 | return HttpResponse.json({ 6 | name: 'core', 7 | full_name: 'eruptionjs/core', 8 | html_url: 'https://github.com/eruptionjs/core', 9 | description: 'Boilerplate for React/Typescript, built on top of Vite ⚡️', 10 | }) 11 | }), 12 | ] 13 | -------------------------------------------------------------------------------- /src/__mocks__/server.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node' 2 | 3 | import { handlers } from './handlers' 4 | 5 | export const server = setupServer(...handlers) 6 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | color-scheme: light dark; 7 | color: rgb(255 255 255 / 87%); 8 | background-color: #242424; 9 | font-synthesis: none; 10 | text-rendering: optimizelegibility; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | text-size-adjust: 100%; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | 55 | button:focus, 56 | button:focus-visible { 57 | outline: 4px auto -webkit-focus-ring-color; 58 | } 59 | 60 | @media (prefers-color-scheme: light) { 61 | :root { 62 | color: #213547; 63 | background-color: #fff; 64 | } 65 | 66 | a:hover { 67 | color: #747bff; 68 | } 69 | 70 | button { 71 | background-color: #f9f9f9; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | 4 | import { App } from '@/App' 5 | 6 | import './index.css' 7 | 8 | const root = createRoot(document.getElementById('root') as HTMLElement) 9 | 10 | if (import.meta.env.MODE === 'test') { 11 | void import('@/__mocks__/browser') 12 | .then(({ worker }) => { 13 | void worker.start() 14 | }) 15 | .then(() => { 16 | root.render( 17 | 18 | 19 | , 20 | ) 21 | }) 22 | } else { 23 | root.render( 24 | 25 | 26 | , 27 | ) 28 | } 29 | 30 | // Uncomment if you want to see the Lighthouse report in the console 31 | // import reportWebVitals from './reportWebVitals' 32 | // reportWebVitals(console.log) 33 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { type ReportCallback } from 'web-vitals' 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportCallback) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | void import('web-vitals').then(({ onCLS, onFID, onFCP, onLCP, onTTFB }) => { 6 | onCLS(onPerfEntry) 7 | onFID(onPerfEntry) 8 | onFCP(onPerfEntry) 9 | onLCP(onPerfEntry) 10 | onTTFB(onPerfEntry) 11 | }) 12 | } 13 | } 14 | 15 | export default reportWebVitals 16 | -------------------------------------------------------------------------------- /src/setup-test.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | 3 | import { server } from '@/__mocks__/server' 4 | 5 | beforeAll(() => { 6 | server.listen({ onUnhandledRequest: 'error' }) 7 | }) 8 | 9 | afterEach(() => { 10 | server.resetHandlers() 11 | }) 12 | 13 | afterAll(() => { 14 | server.close() 15 | }) 16 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | interface ImportMetaEnv { 2 | readonly VITE_EXAMPLE: string 3 | } 4 | 5 | interface ImportMeta { 6 | readonly env: ImportMetaEnv 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@eruptionjs/config/typescript", 3 | "include": ["@eruptionjs/reset.d.ts", "**/*.ts", "**/*.tsx"], 4 | "compilerOptions": { 5 | /* Absolute import */ 6 | "baseUrl": ".", 7 | "paths": { "@/*": ["src/*"] }, 8 | "types": ["vite/client", "vitest/globals"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react-swc' 2 | import { resolve } from 'path' 3 | import { type UserConfig, defineConfig } from 'vite' 4 | import { compression } from 'vite-plugin-compression2' 5 | import { qrcode } from 'vite-plugin-qrcode' 6 | 7 | export default defineConfig({ 8 | resolve: { 9 | alias: { 10 | '@': resolve(__dirname, './src'), 11 | }, 12 | }, 13 | envDir: './env/', 14 | plugins: [ 15 | react(), 16 | qrcode(), 17 | compression({ 18 | algorithm: 'gzip', 19 | exclude: [/\.(br)$/, /\.(gz)$/], 20 | }), 21 | compression({ 22 | algorithm: 'brotliCompress', 23 | exclude: [/\.(br)$/, /\.(gz)$/], 24 | }), 25 | ], 26 | build: { 27 | sourcemap: true, 28 | target: 'esnext', 29 | minify: true, 30 | cssTarget: 'esnext', 31 | cssMinify: true, 32 | cssCodeSplit: true, 33 | modulePreload: true, 34 | rollupOptions: { 35 | output: { 36 | manualChunks: { 37 | 'vendor-react': ['react', 'react/jsx-runtime', 'react-dom'], 38 | }, 39 | }, 40 | }, 41 | }, 42 | }) satisfies UserConfig 43 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, mergeConfig } from 'vitest/config' 2 | 3 | import viteConfig from './vite.config' 4 | 5 | export default mergeConfig( 6 | viteConfig, 7 | defineConfig({ 8 | test: { 9 | globals: true, 10 | watch: false, 11 | environment: 'happy-dom', 12 | setupFiles: './src/setup-test.ts', 13 | }, 14 | }), 15 | ) 16 | --------------------------------------------------------------------------------