├── .eslintrc.json ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .yarn └── releases │ └── yarn-4.0.1.cjs ├── .yarnrc.yml ├── README.md ├── index.html ├── package.json ├── postcss.config.cjs ├── public ├── about.txt ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico └── site.webmanifest ├── server.ts ├── src ├── __tests__ │ ├── demo.test.tsx │ ├── setupTests.ts │ ├── testingHook.ts │ └── tsconfig.json ├── client │ ├── App.tsx │ ├── Context.tsx │ ├── components │ │ └── Footer.tsx │ ├── entry-client.tsx │ ├── entry-server.tsx │ ├── index.css │ └── pages │ │ └── Main.tsx └── server │ └── routes │ └── api.ts ├── tailwind.config.cjs ├── tsconfig.json ├── types └── types.d.ts ├── video.gif ├── vite.config.ts └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es2021": true 6 | }, 7 | "extends": ["plugin:react/recommended", "prettier", "eslint:recommended"], 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "ecmaFeatures": { 11 | "jsx": true 12 | }, 13 | "ecmaVersion": 12, 14 | "sourceType": "module" 15 | }, 16 | "plugins": ["react", "@typescript-eslint", "react-hooks"], 17 | "rules": { 18 | "no-use-before-define": "off", 19 | "no-unused-vars": "off", 20 | "@typescript-eslint/no-use-before-define": ["error"], 21 | "react-hooks/rules-of-hooks": "error", 22 | "react-hooks/exhaustive-deps": "warn" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Use Node.js 19 12 | uses: actions/setup-node@v3 13 | with: 14 | node-version: 19 15 | - name: yarn install, build, and test 16 | run: | 17 | yarn install --immutable 18 | yarn build 19 | yarn typecheck 20 | yarn test 21 | env: 22 | CI: true 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | 11 | pids 12 | logs 13 | results 14 | tmp 15 | 16 | # Build 17 | public/css/main.css 18 | 19 | # Coverage reports 20 | coverage 21 | 22 | # API keys and secrets 23 | .env 24 | 25 | # Dependency directory 26 | node_modules 27 | bower_components 28 | 29 | # Editors 30 | .idea 31 | *.iml 32 | 33 | # OS metadata 34 | .DS_Store 35 | Thumbs.db 36 | 37 | # Ignore built ts files 38 | dist/**/* 39 | 40 | .pnp.* 41 | .yarn/* 42 | !.yarn/patches 43 | !.yarn/plugins 44 | !.yarn/releases 45 | !.yarn/sdks 46 | !.yarn/versions 47 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v19 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | build 3 | node_modules 4 | .github 5 | .yarn 6 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.0.1.cjs 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vite Typescript React 18 SSR 2 | 3 | [![Node CI](https://github.com/jonluca/vite-typescript-ssr-react/actions/workflows/nodejs.yml/badge.svg)](https://github.com/jonluca/vite-typescript-ssr-react/actions/workflows/nodejs.yml) 4 | 5 | A _blazingly_ modern web development stack. This template repo tries to achieve the minimum viable example for each of the following: 6 | 7 | ![video](video.gif) 8 | 9 | - [React 18](https://reactjs.org/blog/2022/03/29/react-v18.html) 10 | - [Typescript 4.9](https://devblogs.microsoft.com/typescript/announcing-typescript-4-7/) 11 | - [Vite with Vite SSR](https://vitejs.dev/guide/ssr.html) 12 | - [GitHub Actions](https://github.com/features/actions) 13 | - [Tailwind CSS](https://tailwindui.com/) 14 | - [Prettier](https://prettier.io/) & [ESLint](https://eslint.org/) 15 | 16 | ## Development 17 | 18 | ``` 19 | yarn 20 | yarn dev:server 21 | ``` 22 | 23 | That should start the server. It will open to http://localhost:7456. 24 | 25 | If you'd like to just develop the UI, you can use 26 | 27 | ```bash 28 | yarn 29 | yarn dev:client 30 | ``` 31 | 32 | To start the native vite client. 33 | 34 | ## Building 35 | 36 | ``` 37 | yarn build 38 | yarn serve 39 | ``` 40 | 41 | yarn build will create the assets in `dist` - a `client` and `server` folder. Serve will run `dist/server.js` with Node, but feel free to change this to use Docker or some other process manager to suit your deployment needs. 42 | 43 | ## Files 44 | 45 | `eslintrc.js` - a barebones eslint configuration for 2021, that extends off of the recommended ESLint config and prettier 46 | 47 | `.prettierrc.js` - the prettier config 48 | 49 | `index.html` - the vite entrypoint, that includes the entry point for the client 50 | 51 | `postcss.config.cjs` - CommonJS module that defines the PostCSS config 52 | 53 | `server.ts` - The barebones Express server with logic for SSRing Vite pages 54 | 55 | `tailwind.config.cjs` - CommonJS module that defines the Tailwind config 56 | 57 | `tsconfig.json` - TypeScript configuration 58 | 59 | `vite.config.ts` - Vite configuration 60 | 61 | ## CI 62 | 63 | We use GitHub actions to build the app. The badge is at the top of the repo. Currently it just confirms that everything builds properly. 64 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-typescript-ssr-react", 3 | "version": "1.0.1", 4 | "type": "module", 5 | "description": "Boilerplate for a modern web stack", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/jonluca/vite-typescript-ssr-react" 9 | }, 10 | "author": "JonLuca DeCaro", 11 | "license": "MIT", 12 | "scripts": { 13 | "dev:server": "tsx server.ts", 14 | "dev:client": "yarn build:client && vite --config vite.config.ts dev", 15 | "build": "rimraf dist && tsc -p tsconfig.json && yarn build:client && yarn build:server && yarn copy-files", 16 | "build:client": "vite build --outDir dist/client --ssrManifest", 17 | "build:server": "vite build --ssr src/client/entry-server.tsx --outDir dist/server", 18 | "test": "vitest", 19 | "test:watch": "vitest --watch", 20 | "coverage": "vitest --coverage", 21 | "typecheck": "tsc --noEmit", 22 | "serve": "yarn build && cross-env NODE_ENV=production node ./dist/server", 23 | "serve:local": "vite serve", 24 | "clean": "rimraf dist/", 25 | "copy-files": "copyfiles \"public/**/*\" dist && copyfiles -u 2 \"dist/client/**/*\" dist && copyfiles -u 2 \"dist/client/assets/**/*\" dist/public", 26 | "format": "prettier --write src types" 27 | }, 28 | "dependencies": { 29 | "@types/serve-static": "^1.15.4", 30 | "autoprefixer": "^10.4.16", 31 | "compression": "1.7.4", 32 | "cross-env": "^7.0.3", 33 | "express": "4.18.2", 34 | "react": "^18.2.0", 35 | "react-dom": "^18.2.0", 36 | "react-router-dom": "^6.18.0", 37 | "serve-static": "^1.15.0", 38 | "tsx": "^3.14.0" 39 | }, 40 | "devDependencies": { 41 | "@testing-library/jest-dom": "^6.1.4", 42 | "@testing-library/react": "^14.0.0", 43 | "@types/compression": "1.7.4", 44 | "@types/concurrently": "6.4.0", 45 | "@types/eslint": "8.44.6", 46 | "@types/express": "^4.17.20", 47 | "@types/node": "20.8.10", 48 | "@types/react": "^18.2.34", 49 | "@types/react-dom": "^18.2.14", 50 | "@types/react-router-dom": "^5.3.3", 51 | "@typescript-eslint/eslint-plugin": "^6.9.1", 52 | "@typescript-eslint/parser": "^6.9.1", 53 | "@vitejs/plugin-react": "^4.1.1", 54 | "@vitest/coverage-v8": "^0.34.6", 55 | "concurrently": "8.2.2", 56 | "copyfiles": "^2.4.1", 57 | "eslint": "^8.52.0", 58 | "eslint-config-prettier": "^9.0.0", 59 | "eslint-config-standard": "^17.1.0", 60 | "eslint-plugin-react": "^7.33.2", 61 | "eslint-plugin-react-hooks": "^4.6.0", 62 | "jsdom": "^22.1.0", 63 | "postcss": "8.4.31", 64 | "prettier": "^3.0.3", 65 | "rimraf": "^5.0.5", 66 | "tailwindcss": "3.3.5", 67 | "typescript": "5.2.2", 68 | "vite": "4.5.0", 69 | "vitest": "^0.34.6" 70 | }, 71 | "packageManager": "yarn@4.0.1", 72 | "prettier": { 73 | "printWidth": 120, 74 | "semi": true, 75 | "singleQuote": false, 76 | "trailingComma": "all", 77 | "bracketSpacing": true, 78 | "arrowParens": "avoid" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | // postcss.config.cjs 2 | module.exports = { 3 | plugins: { 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /public/about.txt: -------------------------------------------------------------------------------- 1 | This favicon was generated using the following font: 2 | 3 | - Font Title: Lekton 4 | - Font Author: Copyright (c) 2008, 2009, 2010, Accademia di Belle Arti di Urbino (luciano.perondi@isiaurbino.net). Licensed under the SIL Open Font License, Version 1.1 5 | - Font Source: http://fonts.gstatic.com/s/lekton/v17/SZc43FDmLaWmWpBeXxfonUPL6Q.ttf 6 | - Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL)) 7 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonluca/vite-typescript-ssr-react/23b550af752a4642185359146b0b991abfe781d7/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonluca/vite-typescript-ssr-react/23b550af752a4642185359146b0b991abfe781d7/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonluca/vite-typescript-ssr-react/23b550af752a4642185359146b0b991abfe781d7/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonluca/vite-typescript-ssr-react/23b550af752a4642185359146b0b991abfe781d7/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonluca/vite-typescript-ssr-react/23b550af752a4642185359146b0b991abfe781d7/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonluca/vite-typescript-ssr-react/23b550af752a4642185359146b0b991abfe781d7/public/favicon.ico -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, 6 | { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } 7 | ], 8 | "theme_color": "#ffffff", 9 | "background_color": "#ffffff", 10 | "display": "standalone" 11 | } 12 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response, NextFunction } from "express"; 2 | import fs from "fs/promises"; 3 | import path, { dirname } from "path"; 4 | import express from "express"; 5 | import compression from "compression"; 6 | import serveStatic from "serve-static"; 7 | import { createServer as createViteServer } from "vite"; 8 | import { fileURLToPath } from "url"; 9 | const isTest = process.env.NODE_ENV === "test" || !!process.env.VITE_TEST_BUILD; 10 | 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = dirname(__filename); 13 | 14 | const resolve = (p: string) => path.resolve(__dirname, p); 15 | 16 | const getStyleSheets = async () => { 17 | try { 18 | const assetpath = resolve("public"); 19 | const files = await fs.readdir(assetpath); 20 | const cssAssets = files.filter(l => l.endsWith(".css")); 21 | const allContent = []; 22 | for (const asset of cssAssets) { 23 | const content = await fs.readFile(path.join(assetpath, asset), "utf-8"); 24 | allContent.push(``); 25 | } 26 | return allContent.join("\n"); 27 | } catch { 28 | return ""; 29 | } 30 | }; 31 | 32 | async function createServer(isProd = process.env.NODE_ENV === "production") { 33 | const app = express(); 34 | // Create Vite server in middleware mode and configure the app type as 35 | // 'custom', disabling Vite's own HTML serving logic so parent server 36 | // can take control 37 | const vite = await createViteServer({ 38 | server: { middlewareMode: true }, 39 | appType: "custom", 40 | logLevel: isTest ? "error" : "info", 41 | root: isProd ? "dist" : "", 42 | optimizeDeps: { include: [] }, 43 | }); 44 | 45 | // use vite's connect instance as middleware 46 | // if you use your own express router (express.Router()), you should use router.use 47 | app.use(vite.middlewares); 48 | const assetsDir = resolve("public"); 49 | const requestHandler = express.static(assetsDir); 50 | app.use(requestHandler); 51 | app.use("/public", requestHandler); 52 | 53 | if (isProd) { 54 | app.use(compression()); 55 | app.use( 56 | serveStatic(resolve("client"), { 57 | index: false, 58 | }), 59 | ); 60 | } 61 | const stylesheets = getStyleSheets(); 62 | 63 | // 1. Read index.html 64 | const baseTemplate = await fs.readFile(isProd ? resolve("client/index.html") : resolve("index.html"), "utf-8"); 65 | const productionBuildPath = path.join(__dirname, "./server/entry-server.js"); 66 | const devBuildPath = path.join(__dirname, "./src/client/entry-server.tsx"); 67 | const buildModule = isProd ? productionBuildPath : devBuildPath; 68 | const { render } = await vite.ssrLoadModule(buildModule); 69 | 70 | app.use("*", async (req: Request, res: Response, next: NextFunction) => { 71 | const url = req.originalUrl; 72 | 73 | try { 74 | // 2. Apply Vite HTML transforms. This injects the Vite HMR client, and 75 | // also applies HTML transforms from Vite plugins, e.g. global preambles 76 | // from @vitejs/plugin-react 77 | const template = await vite.transformIndexHtml(url, baseTemplate); 78 | // 3. Load the server entry. vite.ssrLoadModule automatically transforms 79 | // your ESM source code to be usable in Node.js! There is no bundling 80 | // required, and provides efficient invalidation similar to HMR. 81 | 82 | // 4. render the app HTML. This assumes entry-server.js's exported `render` 83 | // function calls appropriate framework SSR APIs, 84 | // e.g. ReactDOMServer.renderToString() 85 | const appHtml = await render(url); 86 | const cssAssets = await stylesheets; 87 | 88 | // 5. Inject the app-rendered HTML into the template. 89 | const html = template.replace(``, appHtml).replace(``, cssAssets); 90 | 91 | // 6. Send the rendered HTML back. 92 | res.status(200).set({ "Content-Type": "text/html" }).end(html); 93 | } catch (e: any) { 94 | !isProd && vite.ssrFixStacktrace(e); 95 | console.log(e.stack); 96 | // If an error is caught, let Vite fix the stack trace so it maps back to 97 | // your actual source code. 98 | vite.ssrFixStacktrace(e); 99 | next(e); 100 | } 101 | }); 102 | const port = process.env.PORT || 7456; 103 | app.listen(Number(port), "0.0.0.0", () => { 104 | console.log(`App is listening on http://localhost:${port}`); 105 | }); 106 | } 107 | 108 | createServer(); 109 | -------------------------------------------------------------------------------- /src/__tests__/demo.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, renderHook, act } from "@testing-library/react"; 3 | import { Footer } from "../client/components/Footer"; 4 | import useCounter from "./testingHook"; 5 | import { describe, test } from "vitest"; 6 | describe("demo", () => { 7 | test("should be testable", ({ expect }) => { 8 | expect(1 + 1).toBe(2); 9 | }); 10 | test("should be able to test component", ({ expect }) => { 11 | const { getByText } = render(