├── .github └── workflows │ └── workflow.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── .vscode └── settings.json ├── @react-vite-trpc ├── config │ ├── .eslintrc │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── services │ │ │ ├── httpService.ts │ │ │ └── index.ts │ └── tsconfig.json ├── eslint-config │ ├── eslint.js │ ├── package.json │ └── react.js ├── tsconfig │ ├── base.json │ ├── node.json │ ├── package.json │ └── react.json └── ui │ ├── package.json │ ├── src │ ├── Label.tsx │ └── index.tsx │ └── tsconfig.json ├── LICENSE ├── README.md ├── apps ├── server │ ├── .env-example │ ├── .env.test-example │ ├── .eslintrc │ ├── .prettierrc │ ├── nodemon.json │ ├── package.json │ ├── src │ │ ├── aliases.ts │ │ ├── aliases.unit.test.ts │ │ ├── core │ │ │ ├── httpServer.ts │ │ │ └── index.ts │ │ ├── env │ │ │ ├── env.ts │ │ │ └── index.ts │ │ ├── middlewares │ │ │ └── index.ts │ │ ├── server.ts │ │ ├── trpc │ │ │ ├── api │ │ │ │ ├── resolvers │ │ │ │ │ ├── getRole │ │ │ │ │ │ ├── getRole.e2e.test.ts │ │ │ │ │ │ ├── getRole.integration.test.ts │ │ │ │ │ │ └── getRole.ts │ │ │ │ │ └── index.ts │ │ │ │ └── router.ts │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ └── trpc.ts │ │ └── types │ │ │ └── index.ts │ ├── tsconfig.json │ ├── vitest.config.ts │ └── vitest.setup.ts └── web │ ├── .eslintrc │ ├── .prettierrc │ ├── cypress.config.ts │ ├── cypress │ └── e2e │ │ └── Home.cy.ts │ ├── index.html │ ├── package.json │ ├── src │ ├── components │ │ ├── core │ │ │ ├── App.tsx │ │ │ └── index.ts │ │ ├── features │ │ │ ├── Home │ │ │ │ ├── Home.tsx │ │ │ │ └── styled │ │ │ │ │ ├── Gif.tsx │ │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ └── shared │ │ │ ├── Providers.tsx │ │ │ └── index.ts │ ├── env │ │ ├── env.ts │ │ └── index.ts │ ├── hooks │ │ ├── index.ts │ │ └── useTrpc.ts │ ├── main.integration.test.tsx │ ├── main.tsx │ ├── main.unit.test.tsx │ ├── styles │ │ ├── globalStyle.ts │ │ ├── index.scss │ │ ├── index.ts │ │ ├── mediaQueries.ts │ │ └── theme.ts │ ├── trpc │ │ ├── index.ts │ │ └── trpc.ts │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── vite.config.ts │ ├── vitest.config.ts │ └── vitest.setup.ts ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── turbo.json └── vitest.config.ts /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | 7 | jobs: 8 | lint_and_test: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v3 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: '18.x' 19 | 20 | - uses: pnpm/action-setup@v2 21 | name: Install pnpm 22 | with: 23 | version: 8 24 | run_install: false 25 | 26 | - name: Get pnpm store directory 27 | shell: bash 28 | run: | 29 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 30 | 31 | - uses: actions/cache@v3 32 | name: Setup pnpm cache 33 | with: 34 | path: ${{ env.STORE_PATH }} 35 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 36 | restore-keys: | 37 | ${{ runner.os }}-pnpm-store- 38 | 39 | - name: Install dependencies 40 | run: pnpm install 41 | 42 | - name: Run pnpm check 43 | run: pnpm check 44 | 45 | - name: Run unit tests 46 | run: pnpm test:unit:run 47 | 48 | - name: Run integration tests 49 | run: pnpm test:integration:run 50 | 51 | - name: Start server background process (for e2e tests) 52 | run: pnpm pm2:start 53 | 54 | - name: Install cypress 55 | run: pnpm cypress:install # NOTE: Install cypress manually due to pnpm cache issue 56 | 57 | - name: Run e2e tests 58 | run: pnpm test:e2e:run 59 | 60 | - name: Stop server background process 61 | run: pnpm pm2:delete 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # testing 5 | coverage 6 | 7 | # builds 8 | dist 9 | 10 | # vite 11 | dev-dist 12 | vite.config.ts.timestamp* 13 | 14 | # misc 15 | .DS_Store 16 | 17 | # debug 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | .pnpm-debug.log* 22 | tsconfig.tsbuildinfo 23 | 24 | # env files 25 | .env 26 | .env.test 27 | 28 | # turbo 29 | .turbo 30 | 31 | # cypress 32 | screenshots 33 | videos 34 | downloads 35 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # lockfile 2 | pnpm-lock.yaml 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # testing 8 | coverage 9 | 10 | # builds 11 | dist 12 | 13 | # vite 14 | dev-dist 15 | 16 | # misc 17 | .DS_Store 18 | 19 | # debug 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | .pnpm-debug.log* 24 | tsconfig.tsbuildinfo 25 | 26 | # env files 27 | .env 28 | .env.test 29 | 30 | # turbo 31 | .turbo 32 | 33 | # cypress 34 | screenshots 35 | videos 36 | downloads 37 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 3, 3 | "printWidth": 120, 4 | "semi": false, 5 | "singleQuote": true, 6 | "arrowParens": "avoid", 7 | "trailingComma": "es5", 8 | "plugins": ["./node_modules/@trivago/prettier-plugin-sort-imports"], 9 | "importOrderSeparation": true, 10 | "importOrderSortSpecifiers": true, 11 | "importOrder": [ 12 | "", 13 | 14 | "^helpers$", 15 | "(.*)helpers$", 16 | 17 | "^utils$", 18 | "(.*)utils$", 19 | 20 | "^types(.*)", 21 | "(.*)types$", 22 | 23 | "^[./]" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "customSyntax": "postcss-styled-syntax", 3 | "rules": { 4 | "custom-property-pattern": "^[a-z][a-zA-Z0-9]+$", 5 | "declaration-block-no-duplicate-properties": true, 6 | "no-descending-specificity": true, 7 | "value-no-vendor-prefix": null, 8 | "property-no-vendor-prefix": null 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsserver.experimental.enableProjectDiagnostics": true, 3 | "editor.codeActionsOnSave": { 4 | "source.organizeImports": true, 5 | "source.fixAll.eslint": true 6 | }, 7 | "search.exclude": { "**/node_modules": false } 8 | } 9 | -------------------------------------------------------------------------------- /@react-vite-trpc/config/.eslintrc: -------------------------------------------------------------------------------- 1 | { "extends": ["@react-vite-trpc/eslint-config"] } 2 | -------------------------------------------------------------------------------- /@react-vite-trpc/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-vite-trpc/config", 3 | "version": "1.0.0", 4 | "private": true, 5 | "sideEffects": false, 6 | "main": "src/index.ts", 7 | "types": "src/index.ts", 8 | "scripts": { 9 | "dev": "pnpm build:lib --watch", 10 | "lint": "eslint .", 11 | "lint:fix": "eslint . --fix", 12 | "ts:check": "tsc -b", 13 | "check": "pnpm lint && pnpm ts:check", 14 | "build:lib": "tsup src/index.ts --format cjs" 15 | }, 16 | "dependencies": { 17 | "envalid": "^7.3.1" 18 | }, 19 | "devDependencies": { 20 | "@react-vite-trpc/eslint-config": "workspace:*", 21 | "@react-vite-trpc/tsconfig": "workspace:*", 22 | "@types/node": "^20.4.5", 23 | "tsup": "^7.1.0", 24 | "typescript": "^5.1.6" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /@react-vite-trpc/config/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './services' 2 | -------------------------------------------------------------------------------- /@react-vite-trpc/config/src/services/httpService.ts: -------------------------------------------------------------------------------- 1 | import { cleanEnv } from 'envalid' 2 | 3 | const { isProd } = cleanEnv(process.env, {}) 4 | 5 | export class HttpService { 6 | private static readonly host = 'react-vite-trpc.onrender.com' // NOTE: must be a raw hostname 7 | 8 | private static readonly serverPort = 3001 9 | 10 | private static readonly clientPort = 3000 11 | 12 | public static readonly serverUrl = isProd ? `https://${this.host}` : `http://localhost:${this.serverPort}` 13 | 14 | public static readonly clientUrl = isProd ? `https://${this.host}` : `http://localhost:${this.clientPort}` 15 | } 16 | -------------------------------------------------------------------------------- /@react-vite-trpc/config/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export { HttpService } from './httpService' 2 | -------------------------------------------------------------------------------- /@react-vite-trpc/config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@react-vite-trpc/tsconfig/base.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "outDir": "dist" 6 | }, 7 | "references": [] 8 | } 9 | -------------------------------------------------------------------------------- /@react-vite-trpc/eslint-config/eslint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint', 'prettier', 'unused-imports'], 5 | ignorePatterns: ['node_modules', '.turbo', 'dist', 'dev-dist', 'coverage'], 6 | rules: { 7 | 'prettier/prettier': ['error', { endOfLine: 'auto' }], 8 | 'no-return-await': ['error'], 9 | 'prefer-destructuring': ['error'], 10 | 'object-shorthand': ['error'], 11 | 'no-unneeded-ternary': ['error'], 12 | 'prefer-template': ['error'], 13 | '@typescript-eslint/consistent-type-imports': ['error', { fixStyle: 'inline-type-imports' }], 14 | 'no-empty': ['error', { allowEmptyCatch: true }], 15 | 'unused-imports/no-unused-imports': 'warn', 16 | '@typescript-eslint/no-unused-vars': [ 17 | 'error', 18 | { 19 | argsIgnorePattern: '^_', 20 | varsIgnorePattern: '^_', 21 | }, 22 | ], 23 | 'object-curly-newline': [ 24 | 'warn', 25 | { 26 | ObjectExpression: { 27 | multiline: true, 28 | minProperties: 2, 29 | }, 30 | }, 31 | ], 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /@react-vite-trpc/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-vite-trpc/eslint-config", 3 | "version": "1.0.0", 4 | "private": true, 5 | "sideEffects": false, 6 | "main": "eslint.js", 7 | "dependencies": { 8 | "@typescript-eslint/eslint-plugin": "^6.2.1", 9 | "@typescript-eslint/parser": "^6.2.1", 10 | "eslint": "^8.46.0", 11 | "eslint-config-prettier": "^9.0.0", 12 | "eslint-plugin-prettier": "^4.2.1", 13 | "eslint-plugin-react": "^7.33.1", 14 | "eslint-plugin-unused-imports": "^3.0.0", 15 | "prettier": "^2.8.8", 16 | "typescript": "^5.1.6" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /@react-vite-trpc/eslint-config/react.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['./eslint', 'plugin:react/recommended'], 3 | settings: { react: { version: '18.2.0' } }, 4 | rules: { 5 | '@typescript-eslint/ban-types': 'warn', 6 | 'react/no-unescaped-entities': 'warn', 7 | 'react/react-in-jsx-scope': 'off', 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /@react-vite-trpc/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "strict": true, 5 | "strictNullChecks": true, 6 | "composite": true, 7 | "lib": ["ESNext", "DOM"], 8 | "module": "ESNext", 9 | "target": "ESNext", 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "strictPropertyInitialization": true, 14 | "isolatedModules": true, 15 | "moduleResolution": "node", 16 | "preserveWatchOutput": true, 17 | "skipLibCheck": true, 18 | "useDefineForClassFields": true, 19 | "allowSyntheticDefaultImports": true, 20 | "allowJs": false, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "strictFunctionTypes": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /@react-vite-trpc/tsconfig/node.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { "module": "commonjs" }, 5 | "ts-node": { "swc": true } 6 | } 7 | -------------------------------------------------------------------------------- /@react-vite-trpc/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-vite-trpc/tsconfig", 3 | "version": "1.0.0", 4 | "private": true, 5 | "sideEffects": false, 6 | "files": [ 7 | "base.json", 8 | "react.json", 9 | "node.json" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /@react-vite-trpc/tsconfig/react.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "jsx": "react-jsx", 7 | "module": "ESNext", 8 | "target": "es6" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /@react-vite-trpc/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-vite-trpc/ui", 3 | "version": "1.0.0", 4 | "main": "src/index.tsx", 5 | "types": "src/index.tsx", 6 | "license": "MIT", 7 | "dependencies": { 8 | "styled-components": "^5.3.11" 9 | }, 10 | "devDependencies": { 11 | "@types/react": "^18.0.17", 12 | "@types/react-dom": "^18.0.6", 13 | "eslint": "^7.32.0", 14 | "@react-vite-trpc/eslint-config": "workspace:*", 15 | "react": "^18.2.0", 16 | "@react-vite-trpc/tsconfig": "workspace:*", 17 | "typescript": "^4.9.5", 18 | "@types/styled-components": "^5.1.26" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /@react-vite-trpc/ui/src/Label.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | type LabelProps = { 4 | children: React.ReactNode 5 | } 6 | 7 | export const Label = ({ children }: LabelProps) => {children} 8 | 9 | const LabelContainer = styled.div` 10 | padding: 12px 24px; 11 | font-size: 20px; 12 | background-color: white; 13 | font-family: monospace; 14 | border-radius: 4px; 15 | cursor: pointer; 16 | margin-bottom: 15px; 17 | box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2); 18 | ` 19 | -------------------------------------------------------------------------------- /@react-vite-trpc/ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | export { Label } from './Label' 2 | -------------------------------------------------------------------------------- /@react-vite-trpc/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@react-vite-trpc/tsconfig/react.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "outDir": "dist" 6 | }, 7 | "references": [] 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 kuubson 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 | # 🛠️ Monorepo template 2 | 3 | A template that provides a straightforward and flexible way to use the benefits of tRPC in your React projects. It emphasizes the use of absolute paths and a monorepo approach that significantly improves the developer experience. If you're looking for a clean setup with pure React and modularization, this template is an excellent place to start! 4 | 5 | | [Stack](#-stack) | [Highlights](#-highlights) | [Quick start](#-quick-start) | [Challenge](#-the-challenge) | [Scripts](#-scripts) | [Env](#-envs) | [Ports](#-ports) | [License](#-license) | 6 | | ---------------- | -------------------------- | ---------------------------- | ---------------------------- | -------------------- | ------------- | ---------------- | -------------------- | 7 | 8 | ## 🔧 Stack 9 | 10 | ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) 11 | ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) 12 | ![Vite](https://img.shields.io/badge/Vite-646CFF.svg?style=for-the-badge&logo=Vite&logoColor=white) 13 | ![Styled Components](https://img.shields.io/badge/styled--components-DB7093?style=for-the-badge&logo=styled-components&logoColor=white) 14 | ![Cypress](https://img.shields.io/badge/-cypress-%23E5E5E5?style=for-the-badge&logo=cypress&logoColor=058a5e) 15 | 16 | ![NodeJS](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white) 17 | ![Express](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) 18 | ![tRPC](https://img.shields.io/badge/tRPC-2596BE.svg?style=for-the-badge&logo=tRPC&logoColor=white) 19 | [![Vitest](https://img.shields.io/badge/Vitest-%2314151B.svg?style=for-the-badge&logo=vitest&logoColor=white&color=green)](https://vitest.dev/) 20 | ![Turborepo](https://img.shields.io/badge/Turborepo-EF4444.svg?style=for-the-badge&logo=Turborepo&logoColor=white) 21 | 22 | ![Render](https://img.shields.io/badge/Render-%46E3B7.svg?style=for-the-badge&logo=render&logoColor=white) 23 | ![GitHub Actions](https://img.shields.io/badge/github%20actions-%232671E5.svg?style=for-the-badge&logo=githubactions&logoColor=white) 24 | 25 | ## 🌟 Highlights 26 | 27 | | Global | Server | Web | 28 | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 29 | | | | | 30 | 31 | ## 🚀 Quick start 32 | 33 | Preview the deployment: https://react-vite-trpc.onrender.com 34 | 35 | For local execution, use `pnpm install` and `pnpm dev`. 36 | 37 | #### Customization & tips: 38 | 39 | - To tailor the template to your needs, replace of `@react-vite-trpc` with `@your-idea` (you can use `CMD + SHIFT + H` in VS Code to do this globally) 40 | 41 | - When deploying, make sure to update the `host` property in the `HttpService` class (`@react-vite-trpc\config\src\services\httpService.ts`) 42 | 43 | - Adjust the preferred order of imports in `.prettierrc` files using the `importOrder` property 44 | 45 | - When adding a new local package (i.e. `/@react-vite-trpc/new-package`), remember to update the `watch` array in `nodemon.json` and provide appropriate TypeScript references in `tsconfig.json` files for an enhanced DX 46 | 47 | - The test coverage `.lcov` files (generated with `pnpm test:coverage`), can be easily leveraged with VS Code extension [Coverage Gutters](https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters) 48 | 49 | ## 🧩 The challenge 50 | 51 | This setup faced a challenge while importing the `AppRouter` from the server folder to the client folder, which resulted in Typescript server errors related to imports from the 'trpc' path on the server side. 52 | 53 | The solution leverages Typescript references to allow importing the `AppRouter` on the client side while using absolute paths on the server side. 54 | 55 | ```js 56 | // apps/web/tsconfig.json 57 | { 58 | "compilerOptions": { 59 | "baseUrl": "src", 60 | "outDir": "dist", 61 | }, 62 | "references": [{ "path": "../server" }] <~ fixes the /server references on the /web 63 | } 64 | 65 | // apps/web/package.json 66 | { 67 | "scripts": { 68 | "ts:check": "tsc -b", <~ the -b flag is crucial when building an app that has references in its tsconfig.json 69 | "build": "pnpm ts:check && vite build" 70 | } 71 | } 72 | 73 | // apps/server/tsconfig.json 74 | { 75 | "compilerOptions": { 76 | "baseUrl": "src", 77 | "outDir": "dist", <~ required, sets the build destination folder 78 | "composite": true <~ required to make TS references work 79 | }, 80 | "ts-node": { "swc": true } 81 | } 82 | ``` 83 | 84 | ## ⌨ Scripts 85 | 86 | | command | description | 87 | | ---------------------------- | ----------------------------------------------------------------------------------------------- | 88 | | `pnpm start` | Runs the production build of the server (`/server`) | 89 | | `pnpm pm2:start` | Runs the server production build as a background process, using pm2 (`/server`) | 90 | | `pnpm pm2:delete` | Deletes all pm2 processes (`/server`) | 91 | | `pnpm pm2:logs` | Shows logs for pm2 (`/server`) | 92 | | `pnpm dev` | Launches apps and bundles all packages in watch mode | 93 | | `pnpm lint` | Performs an eslint check through all workspaces | 94 | | `pnpm lint:fix` | Performs an eslint fix through all workspaces | 95 | | `pnpm ts:check` | Performs a TypeScript check through all workspaces | 96 | | `pnpm ts:references` | Syncs TypeScript references in all `tsconfig.json` files + updates `nodemon.json` `watch` array | 97 | | `pnpm stylelint` | Performs an stylelint check through all workspaces | 98 | | `pnpm check` | Performs eslint, TypeScript, and stylelint checks through all workspaces | 99 | | `pnpm build` | Builds all apps | 100 | | `pnpm build:lib` | Bundles all packages | 101 | | `pnpm test:unit` | Runs unit tests in watch mode | 102 | | `pnpm test:unit:run` | Runs unit tests once | 103 | | `pnpm test:integration` | Runs integration tests in watch mode | 104 | | `pnpm test:integration:run` | Runs integration tests once | 105 | | `pnpm test:e2e` | Runs e2e tests in watch mode | 106 | | `pnpm test:e2e:run` | Runs e2e tests once | 107 | | `pnpm test:coverage` | Generates test coverage reports | 108 | | `pnpm test:coverage:preview` | Generates test coverage reports and opens preview | 109 | | `pnpm cypress` | Opens the Cypress UI (`/web`) | 110 | | `pnpm cypress:install` | Installs the Cypress (`/web`) | 111 | | `pnpm postinstall` | Ensures that local or CI environment is ready after installing packages | 112 | 113 | ## 🔒 Envs 114 | 115 | Envs are validated with the package `envalid`. Check out `.env-example` & `.env.test-example` files 116 | 117 | If the `pnpm dev` script is executed without the required environment variables, the application will output similar details in the console: 118 | 119 | ```js 120 | ================================ 121 | Missing environment variables: 122 | PORT: Port the Express server is running on (eg. "3001"). See https://expressjs.com/en/starter/hello-world.html 123 | ================================ 124 | ``` 125 | 126 | ## 🌐 Ports 127 | 128 | - 🌐 :3000 - Web 129 | - 🖥️ :3001 - Server 130 | 131 | ## 📜 License 132 | 133 | [The MIT License (MIT)](https://github.com/kuubson/react-vite-trpc/blob/main/LICENSE) 134 | -------------------------------------------------------------------------------- /apps/server/.env-example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development -------------------------------------------------------------------------------- /apps/server/.env.test-example: -------------------------------------------------------------------------------- 1 | NODE_ENV=test -------------------------------------------------------------------------------- /apps/server/.eslintrc: -------------------------------------------------------------------------------- 1 | { "extends": ["@react-vite-trpc/eslint-config"] } 2 | -------------------------------------------------------------------------------- /apps/server/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 3, 3 | "printWidth": 120, 4 | "semi": false, 5 | "singleQuote": true, 6 | "arrowParens": "avoid", 7 | "trailingComma": "es5", 8 | "plugins": ["./node_modules/@trivago/prettier-plugin-sort-imports"], 9 | "importOrderSeparation": true, 10 | "importOrderSortSpecifiers": true, 11 | "importOrder": [ 12 | "", 13 | 14 | "^dotenv/config$", 15 | 16 | "^(.*)aliases$", 17 | 18 | "core", 19 | 20 | "^env$", 21 | 22 | "^@react-vite-trpc(.*)", 23 | 24 | "^trpc(.*)", 25 | "^trpc$", 26 | 27 | "^middlewares(.*)", 28 | 29 | "^helpers$", 30 | "(.*)helpers$", 31 | 32 | "^utils$", 33 | "(.*)utils$", 34 | 35 | "^types(.*)", 36 | "(.*)types$", 37 | 38 | "^[./]" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /apps/server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "ts,json", 3 | "execMap": { 4 | "ts": "node --inspect --require ts-node/register" 5 | }, 6 | "watch": [ 7 | "./src", 8 | "../../@react-vite-trpc/config/dist/index.js" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /apps/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-vite-trpc/server", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "cross-env NODE_ENV=production node dist/src/server.js", 7 | "pm2:start": "pnpm pm2:delete && pnpm build && cross-env NODE_ENV=test pm2 start dist/src/server.js --name @react-vite-trpc/server", 8 | "pm2:delete": "pm2 delete all &", 9 | "pm2:logs": "pm2 logs", 10 | "dev": "nodemon src/server.ts", 11 | "lint": "eslint .", 12 | "lint:fix": "eslint . --fix", 13 | "ts:check": "tsc -b", 14 | "check": "pnpm lint && pnpm ts:check", 15 | "build": "tsc -b", 16 | "test:unit": "vitest .unit.test.ts", 17 | "test:unit:run": "pnpm test:unit --run", 18 | "test:integration": "vitest .integration.test.ts", 19 | "test:integration:run": "pnpm test:integration --run", 20 | "test:e2e": "vitest .e2e.test.ts", 21 | "test:e2e:run": "pnpm test:e2e --run", 22 | "test:coverage": "pnpm pm2:start && vitest run --coverage && pnpm pm2:delete", 23 | "test:coverage:preview": "pnpm test:coverage && vite preview --outDir ./coverage --open" 24 | }, 25 | "dependencies": { 26 | "@trpc/server": "^10.29.1", 27 | "cookie-parser": "^1.4.6", 28 | "@trpc/client": "^10.29.1", 29 | "express": "^4.18.2", 30 | "express-serve-static-core": "^0.1.1", 31 | "zod": "^3.21.4" 32 | }, 33 | "devDependencies": { 34 | "@react-vite-trpc/tsconfig": "workspace:*", 35 | "@react-vite-trpc/config": "workspace:*", 36 | "@swc/core": "^1.3.62", 37 | "@types/cookie-parser": "^1.4.3", 38 | "@types/express": "^4.17.17", 39 | "@types/fs-extra": "^11.0.1", 40 | "@types/module-alias": "^2.0.1", 41 | "@types/node": "^18.16.17", 42 | "cross-env": "^7.0.3", 43 | "dotenv": "^16.1.4", 44 | "envalid": "^7.3.1", 45 | "fs-extra": "^11.1.1", 46 | "module-alias": "^2.2.3", 47 | "nodemon": "^2.0.22", 48 | "ts-node": "^10.9.1", 49 | "@vitest/coverage-istanbul": "^0.34.1", 50 | "typescript": "^4.9.5", 51 | "@trivago/prettier-plugin-sort-imports": "^4.2.0", 52 | "pm2": "^5.3.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /apps/server/src/aliases.ts: -------------------------------------------------------------------------------- 1 | import { readJsonSync, readdirSync } from 'fs-extra' 2 | import moduleAlias from 'module-alias' 3 | import path from 'path' 4 | 5 | type TsConfig = { 6 | extends: string 7 | compilerOptions: Record 8 | references: { path: string }[] 9 | } 10 | 11 | export class Aliases { 12 | public static async config() { 13 | this.configInternalPackages() 14 | 15 | this.configDirectories() 16 | } 17 | 18 | public static configInternalPackages() { 19 | const tsConfig = readJsonSync('tsconfig.json') as TsConfig 20 | 21 | const aliases = tsConfig.references.map(({ path }) => { 22 | const [_, internalPackage] = path.split('@') 23 | 24 | const packageName = `@${internalPackage}`.replace('/tsconfig.json', '') 25 | 26 | const packageEntry = `${packageName}/dist/index.js` 27 | 28 | return { [packageName]: packageEntry } 29 | }) 30 | 31 | const flatAliases = Object.assign({}, ...aliases) 32 | 33 | moduleAlias.addAliases(flatAliases) 34 | } 35 | 36 | public static configDirectories() { 37 | const directories = readdirSync('./src', { withFileTypes: true }) 38 | .filter(directory => directory.isDirectory()) 39 | .map(({ name }) => name) 40 | 41 | const aliases = directories.map(directory => ({ [directory]: path.resolve(__dirname, directory) })) 42 | 43 | const flatAliases = Object.assign({}, ...aliases) 44 | 45 | moduleAlias.addAliases(flatAliases) 46 | } 47 | } 48 | 49 | Aliases.config() 50 | -------------------------------------------------------------------------------- /apps/server/src/aliases.unit.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import moduleAlias from 'module-alias' 3 | import { type SpyInstance, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' 4 | 5 | import { Aliases } from './aliases' 6 | 7 | describe('Aliases', () => { 8 | let mockReadJsonSync: SpyInstance 9 | let mockReaddirSync: SpyInstance 10 | let mockAddAliases: SpyInstance 11 | 12 | beforeEach(() => { 13 | mockReadJsonSync = vi.spyOn(fs, 'readJsonSync') 14 | mockReaddirSync = vi.spyOn(fs, 'readdirSync') 15 | mockAddAliases = vi.spyOn(moduleAlias, 'addAliases') 16 | }) 17 | 18 | afterEach(() => { 19 | vi.restoreAllMocks() 20 | }) 21 | 22 | describe('.config', () => { 23 | it('should configure internal packages and directories', () => { 24 | const configInternalPackagesSpy = vi.spyOn(Aliases, 'configInternalPackages') 25 | 26 | const configDirectoriesSpy = vi.spyOn(Aliases, 'configDirectories') 27 | 28 | Aliases.config() 29 | 30 | expect(configInternalPackagesSpy).toHaveBeenCalled() 31 | 32 | expect(configDirectoriesSpy).toHaveBeenCalled() 33 | }) 34 | }) 35 | 36 | describe('.configInternalPackages', () => { 37 | it('should read tsconfig.json file', () => { 38 | Aliases.configInternalPackages() 39 | 40 | expect(mockReadJsonSync).toBeCalledWith('tsconfig.json') 41 | }) 42 | 43 | it('should add aliases for internal packages', () => { 44 | const references = [{ path: '../@react-vite-trpc/config' }] 45 | 46 | const tsConfig = { 47 | extends: '', 48 | compilerOptions: {}, 49 | references, 50 | } 51 | 52 | mockReadJsonSync.mockReturnValueOnce(tsConfig) 53 | 54 | Aliases.configInternalPackages() 55 | 56 | expect(mockAddAliases).toBeCalled() 57 | 58 | references.forEach(({ path }) => { 59 | const [_, internalPackage] = path.split('@') 60 | 61 | const packageName = `@${internalPackage}` 62 | 63 | expect(mockAddAliases).toBeCalledWith({ [packageName]: `${packageName}/dist/index.js` }) 64 | }) 65 | }) 66 | }) 67 | 68 | describe('.configDirectories', () => { 69 | it('should read and add aliases for directories', () => { 70 | const directories = ['core', 'env', 'middlewares', 'modules', 'testing', 'types'].map(name => ({ 71 | name, 72 | isDirectory: () => true, 73 | })) 74 | 75 | mockReaddirSync.mockReturnValueOnce(directories) 76 | 77 | Aliases.configDirectories() 78 | 79 | expect(mockReaddirSync).toHaveBeenCalledOnce() 80 | 81 | expect(mockAddAliases).toBeCalled() 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /apps/server/src/core/httpServer.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import http from 'http' 3 | 4 | import { PORT } from 'env' 5 | 6 | export class HttpServer { 7 | public static create() { 8 | const app = express() 9 | 10 | const server = http.createServer(app) 11 | 12 | server.listen(PORT, () => console.log(`🚀 Server has launched`)) 13 | 14 | return { app } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/server/src/core/index.ts: -------------------------------------------------------------------------------- 1 | export { HttpServer } from './httpServer' 2 | -------------------------------------------------------------------------------- /apps/server/src/env/env.ts: -------------------------------------------------------------------------------- 1 | import { cleanEnv, port, str as string } from 'envalid' 2 | 3 | export class Environment { 4 | public static config() { 5 | return cleanEnv(process.env, { 6 | PORT: port({ 7 | desc: 'Port the Express server is running on', 8 | example: '3001', 9 | default: 3001, 10 | docs: 'https://expressjs.com/en/starter/hello-world.html', 11 | }), 12 | NODE_ENV: string({ 13 | desc: 'The mode the Express is running in', 14 | example: 'development', 15 | choices: ['development', 'test', 'production'] as const, 16 | default: 'development', 17 | docs: 'https://nodejs.dev/en/learn/how-to-read-environment-variables-from-nodejs/', 18 | }), 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/server/src/env/index.ts: -------------------------------------------------------------------------------- 1 | import { Environment } from './env' 2 | 3 | export const { PORT, isProd, isDev } = Environment.config() 4 | -------------------------------------------------------------------------------- /apps/server/src/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | import cookieParser from 'cookie-parser' 2 | import express, { type Application } from 'express' 3 | import { join, resolve } from 'path' 4 | 5 | import { type RequestHandler } from 'express-serve-static-core' 6 | 7 | import { isProd } from 'env' 8 | 9 | import { initializeTrpc } from 'trpc/api/router' 10 | 11 | export class Middlewares { 12 | public static config(app: Application) { 13 | app.use(cookieParser() as RequestHandler) 14 | 15 | initializeTrpc(app) 16 | 17 | if (isProd) { 18 | this.serveWeb(app) 19 | } 20 | } 21 | 22 | private static serveWeb(app: Application) { 23 | const buildPath = resolve(__dirname, '../../../../web/dist') 24 | 25 | app.use(express.static(buildPath) as unknown as RequestHandler) 26 | 27 | app.get('*', (_, res) => res.sendFile(join(buildPath, 'index.html'))) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/server/src/server.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | 3 | import './aliases' 4 | 5 | import { HttpServer } from 'core' 6 | 7 | import { Middlewares } from 'middlewares' 8 | 9 | const { app } = HttpServer.create() 10 | 11 | Middlewares.config(app) 12 | -------------------------------------------------------------------------------- /apps/server/src/trpc/api/resolvers/getRole/getRole.e2e.test.ts: -------------------------------------------------------------------------------- 1 | import { describe } from 'node:test' 2 | import { expect, it } from 'vitest' 3 | 4 | import { client } from 'trpc/client' 5 | 6 | describe('getRole', () => { 7 | it('should return valid Role', async () => { 8 | const role = await client.getRole.query() 9 | 10 | expect(role).toStrictEqual({ role: 'USER' }) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /apps/server/src/trpc/api/resolvers/getRole/getRole.integration.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | 3 | it('getRole', async () => { 4 | expect(true).toBe(true) 5 | }) 6 | -------------------------------------------------------------------------------- /apps/server/src/trpc/api/resolvers/getRole/getRole.ts: -------------------------------------------------------------------------------- 1 | import { userProcedure } from 'trpc' 2 | 3 | export const getRole = userProcedure.query(() => ({ role: 'USER' as const })) 4 | -------------------------------------------------------------------------------- /apps/server/src/trpc/api/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | export { getRole } from './getRole/getRole' 2 | -------------------------------------------------------------------------------- /apps/server/src/trpc/api/router.ts: -------------------------------------------------------------------------------- 1 | import type { inferAsyncReturnType } from '@trpc/server' 2 | import type { CreateExpressContextOptions } from '@trpc/server/adapters/express' 3 | import { createExpressMiddleware } from '@trpc/server/adapters/express' 4 | import type { Application } from 'express' 5 | 6 | import { router } from 'trpc' 7 | 8 | import { getRole } from './resolvers' 9 | 10 | export type AppRouter = typeof appRouter 11 | 12 | export type Context = inferAsyncReturnType 13 | 14 | const appRouter = router({ getRole }) 15 | 16 | const createContext = ({ req, res }: CreateExpressContextOptions) => ({ 17 | req, 18 | res, 19 | }) 20 | 21 | export const initializeTrpc = async (app: Application) => { 22 | app.use( 23 | '/trpc', 24 | createExpressMiddleware({ 25 | router: appRouter, 26 | createContext, 27 | }) 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /apps/server/src/trpc/client.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCProxyClient, httpLink } from '@trpc/client' 2 | 3 | import { HttpService } from '@react-vite-trpc/config' 4 | 5 | import { type AppRouter } from './api/router' 6 | 7 | export const client = createTRPCProxyClient({ links: [httpLink({ url: `${HttpService.serverUrl}/trpc` })] }) 8 | -------------------------------------------------------------------------------- /apps/server/src/trpc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './trpc' 2 | -------------------------------------------------------------------------------- /apps/server/src/trpc/trpc.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC } from '@trpc/server' 2 | 3 | import { type Context } from './api/router' 4 | 5 | const t = initTRPC.context().create() 6 | 7 | export const userProcedure = t.procedure 8 | 9 | export const { middleware, router } = t 10 | -------------------------------------------------------------------------------- /apps/server/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type {} from 'express-serve-static-core' 2 | -------------------------------------------------------------------------------- /apps/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@react-vite-trpc/tsconfig/node.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": [ 8 | "src", 9 | "vite.config.ts", 10 | "vitest.setup.ts" 11 | ], 12 | "references": [ 13 | { 14 | "path": "../../@react-vite-trpc/config/tsconfig.json" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /apps/server/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { type UserConfig, defineConfig, mergeConfig } from 'vitest/config' 2 | 3 | import { vitestConfig } from '../../vitest.config' 4 | 5 | export default mergeConfig(vitestConfig, defineConfig({}) as UserConfig) 6 | -------------------------------------------------------------------------------- /apps/server/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv' 2 | 3 | config({ path: '.env.test' }) 4 | -------------------------------------------------------------------------------- /apps/web/.eslintrc: -------------------------------------------------------------------------------- 1 | { "extends": ["@react-vite-trpc/eslint-config/react"] } 2 | -------------------------------------------------------------------------------- /apps/web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 3, 3 | "printWidth": 120, 4 | "semi": false, 5 | "singleQuote": true, 6 | "arrowParens": "avoid", 7 | "trailingComma": "es5", 8 | "plugins": ["./node_modules/@trivago/prettier-plugin-sort-imports"], 9 | "importOrderSeparation": true, 10 | "importOrderSortSpecifiers": true, 11 | "importOrder": [ 12 | "", 13 | 14 | "core", 15 | 16 | "^env$", 17 | 18 | "^@react-vite-trpc(.*)", 19 | 20 | "^trpc(.*)", 21 | "^@trpc$", 22 | 23 | "^shared$", 24 | 25 | "^@redux$", 26 | "^@redux/reducers(.*)", 27 | 28 | "^styles(.*)", 29 | 30 | "^assets(.*)", 31 | 32 | "(.*)styled$", 33 | "(.*)styled/(.*)", 34 | 35 | "^components/shared$", 36 | 37 | "^components(.*)", 38 | "(.*)/components$", 39 | 40 | "^./modules$", 41 | 42 | "^@redux/hooks$", 43 | "^hooks$", 44 | "(.*)hooks$", 45 | 46 | "^services$", 47 | "(.*)services$", 48 | 49 | "^helpers$", 50 | "(.*)helpers$", 51 | 52 | "^utils$", 53 | "(.*)utils$", 54 | 55 | "^types(.*)", 56 | "(.*)types$", 57 | 58 | "^[./]" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /apps/web/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | baseUrl: 'http://127.0.0.1:3001', 6 | experimentalStudio: true, 7 | supportFile: false, 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /apps/web/cypress/e2e/Home.cy.ts: -------------------------------------------------------------------------------- 1 | it('Home', () => { 2 | expect(true).to.equal(true) 3 | }) 4 | 5 | export {} 6 | -------------------------------------------------------------------------------- /apps/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Typescript + React + Vite + Express + tRPC 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-vite-trpc/web", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "lint": "eslint .", 9 | "lint:fix": "eslint . --fix", 10 | "ts:check": "tsc -b", 11 | "stylelint": "stylelint src/**/*.{ts,tsx,scss}", 12 | "check": "pnpm lint && pnpm ts:check && pnpm stylelint", 13 | "build": "pnpm ts:check && vite build", 14 | "test:unit": "vitest .unit.test.ts .unit.test.tsx", 15 | "test:unit:run": "pnpm test:unit --run", 16 | "test:integration": "vitest .integration.test.ts .integration.test.tsx", 17 | "test:integration:run": "pnpm test:integration --run", 18 | "test:e2e": "cypress run --browser chrome", 19 | "test:e2e:run": "cypress run", 20 | "test:coverage": "vitest run --coverage", 21 | "test:coverage:preview": "pnpm test:coverage && vite preview --outDir ./coverage --open", 22 | "cypress": "cypress open", 23 | "cypress:install": "cypress install" 24 | }, 25 | "dependencies": { 26 | "@react-vite-trpc/ui": "workspace:*", 27 | "@tanstack/react-query": "^4.29.12", 28 | "@trpc/client": "^10.29.1", 29 | "@trpc/react-query": "^10.29.1", 30 | "@trpc/server": "^10.29.1", 31 | "react": "^18.2.0", 32 | "react-dom": "^18.2.0", 33 | "react-query": "^3.39.3", 34 | "styled-components": "^5.3.11" 35 | }, 36 | "devDependencies": { 37 | "@testing-library/jest-dom": "^5.17.0", 38 | "@testing-library/react": "^14.0.0", 39 | "@types/testing-library__jest-dom": "^5.14.9", 40 | "@types/jest": "^29.5.3", 41 | "@types/react": "^18.2.11", 42 | "@types/react-dom": "^18.2.4", 43 | "cypress": "^12.17.3", 44 | "@vitest/coverage-istanbul": "^0.34.1", 45 | "@types/styled-components": "^5.1.26", 46 | "@vitejs/plugin-react": "^3.1.0", 47 | "sass": "^1.63.3", 48 | "envalid": "^7.3.1", 49 | "@tanstack/router": "0.0.1-beta.139", 50 | "postcss-styled-syntax": "^0.4.0", 51 | "stylelint": "^15.10.2", 52 | "jsdom": "^22.1.0", 53 | "typescript": "^4.9.5", 54 | "vite": "^4.3.9", 55 | "vitest": "^0.34.1", 56 | "vite-tsconfig-paths": "^4.2.0", 57 | "@react-vite-trpc/tsconfig": "workspace:*", 58 | "@react-vite-trpc/eslint-config": "workspace:*", 59 | "@trivago/prettier-plugin-sort-imports": "^4.2.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /apps/web/src/components/core/App.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet, RootRoute, Route, Router, RouterProvider } from '@tanstack/router' 2 | import styled from 'styled-components' 3 | 4 | import { GlobalStyle } from 'styles' 5 | 6 | import { Home } from 'components/features' 7 | 8 | const rootRoute = new RootRoute({ component: () => }) 9 | 10 | const home = new Route({ 11 | getParentRoute: () => rootRoute, 12 | path: '/', 13 | component: Home, 14 | }) 15 | 16 | const routeTree = rootRoute.addChildren([home]) 17 | 18 | const router = new Router({ routeTree }) 19 | 20 | window.navigate = router.navigate 21 | 22 | export const App = () => ( 23 | 24 | 25 | 26 | 27 | ) 28 | 29 | const AppContainer = styled.div` 30 | height: 100%; 31 | ` 32 | 33 | declare global { 34 | interface Window { 35 | navigate: typeof router.navigate 36 | } 37 | } 38 | 39 | declare module '@tanstack/router' { 40 | interface Register { 41 | router: typeof router 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /apps/web/src/components/core/index.ts: -------------------------------------------------------------------------------- 1 | export { App } from './App' 2 | -------------------------------------------------------------------------------- /apps/web/src/components/features/Home/Home.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | import { Label } from '@react-vite-trpc/ui' 4 | 5 | import { trpc } from 'trpc' 6 | 7 | import * as Styled from './styled' 8 | 9 | export const Home = () => { 10 | const { data } = trpc.getRole.useQuery() 11 | 12 | return ( 13 | 14 | 15 | 19 | 20 | ) 21 | } 22 | 23 | const HomeContainer = styled.div` 24 | display: flex; 25 | flex-direction: column; 26 | align-items: center; 27 | justify-content: center; 28 | min-height: 100vh; 29 | background: linear-gradient(to right, #434343, #000000); 30 | ` 31 | -------------------------------------------------------------------------------- /apps/web/src/components/features/Home/styled/Gif.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Gif = styled.img` 4 | margin-top: 25px; 5 | ` 6 | -------------------------------------------------------------------------------- /apps/web/src/components/features/Home/styled/index.ts: -------------------------------------------------------------------------------- 1 | export { Gif } from './Gif' 2 | -------------------------------------------------------------------------------- /apps/web/src/components/features/index.ts: -------------------------------------------------------------------------------- 1 | export { Home } from './Home/Home' 2 | -------------------------------------------------------------------------------- /apps/web/src/components/shared/Providers.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClientProvider } from '@tanstack/react-query' 2 | import { type PropsWithChildren } from 'react' 3 | import { ThemeProvider } from 'styled-components' 4 | 5 | import { trpc } from 'trpc' 6 | 7 | import { theme } from 'styles' 8 | import 'styles/index.scss' 9 | 10 | import { useTrpc } from 'hooks' 11 | 12 | export const Providers = ({ children }: PropsWithChildren) => { 13 | const { trpcQueryClient, trpcClient } = useTrpc() 14 | 15 | return ( 16 | 17 | 18 | {children} 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /apps/web/src/components/shared/index.ts: -------------------------------------------------------------------------------- 1 | export { Providers } from './Providers' 2 | -------------------------------------------------------------------------------- /apps/web/src/env/env.ts: -------------------------------------------------------------------------------- 1 | import { cleanEnv, str as string } from 'envalid' 2 | 3 | export class Environment { 4 | public static config(env: unknown) { 5 | return cleanEnv(env, { 6 | MODE: string({ 7 | desc: 'The mode the web is running in', 8 | example: 'development', 9 | choices: ['development', 'test', 'production'] as const, 10 | default: 'development', 11 | docs: 'https://vitejs.dev/guide/env-and-mode.html', 12 | }), 13 | }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/src/env/index.ts: -------------------------------------------------------------------------------- 1 | import { Environment } from './env' 2 | 3 | export const { MODE } = Environment.config(import.meta.env) 4 | -------------------------------------------------------------------------------- /apps/web/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useTrpc } from './useTrpc' 2 | -------------------------------------------------------------------------------- /apps/web/src/hooks/useTrpc.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from '@tanstack/react-query' 2 | import { httpLink } from '@trpc/client/links/httpLink' 3 | import { useState } from 'react' 4 | 5 | import { trpc } from 'trpc' 6 | 7 | export const useTrpc = () => { 8 | const [trpcQueryClient] = useState( 9 | () => 10 | new QueryClient({ 11 | defaultOptions: { 12 | queries: { 13 | staleTime: Infinity, 14 | refetchOnWindowFocus: false, 15 | }, 16 | }, 17 | }) 18 | ) 19 | 20 | const [trpcClient] = useState(() => trpc.createClient({ links: [httpLink({ url: '/trpc' })] })) 21 | 22 | return { 23 | trpcQueryClient, 24 | trpcClient, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/web/src/main.integration.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react' 2 | 3 | import { App } from 'components/core' 4 | 5 | import { Providers } from 'components/shared' 6 | 7 | describe('main.tsx', () => { 8 | it('should render App within Providers', () => { 9 | render( 10 | 11 | 12 | 13 | ) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /apps/web/src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client' 2 | 3 | import { App } from 'components/core/App' 4 | 5 | import { Providers } from 'components/shared' 6 | 7 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 8 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /apps/web/src/main.unit.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react' 2 | import { vi } from 'vitest' 3 | 4 | import { act } from 'react-dom/test-utils' 5 | 6 | describe('main.tsx', () => { 7 | it('should call document.getElementById', async () => { 8 | render(
) 9 | 10 | const spiedDocument = vi.spyOn(document, 'getElementById') 11 | 12 | await act(async () => { 13 | await import('./main') 14 | }) 15 | 16 | expect(spiedDocument).toBeCalled() 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /apps/web/src/styles/globalStyle.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components' 2 | 3 | export const GlobalStyle = createGlobalStyle`` 4 | -------------------------------------------------------------------------------- /apps/web/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0px; 3 | padding: 0px; 4 | box-sizing: border-box; 5 | } 6 | 7 | html, 8 | body, 9 | #root { 10 | height: 100%; 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/src/styles/index.ts: -------------------------------------------------------------------------------- 1 | export { GlobalStyle } from './globalStyle' 2 | export { mediaQueries } from './mediaQueries' 3 | export { theme } from './theme' 4 | -------------------------------------------------------------------------------- /apps/web/src/styles/mediaQueries.ts: -------------------------------------------------------------------------------- 1 | const breakpoints = { 2 | largeDesktop: '1600px', 3 | desktop: '900px', 4 | tablet: '768px', 5 | smallTablet: ' 500px', 6 | mobile: '400px', 7 | } 8 | 9 | export const mediaQueries = { 10 | largeDesktop: `(max-width: ${breakpoints.largeDesktop})`, 11 | desktop: `(max-width: ${breakpoints.desktop})`, 12 | tablet: `(max-width: ${breakpoints.tablet})`, 13 | smallTablet: `(max-width: ${breakpoints.smallTablet})`, 14 | mobile: `(max-width: ${breakpoints.mobile})`, 15 | 16 | minLargeDesktop: `(min-width: ${breakpoints.largeDesktop})`, 17 | minDesktop: `(min-width: ${breakpoints.desktop})`, 18 | minTablet: `(min-width: ${breakpoints.tablet})`, 19 | minSmallTablet: `(min-width: ${breakpoints.smallTablet})`, 20 | minMobile: `(min-width: ${breakpoints.mobile})`, 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/src/styles/theme.ts: -------------------------------------------------------------------------------- 1 | export const theme = { 2 | colors: { 3 | primary: '#000', 4 | }, 5 | } as const 6 | -------------------------------------------------------------------------------- /apps/web/src/trpc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './trpc' 2 | -------------------------------------------------------------------------------- /apps/web/src/trpc/trpc.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCReact } from '@trpc/react-query' 2 | 3 | import { type AppRouter } from '../../../server/src/trpc/api/router' 4 | 5 | export const trpc = createTRPCReact() 6 | -------------------------------------------------------------------------------- /apps/web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@react-vite-trpc/tsconfig/react.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "outDir": "dist", 6 | "composite": false 7 | }, 8 | "include": ["src", "cypress", "vite.config.ts", "vitest.setup.ts"], 9 | "references": [ 10 | { 11 | "path": "../server" 12 | }, 13 | { 14 | "path": "../../@react-vite-trpc/ui/tsconfig.json" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /apps/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import { defineConfig, loadEnv } from 'vite' 3 | import tsconfigPaths from 'vite-tsconfig-paths' 4 | 5 | import { Environment } from './src/env/env' 6 | 7 | // --------------------plugins-------------------- 8 | 9 | type Env = Record 10 | 11 | const envPlugin = (env: Env) => ({ 12 | name: 'env', 13 | transform: () => { 14 | Environment.config(env) 15 | }, 16 | }) 17 | 18 | // --------------------config-------------------- 19 | 20 | export default defineConfig(({ mode }) => { 21 | const env = loadEnv(mode, process.cwd()) 22 | 23 | return { 24 | plugins: [envPlugin(env), tsconfigPaths(), react({ babel: { plugins: [['babel-plugin-styled-components']] } })], 25 | server: { 26 | host: true, 27 | port: 3000, 28 | open: true, 29 | proxy: { '/trpc': { target: 'http://localhost:3001' } }, 30 | }, 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /apps/web/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, mergeConfig } from 'vitest/config' 2 | 3 | import { vitestConfig } from '../../vitest.config' 4 | 5 | export default mergeConfig( 6 | vitestConfig, 7 | defineConfig({ 8 | test: { 9 | globals: true, 10 | environment: 'jsdom', 11 | }, 12 | }) 13 | ) 14 | -------------------------------------------------------------------------------- /apps/web/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import * as matchers from '@testing-library/jest-dom/matchers' 2 | import { cleanup } from '@testing-library/react' 3 | import { afterEach, expect } from 'vitest' 4 | 5 | expect.extend(matchers) 6 | 7 | afterEach(() => { 8 | cleanup() 9 | }) 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-vite-trpc", 3 | "version": "1.0.0", 4 | "private": true, 5 | "packageManager": "pnpm@8.6.5", 6 | "engines": { 7 | "node": "18.x" 8 | }, 9 | "scripts": { 10 | "start": "pnpm -F @react-vite-trpc/server start", 11 | "pm2:start": "pnpm -F @react-vite-trpc/server pm2:start", 12 | "pm2:delete": "pnpm -F @react-vite-trpc/server pm2:delete", 13 | "pm2:logs": "pnpm -F @react-vite-trpc/server pm2:logs", 14 | "dev": "turbo run dev", 15 | "lint": "turbo run lint", 16 | "lint:fix": "turbo run lint:fix", 17 | "stylelint": "turbo run stylelint", 18 | "ts:check": "turbo run ts:check", 19 | "check": "turbo run check", 20 | "build": "turbo run build", 21 | "build:lib": "turbo run build:lib", 22 | "test:unit:run": "turbo run test:unit:run", 23 | "test:integration:run": "turbo run test:integration:run", 24 | "test:e2e:run": "turbo run test:e2e:run", 25 | "test:coverage": "turbo run test:coverage", 26 | "test:coverage:preview": "turbo run test:coverage:preview", 27 | "cypress:install": "pnpm -F @react-vite-trpc/web cypress:install", 28 | "postinstall": "pnpm build:lib" 29 | }, 30 | "devDependencies": { 31 | "@react-vite-trpc/eslint-config": "workspace:*", 32 | "@trivago/prettier-plugin-sort-imports": "^4.2.0", 33 | "@types/glob": "^8.1.0", 34 | "eslint": "^8.46.0", 35 | "glob": "^10.3.3", 36 | "stylelint": "^15.10.2", 37 | "ts-node": "^10.9.1", 38 | "turbo": "^1.10.12", 39 | "typescript": "^5.1.6", 40 | "vite-tsconfig-paths": "^4.2.0", 41 | "vitest": "^0.34.1", 42 | "@swc/core": "^1.3.71" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - '@react-vite-trpc/*' 3 | - 'apps/*' 4 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "pipeline": { 4 | "dev": { "persistent": true, "cache": false }, 5 | "lint": { "outputs": [] }, 6 | "lint:fix": { "outputs": [] }, 7 | "ts:check": { "outputs": [] }, 8 | "stylelint": { "outputs": [] }, 9 | "check": { "outputs": [] }, 10 | "build": { 11 | "dependsOn": ["^build:lib"], 12 | "outputs": ["dist/**"] 13 | }, 14 | "build:lib": { "outputs": ["dist/**"] }, 15 | "test:unit:run": { "outputs": [] }, 16 | "test:integration:run": { "outputs": [] }, 17 | "test:e2e:run": { "outputs": [] }, 18 | "test:coverage": { "outputs": ["coverage/**"] }, 19 | "test:coverage:preview": { "outputs": [] } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths' 2 | import { defineConfig } from 'vitest/config' 3 | 4 | export const vitestConfig = defineConfig({ 5 | test: { 6 | setupFiles: ['vitest.setup.ts'], 7 | coverage: { 8 | provider: 'istanbul', 9 | reporter: ['html', 'text-summary', 'lcovonly'], 10 | all: true, 11 | }, 12 | testTimeout: 15000, 13 | }, 14 | plugins: [tsconfigPaths()], 15 | }) 16 | --------------------------------------------------------------------------------