├── .all-contributorsrc
├── .env
├── .env.sample
├── .github
├── FUNDING.yml
├── actions
│ └── prepare
│ │ └── action.yml
├── dependabot.yml
└── workflows
│ ├── build.yml
│ ├── dependabot-auto-merge.yml
│ ├── lint.yml
│ ├── test.yml
│ └── typecheck.yml
├── .gitignore
├── .husky
└── pre-commit
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── eslint.config.mjs
├── index.html
├── mocks
├── browser.js
├── handlers.ts
└── server.js
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── prettier.config.js
├── public
├── favicon.svg
└── mockServiceWorker.js
├── scripts
├── remove_tailwind.js
└── validate
├── src
├── @types
│ ├── __tests__
│ │ └── URLType.test.ts
│ ├── app.d.ts
│ └── utility.d.ts
├── App.test.tsx
├── App.tsx
├── components
│ ├── Box
│ │ ├── index.module.css
│ │ └── index.tsx
│ ├── ErrorBoundary.tsx
│ ├── Layout
│ │ ├── Layout.module.css
│ │ └── Layout.tsx
│ └── Spinner.tsx
├── global.css
├── hooks
│ ├── useUpdateEffect.test.ts
│ └── useUpdateEffect.ts
├── logo.svg
├── main.tsx
├── pages
│ ├── Index
│ │ ├── Counter.tsx
│ │ ├── DocList.module.css
│ │ ├── DocList.tsx
│ │ ├── index.module.css
│ │ └── index.tsx
│ └── Notfound
│ │ ├── index.module.css
│ │ └── index.tsx
├── router.tsx
├── setupTests.ts
└── vite-env.d.ts
├── tsconfig.json
├── vite.config.ts
└── vitest.config.ts
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "create-react-app-vite",
3 | "projectOwner": "laststance",
4 | "repoType": "github",
5 | "repoHost": "https://github.com",
6 | "files": [
7 | "README.md"
8 | ],
9 | "imageSize": 100,
10 | "commit": true,
11 | "commitConvention": "none",
12 | "contributors": [
13 | {
14 | "login": "ryota-murakami",
15 | "name": "ryota-murakami",
16 | "avatar_url": "https://avatars1.githubusercontent.com/u/5501268?s=400&u=7bf6b1580b95930980af2588ef0057f3e9ec1ff8&v=4",
17 | "profile": "http://ryota-murakami.github.io/",
18 | "contributions": [
19 | "code",
20 | "doc",
21 | "test"
22 | ]
23 | },
24 | {
25 | "login": "nvh95",
26 | "name": "Hung Viet Nguyen",
27 | "avatar_url": "https://avatars.githubusercontent.com/u/8603085?v=4",
28 | "profile": "https://hung.dev",
29 | "contributions": [
30 | "code"
31 | ]
32 | },
33 | {
34 | "login": "shayc",
35 | "name": "Shay Cojocaru",
36 | "avatar_url": "https://avatars.githubusercontent.com/u/6969966?v=4",
37 | "profile": "https://github.com/shayc",
38 | "contributions": [
39 | "doc"
40 | ]
41 | },
42 | {
43 | "login": "NateAGeek",
44 | "name": "NateAGeek",
45 | "avatar_url": "https://avatars.githubusercontent.com/u/1813055?v=4",
46 | "profile": "https://github.com/NateAGeek",
47 | "contributions": [
48 | "bug"
49 | ]
50 | }
51 | ],
52 | "contributorsPerLine": 7,
53 | "skipCi": true,
54 | "commitType": "docs"
55 | }
56 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_TEXT="I'm REACT_APP_TEXT from .env"
--------------------------------------------------------------------------------
/.env.sample:
--------------------------------------------------------------------------------
1 | REACT_APP_TEXT="I'm REACT_APP_TEXT from .env"
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: ryota-murakami
4 |
--------------------------------------------------------------------------------
/.github/actions/prepare/action.yml:
--------------------------------------------------------------------------------
1 | description: Prepares the repo for a typical CI job
2 |
3 | name: Prepare
4 |
5 | runs:
6 | steps:
7 | - name: Install pnpm
8 | uses: pnpm/action-setup@v4
9 | with:
10 | version: 9
11 | - name: Use Node.js
12 | uses: actions/setup-node@v4
13 | with:
14 | node-version: '20'
15 | cache: 'pnpm'
16 | - name: Install dependencies
17 | run: pnpm install
18 | shell: bash
19 | using: composite
20 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: 'npm'
4 | # Files stored in `app` directory
5 | directory: '/'
6 | schedule:
7 | interval: 'daily'
8 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - uses: ./.github/actions/prepare
15 | - run: pnpm build
16 |
--------------------------------------------------------------------------------
/.github/workflows/dependabot-auto-merge.yml:
--------------------------------------------------------------------------------
1 | name: Auto merge
2 | on: pull_request
3 | jobs:
4 | merge:
5 | if: ${{ github.actor == 'dependabot[bot]' }}
6 | runs-on: ubuntu-latest
7 | permissions:
8 | contents: write
9 | pull-requests: write
10 | env:
11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
12 | steps:
13 | - uses: actions/checkout@v4
14 | - run: gh pr merge "${GITHUB_HEAD_REF}" --merge --auto
15 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 | jobs:
10 | lint:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - uses: ./.github/actions/prepare
15 | - run: pnpm lint
16 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - uses: ./.github/actions/prepare
15 | - run: pnpm test
16 |
--------------------------------------------------------------------------------
/.github/workflows/typecheck.yml:
--------------------------------------------------------------------------------
1 | name: Typecheck
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 | jobs:
10 | typecheck:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - uses: ./.github/actions/prepare
15 | - run: pnpm typecheck
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
6 | .env
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | pnpm lint-staged
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .git
2 | node_modules
3 | .eslintignore
4 | .gitignore
5 | LICENSE
6 | build
7 | dist
8 | dist-ssr
9 | .all-contributorsrc
10 | .vscode
11 | .idea
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false
4 | }
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Laststance.io
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Create React App Vite [](https://github.com/laststance/vite-react-ts-alter/actions/workflows/typecheck.yml) [](https://github.com/laststance/create-react-app-vite/actions/workflows/test.yml) [](https://github.com/laststance/create-react-app-vite/actions/workflows/build.yml) [](https://github.com/laststance/create-react-app-vite/actions/workflows/lint.yml)
2 |
3 | > Simple CRA style Vite teimpate.
4 | > Create plain and lightweight React+TS programming environment.
5 | > And a easy migration base for create-react-app to Vite.
6 |
7 | ## [Try this Online!](https://codesandbox.io/p/github/laststance/create-react-app-vite/main?file=%2FREADME.md&workspace=%257B%2522activeFileId%2522%253A%2522clfgsr6q10016g2hjg3xq06lt%2522%252C%2522openFiles%2522%253A%255B%2522%252FREADME.md%2522%255D%252C%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522gitSidebarPanel%2522%253A%2522COMMIT%2522%252C%2522spaces%2522%253A%257B%2522clfgsra1u000x3b6mbdjl3ahb%2522%253A%257B%2522key%2522%253A%2522clfgsra1u000x3b6mbdjl3ahb%2522%252C%2522name%2522%253A%2522Default%2522%252C%2522devtools%2522%253A%255B%257B%2522key%2522%253A%2522clfgsra1u000y3b6meoz3zcev%2522%252C%2522type%2522%253A%2522PROJECT_SETUP%2522%252C%2522isMinimized%2522%253Afalse%257D%252C%257B%2522type%2522%253A%2522PREVIEW%2522%252C%2522taskId%2522%253A%2522dev%2522%252C%2522port%2522%253A5173%252C%2522key%2522%253A%2522clfgss4o700dz3b6mz869sru3%2522%252C%2522isMinimized%2522%253Afalse%257D%252C%257B%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522dev%2522%252C%2522key%2522%253A%2522clfgss3ug00ba3b6mpaataz0k%2522%252C%2522isMinimized%2522%253Afalse%257D%255D%257D%257D%252C%2522currentSpace%2522%253A%2522clfgsra1u000x3b6mbdjl3ahb%2522%252C%2522spacesOrder%2522%253A%255B%2522clfgsra1u000x3b6mbdjl3ahb%2522%255D%252C%2522hideCodeEditor%2522%253Afalse%257D)
8 |
9 | This is a Vite template top of the official [Vite](https://vitejs.dev/) [react-ts](https://stackblitz.com/edit/vitejs-vite-is3dmk?file=index.html&terminal=dev) template(`npm init vite@latest myapp -- --template react-ts`) and some extended setup.
10 | I'd like to keep CRA like experience as much as possible, So improving/adding feature Rull Request is really welcome!
11 |
12 | - Support CRA's [Custom Environment Variables](https://create-react-app.dev/docs/adding-custom-environment-variables/) like `REACT_APP_`.
13 | - [eslint-config-ts-prefixer](https://github.com/laststance/eslint-config-ts-prefixer). Specialized fixable(`--fix` option) rule sets. Zero extend any recommended for confortable DX.
14 | - [Vitest](https://vitest.dev/), [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/), [MSW](https://mswjs.io/)
15 | - [tailwindcss](https://tailwindcss.com/)
16 | - [Github Actions](https://github.com/features/actions)
17 |
18 | All npm package are keeping least release version powered by [Dependabot](https://github.com/dependabot).
19 |
20 | # Installation
21 |
22 | ```
23 | npx tiged laststance/create-react-app-vite myapp
24 | ```
25 |
26 | ### pnpm
27 |
28 | ```sh
29 | cd myapp
30 | pnpm install
31 | pnpm validate
32 | pnpm start
33 | ```
34 |
35 | If you don't need TailwindCSS, run `pnpm remove:tailwind` after npm installed.
36 |
37 | ### Commands
38 |
39 | ```sh
40 | pnpm dev # start development server
41 | pnpm start # start development server
42 | pnpm validate # run test,lint,build,typecheck concurrently
43 | pnpm test # run jest
44 | pnpm lint # run eslint
45 | pnpm lint:fix # run eslint with --fix option
46 | pnpm typecheck # run TypeScript compiler check
47 | pnpm build # build production bundle to 'dist' directly
48 | pnpm prettier # run prettier for json|yml|css|md|mdx files
49 | pnpm clean # remove 'node_modules' 'yarn.lock' 'dist' completely
50 | pnpm serve # launch server for production bundle in local
51 | pnpm remove:tailwind # remove TailwindCSS
52 | ```
53 |
54 | # CRA to Vite migration guides
55 |
56 | - [Migrate to Vite from Create React App (CRA)](https://www.robinwieruch.de/vite-create-react-app/)
57 | - [Migrating from Create React App (CRA) to Vite](https://cathalmacdonnacha.com/migrating-from-create-react-app-cra-to-vite)
58 | - [Migrating a Create React App (CRA) application to Vite](https://www.darraghoriordan.com/2021/05/16/migrating-from-create-react-app-to-vite)
59 |
60 | # Background
61 |
62 | Simply put, CRA development has stopped as of 2023.
63 | This has sparked a discussion about replacing CRA with Vite for official documentation recommendations.
64 | [Replace Create React App recommendation with Vite](https://github.com/reactjs/react.dev/pull/5487)
65 | Dan Abramov offered some plans for the future of the CRA in his comments, but no direct answers were given.
66 | https://github.com/reactjs/react.dev/pull/5487#issuecomment-1409720741
67 |
68 | The React community is still buzzing around Server Component after May 2023, but there is still a high demand for the React SinglePageAplication starter that the CRA has served in the past, and I was one of the people who needed it, I was one of the people who needed it, so I decided to create a template in Vite that could be used as much as possible like CRA.
69 |
70 | # License
71 |
72 | MIT
73 |
74 | ## Contributors ✨
75 |
76 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
77 |
78 |
79 |
80 |
81 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
98 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import path from 'node:path'
2 | import { fileURLToPath } from 'node:url'
3 |
4 | import { fixupPluginRules } from '@eslint/compat'
5 | import { FlatCompat } from '@eslint/eslintrc'
6 | import js from '@eslint/js'
7 | import tsParser from '@typescript-eslint/parser'
8 | import jsxA11Y from 'eslint-plugin-jsx-a11y'
9 | import reactHooks from 'eslint-plugin-react-hooks'
10 |
11 | const __filename = fileURLToPath(import.meta.url)
12 | const __dirname = path.dirname(__filename)
13 | const compat = new FlatCompat({
14 | baseDirectory: __dirname,
15 | recommendedConfig: js.configs.recommended,
16 | allConfig: js.configs.all,
17 | })
18 |
19 | export default [
20 | {
21 | ignores: [
22 | '**/.vscode',
23 | '**/node_modules',
24 | '**/build',
25 | '**/dist',
26 | '**/.github',
27 | '**/.idea',
28 | 'public/mockServiceWorker.js',
29 | ],
30 | },
31 | ...compat.extends('ts-prefixer', 'plugin:jsx-a11y/recommended'),
32 | {
33 | plugins: {
34 | 'react-hooks': fixupPluginRules(reactHooks),
35 | 'jsx-a11y': jsxA11Y,
36 | },
37 |
38 | languageOptions: {
39 | globals: {},
40 | parser: tsParser,
41 | ecmaVersion: 5,
42 | sourceType: 'script',
43 |
44 | parserOptions: {
45 | project: ['tsconfig.json'],
46 | },
47 | },
48 |
49 | settings: {},
50 |
51 | rules: {
52 | 'react-hooks/rules-of-hooks': 'error',
53 | },
54 | },
55 | ]
56 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Create React App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/mocks/browser.js:
--------------------------------------------------------------------------------
1 | import { setupWorker } from 'msw/browser'
2 |
3 | import { handlers } from './handlers'
4 |
5 | // This configures a Service Worker with the given request handlers.
6 | export const worker = setupWorker(...handlers)
7 |
--------------------------------------------------------------------------------
/mocks/handlers.ts:
--------------------------------------------------------------------------------
1 | import { http, HttpResponse } from 'msw'
2 |
3 | const sleep = async (ms: number): Promise =>
4 | new Promise((resolve) => setTimeout(resolve, ms))
5 |
6 | export const handlers = [
7 | http.get('http://localhost:3000/api/doclist', async () => {
8 | const data: DocList = [
9 | { name: 'React', url: 'https://react.dev/' },
10 | { name: 'Vite', url: 'https://vitejs.dev/' },
11 | {
12 | name: 'React Router',
13 | url: 'https://reactrouter.com/en/main/start/overview',
14 | },
15 | { name: 'MSW', url: 'https://mswjs.io/' },
16 | { name: 'Tailwind CSS', url: 'https://tailwindcss.com/' },
17 | ]
18 |
19 | await sleep(2000)
20 |
21 | return HttpResponse.json(data)
22 | }),
23 | ]
24 |
--------------------------------------------------------------------------------
/mocks/server.js:
--------------------------------------------------------------------------------
1 | // from https://github.com/mswjs/examples/blob/master/examples/with-jest/src/mocks/server.js
2 | import { setupServer } from 'msw/node'
3 |
4 | import { handlers } from './handlers'
5 |
6 | export const server = setupServer(...handlers)
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "create-react-app-vite",
3 | "version": "0.4.0",
4 | "license": "MIT",
5 | "msw": {
6 | "workerDirectory": "public"
7 | },
8 | "lint-staged": {
9 | "*": "prettier --ignore-unknown --write"
10 | },
11 | "scripts": {
12 | "start": "vite",
13 | "dev": "vite",
14 | "build": "vite build",
15 | "preview": "vite preview",
16 | "test": "vitest --run",
17 | "test:ui": "vitest --ui",
18 | "test:watch": "vitest",
19 | "lint": "eslint . -c eslint.config.mjs",
20 | "lint:fix": "eslint . -c eslint.config.mjs --fix",
21 | "typecheck": "tsc --noEmit",
22 | "prettier": "prettier --ignore-unknown --write .",
23 | "clean": "rimraf node_modules pnpm-lock.yaml dist",
24 | "validate": "./scripts/validate",
25 | "remove:tailwind": "scripts/remove_tailwind.js",
26 | "prepare": "husky"
27 | },
28 | "dependencies": {
29 | "clsx": "^2.1.1",
30 | "react": "^19.1.0",
31 | "react-dom": "^19.1.0",
32 | "react-router": "^7.6.2"
33 | },
34 | "devDependencies": {
35 | "@eslint/compat": "^1.2.9",
36 | "@eslint/eslintrc": "^3.3.1",
37 | "@eslint/js": "^9.27.0",
38 | "@tailwindcss/aspect-ratio": "^0.4.2",
39 | "@tailwindcss/forms": "^0.5.10",
40 | "@tailwindcss/line-clamp": "^0.4.4",
41 | "@tailwindcss/postcss": "^4.1.8",
42 | "@tailwindcss/typography": "^0.5.16",
43 | "@testing-library/dom": "^10.4.0",
44 | "@testing-library/jest-dom": "^6.6.3",
45 | "@testing-library/react": "^16.3.0",
46 | "@testing-library/user-event": "^14.6.1",
47 | "@types/eslint": "^9.6.1",
48 | "@types/react": "^19.1.6",
49 | "@types/react-dom": "^19.1.6",
50 | "@typescript-eslint/eslint-plugin": "^8.33.0",
51 | "@typescript-eslint/parser": "^8.32.1",
52 | "@vitejs/plugin-react-swc": "^3.10.0",
53 | "@vitest/ui": "^3.1.1",
54 | "all-contributors-cli": "^6.26.1",
55 | "concurrently": "^9.1.2",
56 | "cross-fetch": "^4.1.0",
57 | "eslint": "^9.27.0",
58 | "eslint-config-ts-prefixer": "^1.14.2",
59 | "eslint-import-resolver-typescript": "^4.4.1",
60 | "eslint-plugin-import": "^2.31.0",
61 | "eslint-plugin-jsx-a11y": "^6.10.2",
62 | "eslint-plugin-prettier": "^5.4.1",
63 | "eslint-plugin-react-hooks": "^5.2.0",
64 | "husky": "^9.1.7",
65 | "jsdom": "^26.1.0",
66 | "lint-staged": "^16.0.0",
67 | "msw": "^2.9.0",
68 | "postcss": "^8.5.3",
69 | "prettier": "^3.5.3",
70 | "prettier-plugin-tailwindcss": "^0.6.11",
71 | "rimraf": "^6.0.1",
72 | "tailwindcss": "^4.1.7",
73 | "ts-expect": "^1.3.0",
74 | "typescript": "^5.8.3",
75 | "vite": "^6.3.5",
76 | "vite-plugin-environment": "^1.1.3",
77 | "vite-plugin-svgr": "^4.3.0",
78 | "vitest": "^3.2.0"
79 | },
80 | "pnpm": {
81 | "onlyBuiltDependencies": [
82 | "@swc/core",
83 | "esbuild",
84 | "msw",
85 | "unrs-resolver"
86 | ]
87 | },
88 | "volta": {
89 | "node": "22.16.0"
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | '@tailwindcss/postcss': {},
4 | },
5 | }
6 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [require('prettier-plugin-tailwindcss')],
3 | tailwindConfig: './tailwind.config.js',
4 | }
5 |
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
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 | */
9 |
10 | const PACKAGE_VERSION = '2.9.0'
11 | const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
12 | const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
13 | const activeClientIds = new Set()
14 |
15 | addEventListener('install', function () {
16 | self.skipWaiting()
17 | })
18 |
19 | addEventListener('activate', function (event) {
20 | event.waitUntil(self.clients.claim())
21 | })
22 |
23 | addEventListener('message', async function (event) {
24 | const clientId = Reflect.get(event.source || {}, 'id')
25 |
26 | if (!clientId || !self.clients) {
27 | return
28 | }
29 |
30 | const client = await self.clients.get(clientId)
31 |
32 | if (!client) {
33 | return
34 | }
35 |
36 | const allClients = await self.clients.matchAll({
37 | type: 'window',
38 | })
39 |
40 | switch (event.data) {
41 | case 'KEEPALIVE_REQUEST': {
42 | sendToClient(client, {
43 | type: 'KEEPALIVE_RESPONSE',
44 | })
45 | break
46 | }
47 |
48 | case 'INTEGRITY_CHECK_REQUEST': {
49 | sendToClient(client, {
50 | type: 'INTEGRITY_CHECK_RESPONSE',
51 | payload: {
52 | packageVersion: PACKAGE_VERSION,
53 | checksum: INTEGRITY_CHECKSUM,
54 | },
55 | })
56 | break
57 | }
58 |
59 | case 'MOCK_ACTIVATE': {
60 | activeClientIds.add(clientId)
61 |
62 | sendToClient(client, {
63 | type: 'MOCKING_ENABLED',
64 | payload: {
65 | client: {
66 | id: client.id,
67 | frameType: client.frameType,
68 | },
69 | },
70 | })
71 | break
72 | }
73 |
74 | case 'MOCK_DEACTIVATE': {
75 | activeClientIds.delete(clientId)
76 | break
77 | }
78 |
79 | case 'CLIENT_CLOSED': {
80 | activeClientIds.delete(clientId)
81 |
82 | const remainingClients = allClients.filter((client) => {
83 | return client.id !== clientId
84 | })
85 |
86 | // Unregister itself when there are no more clients
87 | if (remainingClients.length === 0) {
88 | self.registration.unregister()
89 | }
90 |
91 | break
92 | }
93 | }
94 | })
95 |
96 | addEventListener('fetch', function (event) {
97 | // Bypass navigation requests.
98 | if (event.request.mode === 'navigate') {
99 | return
100 | }
101 |
102 | // Opening the DevTools triggers the "only-if-cached" request
103 | // that cannot be handled by the worker. Bypass such requests.
104 | if (
105 | event.request.cache === 'only-if-cached' &&
106 | event.request.mode !== 'same-origin'
107 | ) {
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 | const requestId = crypto.randomUUID()
119 | event.respondWith(handleRequest(event, requestId))
120 | })
121 |
122 | /**
123 | * @param {FetchEvent} event
124 | * @param {string} requestId
125 | */
126 | async function handleRequest(event, requestId) {
127 | const client = await resolveMainClient(event)
128 | const requestCloneForEvents = event.request.clone()
129 | const response = await getResponse(event, client, requestId)
130 |
131 | // Send back the response clone for the "response:*" life-cycle events.
132 | // Ensure MSW is active and ready to handle the message, otherwise
133 | // this message will pend indefinitely.
134 | if (client && activeClientIds.has(client.id)) {
135 | const serializedRequest = await serializeRequest(requestCloneForEvents)
136 |
137 | // Clone the response so both the client and the library could consume it.
138 | const responseClone = response.clone()
139 |
140 | sendToClient(
141 | client,
142 | {
143 | type: 'RESPONSE',
144 | payload: {
145 | isMockedResponse: IS_MOCKED_RESPONSE in response,
146 | request: {
147 | id: requestId,
148 | ...serializedRequest,
149 | },
150 | response: {
151 | type: responseClone.type,
152 | status: responseClone.status,
153 | statusText: responseClone.statusText,
154 | headers: Object.fromEntries(responseClone.headers.entries()),
155 | body: responseClone.body,
156 | },
157 | },
158 | },
159 | responseClone.body ? [serializedRequest.body, responseClone.body] : [],
160 | )
161 | }
162 |
163 | return response
164 | }
165 |
166 | /**
167 | * Resolve the main client for the given event.
168 | * Client that issues a request doesn't necessarily equal the client
169 | * that registered the worker. It's with the latter the worker should
170 | * communicate with during the response resolving phase.
171 | * @param {FetchEvent} event
172 | * @returns {Promise}
173 | */
174 | async function resolveMainClient(event) {
175 | const client = await self.clients.get(event.clientId)
176 |
177 | if (activeClientIds.has(event.clientId)) {
178 | return client
179 | }
180 |
181 | if (client?.frameType === 'top-level') {
182 | return client
183 | }
184 |
185 | const allClients = await self.clients.matchAll({
186 | type: 'window',
187 | })
188 |
189 | return allClients
190 | .filter((client) => {
191 | // Get only those clients that are currently visible.
192 | return client.visibilityState === 'visible'
193 | })
194 | .find((client) => {
195 | // Find the client ID that's recorded in the
196 | // set of clients that have registered the worker.
197 | return activeClientIds.has(client.id)
198 | })
199 | }
200 |
201 | /**
202 | * @param {FetchEvent} event
203 | * @param {Client | undefined} client
204 | * @param {string} requestId
205 | * @returns {Promise}
206 | */
207 | async function getResponse(event, client, requestId) {
208 | // Clone the request because it might've been already used
209 | // (i.e. its body has been read and sent to the client).
210 | const requestClone = event.request.clone()
211 |
212 | function passthrough() {
213 | // Cast the request headers to a new Headers instance
214 | // so the headers can be manipulated with.
215 | const headers = new Headers(requestClone.headers)
216 |
217 | // Remove the "accept" header value that marked this request as passthrough.
218 | // This prevents request alteration and also keeps it compliant with the
219 | // user-defined CORS policies.
220 | const acceptHeader = headers.get('accept')
221 | if (acceptHeader) {
222 | const values = acceptHeader.split(',').map((value) => value.trim())
223 | const filteredValues = values.filter(
224 | (value) => value !== 'msw/passthrough',
225 | )
226 |
227 | if (filteredValues.length > 0) {
228 | headers.set('accept', filteredValues.join(', '))
229 | } else {
230 | headers.delete('accept')
231 | }
232 | }
233 |
234 | return fetch(requestClone, { headers })
235 | }
236 |
237 | // Bypass mocking when the client is not active.
238 | if (!client) {
239 | return passthrough()
240 | }
241 |
242 | // Bypass initial page load requests (i.e. static assets).
243 | // The absence of the immediate/parent client in the map of the active clients
244 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
245 | // and is not ready to handle requests.
246 | if (!activeClientIds.has(client.id)) {
247 | return passthrough()
248 | }
249 |
250 | // Notify the client that a request has been intercepted.
251 | const serializedRequest = await serializeRequest(event.request)
252 | const clientMessage = await sendToClient(
253 | client,
254 | {
255 | type: 'REQUEST',
256 | payload: {
257 | id: requestId,
258 | ...serializedRequest,
259 | },
260 | },
261 | [serializedRequest.body],
262 | )
263 |
264 | switch (clientMessage.type) {
265 | case 'MOCK_RESPONSE': {
266 | return respondWithMock(clientMessage.data)
267 | }
268 |
269 | case 'PASSTHROUGH': {
270 | return passthrough()
271 | }
272 | }
273 |
274 | return passthrough()
275 | }
276 |
277 | /**
278 | * @param {Client} client
279 | * @param {any} message
280 | * @param {Array} transferrables
281 | * @returns {Promise}
282 | */
283 | function sendToClient(client, message, transferrables = []) {
284 | return new Promise((resolve, reject) => {
285 | const channel = new MessageChannel()
286 |
287 | channel.port1.onmessage = (event) => {
288 | if (event.data && event.data.error) {
289 | return reject(event.data.error)
290 | }
291 |
292 | resolve(event.data)
293 | }
294 |
295 | client.postMessage(message, [
296 | channel.port2,
297 | ...transferrables.filter(Boolean),
298 | ])
299 | })
300 | }
301 |
302 | /**
303 | * @param {Response} response
304 | * @returns {Response}
305 | */
306 | function respondWithMock(response) {
307 | // Setting response status code to 0 is a no-op.
308 | // However, when responding with a "Response.error()", the produced Response
309 | // instance will have status code set to 0. Since it's not possible to create
310 | // a Response instance with status code 0, handle that use-case separately.
311 | if (response.status === 0) {
312 | return Response.error()
313 | }
314 |
315 | const mockedResponse = new Response(response.body, response)
316 |
317 | Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
318 | value: true,
319 | enumerable: true,
320 | })
321 |
322 | return mockedResponse
323 | }
324 |
325 | /**
326 | * @param {Request} request
327 | */
328 | async function serializeRequest(request) {
329 | return {
330 | url: request.url,
331 | mode: request.mode,
332 | method: request.method,
333 | headers: Object.fromEntries(request.headers.entries()),
334 | cache: request.cache,
335 | credentials: request.credentials,
336 | destination: request.destination,
337 | integrity: request.integrity,
338 | redirect: request.redirect,
339 | referrer: request.referrer,
340 | referrerPolicy: request.referrerPolicy,
341 | body: await request.arrayBuffer(),
342 | keepalive: request.keepalive,
343 | }
344 | }
345 |
--------------------------------------------------------------------------------
/scripts/remove_tailwind.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const { execSync } = require('node:child_process')
4 | const fs = require('node:fs')
5 | const path = require('node:path')
6 | const { chdir, exit } = require('node:process')
7 |
8 | const rootDir = path.join(__dirname, '..')
9 |
10 | function removeTailwind() {
11 | try {
12 | fs.unlinkSync(rootDir + '/tailwind.config.js')
13 | console.log('remove tailwind.config.js\n')
14 | } catch (e) {
15 | if (e.message.includes('no such file or directory'))
16 | console.log('tailwind.config.js has already been removed.\n')
17 | }
18 |
19 | // npm unlinstall
20 | const packageJson = require(rootDir + '/package.json')
21 |
22 | const dependencies = packageJson.dependencies || {}
23 | const devDependencies = packageJson.devDependencies || {}
24 |
25 | const tailwindPackages = []
26 |
27 | for (const dep in dependencies) {
28 | if (dep.includes('tailwind')) {
29 | tailwindPackages.push(dep)
30 | }
31 | }
32 |
33 | for (const dep in devDependencies) {
34 | if (dep.includes('tailwind')) {
35 | tailwindPackages.push(dep)
36 | }
37 | }
38 |
39 | if (tailwindPackages.length === 0) {
40 | console.log('TailwindCSS has already been removed.\n')
41 | exit()
42 | }
43 |
44 | const uninstallCommand = 'npm uninstall ' + tailwindPackages.join(' ')
45 |
46 | chdir(rootDir)
47 | // execSync return null when command successful
48 | const res = execSync(uninstallCommand, {
49 | stdio: [0, 1, 2],
50 | })
51 |
52 | if (res !== null && res.status !== 0)
53 | console.error('Command Failed: ' + uninstallCommand)
54 |
55 | console.log(tailwindPackages.join('\n'))
56 | console.log('Above packages uninstall has been successful.\n')
57 | console.log()
58 | console.log('Completed remove TailwindCSS.\n')
59 | }
60 |
61 | // run
62 | removeTailwind()
63 |
--------------------------------------------------------------------------------
/scripts/validate:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | concurrently \
4 | --kill-others-on-fail \
5 | --prefix "[{name}]" \
6 | --names "test,lint:fix,typecheck,build" \
7 | --prefix-colors "bgRed.bold.white,bgGreen.bold.white,bgBlue.bold.white,bgMagenta.bold.white" \
8 | "npm run test --silent -- --watch=false" \
9 | "npm run lint:fix --silent" \
10 | "npm run typecheck --silent" \
11 | "npm run build --silent"
12 |
--------------------------------------------------------------------------------
/src/@types/__tests__/URLType.test.ts:
--------------------------------------------------------------------------------
1 | import { expectType } from 'ts-expect'
2 |
3 | expectType('http://example.com')
4 | expectType('https://example.com/news')
5 | // @ts-expect-error give non URL
6 | expectType('example.com/news')
7 |
--------------------------------------------------------------------------------
/src/@types/app.d.ts:
--------------------------------------------------------------------------------
1 | declare interface Doc {
2 | name: string
3 | url: URLType
4 | }
5 | declare type DocList = Doc[]
6 |
--------------------------------------------------------------------------------
/src/@types/utility.d.ts:
--------------------------------------------------------------------------------
1 | import type { Dispatch, SetStateAction } from 'react'
2 |
3 | declare global {
4 | type _ = any
5 |
6 | // @see https://youtu.be/QSIXYMIJkQg?si=CyycYgaAGNZCEuYj&t=188
7 | type TODO = any
8 |
9 | type AnyFunction = (...args: any[]) => any
10 |
11 | type URLType = `http${'s' | ''}://${string}.${string}`
12 |
13 | // https://stackoverflow.com/a/69288824/8440230
14 | type Expand = T extends (...args: infer A) => infer R
15 | ? (...args: Expand) => Expand
16 | : T extends infer O
17 | ? { [K in keyof O]: O[K] }
18 | : never
19 |
20 | type ExpandRecursively = T extends (...args: infer A) => infer R
21 | ? (...args: ExpandRecursively) => ExpandRecursively
22 | : T extends object
23 | ? T extends infer O
24 | ? { [K in keyof O]: ExpandRecursively }
25 | : never
26 | : T
27 | }
28 |
29 | declare module 'react' {
30 | type SetState = Dispatch>
31 | }
32 |
--------------------------------------------------------------------------------
/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen, waitFor } from '@testing-library/react'
2 | import userEvent from '@testing-library/user-event'
3 | import React from 'react'
4 |
5 | import App from './App'
6 |
7 | test('Work App Component without error', () => {
8 | render()
9 |
10 | expect(screen.getByText("I'm REACT_APP_TEXT from .env")).toBeInTheDocument()
11 | })
12 |
13 | test('Working Counter', async () => {
14 | const user = userEvent.setup()
15 | const { getByText } = render()
16 | expect(getByText('count is: 0')).toBeInTheDocument()
17 |
18 | const button = getByText(/count is: \d/)
19 |
20 | await user.click(button)
21 | expect(getByText('count is: 1')).toBeInTheDocument()
22 |
23 | await user.click(button)
24 | expect(getByText('count is: 2')).toBeInTheDocument()
25 |
26 | await user.click(button)
27 | expect(getByText('count is: 3')).toBeInTheDocument()
28 | })
29 |
30 | test('working with msw', async () => {
31 | render()
32 |
33 | await waitFor(
34 | () => {
35 | expect(screen.getByText('MSW')).toBeInTheDocument()
36 | expect(screen.getByText('Tailwind CSS')).toBeInTheDocument()
37 | },
38 | { timeout: 5000 },
39 | )
40 | })
41 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { RouterProvider } from 'react-router'
3 |
4 | import ErrorBoundary from './components/ErrorBoundary'
5 | import router from './router'
6 |
7 | const App: React.FC = () => (
8 |
9 |
10 |
11 | )
12 | App.displayName = 'App'
13 | export default App
14 |
--------------------------------------------------------------------------------
/src/components/Box/index.module.css:
--------------------------------------------------------------------------------
1 | .box {
2 | display: grid;
3 | place-content: center;
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/Box/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import type { PropsWithChildren } from 'react'
3 |
4 | import styles from './index.module.css'
5 |
6 | const Box: React.FC = ({ children, ...rest }) => {
7 | return (
8 |
9 | {children}
10 |
11 | )
12 | }
13 |
14 | export default Box
15 |
--------------------------------------------------------------------------------
/src/components/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import type { ErrorInfo, ReactNode } from 'react'
2 | import React, { Component } from 'react'
3 |
4 | interface Props {
5 | children?: ReactNode
6 | }
7 |
8 | interface State {
9 | error: Error | null
10 | info: ErrorInfo | null
11 | }
12 | class ErrorBoundary extends Component {
13 | state = {
14 | error: null,
15 | info: null,
16 | }
17 |
18 | componentDidCatch(error: Error, info: ErrorInfo): void {
19 | this.setState({ error, info })
20 | }
21 |
22 | render(): ReactNode {
23 | const { error } = this.state
24 | if (error) {
25 | //Sentry.captureException(error)
26 | return
27 | }
28 | return this.props.children
29 | }
30 | }
31 |
32 | export default ErrorBoundary
33 |
34 | const LayoutStyle: React.CSSProperties = {
35 | alignItems: 'center',
36 | display: 'flex',
37 | justifyContent: 'center',
38 | minHeight: '100vh',
39 | minWidth: '100%',
40 | }
41 |
42 | const MessageStyle: React.CSSProperties = {
43 | border: '2px #78909c solid',
44 | borderRadius: '5px',
45 | color: '#78909c',
46 | fontSize: '24px',
47 | padding: '40px',
48 | }
49 |
50 | export const ErrorBoundaryFallbackComponent: React.FC<
51 | React.PropsWithChildren
52 | > = () => (
53 |
54 |
55 | Something Error Ooccurring
56 |
57 | 😞
58 |
59 |
60 |
61 | )
62 |
--------------------------------------------------------------------------------
/src/components/Layout/Layout.module.css:
--------------------------------------------------------------------------------
1 | .layout {
2 | min-height: 100vh;
3 | min-width: 100vw;
4 | background-color: #282c34;
5 | font-size: calc(10px + 2vmin);
6 | color: white;
7 | display: grid;
8 | place-items: center;
9 | }
10 |
11 | .container {
12 | min-height: 100vh;
13 | min-width: 90%;
14 | max-width: 90%;
15 | display: flex;
16 | flex-direction: column;
17 | gap: 20px;
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Layout/Layout.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren } from 'react'
2 | import React from 'react'
3 |
4 | import styles from './Layout.module.css'
5 |
6 | const Layout: React.FC = ({ children, ...rest }) => {
7 | return (
8 |
9 | {children}
10 |
11 | )
12 | }
13 |
14 | export default Layout
15 |
--------------------------------------------------------------------------------
/src/components/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx'
2 | import React from 'react'
3 |
4 | const sizes = {
5 | lg: 'h-16 w-16',
6 | md: 'h-8 w-8',
7 | sm: 'h-4 w-4',
8 | xl: 'h-24 w-24',
9 | }
10 |
11 | const variants = {
12 | light: 'text-white',
13 | primary: 'text-blue-200',
14 | }
15 |
16 | export type SpinnerProps = {
17 | className?: string
18 | size?: keyof typeof sizes
19 | variant?: keyof typeof variants
20 | }
21 |
22 | const Spinner: React.FC = ({
23 | className = '',
24 | size = 'md',
25 | variant = 'primary',
26 | }: SpinnerProps) => {
27 | return (
28 | <>
29 |
55 | Loading
56 | >
57 | )
58 | }
59 |
60 | Spinner.displayName = 'Spinner'
61 |
62 | export default Spinner
63 |
--------------------------------------------------------------------------------
/src/global.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss';
2 |
3 | @plugin '@tailwindcss/forms';
4 |
5 | @theme {
6 | --font-sans: Inter var, ui-sans-serif, system-ui, sans-serif,
7 | 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
8 | }
9 |
10 | /*
11 | The default border color has changed to `currentColor` in Tailwind CSS v4,
12 | so we've added these compatibility styles to make sure everything still
13 | looks the same as it did with Tailwind CSS v3.
14 |
15 | If we ever want to remove these styles, we need to add an explicit border
16 | color utility to any element that depends on these defaults.
17 | */
18 | @layer base {
19 | *,
20 | ::after,
21 | ::before,
22 | ::backdrop,
23 | ::file-selector-button {
24 | border-color: var(--color-gray-200, currentColor);
25 | }
26 | }
27 |
28 | html,
29 | body {
30 | height: 100%;
31 | width: 100%;
32 | margin: 0;
33 | padding: 0;
34 | }
35 |
36 | #root {
37 | height: 100%;
38 | width: 100%;
39 | }
40 |
41 | body {
42 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
43 | 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
44 | 'Helvetica Neue', sans-serif;
45 | -webkit-font-smoothing: antialiased;
46 | -moz-osx-font-smoothing: grayscale;
47 | }
48 |
49 | code {
50 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
51 | monospace;
52 | }
53 |
54 | .react-logo {
55 | margin: auto;
56 | height: 30vmin;
57 | pointer-events: none;
58 | animation: react-logo-spin infinite 20s linear;
59 | }
60 |
61 | @keyframes react-logo-spin {
62 | from {
63 | transform: rotate(0deg);
64 | }
65 | to {
66 | transform: rotate(360deg);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/hooks/useUpdateEffect.test.ts:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react'
2 |
3 | import useUpdateEffect from './useUpdateEffect'
4 |
5 | test('useUpdateEffect simulates componentDidUpdate', () => {
6 | const effect = vi.fn()
7 | const { rerender } = renderHook(() => useUpdateEffect(effect))
8 |
9 | expect(effect).toHaveBeenCalledTimes(0)
10 | rerender()
11 | expect(effect).toHaveBeenCalledTimes(1)
12 | rerender()
13 | expect(effect).toHaveBeenCalledTimes(2)
14 | rerender()
15 | expect(effect).toHaveBeenCalledTimes(3)
16 | })
17 |
--------------------------------------------------------------------------------
/src/hooks/useUpdateEffect.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect } from 'react'
2 | /**
3 | * Simulate componentDidUpdate() method of Class Component
4 | * https://reactjs.org/docs/react-component.html#componentdidupdate
5 | */
6 | const useUpdateEffect = (
7 | effect: AnyFunction,
8 | deps: any[] | undefined = undefined,
9 | ): void => {
10 | const mounted = useRef(false)
11 | useEffect(() => {
12 | if (!mounted.current) {
13 | // fire componentDidMount
14 | mounted.current = true
15 | } else {
16 | effect()
17 | }
18 | }, deps)
19 | }
20 |
21 | export default useUpdateEffect
22 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import './global.css'
4 |
5 | import App from './App'
6 |
7 | const root = ReactDOM.createRoot(document.getElementById('root')!)
8 |
9 | // Setup MSW mock server in both development and production
10 | // Certify MSW's Service Worker is available before starting React app
11 | import('../mocks/browser')
12 | .then(async ({ worker }) => {
13 | return worker.start()
14 | }) // Run when Service Worker is ready to intercept requests
15 | .then(() => {
16 | root.render()
17 | })
18 |
--------------------------------------------------------------------------------
/src/pages/Index/Counter.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useState } from 'react'
2 |
3 | interface Props {}
4 |
5 | const Counter: React.FC = memo(() => {
6 | const [count, setCount] = useState(0)
7 |
8 | return (
9 | <>
10 |
16 | >
17 | )
18 | })
19 | Counter.displayName = 'Counter'
20 |
21 | export default Counter
22 |
--------------------------------------------------------------------------------
/src/pages/Index/DocList.module.css:
--------------------------------------------------------------------------------
1 | .documentList {
2 | margin-top: 20px;
3 | display: flex;
4 | gap: 10px;
5 | flex-wrap: wrap;
6 | }
7 |
8 | .button {
9 | flex: 1 0 calc(33.3333% - 10px);
10 | color: white;
11 | margin-top: 10px;
12 | font-size: 24px;
13 | padding: 14px 20px;
14 | border: 1px solid #fff;
15 | border-radius: 8px;
16 | cursor: pointer;
17 | text-align: center;
18 | text-decoration: none;
19 | display: inline-block;
20 | transition: background-color 0.5s;
21 |
22 | &:hover {
23 | background-color: rgba(255, 255, 255, 0.5);
24 | }
25 | }
26 |
27 | .link {
28 | position: absolute;
29 | top: 20px;
30 | left: 20px;
31 | font-size: 32px;
32 | color: #61dafb;
33 | }
34 |
--------------------------------------------------------------------------------
/src/pages/Index/DocList.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 |
3 | import Spinner from '@/components/Spinner'
4 |
5 | import styles from './DocList.module.css'
6 | interface Props {}
7 |
8 | const DocList: React.FC = () => {
9 | const [docList, setDocList] = useState([])
10 |
11 | useEffect(() => {
12 | fetch('/api/doclist')
13 | .then(async (res) => res.json())
14 | .then((data) => setDocList(data))
15 | }, [])
16 |
17 | return (
18 | <>
19 |
20 | {docList.length === 0 ? (
21 |
22 | ) : (
23 | docList.map((doc, i) => (
24 |
25 | {doc.name}
26 |
27 | ))
28 | )}
29 |
30 | >
31 | )
32 | }
33 | DocList.displayName = 'DocList'
34 |
35 | export default DocList
36 |
--------------------------------------------------------------------------------
/src/pages/Index/index.module.css:
--------------------------------------------------------------------------------
1 | .h1 {
2 | font-size: 44px;
3 | font-weight: 500;
4 | padding: 40px 0;
5 | }
6 |
--------------------------------------------------------------------------------
/src/pages/Index/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, Suspense } from 'react'
2 |
3 | import Box from '../../components/Box'
4 | import Spinner from '../../components/Spinner'
5 | import logo from '../../logo.svg'
6 |
7 | import Counter from './Counter'
8 | import DocList from './DocList'
9 | import styles from './index.module.css'
10 |
11 | interface Props {}
12 |
13 | const Index: React.FC = memo(() => {
14 | return (
15 | <>
16 |
17 | I'm REACT_APP_TEXT from .env
18 |
19 |
20 |
21 |
22 |
23 |
24 | }>
25 |
26 |
27 |
28 | >
29 | )
30 | })
31 | Index.displayName = 'Index'
32 |
33 | export default Index
34 |
--------------------------------------------------------------------------------
/src/pages/Notfound/index.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | }
4 |
5 | .h1 {
6 | font-size: 3.75rem;
7 | line-height: 1;
8 | }
9 |
--------------------------------------------------------------------------------
/src/pages/Notfound/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 |
3 | import styles from './index.module.css'
4 |
5 | const Notfound: React.FC = memo(() => (
6 |
7 |
404: Page Not Found
8 |
9 | ))
10 | Notfound.displayName = 'Notfound'
11 |
12 | export default Notfound
13 |
--------------------------------------------------------------------------------
/src/router.tsx:
--------------------------------------------------------------------------------
1 | import { createBrowserRouter } from 'react-router'
2 |
3 | import Layout from './components/Layout/Layout'
4 | import Index from './pages/Index'
5 | import Notfound from './pages/Notfound'
6 |
7 | const router = createBrowserRouter([
8 | {
9 | path: '/',
10 | element: (
11 |
12 |
13 |
14 | ),
15 | },
16 | {
17 | path: '*',
18 | element: (
19 |
20 |
21 |
22 | ),
23 | },
24 | ])
25 |
26 | export default router
27 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/vitest'
2 |
3 | import { server } from '../mocks/server'
4 |
5 | beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
6 |
7 | afterEach(() => server.resetHandlers())
8 |
9 | afterAll(() => server.close())
10 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
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": "Bundler",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 | "baseUrl": ".",
19 | "paths": {
20 | "@/*": ["./src/*"]
21 | },
22 | "types": ["vitest/globals", "@testing-library/jest-dom"]
23 | },
24 | "include": [
25 | "./src",
26 | "./@types",
27 | "./mocks",
28 | "scripts",
29 | "./**.js",
30 | "./**.ts",
31 | "eslint.config.mjs"
32 | ],
33 | "exclude": ["dist"]
34 | }
35 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path'
2 |
3 | import react from '@vitejs/plugin-react-swc'
4 | import { defineConfig } from 'vite'
5 | import EnvironmentPlugin from 'vite-plugin-environment'
6 |
7 | // https://vitejs.dev/config/
8 | export default defineConfig({
9 | build: {
10 | sourcemap: true,
11 | },
12 | plugins: [react(), EnvironmentPlugin(['REACT_APP_TEXT'])],
13 | publicDir: 'public',
14 | server: {
15 | host: true,
16 | port: 3000,
17 | },
18 | resolve: {
19 | alias: {
20 | '@': path.resolve(__dirname, './src'),
21 | },
22 | },
23 | })
24 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path'
2 |
3 | import EnvironmentPlugin from 'vite-plugin-environment'
4 | import { defineConfig } from 'vitest/config'
5 | export default defineConfig({
6 | plugins: [EnvironmentPlugin(['REACT_APP_TEXT']) as any],
7 | resolve: {
8 | alias: {
9 | '@': path.resolve(__dirname, './src'),
10 | },
11 | },
12 | test: {
13 | environment: 'jsdom',
14 | globals: true,
15 | setupFiles: ['./src/setupTests.ts'],
16 | include: ['src/**/*.{test,spec}.{ts,tsx}'],
17 | exclude: ['src/@types', 'node_modules'],
18 | },
19 | })
20 |
--------------------------------------------------------------------------------