├── pages ├── about │ ├── index.css │ └── index.page.tsx ├── _default │ ├── types.ts │ ├── PageLayout.tsx │ ├── _default.page.client.tsx │ ├── _default.page.server.tsx │ └── logo.svg ├── _error.page.tsx └── index.page.tsx ├── .env.sample ├── .eslintignore ├── .gitignore ├── .prettierignore ├── screenshot-rocks.jpeg ├── types.d.ts ├── .husky ├── commit-msg └── pre-commit ├── nodemon.json ├── vite.config.ts ├── .commitlintrc.json ├── .prettierrc.js ├── tsconfig.json ├── README.md ├── server ├── auth.ts └── index.ts ├── .eslintrc.js └── package.json /pages/about/index.css: -------------------------------------------------------------------------------- 1 | h1, 2 | p { 3 | color: green; 4 | } 5 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | NEXTAUTH_URL="" 2 | GOOGLE_CLIENT_ID="" 3 | GOOGLE_CLIENT_SECRET="" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | node_modules/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | *.local 5 | yarn-error.log 6 | .env 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | node_modules/* -------------------------------------------------------------------------------- /screenshot-rocks.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-kris/vite-ssr-starter/HEAD/screenshot-rocks.jpeg -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const imageUrl: string; 3 | export default imageUrl; 4 | } 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit 5 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["server"], 3 | "exec": "ts-node server/index.ts", 4 | "ext": "js ts" 5 | } -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | tsc --noEmit -p tsconfig.json && yarn lint-staged 5 | -------------------------------------------------------------------------------- /pages/about/index.page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./index.css"; 3 | 4 | export { Page }; 5 | 6 | function Page() { 7 | return ( 8 | <> 9 |

About

10 |

A colored page.

11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import reactRefresh from "@vitejs/plugin-react-refresh"; 2 | import ssr from "vite-plugin-ssr/plugin"; 3 | import { UserConfig } from "vite"; 4 | 5 | const config: UserConfig = { 6 | plugins: [reactRefresh(), ssr()], 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /pages/_default/types.ts: -------------------------------------------------------------------------------- 1 | export type ReactComponent = (pageProps: PageProps) => JSX.Element; 2 | export type PageProps = {}; 3 | export type PageContext = { 4 | Page: ReactComponent; 5 | pageProps: PageProps; 6 | documentProps?: { 7 | title?: string; 8 | description?: string; 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional" 4 | ], 5 | "rules": { 6 | "scope-enum": [ 7 | 2, 8 | "always", 9 | [ 10 | "api", 11 | "ui", 12 | "tests" 13 | ] 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /pages/_default/PageLayout.tsx: -------------------------------------------------------------------------------- 1 | import { ChakraProvider } from '@chakra-ui/react'; 2 | import React from 'react'; 3 | 4 | export { PageLayout }; 5 | 6 | type Children = React.ReactNode; 7 | 8 | function PageLayout({ children }: { children: Children }) { 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /pages/_error.page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export { Page }; 4 | 5 | function Page({ is404 }: { is404: boolean }) { 6 | if (is404) { 7 | return ( 8 | <> 9 |

404 Page Not Found

10 | This page could not be found. 11 | 12 | ); 13 | } else { 14 | return ( 15 | <> 16 |

500 Internal Server Error

17 | Something went wrong. 18 | 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pages/_default/_default.page.client.tsx: -------------------------------------------------------------------------------- 1 | import process from 'process'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { getPage } from 'vite-plugin-ssr/client'; 5 | 6 | import { PageLayout } from './PageLayout'; 7 | 8 | window.process = process; 9 | 10 | hydrate(); 11 | 12 | async function hydrate() { 13 | const pageContext = await getPage(); 14 | const { Page, pageProps } = pageContext; 15 | ReactDOM.hydrate( 16 | 17 | 18 | , 19 | document.getElementById('page-view'), 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 90, 6 | tabWidth: 2, 7 | jsxBracketSameLine: true, 8 | endOfLine: 'auto', 9 | }; 10 | 11 | // { 12 | // "arrowParens": "always", 13 | // "bracketSpacing": true, 14 | // "embeddedLanguageFormatting": "auto", 15 | // "htmlWhitespaceSensitivity": "css", 16 | // "insertPragma": false, 17 | // "jsxBracketSameLine": false, 18 | // "jsxSingleQuote": false, 19 | // "proseWrap": "preserve", 20 | // "quoteProps": "as-needed", 21 | // "requirePragma": false, 22 | // "semi": true, 23 | // "singleQuote": false, 24 | // "tabWidth": 2, 25 | // "trailingComma": "es5", 26 | // "useTabs": false, 27 | // "vueIndentScriptAndStyle": false, 28 | // "printWidth": 100 29 | // } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "lib": [ 7 | "DOM", 8 | "DOM.Iterable", 9 | "ESNext" 10 | ], 11 | "jsx": "react", 12 | "skipLibCheck": true, 13 | "types": [ 14 | "vite/client" 15 | ], 16 | "allowJs": false, 17 | "esModuleInterop": true, 18 | "allowSyntheticDefaultImports": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "moduleResolution": "Node", 21 | "resolveJsonModule": true, 22 | "isolatedModules": true, 23 | "noEmit": true 24 | }, 25 | "ts-node": { 26 | "transpileOnly": true, 27 | "compilerOptions": { 28 | "module": "CommonJS" 29 | } 30 | }, 31 | "include": [ 32 | "./server", 33 | "./pages" 34 | ], 35 | "exclude": [ 36 | "node_modules" 37 | ] 38 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Vite SSR Starter 3 | Quickly build full-stack webapps with SSR using vitejs. This is a completely opinionated repo which I built for my own purposes. 4 | 5 | screenshot-rocks 6 | 7 | It is built using following stack for linting, formatting, type-checking, authentication: 8 | 9 | 1. vite-plugin-ssr 10 | 2. reactjs 11 | 3. typescript 12 | 4. eslint 13 | 5. prettier 14 | 6. husky 15 | 7. commit-lint 16 | 8. chakra-ui 17 | 9. next-auth 18 | 10. lint-staged 19 | 20 | 21 | ## Usage 22 | 23 | 1. ```git clone https://github.com/s-kris/vite-ssr-starter``` 24 | 2. Configure ```./server/auth/next.ts``` as mentioned [here](https://next-auth.js.org/configuration/options). 25 | 3. ```yarn install``` 26 | 4. ```yarn dev``` 27 | 28 | 29 | ## Todo 30 | 1. Add a testing framework 31 | 32 | ## Contributing 33 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 34 | 35 | 36 | 37 | ## License 38 | [MIT](https://choosealicense.com/licenses/mit/) -------------------------------------------------------------------------------- /server/auth.ts: -------------------------------------------------------------------------------- 1 | import { json, urlencoded } from 'body-parser'; 2 | import cookieParser from 'cookie-parser'; 3 | import { Router } from 'express'; 4 | import { IncomingMessage, ServerResponse } from 'http'; 5 | import NextAuth, { NextAuthOptions } from 'next-auth'; 6 | 7 | /** 8 | * Should match the following paths: 9 | * /api/auth/signin 10 | * /api/auth/signin/:provider 11 | * /api/auth/callback/:provider 12 | * /api/auth/signout 13 | * /api/auth/session 14 | * /api/auth/csrf 15 | * /api/auth/providers 16 | * /api/auth/_log 17 | * 18 | * See: https://next-auth.js.org/getting-started/rest-api 19 | */ 20 | const authActions = 21 | /^\/api\/auth\/(session|signin\/?\w*|signout|csrf|providers|callback\/\w+|_log)$/; 22 | 23 | const router = Router(); 24 | 25 | /** Compatibility layer for `next-auth` for `express` apps. */ 26 | export default function NextAuthMiddleware(options: NextAuthOptions) { 27 | return router 28 | .use(urlencoded({ extended: false })) 29 | .use(json()) 30 | .use(cookieParser()) 31 | .all(authActions, (req: IncomingMessage, res: ServerResponse, next) => { 32 | if (req.method !== 'POST' && req.method !== 'GET') { 33 | return next(); 34 | } 35 | //@ts-ignore 36 | req.query.nextauth = req.path.split('/').slice(3); 37 | //@ts-ignore 38 | NextAuth(req, res, options); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | parserOptions: { 5 | ecmaVersion: 2020, 6 | sourceType: 'module', 7 | ecmaFeatures: { 8 | jsx: true, 9 | }, 10 | }, 11 | settings: { 12 | react: { 13 | version: 'detect', 14 | }, 15 | }, 16 | env: { 17 | browser: true, 18 | amd: true, 19 | node: true, 20 | }, 21 | extends: [ 22 | 'eslint:recommended', 23 | 'plugin:react/recommended', 24 | 'plugin:jsx-a11y/recommended', 25 | 'plugin:prettier/recommended', // Make sure this is always the last element in the array. 26 | ], 27 | plugins: ['simple-import-sort', 'prettier'], 28 | rules: { 29 | 'prettier/prettier': ['error', {}, { usePrettierrc: true }], 30 | 'react/react-in-jsx-scope': 'off', 31 | 'jsx-a11y/accessible-emoji': 'off', 32 | 'react/prop-types': 'off', 33 | '@typescript-eslint/explicit-function-return-type': 'off', 34 | 'simple-import-sort/imports': 'error', 35 | 'simple-import-sort/exports': 'error', 36 | 'jsx-a11y/anchor-is-valid': [ 37 | 'error', 38 | { 39 | components: ['Link'], 40 | specialLink: ['hrefLeft', 'hrefRight'], 41 | aspects: ['invalidHref', 'preferButton'], 42 | }, 43 | ], 44 | }, 45 | }; -------------------------------------------------------------------------------- /pages/_default/_default.page.server.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | import { html } from 'vite-plugin-ssr'; 4 | 5 | import logoUrl from './logo.svg'; 6 | import { PageLayout } from './PageLayout'; 7 | import { PageContext } from './types'; 8 | 9 | export { render }; 10 | export { passToClient }; 11 | 12 | // See https://github.com/brillout/vite-plugin-ssr#data-fetching 13 | const passToClient = ['pageProps']; 14 | 15 | function render(pageContext: PageContext) { 16 | const { Page, pageProps } = pageContext; 17 | const pageHtml = ReactDOMServer.renderToString( 18 | 19 | 20 | , 21 | ); 22 | 23 | // See https://github.com/brillout/vite-plugin-ssr#html-head 24 | const { documentProps } = pageContext; 25 | const title = (documentProps && documentProps.title) || 'Vite SSR app'; 26 | const desc = 27 | (documentProps && documentProps.description) || 'App using Vite + vite-plugin-ssr'; 28 | 29 | return html` 30 | 31 | 32 | 33 | 34 | 35 | 36 | ${title} 37 | 38 | 39 |
${html.dangerouslySkipEscape(pageHtml)}
40 | 41 | `; 42 | } 43 | -------------------------------------------------------------------------------- /server/index.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | import express from 'express'; 3 | import fetch from 'isomorphic-fetch'; 4 | import Providers from 'next-auth/providers'; 5 | import { createPageRender } from 'vite-plugin-ssr'; 6 | 7 | import NextAuth from './auth'; 8 | 9 | const isProduction = process.env.NODE_ENV === 'production'; 10 | const root = `${__dirname}/..`; 11 | 12 | global.fetch = fetch; 13 | 14 | startServer(); 15 | 16 | async function startServer() { 17 | const app = express(); 18 | 19 | let viteDevServer; 20 | if (isProduction) { 21 | app.use(express.static(`${root}/dist/client`, { index: false })); 22 | } else { 23 | const vite = require('vite'); 24 | viteDevServer = await vite.createServer({ 25 | root, 26 | server: { middlewareMode: true }, 27 | }); 28 | app.use(viteDevServer.middlewares); 29 | } 30 | 31 | app.use( 32 | NextAuth({ 33 | providers: [ 34 | Providers.Google({ 35 | clientId: process.env.GOOGLE_CLIENT_ID, 36 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 37 | authorizationUrl: 38 | 'https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline&response_type=code', 39 | }), 40 | ], 41 | }), 42 | ); 43 | 44 | const renderPage = createPageRender({ viteDevServer, isProduction, root }); 45 | 46 | app.get('*', async (req, res, next) => { 47 | const url = req.originalUrl; 48 | const pageContext = { url }; 49 | const result = await renderPage(pageContext); 50 | if (result.nothingRendered) return next(); 51 | res.status(result.statusCode).send(result.renderResult); 52 | }); 53 | 54 | const port = process.env.PORT || 3000; 55 | app.listen(port); 56 | console.log(`Server running at http://localhost:${port}`); 57 | } 58 | -------------------------------------------------------------------------------- /pages/index.page.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Flex, Heading, Link, Text } from '@chakra-ui/react'; 2 | import styled from '@emotion/styled'; 3 | import { useSession } from 'next-auth/client'; 4 | import React from 'react'; 5 | 6 | export { Page }; 7 | 8 | const modules = [ 9 | 'ViteJS', 10 | 'Vite-Plugin-SSR', 11 | 'TypeScript', 12 | 'React', 13 | 'Next-Auth', 14 | 'Chakra-UI', 15 | 'ESLint', 16 | 'Prettier', 17 | 'Husky', 18 | 'Commitlint', 19 | 'Lint-staged', 20 | ]; 21 | 22 | const Root = styled(Flex)` 23 | width: 100vw; 24 | height: 100vh; 25 | flex-direction: column; 26 | justify-content: center; 27 | align-items: center; 28 | text-align: center; 29 | `; 30 | 31 | const GradText = styled(Heading)` 32 | background-color: #f3ec78; 33 | background-image: linear-gradient( 34 | 45deg, 35 | rgba(62, 161, 219, 1) 11.2%, 36 | rgba(93, 52, 236, 1) 100.2% 37 | ); 38 | background-size: 100%; 39 | background-clip: text; 40 | -webkit-background-clip: text; 41 | -moz-background-clip: text; 42 | -webkit-text-fill-color: transparent; 43 | -moz-text-fill-color: transparent; 44 | `; 45 | 46 | function Page() { 47 | const [session, loading] = useSession(); 48 | if (loading) { 49 | return

Loading...

; 50 | } 51 | 52 | if (session) { 53 | return ( 54 | 55 | Welcome {session.user?.name} 56 | 57 | 60 | 61 | Your details are not stored on our server 62 | 63 | ); 64 | } 65 | 66 | return ( 67 | 68 | Vite SSR Starter 69 | 70 | {modules.map((m) => ( 71 | 72 | {m} 73 | 74 | ))} 75 | 76 | 77 | 78 | 79 | 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "npm run server", 4 | "prod": "npm run build && npm run server:prod", 5 | "build": "tsc && vite build && vite build --ssr", 6 | "server": "nodemon", 7 | "server:prod": "cross-env NODE_ENV=production ts-node ./server", 8 | "lint": "eslint --quiet --fix", 9 | "format": "prettier --write", 10 | "prepare": "husky install" 11 | }, 12 | "dependencies": { 13 | "@chakra-ui/react": "^1.6.4", 14 | "@emotion/react": "^11", 15 | "@emotion/styled": "^11", 16 | "@vitejs/plugin-react-refresh": "^1.3.3", 17 | "body-parser": "^1.19.0", 18 | "cookie-parser": "^1.4.5", 19 | "cross-env": "^7.0.3", 20 | "dotenv": "^10.0.0", 21 | "express": "^4.17.1", 22 | "framer-motion": "^4", 23 | "isomorphic-fetch": "^3.0.0", 24 | "next-auth": "^3.27.1", 25 | "process": "^0.11.10", 26 | "react": "^17.0.2", 27 | "react-dom": "^17.0.2", 28 | "ts-node": "^9.1.1", 29 | "typescript": "^4.3.2", 30 | "vite": "^2.3.6", 31 | "vite-plugin-ssr": "^0.1.2" 32 | }, 33 | "devDependencies": { 34 | "@commitlint/cli": "^12.1.4", 35 | "@commitlint/config-conventional": "^12.1.4", 36 | "@types/cookie-parser": "^1.4.2", 37 | "@types/express": "^4.17.12", 38 | "@types/isomorphic-fetch": "^0.0.35", 39 | "@types/node": "^15.12.1", 40 | "@types/react": "^17.0.9", 41 | "@types/react-dom": "^17.0.6", 42 | "@typescript-eslint/eslint-plugin": "^4.28.1", 43 | "@typescript-eslint/parser": "^4.28.1", 44 | "eslint": "^7.29.0", 45 | "eslint-config-prettier": "^8.3.0", 46 | "eslint-plugin-import": "^2.23.4", 47 | "eslint-plugin-jsx-a11y": "^6.4.1", 48 | "eslint-plugin-prettier": "^3.4.0", 49 | "eslint-plugin-react": "^7.24.0", 50 | "eslint-plugin-simple-import-sort": "^7.0.0", 51 | "husky": "^7.0.0", 52 | "lint-staged": "^11.0.0", 53 | "nodemon": "^2.0.9", 54 | "prettier": "^2.3.2" 55 | }, 56 | "lint-staged": { 57 | "*.{js,ts,tsx}": [ 58 | "yarn lint", 59 | "yarn format" 60 | ] 61 | }, 62 | "license": "MIT", 63 | "author": { 64 | "name": "Sai Krishna", 65 | "url": "https://twitter.com/_skris" 66 | }, 67 | "repository": { 68 | "url": "https://github.com/s-kris/vite-ssr-starter" 69 | }, 70 | "description": "Opinionated modern vitejs ssr starter", 71 | "keywords": [ 72 | "vitejs", 73 | "vite-plugin-ssr", 74 | "react", 75 | "typescript", 76 | "next-auth", 77 | "chakra-ui", 78 | "eslint", 79 | "husky", 80 | "lint-staged", 81 | "prettier", 82 | "commitlint" 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /pages/_default/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | --------------------------------------------------------------------------------