├── .github ├── renovate.json └── workflows │ ├── ci.yml │ ├── cq.yml │ └── publish.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc ├── CONTRIBUTING.md ├── ci ├── ci.test.ts ├── package.json └── tsconfig.json ├── examples ├── express │ ├── .stackblitzrc │ ├── package.json │ ├── readme.md │ ├── routes │ │ ├── bar.ts │ │ ├── foo.ts │ │ ├── home.ts │ │ └── nav.ts │ ├── server.ts │ ├── tsconfig.json │ └── vite.config.ts ├── fastify-vike │ ├── .gitignore │ ├── .stackblitzrc │ ├── package.json │ ├── pages │ │ ├── _error │ │ │ └── +Page.tsx │ │ ├── about │ │ │ ├── +Page.tsx │ │ │ └── code.css │ │ ├── index │ │ │ ├── +Page.tsx │ │ │ └── Counter.tsx │ │ └── star-wars │ │ │ ├── @id │ │ │ ├── +Page.tsx │ │ │ └── +data.ts │ │ │ ├── index │ │ │ ├── +Page.tsx │ │ │ └── +data.ts │ │ │ └── types.ts │ ├── readme.md │ ├── renderer │ │ ├── +config.ts │ │ ├── +onPageTransitionEnd.ts │ │ ├── +onPageTransitionStart.ts │ │ ├── +onRenderClient.tsx │ │ ├── +onRenderHtml.tsx │ │ ├── Layout.css │ │ ├── Layout.tsx │ │ ├── Link.tsx │ │ ├── css │ │ │ ├── code.css │ │ │ ├── index.css │ │ │ ├── links.css │ │ │ ├── page-transition-loading-animation.css │ │ │ ├── page-transition-loading-animation │ │ │ │ └── loading.svg │ │ │ └── reset.css │ │ ├── getPageTitle.ts │ │ ├── logo.svg │ │ ├── types.ts │ │ ├── useData.ts │ │ └── usePageContext.tsx │ ├── server │ │ └── index.ts │ ├── tsconfig.json │ └── vite.config.ts ├── fastify │ ├── .stackblitzrc │ ├── handler.ts │ ├── package.json │ ├── readme.md │ ├── routes │ │ ├── bar.ts │ │ ├── foo.ts │ │ ├── home.ts │ │ └── nav.ts │ ├── tsconfig.json │ └── vite.config.ts ├── hapi │ ├── .stackblitzrc │ ├── package.json │ ├── readme.md │ ├── routes │ │ ├── bar.ts │ │ ├── foo.ts │ │ ├── home.ts │ │ └── nav.ts │ ├── server.ts │ ├── tsconfig.json │ └── vite.config.ts ├── koa │ ├── .stackblitzrc │ ├── package.json │ ├── readme.md │ ├── routes │ │ ├── bar.ts │ │ ├── foo.ts │ │ ├── home.ts │ │ └── nav.ts │ ├── server.ts │ ├── tsconfig.json │ └── vite.config.ts ├── nestjs-vike │ ├── index.ts │ ├── package.json │ ├── pages │ │ ├── _error │ │ │ └── +Page.tsx │ │ ├── about │ │ │ ├── +Page.tsx │ │ │ └── code.css │ │ ├── index │ │ │ ├── +Page.tsx │ │ │ └── Counter.tsx │ │ └── star-wars │ │ │ ├── @id │ │ │ ├── +Page.tsx │ │ │ └── +data.ts │ │ │ ├── index │ │ │ ├── +Page.tsx │ │ │ └── +data.ts │ │ │ └── types.ts │ ├── readme.md │ ├── renderer │ │ ├── +config.ts │ │ ├── +onPageTransitionEnd.ts │ │ ├── +onPageTransitionStart.ts │ │ ├── +onRenderClient.tsx │ │ ├── +onRenderHtml.tsx │ │ ├── Layout.css │ │ ├── Layout.tsx │ │ ├── Link.tsx │ │ ├── css │ │ │ ├── code.css │ │ │ ├── index.css │ │ │ ├── links.css │ │ │ ├── page-transition-loading-animation.css │ │ │ ├── page-transition-loading-animation │ │ │ │ └── loading.svg │ │ │ └── reset.css │ │ ├── getPageTitle.ts │ │ ├── logo.svg │ │ ├── types.ts │ │ ├── useData.ts │ │ └── usePageContext.tsx │ ├── server │ │ ├── app.module.ts │ │ ├── main.ts │ │ └── vps.module.ts │ ├── tsconfig.json │ └── vite.config.ts ├── nestjs │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ └── main.ts │ ├── tsconfig.json │ └── vite.config.ts ├── readme.md ├── simple-standalone │ ├── .stackblitzrc │ ├── handler.ts │ ├── package.json │ ├── readme.md │ ├── tsconfig.json │ └── vite.config.ts ├── socket-io │ ├── .stackblitzrc │ ├── package.json │ ├── readme.md │ ├── server.ts │ ├── tsconfig.json │ ├── view.html │ └── vite.config.ts ├── ssr-react-express │ ├── .stackblitzrc │ ├── App.tsx │ ├── client-entry.tsx │ ├── components │ │ ├── Counter.tsx │ │ └── Nav.tsx │ ├── package.json │ ├── pages │ │ ├── Bar.tsx │ │ ├── Foo.tsx │ │ └── Home.tsx │ ├── readme.md │ ├── server-entry.tsx │ ├── tsconfig.json │ └── vite.config.ts ├── ssr-vue-express │ ├── .stackblitzrc │ ├── App.vue │ ├── client-entry.ts │ ├── components │ │ ├── Counter.vue │ │ └── Nav.vue │ ├── package.json │ ├── pages │ │ ├── Bar.vue │ │ ├── Foo.vue │ │ └── Home.vue │ ├── readme.md │ ├── server-entry.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── vue-sfc.d.ts ├── vike │ ├── .gitignore │ ├── .stackblitzrc │ ├── package.json │ ├── pages │ │ ├── _error │ │ │ └── +Page.tsx │ │ ├── about │ │ │ ├── +Page.tsx │ │ │ └── code.css │ │ ├── index │ │ │ ├── +Page.tsx │ │ │ └── Counter.tsx │ │ └── star-wars │ │ │ ├── @id │ │ │ ├── +Page.tsx │ │ │ └── +data.ts │ │ │ ├── index │ │ │ ├── +Page.tsx │ │ │ └── +data.ts │ │ │ └── types.ts │ ├── readme.md │ ├── renderer │ │ ├── +config.ts │ │ ├── +onPageTransitionEnd.ts │ │ ├── +onPageTransitionStart.ts │ │ ├── +onRenderClient.tsx │ │ ├── +onRenderHtml.tsx │ │ ├── Layout.css │ │ ├── Layout.tsx │ │ ├── Link.tsx │ │ ├── css │ │ │ ├── code.css │ │ │ ├── index.css │ │ │ ├── links.css │ │ │ ├── page-transition-loading-animation.css │ │ │ ├── page-transition-loading-animation │ │ │ │ └── loading.svg │ │ │ └── reset.css │ │ ├── getPageTitle.ts │ │ ├── logo.svg │ │ ├── types.ts │ │ ├── useData.ts │ │ └── usePageContext.tsx │ ├── server │ │ └── index.ts │ ├── tsconfig.json │ └── vite.config.ts └── ws │ ├── .stackblitzrc │ ├── index.html │ ├── package.json │ ├── readme.md │ ├── server.ts │ ├── tsconfig.json │ └── vite.config.ts ├── lint-staged.config.mjs ├── package.json ├── packages ├── connect │ ├── eslint.config.js │ ├── lint-staged.config.mjs │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── entry-standalone-with-sirv.ts │ │ ├── entry-standalone.ts │ │ └── index.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── expose-vite-dev-server │ ├── eslint.config.js │ ├── lint-staged.config.mjs │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── index.ts │ │ └── vite-dev-server.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── multibuild-cli │ ├── cli.js │ ├── eslint.config.js │ ├── index.d.ts │ ├── lint-staged.config.mjs │ ├── package.json │ ├── readme.md │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── multibuild │ ├── eslint.config.js │ ├── lint-staged.config.mjs │ ├── package.json │ ├── readme.md │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── node-loader │ ├── cli.js │ ├── eslint.config.js │ ├── lint-staged.config.mjs │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── cli.ts │ │ ├── index.ts │ │ ├── plugin.ts │ │ └── suppress-warnings.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── reloader │ ├── eslint.config.js │ ├── lint-staged.config.mjs │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── http-dev-server.ts │ │ └── index.ts │ ├── tsconfig.json │ └── tsup.config.ts └── vavite │ ├── cli.js │ ├── eslint.config.js │ ├── lint-staged.config.mjs │ ├── node-loader.mjs │ ├── package.json │ ├── readme.md │ ├── src │ ├── cli.ts │ ├── http-dev-server.ts │ ├── index.ts │ └── vite-dev-server.ts │ ├── suppress-loader-warnings.cjs │ ├── tsconfig.json │ └── tsup.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── readme.md └── version /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base", "schedule:weekly", "group:allNonMajor"], 4 | "ignorePresets": ["ignorePaths"], 5 | "ignorePaths": ["**/node_modules/**", "**/dist/**"], 6 | "labels": ["dependencies"], 7 | "rangeStrategy": "bump", 8 | "packageRules": [ 9 | { 10 | "depTypeList": ["peerDependencies"], 11 | "enabled": false 12 | }, 13 | { 14 | "matchPackagePatterns": ["^@types/node$"], 15 | "allowedVersions": "18" 16 | } 17 | ], 18 | "ignoreDeps": [] 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI tests 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | concurrency: 13 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref || github.run_id }}" 14 | cancel-in-progress: true 15 | 16 | defaults: 17 | run: 18 | working-directory: . 19 | 20 | jobs: 21 | test: 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest, macos-latest, windows-latest] 26 | node_version: [20] 27 | include: 28 | - os: ubuntu-latest 29 | node_version: 18 30 | - os: ubuntu-latest 31 | node_version: 22 32 | fail-fast: false 33 | name: "CI tests on node-${{ matrix.node_version }}, ${{ matrix.os }}" 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v4 37 | 38 | - name: Install pnpm 39 | uses: pnpm/action-setup@v4 40 | with: 41 | version: 9 42 | 43 | - name: Set node version to ${{ matrix.node_version }} 44 | uses: actions/setup-node@v4 45 | with: 46 | node-version: ${{ matrix.node_version }} 47 | cache: "pnpm" 48 | 49 | - name: Install 50 | run: pnpm install 51 | 52 | - name: Build 53 | run: pnpm run build 54 | 55 | - name: Run CI tests 56 | run: pnpm run ci 57 | -------------------------------------------------------------------------------- /.github/workflows/cq.yml: -------------------------------------------------------------------------------- 1 | name: Code quality checks 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | concurrency: 13 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref || github.run_id }}" 14 | cancel-in-progress: true 15 | 16 | defaults: 17 | run: 18 | working-directory: . 19 | 20 | jobs: 21 | test: 22 | runs-on: ubuntu-latest 23 | name: "Code quality checks" 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | 28 | - name: Install pnpm 29 | uses: pnpm/action-setup@v4 30 | with: 31 | version: 9 32 | 33 | - name: Set node version to 20 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: 20 37 | cache: "pnpm" 38 | 39 | - name: Install 40 | run: pnpm install 41 | 42 | - name: Build 43 | run: pnpm run build 44 | 45 | - name: Run code quality checks and tests 46 | run: pnpm run cq 47 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "Version to publish" 8 | required: true 9 | type: "string" 10 | tag: 11 | description: "Tag to publish" 12 | required: true 13 | type: "string" 14 | default: "latest" 15 | commit: 16 | description: "Should we commit the version bump?" 17 | required: false 18 | type: "boolean" 19 | default: true 20 | 21 | defaults: 22 | run: 23 | working-directory: . 24 | 25 | jobs: 26 | publish: 27 | runs-on: ubuntu-latest 28 | name: "Publish to NPM" 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | 33 | - name: Install pnpm 34 | uses: pnpm/action-setup@v4 35 | with: 36 | version: 9 37 | 38 | - name: Set node version to 20 39 | uses: actions/setup-node@v4 40 | with: 41 | node-version: 20 42 | registry-url: "https://registry.npmjs.org" 43 | cache: "pnpm" 44 | 45 | - name: Install 46 | run: pnpm install 47 | 48 | - name: Set up git user 49 | run: | 50 | git config --global user.name "GitHub Action Bot" 51 | git config --global user.email "<>" 52 | 53 | - name: Bump version 54 | run: "./version ${{ github.event.inputs.version }}" 55 | 56 | - name: "Publish to NPM" 57 | env: 58 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 59 | run: pnpm -r publish --access public --no-git-checks --tag ${{ github.event.inputs.tag }} 60 | 61 | - name: "Commit version bump" 62 | if: ${{ github.event.inputs.commit == 'true' }} 63 | run: | 64 | git status 65 | git commit -am "release: ${{ github.event.inputs.version }}" 66 | git push 67 | git tag ${{ github.event.inputs.version }} 68 | git push --tags 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Serverless directories 108 | .serverless/ 109 | 110 | # FuseBox cache 111 | .fusebox/ 112 | 113 | # DynamoDB Local files 114 | .dynamodb/ 115 | 116 | # TernJS port file 117 | .tern-port 118 | 119 | # Stores VSCode versions used for testing VSCode extensions 120 | .vscode-test 121 | .vscode/ 122 | 123 | # yarn v2 124 | .yarn/cache 125 | .yarn/unplugged 126 | .yarn/build-state.yml 127 | .yarn/install-state.gz 128 | .pnp.* 129 | 130 | # Local files 131 | *.local.* 132 | 133 | # Packed package files 134 | *.tgz 135 | 136 | .DS_Store -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm run precommit -- 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | side-effects-cache=false 3 | link-workspace-packages=true 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist 3 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "useTabs": true, 4 | "overrides": [ 5 | { 6 | "files": "**/*.md", 7 | "options": { 8 | "tabWidth": 2, 9 | "useTabs": false 10 | } 11 | }, 12 | { 13 | "files": "**/package.json", 14 | "options": { 15 | "tabWidth": 2, 16 | "useTabs": false 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | `vavite` is a `pnpm` monorepo. To start local development: 4 | 5 | - Install `pnpm` 6 | - Fork and clone the repo 7 | - Run pnpm dev 8 | - Start hacking 9 | - Send a PR 10 | -------------------------------------------------------------------------------- /ci/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/ci", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "ci": "vitest run --reporter=verbose" 7 | }, 8 | "dependencies": { 9 | "@types/node": "^18.19.67", 10 | "kill-port": "^2.0.1", 11 | "ps-tree": "^1.2.0", 12 | "puppeteer": "^23.9.0", 13 | "typescript": "^5.7.2", 14 | "vitest": "^2.1.6" 15 | }, 16 | "devDependencies": { 17 | "@types/ps-tree": "^1.1.6" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ci/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "Bundler", 10 | "customConditions": ["import"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/express/.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev" 4 | } 5 | -------------------------------------------------------------------------------- /examples/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/example-reloader-express", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "start": "node dist", 7 | "dev": "vite", 8 | "build": "vite build --ssr --mode=production" 9 | }, 10 | "files": [ 11 | "dist" 12 | ], 13 | "devDependencies": { 14 | "@types/express": "^5.0.0", 15 | "@types/node": "^18.19.67", 16 | "typescript": "^5.7.2", 17 | "vavite": "5.1.0", 18 | "vite": "^5.4.11" 19 | }, 20 | "dependencies": { 21 | "express": "^4.21.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/express/readme.md: -------------------------------------------------------------------------------- 1 | # Vavite Express example 2 | 3 | `serverEntry` example that shows how to integrate with [Express](https://expressjs.com/). 4 | 5 | > [Try on StackBlitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/express) 6 | 7 | Clone with: 8 | 9 | ```bash 10 | npx degit cyco130/vavite/examples/express 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/express/routes/bar.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "express"; 2 | import viteDevServer from "vavite/vite-dev-server"; 3 | import nav from "./nav"; 4 | 5 | const barRoute: RequestHandler = async (req, res, next) => { 6 | let html = "

Hello from page /bar

" + nav; 7 | 8 | if (viteDevServer) { 9 | html = await viteDevServer.transformIndexHtml(req.url, html); 10 | } 11 | 12 | res.send(html); 13 | }; 14 | 15 | export default barRoute; 16 | -------------------------------------------------------------------------------- /examples/express/routes/foo.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "express"; 2 | import viteDevServer from "vavite/vite-dev-server"; 3 | import nav from "./nav"; 4 | 5 | const fooRoute: RequestHandler = async (req, res, next) => { 6 | let html = "

Hello from page /foo

" + nav; 7 | 8 | if (viteDevServer) { 9 | html = await viteDevServer.transformIndexHtml(req.url, html); 10 | } 11 | 12 | res.send(html); 13 | }; 14 | 15 | export default fooRoute; 16 | -------------------------------------------------------------------------------- /examples/express/routes/home.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "express"; 2 | import viteDevServer from "vavite/vite-dev-server"; 3 | import nav from "./nav"; 4 | 5 | const homeRoute: RequestHandler = async (req, res, next) => { 6 | let html = "

Hello from home page

" + nav; 7 | 8 | if (viteDevServer) { 9 | html = await viteDevServer.transformIndexHtml(req.url, html); 10 | } 11 | 12 | res.send(html); 13 | }; 14 | 15 | export default homeRoute; 16 | -------------------------------------------------------------------------------- /examples/express/routes/nav.ts: -------------------------------------------------------------------------------- 1 | export default ` 2 | 9 | `; 10 | -------------------------------------------------------------------------------- /examples/express/server.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import express, { RequestHandler } from "express"; 4 | import viteDevServer from "vavite/vite-dev-server"; 5 | 6 | console.log("Running the server"); 7 | 8 | const app = express(); 9 | 10 | // This is an optional optimization to load routes lazily so that 11 | // when reloadOn option is set to "static-deps-change", 12 | // changes to the route handlers will not trigger a reload. 13 | // Feel free to remove this and import routes directly. 14 | function lazy( 15 | importer: () => Promise<{ default: RequestHandler }>, 16 | ): RequestHandler { 17 | return async (req, res, next) => { 18 | try { 19 | const routeHandler = (await importer()).default; 20 | routeHandler(req, res, next); 21 | } catch (err) { 22 | if (err instanceof Error) viteDevServer?.ssrFixStacktrace(err); 23 | next(err); 24 | } 25 | }; 26 | } 27 | 28 | app.get( 29 | "/", 30 | lazy(() => import("./routes/home")), 31 | ); 32 | 33 | app.get( 34 | "/foo", 35 | lazy(() => import("./routes/foo")), 36 | ); 37 | 38 | app.get( 39 | "/bar", 40 | lazy(() => import("./routes/bar")), 41 | ); 42 | 43 | export default app; 44 | -------------------------------------------------------------------------------- /examples/express/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "Bundler", 10 | "customConditions": ["import"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/express/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { vavite } from "vavite"; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | vavite({ 7 | handlerEntry: "/server.ts", 8 | reloadOn: "static-deps-change", 9 | serveClientAssetsInDev: true, 10 | }), 11 | ], 12 | }); 13 | -------------------------------------------------------------------------------- /examples/fastify-vike/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | .env.production 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # yarn v2 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .yarn/install-state.gz 118 | .pnp.* 119 | 120 | # Local files 121 | *.local 122 | -------------------------------------------------------------------------------- /examples/fastify-vike/.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev" 4 | } 5 | -------------------------------------------------------------------------------- /examples/fastify-vike/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/example-fastify-vike", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vavite serve", 7 | "build": "vavite build", 8 | "start": "cross-env NODE_ENV=production node dist/server/index.mjs" 9 | }, 10 | "dependencies": { 11 | "@fastify/static": "^8.0.3", 12 | "@types/node": "^18.19.67", 13 | "@types/react": "^18.3.12", 14 | "@types/react-dom": "^18.3.1", 15 | "@vitejs/plugin-react": "^4.3.4", 16 | "cross-env": "^7.0.3", 17 | "fastify": "^5.1.0", 18 | "react": "^18.3.1", 19 | "react-dom": "^18.3.1", 20 | "ts-node": "^10.9.2", 21 | "typescript": "^5.7.2", 22 | "vavite": "5.1.0", 23 | "vike": "^0.4.205", 24 | "vite": "^5.4.11" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/fastify-vike/pages/_error/+Page.tsx: -------------------------------------------------------------------------------- 1 | export { Page }; 2 | 3 | import { usePageContext } from "../../renderer/usePageContext"; 4 | 5 | function Page() { 6 | const pageContext = usePageContext(); 7 | let { abortReason } = pageContext; 8 | if (!abortReason) { 9 | abortReason = pageContext.is404 10 | ? "Page not found." 11 | : "Something went wrong."; 12 | } 13 | return ( 14 |
15 |

{abortReason}

16 |
17 | ); 18 | } 19 | 20 | function Center({ children }: { children: React.ReactNode }) { 21 | return ( 22 |
30 | {children} 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /examples/fastify-vike/pages/about/+Page.tsx: -------------------------------------------------------------------------------- 1 | import "./code.css"; 2 | 3 | export { Page }; 4 | 5 | function Page() { 6 | return ( 7 | <> 8 |

About

9 |

Example of using Vike.

10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/fastify-vike/pages/about/code.css: -------------------------------------------------------------------------------- 1 | code { 2 | font-family: monospace; 3 | background-color: #eaeaea; 4 | padding: 3px 5px; 5 | border-radius: 4px; 6 | } 7 | -------------------------------------------------------------------------------- /examples/fastify-vike/pages/index/+Page.tsx: -------------------------------------------------------------------------------- 1 | export { Page }; 2 | 3 | import { Counter } from "./Counter"; 4 | 5 | function Page() { 6 | return ( 7 | <> 8 |

Hello from Vike on Fastify!

9 | This page is: 10 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/fastify-vike/pages/index/Counter.tsx: -------------------------------------------------------------------------------- 1 | export { Counter }; 2 | 3 | import { useState } from "react"; 4 | 5 | function Counter() { 6 | const [count, setCount] = useState(0); 7 | return ( 8 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/fastify-vike/pages/star-wars/@id/+Page.tsx: -------------------------------------------------------------------------------- 1 | export { Page }; 2 | 3 | import { useData } from "../../../renderer/useData"; 4 | import type { Data } from "./+data"; 5 | 6 | function Page() { 7 | const { movie } = useData(); 8 | return ( 9 | <> 10 |

{movie.title}

11 | Release Date: {movie.release_date} 12 |
13 | Director: {movie.director} 14 |
15 | Producer: {movie.producer} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/fastify-vike/pages/star-wars/@id/+data.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/data 2 | export { data }; 3 | export type Data = Awaited>; 4 | 5 | import type { MovieDetails } from "../types"; 6 | import type { PageContextServer } from "vike/types"; 7 | 8 | const data = async (pageContext: PageContextServer) => { 9 | await sleep(300); // Simulate slow network 10 | 11 | const response = await fetch( 12 | `https://brillout.github.io/star-wars/api/films/${pageContext.routeParams!.id}.json`, 13 | ); 14 | let movie = (await response.json()) as MovieDetails; 15 | 16 | // We remove data we don't need because the data is passed to the client; we should 17 | // minimize what is sent over the network. 18 | movie = minimize(movie); 19 | 20 | return { 21 | movie, 22 | // The page's 23 | title: movie.title, 24 | }; 25 | }; 26 | 27 | function minimize(movie: MovieDetails & Record<string, unknown>): MovieDetails { 28 | const { id, title, release_date, director, producer } = movie; 29 | movie = { id, title, release_date, director, producer }; 30 | return movie; 31 | } 32 | 33 | function sleep(milliseconds: number) { 34 | return new Promise((r) => setTimeout(r, milliseconds)); 35 | } 36 | -------------------------------------------------------------------------------- /examples/fastify-vike/pages/star-wars/index/+Page.tsx: -------------------------------------------------------------------------------- 1 | export default Page; 2 | 3 | import { useData } from "../../../renderer/useData"; 4 | import type { Data } from "./+data"; 5 | 6 | function Page() { 7 | const { movies } = useData<Data>(); 8 | return ( 9 | <> 10 | <h1>Star Wars Movies</h1> 11 | <ol> 12 | {movies.map(({ id, title, release_date }) => ( 13 | <li key={id}> 14 | <a href={`/star-wars/${id}`}>{title}</a> ({release_date}) 15 | </li> 16 | ))} 17 | </ol> 18 | <p> 19 | Source:{" "} 20 | <a href="https://brillout.github.io/star-wars/"> 21 | brillout.github.io/star-wars 22 | </a> 23 | . 24 | </p> 25 | <p> 26 | Data can be fetched by using the <code>data()</code> hook. 27 | </p> 28 | </> 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /examples/fastify-vike/pages/star-wars/index/+data.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/data 2 | export { data }; 3 | export type Data = Awaited<ReturnType<typeof data>>; 4 | 5 | import type { MovieDetails, Movie } from "../types"; 6 | import type { PageContextServer } from "vike/types"; 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 9 | const data = async (pageContext: PageContextServer) => { 10 | await sleep(700); // Simulate slow network 11 | 12 | const response = await fetch( 13 | "https://brillout.github.io/star-wars/api/films.json", 14 | ); 15 | const moviesData = (await response.json()) as MovieDetails[]; 16 | 17 | // We remove data we don't need because the data is passed to the client; we should 18 | // minimize what is sent over the network. 19 | const movies = minimize(moviesData); 20 | 21 | return { 22 | movies, 23 | // The page's <title> 24 | title: `${movies.length} Star Wars Movies`, 25 | }; 26 | }; 27 | 28 | function minimize(movies: MovieDetails[]): Movie[] { 29 | return movies.map((movie) => { 30 | const { title, release_date, id } = movie; 31 | return { title, release_date, id }; 32 | }); 33 | } 34 | 35 | function sleep(milliseconds: number) { 36 | return new Promise((r) => setTimeout(r, milliseconds)); 37 | } 38 | -------------------------------------------------------------------------------- /examples/fastify-vike/pages/star-wars/types.ts: -------------------------------------------------------------------------------- 1 | export type Movie = { 2 | id: string; 3 | title: string; 4 | release_date: string; 5 | }; 6 | export type MovieDetails = Movie & { 7 | director: string; 8 | producer: string; 9 | }; 10 | -------------------------------------------------------------------------------- /examples/fastify-vike/readme.md: -------------------------------------------------------------------------------- 1 | # Vavite Vike example with Fastify 2 | 3 | Simple example of using [Vike](https://vike.dev/) with Vavite and Fastify that shows how to: 4 | 5 | - Integrate Fastify with Vite's dev server 6 | - Run multiple build steps (for client and server) 7 | - Perform React SSR with Vike 8 | 9 | > [Try on StackBlitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/fastify-vike) 10 | 11 | Clone with: 12 | 13 | ```bash 14 | npx degit cyco130/vavite/examples/fastify-vike 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/fastify-vike/renderer/+config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "vike/types"; 2 | 3 | // https://vike.dev/config 4 | export default { 5 | // https://vike.dev/clientRouting 6 | clientRouting: true, 7 | // https://vike.dev/meta 8 | meta: { 9 | // Define new setting 'title' 10 | title: { 11 | env: { server: true, client: true }, 12 | }, 13 | // Define new setting 'description' 14 | description: { 15 | env: { server: true }, 16 | }, 17 | }, 18 | hydrationCanBeAborted: true, 19 | } satisfies Config; 20 | -------------------------------------------------------------------------------- /examples/fastify-vike/renderer/+onPageTransitionEnd.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/onPageTransitionEnd 2 | export { onPageTransitionEnd }; 3 | 4 | import type { OnPageTransitionEndAsync } from "vike/types"; 5 | 6 | const onPageTransitionEnd: OnPageTransitionEndAsync = 7 | async (): ReturnType<OnPageTransitionEndAsync> => { 8 | console.log("Page transition end"); 9 | document.querySelector("body")!.classList.remove("page-is-transitioning"); 10 | }; 11 | -------------------------------------------------------------------------------- /examples/fastify-vike/renderer/+onPageTransitionStart.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/onPageTransitionStart 2 | export { onPageTransitionStart }; 3 | 4 | import type { OnPageTransitionStartAsync } from "vike/types"; 5 | 6 | const onPageTransitionStart: OnPageTransitionStartAsync = 7 | async (): ReturnType<OnPageTransitionStartAsync> => { 8 | console.log("Page transition start"); 9 | document.querySelector("body")!.classList.add("page-is-transitioning"); 10 | }; 11 | -------------------------------------------------------------------------------- /examples/fastify-vike/renderer/+onRenderClient.tsx: -------------------------------------------------------------------------------- 1 | // https://vike.dev/onRenderClient 2 | export { onRenderClient }; 3 | 4 | import ReactDOM from "react-dom/client"; 5 | import { Layout } from "./Layout"; 6 | import { getPageTitle } from "./getPageTitle"; 7 | import type { OnRenderClientAsync } from "vike/types"; 8 | 9 | let root: ReactDOM.Root; 10 | const onRenderClient: OnRenderClientAsync = async ( 11 | pageContext, 12 | ): ReturnType<OnRenderClientAsync> => { 13 | const { Page } = pageContext; 14 | 15 | // This onRenderClient() hook only supports SSR, see https://vike.dev/render-modes for how to modify onRenderClient() 16 | // to support SPA 17 | if (!Page) 18 | throw new Error( 19 | "My onRenderClient() hook expects pageContext.Page to be defined", 20 | ); 21 | 22 | const container = document.getElementById("react-root"); 23 | if (!container) throw new Error("DOM element #react-root not found"); 24 | 25 | const page = ( 26 | <Layout pageContext={pageContext}> 27 | <Page /> 28 | </Layout> 29 | ); 30 | if (pageContext.isHydration) { 31 | root = ReactDOM.hydrateRoot(container, page); 32 | } else { 33 | if (!root) { 34 | root = ReactDOM.createRoot(container); 35 | } 36 | root.render(page); 37 | } 38 | document.title = getPageTitle(pageContext); 39 | }; 40 | -------------------------------------------------------------------------------- /examples/fastify-vike/renderer/+onRenderHtml.tsx: -------------------------------------------------------------------------------- 1 | // https://vike.dev/onRenderHtml 2 | export { onRenderHtml }; 3 | 4 | import ReactDOMServer from "react-dom/server"; 5 | import { Layout } from "./Layout"; 6 | import { escapeInject, dangerouslySkipEscape } from "vike/server"; 7 | import logoUrl from "./logo.svg"; 8 | import type { OnRenderHtmlAsync } from "vike/types"; 9 | import { getPageTitle } from "./getPageTitle"; 10 | 11 | const onRenderHtml: OnRenderHtmlAsync = async ( 12 | pageContext, 13 | ): ReturnType<OnRenderHtmlAsync> => { 14 | const { Page } = pageContext; 15 | 16 | // This onRenderHtml() hook only supports SSR, see https://vike.dev/render-modes for how to modify 17 | // onRenderHtml() to support SPA 18 | if (!Page) 19 | throw new Error( 20 | "My onRenderHtml() hook expects pageContext.Page to be defined", 21 | ); 22 | 23 | // Alternativly, we can use an HTML stream, see https://vike.dev/streaming 24 | const pageHtml = ReactDOMServer.renderToString( 25 | <Layout pageContext={pageContext}> 26 | <Page /> 27 | </Layout>, 28 | ); 29 | 30 | const title = getPageTitle(pageContext); 31 | const desc = 32 | pageContext.data?.description || 33 | pageContext.config.description || 34 | "Demo of using Vike"; 35 | 36 | const documentHtml = escapeInject`<!DOCTYPE html> 37 | <html lang="en"> 38 | <head> 39 | <meta charset="UTF-8" /> 40 | <link rel="icon" href="${logoUrl}" /> 41 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 42 | <meta name="description" content="${desc}" /> 43 | <title>${title} 44 | 45 | 46 |
${dangerouslySkipEscape(pageHtml)}
47 | 48 | `; 49 | 50 | return { 51 | documentHtml, 52 | pageContext: { 53 | // We can add custom pageContext properties here, see https://vike.dev/pageContext#custom 54 | }, 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /examples/fastify-vike/renderer/Layout.css: -------------------------------------------------------------------------------- 1 | #sidebar a { 2 | padding: 2px 10px; 3 | margin-left: -10px; 4 | } 5 | #sidebar a.is-active { 6 | background-color: #eee; 7 | } 8 | -------------------------------------------------------------------------------- /examples/fastify-vike/renderer/Layout.tsx: -------------------------------------------------------------------------------- 1 | export { Layout }; 2 | 3 | import React from "react"; 4 | import logoUrl from "./logo.svg"; 5 | import { PageContextProvider } from "./usePageContext"; 6 | import { Link } from "./Link"; 7 | import type { PageContext } from "vike/types"; 8 | import "./css/index.css"; 9 | import "./Layout.css"; 10 | 11 | function Layout({ 12 | children, 13 | pageContext, 14 | }: { 15 | children: React.ReactNode; 16 | pageContext: PageContext; 17 | }) { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | Welcome 25 | About 26 | Data Fetching 27 | 28 | {children} 29 | 30 | 31 | 32 | ); 33 | } 34 | 35 | function Frame({ children }: { children: React.ReactNode }) { 36 | return ( 37 |
44 | {children} 45 |
46 | ); 47 | } 48 | 49 | function Sidebar({ children }: { children: React.ReactNode }) { 50 | return ( 51 | 64 | ); 65 | } 66 | 67 | function Content({ children }: { children: React.ReactNode }) { 68 | return ( 69 |
70 |
78 | {children} 79 |
80 |
81 | ); 82 | } 83 | 84 | function Logo() { 85 | return ( 86 |
92 | 93 | logo 94 | 95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /examples/fastify-vike/renderer/Link.tsx: -------------------------------------------------------------------------------- 1 | import { usePageContext } from "./usePageContext"; 2 | 3 | export { Link }; 4 | 5 | function Link(props: { 6 | href: string; 7 | className?: string; 8 | children: React.ReactNode; 9 | }) { 10 | const pageContext = usePageContext(); 11 | const { urlPathname } = pageContext; 12 | const { href } = props; 13 | const isActive = 14 | href === "/" ? urlPathname === href : urlPathname.startsWith(href); 15 | const className = [props.className, isActive && "is-active"] 16 | .filter(Boolean) 17 | .join(" "); 18 | return ; 19 | } 20 | -------------------------------------------------------------------------------- /examples/fastify-vike/renderer/css/code.css: -------------------------------------------------------------------------------- 1 | code { 2 | font-family: monospace; 3 | background-color: #eaeaea; 4 | padding: 3px 5px; 5 | border-radius: 4px; 6 | } 7 | -------------------------------------------------------------------------------- /examples/fastify-vike/renderer/css/index.css: -------------------------------------------------------------------------------- 1 | @import "./reset.css"; 2 | @import "./links.css"; 3 | @import "./code.css"; 4 | @import "./page-transition-loading-animation.css"; 5 | -------------------------------------------------------------------------------- /examples/fastify-vike/renderer/css/links.css: -------------------------------------------------------------------------------- 1 | a { 2 | text-decoration: none; 3 | } 4 | -------------------------------------------------------------------------------- /examples/fastify-vike/renderer/css/page-transition-loading-animation.css: -------------------------------------------------------------------------------- 1 | #page-container { 2 | position: relative; 3 | width: 100%; 4 | } 5 | #page-container::before { 6 | content: ""; 7 | position: absolute; 8 | width: 100%; 9 | height: 100%; 10 | z-index: 999; 11 | background: no-repeat url("./page-transition-loading-animation/loading.svg"); 12 | background-size: 100px; 13 | background-position: center center; 14 | pointer-events: none; 15 | opacity: 0; 16 | } 17 | body.page-is-transitioning #page-container::before { 18 | opacity: 1; 19 | } 20 | #page-content, 21 | #page-container::before { 22 | transition: opacity 0.5s ease-in-out; 23 | } 24 | body.page-is-transitioning #page-content { 25 | opacity: 0.17; 26 | } 27 | -------------------------------------------------------------------------------- /examples/fastify-vike/renderer/css/page-transition-loading-animation/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/fastify-vike/renderer/css/reset.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: sans-serif; 4 | } 5 | * { 6 | box-sizing: border-box; 7 | } 8 | -------------------------------------------------------------------------------- /examples/fastify-vike/renderer/getPageTitle.ts: -------------------------------------------------------------------------------- 1 | export { getPageTitle }; 2 | 3 | import type { PageContext } from "vike/types"; 4 | 5 | function getPageTitle(pageContext: PageContext): string { 6 | const title = 7 | // Title defined dynamically by data() 8 | pageContext.data?.title || 9 | // Title defined statically by /pages/some-page/+title.js (or by `export default { title }` in /pages/some-page/+config.js) 10 | // The setting 'pageContext.config.title' is a custom setting we defined at ./+config.ts 11 | pageContext.config.title || 12 | "Vike Demo"; 13 | return title; 14 | } 15 | -------------------------------------------------------------------------------- /examples/fastify-vike/renderer/types.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/pageContext#typescript 2 | declare global { 3 | namespace Vike { 4 | interface PageContext { 5 | Page: () => React.ReactElement; 6 | data?: { 7 | /** Value for defined dynmically by by /pages/some-page/+data.js */ 8 | title?: string; 9 | /** Value for <meta name="description"> defined dynmically */ 10 | description?: string; 11 | }; 12 | config: { 13 | /** Value for <title> defined statically by /pages/some-page/+title.js (or by `export default { title }` in /pages/some-page/+config.js) */ 14 | title?: string; 15 | /** Value for <meta name="description"> defined statically */ 16 | description?: string; 17 | }; 18 | /** https://vike.dev/render */ 19 | abortReason?: string; 20 | } 21 | } 22 | } 23 | 24 | // Tell TypeScript this file isn't an ambient module 25 | export {}; 26 | -------------------------------------------------------------------------------- /examples/fastify-vike/renderer/useData.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/useData 2 | export { useData }; 3 | 4 | import { usePageContext } from "./usePageContext"; 5 | 6 | /** https://vike.dev/useData */ 7 | function useData<Data>() { 8 | const { data } = usePageContext(); 9 | return data as Data; 10 | } 11 | -------------------------------------------------------------------------------- /examples/fastify-vike/renderer/usePageContext.tsx: -------------------------------------------------------------------------------- 1 | // https://vike.dev/usePageContext 2 | // eslint-disable-next-line react-refresh/only-export-components 3 | export { usePageContext }; 4 | export { PageContextProvider }; 5 | 6 | import React, { useContext } from "react"; 7 | import type { PageContext } from "vike/types"; 8 | 9 | const Context = React.createContext<PageContext>( 10 | undefined as unknown as PageContext, 11 | ); 12 | 13 | function PageContextProvider({ 14 | pageContext, 15 | children, 16 | }: { 17 | pageContext: PageContext; 18 | children: React.ReactNode; 19 | }) { 20 | return <Context.Provider value={pageContext}>{children}</Context.Provider>; 21 | } 22 | 23 | /** https://vike.dev/usePageContext */ 24 | function usePageContext() { 25 | const pageContext = useContext(Context); 26 | return pageContext; 27 | } 28 | -------------------------------------------------------------------------------- /examples/fastify-vike/server/index.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="vite/client" /> 2 | 3 | import viteDevServer from "vavite/http-dev-server"; 4 | import Fastify, { FastifyInstance } from "fastify"; 5 | import FastifyStatic from "@fastify/static"; 6 | import { renderPage } from "vike/server"; 7 | import { fileURLToPath } from "node:url"; 8 | import { IncomingMessage, ServerResponse } from "node:http"; 9 | 10 | async function startServer() { 11 | const instance = Fastify({}); 12 | 13 | if (!viteDevServer) { 14 | await instance.register(FastifyStatic, { 15 | root: fileURLToPath(new URL("../client/assets", import.meta.url)), 16 | prefix: "/assets/", 17 | }); 18 | } 19 | 20 | instance.get("*", async (request, reply) => { 21 | const pageContext = await renderPage({ urlOriginal: request.url }); 22 | const { httpResponse } = pageContext; 23 | if (!httpResponse) return; 24 | 25 | const { body, statusCode, headers } = httpResponse; 26 | headers.forEach(([name, value]) => reply.header(name, value)); 27 | reply.status(statusCode); 28 | reply.send(body); 29 | }); 30 | 31 | await instance.ready(); 32 | 33 | fastify = instance; 34 | } 35 | 36 | let fastify: FastifyInstance | undefined; 37 | const fastifyHandlerPromise = startServer().catch((error) => { 38 | console.error(error); 39 | process.exit(1); 40 | }); 41 | 42 | export default async function handler( 43 | request: IncomingMessage, 44 | reply: ServerResponse, 45 | ) { 46 | if (!fastify) { 47 | await fastifyHandlerPromise; 48 | } 49 | 50 | fastify!.server.emit("request", request, reply); 51 | } 52 | -------------------------------------------------------------------------------- /examples/fastify-vike/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "ES2020", 5 | "moduleResolution": "Bundler", 6 | "customConditions": ["import"], 7 | "target": "ES2017", 8 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 9 | "jsx": "react-jsx", 10 | "esModuleInterop": true, 11 | "skipLibCheck": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/fastify-vike/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import ssr from "vike/plugin"; 4 | import { vavite } from "vavite"; 5 | 6 | export default defineConfig({ 7 | buildSteps: [ 8 | { name: "client" }, 9 | { 10 | name: "server", 11 | config: { 12 | build: { ssr: true }, 13 | }, 14 | }, 15 | ], 16 | 17 | plugins: [ 18 | vavite({ 19 | handlerEntry: "/server/index.ts", 20 | serveClientAssetsInDev: true, 21 | }), 22 | react(), 23 | ssr({ disableAutoFullBuild: true }), 24 | ], 25 | }); 26 | -------------------------------------------------------------------------------- /examples/fastify/.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev" 4 | } 5 | -------------------------------------------------------------------------------- /examples/fastify/handler.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="vite/client" /> 2 | 3 | import Fastify, { RouteHandlerMethod } from "fastify"; 4 | import viteDevServer from "vavite/vite-dev-server"; 5 | import { IncomingMessage, ServerResponse } from "node:http"; 6 | 7 | const fastify = Fastify({ 8 | // Your Fastify options go here. 9 | }); 10 | 11 | // This is an optional optimization to load routes lazily so that 12 | // when reloadOn option is set to "static-deps-change", 13 | // changes to the route handlers will not trigger a reload. 14 | // Feel free to remove this and import routes directly. 15 | function lazy( 16 | importer: () => Promise<{ default: RouteHandlerMethod }>, 17 | ): RouteHandlerMethod { 18 | return async (req, res) => { 19 | try { 20 | const routeHandler = (await importer()).default; 21 | return routeHandler.bind(fastify)(req, res); 22 | } catch (err) { 23 | if (err instanceof Error) viteDevServer?.ssrFixStacktrace(err); 24 | throw err; 25 | } 26 | }; 27 | } 28 | 29 | fastify.get( 30 | "/", 31 | lazy(() => import("./routes/home")), 32 | ); 33 | 34 | fastify.get( 35 | "/foo", 36 | lazy(() => import("./routes/foo")), 37 | ); 38 | 39 | fastify.get( 40 | "/bar", 41 | lazy(() => import("./routes/bar")), 42 | ); 43 | 44 | let fastifyReadyPromise: PromiseLike<void> | undefined = fastify.ready(); 45 | 46 | export default async function handler( 47 | request: IncomingMessage, 48 | reply: ServerResponse, 49 | ) { 50 | if (fastifyReadyPromise) { 51 | await fastifyReadyPromise; 52 | fastifyReadyPromise = undefined; 53 | } 54 | 55 | fastify.server.emit("request", request, reply); 56 | } 57 | -------------------------------------------------------------------------------- /examples/fastify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/example-reloader-fastify", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "start": "node dist", 7 | "dev": "vite", 8 | "build": "vite build --ssr --mode=production" 9 | }, 10 | "files": [ 11 | "dist" 12 | ], 13 | "devDependencies": { 14 | "@types/node": "^18.19.67", 15 | "typescript": "^5.7.2", 16 | "vavite": "5.1.0", 17 | "vite": "^5.4.11" 18 | }, 19 | "dependencies": { 20 | "fastify": "^5.1.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/fastify/readme.md: -------------------------------------------------------------------------------- 1 | # Vavite Fastify example 2 | 3 | `handlerEntry` example that shows how to integrate with [Fastify](https://fastify.io/). 4 | 5 | > [Try on StackBlitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/fastify) 6 | 7 | Clone with: 8 | 9 | ```bash 10 | npx degit cyco130/vavite/examples/fastify 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/fastify/routes/bar.ts: -------------------------------------------------------------------------------- 1 | import { RouteHandlerMethod } from "fastify"; 2 | import viteDevServer from "vavite/vite-dev-server"; 3 | import nav from "./nav"; 4 | 5 | const barRoute: RouteHandlerMethod = async (req, res) => { 6 | let html = "<h1>Hello from page /bar</h1>" + nav; 7 | 8 | if (viteDevServer) { 9 | html = await viteDevServer.transformIndexHtml(req.url, html); 10 | } 11 | 12 | res.type("text/html"); 13 | res.send(html); 14 | }; 15 | 16 | export default barRoute; 17 | -------------------------------------------------------------------------------- /examples/fastify/routes/foo.ts: -------------------------------------------------------------------------------- 1 | import { RouteHandlerMethod } from "fastify"; 2 | import viteDevServer from "vavite/vite-dev-server"; 3 | import nav from "./nav"; 4 | 5 | const fooRoute: RouteHandlerMethod = async (req, res) => { 6 | let html = "<h1>Hello from page /foo</h1>" + nav; 7 | 8 | if (viteDevServer) { 9 | html = await viteDevServer.transformIndexHtml(req.url, html); 10 | } 11 | 12 | res.type("text/html"); 13 | res.send(html); 14 | }; 15 | 16 | export default fooRoute; 17 | -------------------------------------------------------------------------------- /examples/fastify/routes/home.ts: -------------------------------------------------------------------------------- 1 | import { RouteHandlerMethod } from "fastify"; 2 | import viteDevServer from "vavite/vite-dev-server"; 3 | import nav from "./nav"; 4 | 5 | const homeRoute: RouteHandlerMethod = async (req, res) => { 6 | let html = "<h1>Hello from home page</h1>" + nav; 7 | 8 | if (viteDevServer) { 9 | html = await viteDevServer.transformIndexHtml(req.url, html); 10 | } 11 | 12 | res.type("text/html"); 13 | res.send(html); 14 | }; 15 | 16 | export default homeRoute; 17 | -------------------------------------------------------------------------------- /examples/fastify/routes/nav.ts: -------------------------------------------------------------------------------- 1 | export default ` 2 | <nav> 3 | <ul> 4 | <li><a href="/">Home</a></li> 5 | <li><a href="/foo">Foo</a></li> 6 | <li><a href="/bar">Bar</a></li> 7 | </ul> 8 | </nav> 9 | `; 10 | -------------------------------------------------------------------------------- /examples/fastify/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "Bundler", 10 | "customConditions": ["import"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/fastify/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { vavite } from "vavite"; 3 | 4 | export default defineConfig({ 5 | plugins: [vavite({ handlerEntry: "/handler.ts" })], 6 | }); 7 | -------------------------------------------------------------------------------- /examples/hapi/.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev" 4 | } 5 | -------------------------------------------------------------------------------- /examples/hapi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/example-hapi", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "start": "node dist", 7 | "dev": "vite", 8 | "build": "vite build --ssr --mode=production" 9 | }, 10 | "files": [ 11 | "dist" 12 | ], 13 | "devDependencies": { 14 | "@types/node": "^18.19.67", 15 | "typescript": "^5.7.2", 16 | "vavite": "5.1.0", 17 | "vite": "^5.4.11" 18 | }, 19 | "dependencies": { 20 | "@hapi/hapi": "^21.3.12" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/hapi/readme.md: -------------------------------------------------------------------------------- 1 | # Vavite Hapi example 2 | 3 | `serverEntry` example that shows how to integrate with [Hapi](https://hapijs.com). 4 | 5 | > [Try on StackBlitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/hapi) 6 | 7 | Clone with: 8 | 9 | ```bash 10 | npx degit cyco130/vavite/examples/hapi 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/hapi/routes/bar.ts: -------------------------------------------------------------------------------- 1 | import { Lifecycle } from "@hapi/hapi"; 2 | import viteDevServer from "vavite/vite-dev-server"; 3 | import nav from "./nav"; 4 | 5 | const barRoute: Lifecycle.Method = async function (req, h) { 6 | let html = "<h1>Hello from page /bar</h1>" + nav; 7 | 8 | if (viteDevServer) { 9 | html = await viteDevServer.transformIndexHtml(req.path, html); 10 | } 11 | 12 | return html; 13 | }; 14 | 15 | export default barRoute; 16 | -------------------------------------------------------------------------------- /examples/hapi/routes/foo.ts: -------------------------------------------------------------------------------- 1 | import { Lifecycle } from "@hapi/hapi"; 2 | import viteDevServer from "vavite/vite-dev-server"; 3 | import nav from "./nav"; 4 | 5 | const fooRoute: Lifecycle.Method = async function (req, h) { 6 | let html = "<h1>Hello from page /foo</h1>" + nav; 7 | 8 | if (viteDevServer) { 9 | html = await viteDevServer.transformIndexHtml(req.path, html); 10 | } 11 | 12 | return html; 13 | }; 14 | 15 | export default fooRoute; 16 | -------------------------------------------------------------------------------- /examples/hapi/routes/home.ts: -------------------------------------------------------------------------------- 1 | import { Lifecycle } from "@hapi/hapi"; 2 | import viteDevServer from "vavite/vite-dev-server"; 3 | import nav from "./nav"; 4 | 5 | const homeRoute: Lifecycle.Method = async (req, h) => { 6 | let html = "<h1>Hello from home page</h1>" + nav; 7 | 8 | if (viteDevServer) { 9 | html = await viteDevServer.transformIndexHtml(req.path, html); 10 | } 11 | 12 | return html; 13 | }; 14 | 15 | export default homeRoute; 16 | -------------------------------------------------------------------------------- /examples/hapi/routes/nav.ts: -------------------------------------------------------------------------------- 1 | export default ` 2 | <nav> 3 | <ul> 4 | <li><a href="/">Home</a></li> 5 | <li><a href="/foo">Foo</a></li> 6 | <li><a href="/bar">Bar</a></li> 7 | </ul> 8 | </nav> 9 | `; 10 | -------------------------------------------------------------------------------- /examples/hapi/server.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="vite/client" /> 2 | 3 | import Hapi, { Lifecycle } from "@hapi/hapi"; 4 | import viteDevServer from "vavite/vite-dev-server"; 5 | import { IncomingMessage, ServerResponse } from "node:http"; 6 | 7 | // This is an optional optimization to load routes lazily so that 8 | // when reloadOn option is set to "static-deps-change", 9 | // changes to the route handlers will not trigger a reload. 10 | // Feel free to remove this and import routes directly. 11 | function lazy( 12 | importer: () => Promise<{ default: Lifecycle.Method }>, 13 | ): Lifecycle.Method { 14 | return async function (req, h) { 15 | try { 16 | const routeHandler = (await importer()).default; 17 | return routeHandler.call(this, req, h); 18 | } catch (err) { 19 | if (err instanceof Error) viteDevServer?.ssrFixStacktrace(err); 20 | throw err; 21 | } 22 | }; 23 | } 24 | 25 | let server: Hapi.Server; 26 | 27 | server = Hapi.server({ 28 | autoListen: false, 29 | }); 30 | 31 | server.route({ 32 | method: "GET", 33 | path: "/", 34 | handler: lazy(() => import("./routes/home")), 35 | }); 36 | 37 | server.route({ 38 | method: "GET", 39 | path: "/foo", 40 | handler: lazy(() => import("./routes/foo")), 41 | }); 42 | 43 | server.route({ 44 | method: "GET", 45 | path: "/bar", 46 | handler: lazy(() => import("./routes/bar")), 47 | }); 48 | 49 | let promise: Promise<void> | undefined = server.start(); 50 | 51 | export default async function handler( 52 | req: IncomingMessage, 53 | res: ServerResponse, 54 | ) { 55 | if (promise) { 56 | await promise; 57 | promise = undefined; 58 | } 59 | 60 | server.listener.emit("request", req, res); 61 | } 62 | -------------------------------------------------------------------------------- /examples/hapi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "Bundler", 10 | "customConditions": ["import"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/hapi/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { vavite } from "vavite"; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | vavite({ 7 | handlerEntry: "/server.ts", 8 | }), 9 | ], 10 | }); 11 | -------------------------------------------------------------------------------- /examples/koa/.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev" 4 | } 5 | -------------------------------------------------------------------------------- /examples/koa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/example-koa", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "start": "node dist", 7 | "dev": "vite", 8 | "build": "vite build --ssr --mode=production" 9 | }, 10 | "files": [ 11 | "dist" 12 | ], 13 | "devDependencies": { 14 | "@types/koa": "^2.15.0", 15 | "@types/koa__router": "^12.0.4", 16 | "@types/node": "^18.19.67", 17 | "typescript": "^5.7.2", 18 | "vavite": "5.1.0", 19 | "vite": "^5.4.11" 20 | }, 21 | "dependencies": { 22 | "@koa/router": "^13.1.0", 23 | "koa": "^2.15.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/koa/readme.md: -------------------------------------------------------------------------------- 1 | # Vavite Koa example 2 | 3 | `serverEntry` example that shows how to integrate with [Koa](https://koajs.com/). 4 | 5 | > [Try on StackBlitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/koa) 6 | 7 | Clone with: 8 | 9 | ```bash 10 | npx degit cyco130/vavite/examples/koa 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/koa/routes/bar.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "koa"; 2 | import viteDevServer from "vavite/vite-dev-server"; 3 | import nav from "./nav"; 4 | 5 | const barRoute: Middleware = async (ctx, next) => { 6 | let html = "<h1>Hello from page /bar</h1>" + nav; 7 | 8 | if (viteDevServer) { 9 | html = await viteDevServer.transformIndexHtml(ctx.url, html); 10 | } 11 | 12 | ctx.body = html; 13 | }; 14 | 15 | export default barRoute; 16 | -------------------------------------------------------------------------------- /examples/koa/routes/foo.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "koa"; 2 | import viteDevServer from "vavite/vite-dev-server"; 3 | import nav from "./nav"; 4 | 5 | const fooRoute: Middleware = async (ctx, next) => { 6 | let html = "<h1>Hello from page /foo</h1>" + nav; 7 | 8 | if (viteDevServer) { 9 | html = await viteDevServer.transformIndexHtml(ctx.url, html); 10 | } 11 | 12 | ctx.body = html; 13 | }; 14 | 15 | export default fooRoute; 16 | -------------------------------------------------------------------------------- /examples/koa/routes/home.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "koa"; 2 | import viteDevServer from "vavite/vite-dev-server"; 3 | import nav from "./nav"; 4 | 5 | const homeRoute: Middleware = async (ctx, next) => { 6 | let html = "<h1>Hello from home page</h1>" + nav; 7 | 8 | if (viteDevServer) { 9 | html = await viteDevServer.transformIndexHtml(ctx.url, html); 10 | } 11 | 12 | ctx.body = html; 13 | }; 14 | 15 | export default homeRoute; 16 | -------------------------------------------------------------------------------- /examples/koa/routes/nav.ts: -------------------------------------------------------------------------------- 1 | export default ` 2 | <nav> 3 | <ul> 4 | <li><a href="/">Home</a></li> 5 | <li><a href="/foo">Foo</a></li> 6 | <li><a href="/bar">Bar</a></li> 7 | </ul> 8 | </nav> 9 | `; 10 | -------------------------------------------------------------------------------- /examples/koa/server.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="vite/client" /> 2 | 3 | import Koa, { Middleware } from "koa"; 4 | import Router from "@koa/router"; 5 | import httpDevServer from "vavite/http-dev-server"; 6 | import viteDevServer from "vavite/vite-dev-server"; 7 | 8 | const app = new Koa(); 9 | const router = new Router(); 10 | 11 | // This is an optional optimization to load routes lazily so that 12 | // when reloadOn option is set to "static-deps-change", 13 | // changes to the route handlers will not trigger a reload. 14 | // Feel free to remove this and import routes directly. 15 | function lazy(importer: () => Promise<{ default: Middleware }>): Middleware { 16 | return async (ctx, next) => { 17 | try { 18 | const routeHandler = (await importer()).default; 19 | return routeHandler(ctx, next); 20 | } catch (err) { 21 | if (err instanceof Error) viteDevServer?.ssrFixStacktrace(err); 22 | throw err; 23 | } 24 | }; 25 | } 26 | 27 | router.get( 28 | "/", 29 | lazy(() => import("./routes/home")), 30 | ); 31 | 32 | router.get( 33 | "/foo", 34 | lazy(() => import("./routes/foo")), 35 | ); 36 | 37 | router.get( 38 | "/bar", 39 | lazy(() => import("./routes/bar")), 40 | ); 41 | 42 | app.use(router.routes()); 43 | 44 | if (httpDevServer) { 45 | httpDevServer.on("request", app.callback()); 46 | } else { 47 | console.log("Starting prod server"); 48 | app.listen(3000); 49 | } 50 | -------------------------------------------------------------------------------- /examples/koa/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "Bundler", 10 | "customConditions": ["import"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/koa/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { vavite } from "vavite"; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | vavite({ 7 | serverEntry: "/server.ts", 8 | reloadOn: "static-deps-change", 9 | serveClientAssetsInDev: true, 10 | }), 11 | ], 12 | }); 13 | -------------------------------------------------------------------------------- /examples/nestjs-vike/index.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="vite/client" /> 2 | 3 | import viteDevServer from "vavite/http-dev-server"; 4 | import Fastify, { FastifyInstance } from "fastify"; 5 | import FastifyStatic from "@fastify/static"; 6 | import { renderPage } from "vike/server"; 7 | import { fileURLToPath } from "node:url"; 8 | import { IncomingMessage, ServerResponse } from "node:http"; 9 | 10 | async function startServer() { 11 | const instance = Fastify({}); 12 | 13 | if (!viteDevServer) { 14 | await instance.register(FastifyStatic, { 15 | root: fileURLToPath(new URL("../client/assets", import.meta.url)), 16 | prefix: "/assets/", 17 | }); 18 | } 19 | 20 | instance.get("*", async (request, reply) => { 21 | const pageContext = await renderPage({ urlOriginal: request.url }); 22 | const { httpResponse } = pageContext; 23 | if (!httpResponse) return; 24 | 25 | const { body, statusCode, headers } = httpResponse; 26 | headers.forEach(([name, value]) => reply.header(name, value)); 27 | reply.status(statusCode); 28 | reply.send(body); 29 | }); 30 | 31 | await instance.ready(); 32 | 33 | fastify = instance; 34 | } 35 | 36 | let fastify: FastifyInstance | undefined; 37 | const fastifyHandlerPromise = startServer().catch((error) => { 38 | console.error(error); 39 | process.exit(1); 40 | }); 41 | 42 | export default async function handler( 43 | request: IncomingMessage, 44 | reply: ServerResponse, 45 | ) { 46 | if (!fastify) { 47 | await fastifyHandlerPromise; 48 | } 49 | 50 | fastify!.server.emit("request", request, reply); 51 | } 52 | -------------------------------------------------------------------------------- /examples/nestjs-vike/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/example-nestjs-vike", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite optimize --force && vavite serve", 7 | "build": "vavite build", 8 | "start": "cross-env NODE_ENV=production node dist/server/index.mjs" 9 | }, 10 | "files": [ 11 | "dist" 12 | ], 13 | "devDependencies": { 14 | "@swc/core": "^1.9.3", 15 | "@types/express": "^5.0.0", 16 | "@types/node": "^18.19.67", 17 | "@types/react": "^18.3.12", 18 | "@types/react-dom": "^18.3.1", 19 | "@vitejs/plugin-react": "^4.3.4", 20 | "cross-env": "^7.0.3", 21 | "rollup-plugin-swc3": "^0.12.1", 22 | "typescript": "^5.7.2", 23 | "vavite": "5.1.0", 24 | "vite": "^5.4.11", 25 | "vite-tsconfig-paths": "^5.1.3" 26 | }, 27 | "dependencies": { 28 | "@nestjs/common": "^10.4.12", 29 | "@nestjs/core": "^10.4.12", 30 | "@nestjs/platform-express": "^10.4.12", 31 | "@nestjs/serve-static": "^4.0.2", 32 | "react": "^18.3.1", 33 | "react-dom": "^18.3.1", 34 | "react-streaming": "0.3.43", 35 | "reflect-metadata": "^0.2.2", 36 | "rxjs": "^7.8.1", 37 | "vike": "^0.4.205" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/nestjs-vike/pages/_error/+Page.tsx: -------------------------------------------------------------------------------- 1 | export { Page }; 2 | 3 | import { usePageContext } from "../../renderer/usePageContext"; 4 | 5 | function Page() { 6 | const pageContext = usePageContext(); 7 | let { abortReason } = pageContext; 8 | if (!abortReason) { 9 | abortReason = pageContext.is404 10 | ? "Page not found." 11 | : "Something went wrong."; 12 | } 13 | return ( 14 | <Center> 15 | <p style={{ fontSize: "1.3em" }}>{abortReason}</p> 16 | </Center> 17 | ); 18 | } 19 | 20 | function Center({ children }: { children: React.ReactNode }) { 21 | return ( 22 | <div 23 | style={{ 24 | height: "calc(100vh - 100px)", 25 | display: "flex", 26 | justifyContent: "center", 27 | alignItems: "center", 28 | }} 29 | > 30 | {children} 31 | </div> 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /examples/nestjs-vike/pages/about/+Page.tsx: -------------------------------------------------------------------------------- 1 | import "./code.css"; 2 | 3 | export { Page }; 4 | 5 | function Page() { 6 | return ( 7 | <> 8 | <h1>About</h1> 9 | <p>Example of using Vike.</p> 10 | </> 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/nestjs-vike/pages/about/code.css: -------------------------------------------------------------------------------- 1 | code { 2 | font-family: monospace; 3 | background-color: #eaeaea; 4 | padding: 3px 5px; 5 | border-radius: 4px; 6 | } 7 | -------------------------------------------------------------------------------- /examples/nestjs-vike/pages/index/+Page.tsx: -------------------------------------------------------------------------------- 1 | export { Page }; 2 | 3 | import { Counter } from "./Counter"; 4 | 5 | function Page() { 6 | return ( 7 | <> 8 | <h1>Hello from Vike on NestJS!</h1> 9 | This page is: 10 | <ul> 11 | <li>Rendered to HTML.</li> 12 | <li> 13 | Interactive. <Counter /> 14 | </li> 15 | </ul> 16 | </> 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/nestjs-vike/pages/index/Counter.tsx: -------------------------------------------------------------------------------- 1 | export { Counter }; 2 | 3 | import { useState } from "react"; 4 | 5 | function Counter() { 6 | const [count, setCount] = useState(0); 7 | return ( 8 | <button type="button" onClick={() => setCount((count) => count + 1)}> 9 | Counter {count} 10 | </button> 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/nestjs-vike/pages/star-wars/@id/+Page.tsx: -------------------------------------------------------------------------------- 1 | export { Page }; 2 | 3 | import { useData } from "../../../renderer/useData"; 4 | import type { Data } from "./+data"; 5 | 6 | function Page() { 7 | const { movie } = useData<Data>(); 8 | return ( 9 | <> 10 | <h1>{movie.title}</h1> 11 | Release Date: {movie.release_date} 12 | <br /> 13 | Director: {movie.director} 14 | <br /> 15 | Producer: {movie.producer} 16 | </> 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/nestjs-vike/pages/star-wars/@id/+data.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/data 2 | export { data }; 3 | export type Data = Awaited<ReturnType<typeof data>>; 4 | 5 | import type { MovieDetails } from "../types"; 6 | import type { PageContextServer } from "vike/types"; 7 | 8 | const data = async (pageContext: PageContextServer) => { 9 | await sleep(300); // Simulate slow network 10 | 11 | const response = await fetch( 12 | `https://brillout.github.io/star-wars/api/films/${pageContext.routeParams!.id}.json`, 13 | ); 14 | let movie = (await response.json()) as MovieDetails; 15 | 16 | // We remove data we don't need because the data is passed to the client; we should 17 | // minimize what is sent over the network. 18 | movie = minimize(movie); 19 | 20 | return { 21 | movie, 22 | // The page's <title> 23 | title: movie.title, 24 | }; 25 | }; 26 | 27 | function minimize(movie: MovieDetails & Record<string, unknown>): MovieDetails { 28 | const { id, title, release_date, director, producer } = movie; 29 | movie = { id, title, release_date, director, producer }; 30 | return movie; 31 | } 32 | 33 | function sleep(milliseconds: number) { 34 | return new Promise((r) => setTimeout(r, milliseconds)); 35 | } 36 | -------------------------------------------------------------------------------- /examples/nestjs-vike/pages/star-wars/index/+Page.tsx: -------------------------------------------------------------------------------- 1 | export default Page; 2 | 3 | import { useData } from "../../../renderer/useData"; 4 | import type { Data } from "./+data"; 5 | 6 | function Page() { 7 | const { movies } = useData<Data>(); 8 | return ( 9 | <> 10 | <h1>Star Wars Movies</h1> 11 | <ol> 12 | {movies.map(({ id, title, release_date }) => ( 13 | <li key={id}> 14 | <a href={`/star-wars/${id}`}>{title}</a> ({release_date}) 15 | </li> 16 | ))} 17 | </ol> 18 | <p> 19 | Source:{" "} 20 | <a href="https://brillout.github.io/star-wars/"> 21 | brillout.github.io/star-wars 22 | </a> 23 | . 24 | </p> 25 | <p> 26 | Data can be fetched by using the <code>data()</code> hook. 27 | </p> 28 | </> 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /examples/nestjs-vike/pages/star-wars/index/+data.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/data 2 | export { data }; 3 | export type Data = Awaited<ReturnType<typeof data>>; 4 | 5 | import type { MovieDetails, Movie } from "../types"; 6 | import type { PageContextServer } from "vike/types"; 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 9 | const data = async (pageContext: PageContextServer) => { 10 | await sleep(700); // Simulate slow network 11 | 12 | const response = await fetch( 13 | "https://brillout.github.io/star-wars/api/films.json", 14 | ); 15 | const moviesData = (await response.json()) as MovieDetails[]; 16 | 17 | // We remove data we don't need because the data is passed to the client; we should 18 | // minimize what is sent over the network. 19 | const movies = minimize(moviesData); 20 | 21 | return { 22 | movies, 23 | // The page's <title> 24 | title: `${movies.length} Star Wars Movies`, 25 | }; 26 | }; 27 | 28 | function minimize(movies: MovieDetails[]): Movie[] { 29 | return movies.map((movie) => { 30 | const { title, release_date, id } = movie; 31 | return { title, release_date, id }; 32 | }); 33 | } 34 | 35 | function sleep(milliseconds: number) { 36 | return new Promise((r) => setTimeout(r, milliseconds)); 37 | } 38 | -------------------------------------------------------------------------------- /examples/nestjs-vike/pages/star-wars/types.ts: -------------------------------------------------------------------------------- 1 | export type Movie = { 2 | id: string; 3 | title: string; 4 | release_date: string; 5 | }; 6 | export type MovieDetails = Movie & { 7 | director: string; 8 | producer: string; 9 | }; 10 | -------------------------------------------------------------------------------- /examples/nestjs-vike/readme.md: -------------------------------------------------------------------------------- 1 | # Vavite Vike example with NestJS 2 | 3 | Simple example of using [Vike](https://vike.dev/) with Vavite and [NestJS](https://nestjs.com/) that shows how to: 4 | 5 | - Integrate NestJS with Vite's dev server 6 | - Run multiple build steps (for client and server) 7 | - Perform React SSR with Vike 8 | 9 | Clone with: 10 | 11 | ```bash 12 | npx degit cyco130/vavite/examples/nestjs-vike 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/nestjs-vike/renderer/+config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "vike/types"; 2 | 3 | // https://vike.dev/config 4 | export default { 5 | // https://vike.dev/clientRouting 6 | clientRouting: true, 7 | // https://vike.dev/meta 8 | meta: { 9 | // Define new setting 'title' 10 | title: { 11 | env: { server: true, client: true }, 12 | }, 13 | // Define new setting 'description' 14 | description: { 15 | env: { server: true }, 16 | }, 17 | }, 18 | hydrationCanBeAborted: true, 19 | } satisfies Config; 20 | -------------------------------------------------------------------------------- /examples/nestjs-vike/renderer/+onPageTransitionEnd.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/onPageTransitionEnd 2 | export { onPageTransitionEnd }; 3 | 4 | import type { OnPageTransitionEndAsync } from "vike/types"; 5 | 6 | const onPageTransitionEnd: OnPageTransitionEndAsync = 7 | async (): ReturnType<OnPageTransitionEndAsync> => { 8 | console.log("Page transition end"); 9 | document.querySelector("body")!.classList.remove("page-is-transitioning"); 10 | }; 11 | -------------------------------------------------------------------------------- /examples/nestjs-vike/renderer/+onPageTransitionStart.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/onPageTransitionStart 2 | export { onPageTransitionStart }; 3 | 4 | import type { OnPageTransitionStartAsync } from "vike/types"; 5 | 6 | const onPageTransitionStart: OnPageTransitionStartAsync = 7 | async (): ReturnType<OnPageTransitionStartAsync> => { 8 | console.log("Page transition start"); 9 | document.querySelector("body")!.classList.add("page-is-transitioning"); 10 | }; 11 | -------------------------------------------------------------------------------- /examples/nestjs-vike/renderer/+onRenderClient.tsx: -------------------------------------------------------------------------------- 1 | // https://vike.dev/onRenderClient 2 | export { onRenderClient }; 3 | 4 | import ReactDOM from "react-dom/client"; 5 | import { Layout } from "./Layout"; 6 | import { getPageTitle } from "./getPageTitle"; 7 | import type { OnRenderClientAsync } from "vike/types"; 8 | 9 | let root: ReactDOM.Root; 10 | const onRenderClient: OnRenderClientAsync = async ( 11 | pageContext, 12 | ): ReturnType<OnRenderClientAsync> => { 13 | const { Page } = pageContext; 14 | 15 | // This onRenderClient() hook only supports SSR, see https://vike.dev/render-modes for how to modify onRenderClient() 16 | // to support SPA 17 | if (!Page) 18 | throw new Error( 19 | "My onRenderClient() hook expects pageContext.Page to be defined", 20 | ); 21 | 22 | const container = document.getElementById("react-root"); 23 | if (!container) throw new Error("DOM element #react-root not found"); 24 | 25 | const page = ( 26 | <Layout pageContext={pageContext}> 27 | <Page /> 28 | </Layout> 29 | ); 30 | if (pageContext.isHydration) { 31 | root = ReactDOM.hydrateRoot(container, page); 32 | } else { 33 | if (!root) { 34 | root = ReactDOM.createRoot(container); 35 | } 36 | root.render(page); 37 | } 38 | document.title = getPageTitle(pageContext); 39 | }; 40 | -------------------------------------------------------------------------------- /examples/nestjs-vike/renderer/+onRenderHtml.tsx: -------------------------------------------------------------------------------- 1 | // https://vike.dev/onRenderHtml 2 | export { onRenderHtml }; 3 | 4 | import ReactDOMServer from "react-dom/server"; 5 | import { Layout } from "./Layout"; 6 | import { escapeInject, dangerouslySkipEscape } from "vike/server"; 7 | import logoUrl from "./logo.svg"; 8 | import type { OnRenderHtmlAsync } from "vike/types"; 9 | import { getPageTitle } from "./getPageTitle"; 10 | 11 | const onRenderHtml: OnRenderHtmlAsync = async ( 12 | pageContext, 13 | ): ReturnType<OnRenderHtmlAsync> => { 14 | const { Page } = pageContext; 15 | 16 | // This onRenderHtml() hook only supports SSR, see https://vike.dev/render-modes for how to modify 17 | // onRenderHtml() to support SPA 18 | if (!Page) 19 | throw new Error( 20 | "My onRenderHtml() hook expects pageContext.Page to be defined", 21 | ); 22 | 23 | // Alternativly, we can use an HTML stream, see https://vike.dev/streaming 24 | const pageHtml = ReactDOMServer.renderToString( 25 | <Layout pageContext={pageContext}> 26 | <Page /> 27 | </Layout>, 28 | ); 29 | 30 | const title = getPageTitle(pageContext); 31 | const desc = 32 | pageContext.data?.description || 33 | pageContext.config.description || 34 | "Demo of using Vike"; 35 | 36 | const documentHtml = escapeInject`<!DOCTYPE html> 37 | <html lang="en"> 38 | <head> 39 | <meta charset="UTF-8" /> 40 | <link rel="icon" href="${logoUrl}" /> 41 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 42 | <meta name="description" content="${desc}" /> 43 | <title>${title} 44 | 45 | 46 |
${dangerouslySkipEscape(pageHtml)}
47 | 48 | `; 49 | 50 | return { 51 | documentHtml, 52 | pageContext: { 53 | // We can add custom pageContext properties here, see https://vike.dev/pageContext#custom 54 | }, 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /examples/nestjs-vike/renderer/Layout.css: -------------------------------------------------------------------------------- 1 | #sidebar a { 2 | padding: 2px 10px; 3 | margin-left: -10px; 4 | } 5 | #sidebar a.is-active { 6 | background-color: #eee; 7 | } 8 | -------------------------------------------------------------------------------- /examples/nestjs-vike/renderer/Layout.tsx: -------------------------------------------------------------------------------- 1 | export { Layout }; 2 | 3 | import React from "react"; 4 | import logoUrl from "./logo.svg"; 5 | import { PageContextProvider } from "./usePageContext"; 6 | import { Link } from "./Link"; 7 | import type { PageContext } from "vike/types"; 8 | import "./css/index.css"; 9 | import "./Layout.css"; 10 | 11 | function Layout({ 12 | children, 13 | pageContext, 14 | }: { 15 | children: React.ReactNode; 16 | pageContext: PageContext; 17 | }) { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | Welcome 25 | About 26 | Data Fetching 27 | 28 | {children} 29 | 30 | 31 | 32 | ); 33 | } 34 | 35 | function Frame({ children }: { children: React.ReactNode }) { 36 | return ( 37 |
44 | {children} 45 |
46 | ); 47 | } 48 | 49 | function Sidebar({ children }: { children: React.ReactNode }) { 50 | return ( 51 | 64 | ); 65 | } 66 | 67 | function Content({ children }: { children: React.ReactNode }) { 68 | return ( 69 |
70 |
78 | {children} 79 |
80 |
81 | ); 82 | } 83 | 84 | function Logo() { 85 | return ( 86 |
92 | 93 | logo 94 | 95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /examples/nestjs-vike/renderer/Link.tsx: -------------------------------------------------------------------------------- 1 | import { usePageContext } from "./usePageContext"; 2 | 3 | export { Link }; 4 | 5 | function Link(props: { 6 | href: string; 7 | className?: string; 8 | children: React.ReactNode; 9 | }) { 10 | const pageContext = usePageContext(); 11 | const { urlPathname } = pageContext; 12 | const { href } = props; 13 | const isActive = 14 | href === "/" ? urlPathname === href : urlPathname.startsWith(href); 15 | const className = [props.className, isActive && "is-active"] 16 | .filter(Boolean) 17 | .join(" "); 18 | return ; 19 | } 20 | -------------------------------------------------------------------------------- /examples/nestjs-vike/renderer/css/code.css: -------------------------------------------------------------------------------- 1 | code { 2 | font-family: monospace; 3 | background-color: #eaeaea; 4 | padding: 3px 5px; 5 | border-radius: 4px; 6 | } 7 | -------------------------------------------------------------------------------- /examples/nestjs-vike/renderer/css/index.css: -------------------------------------------------------------------------------- 1 | @import "./reset.css"; 2 | @import "./links.css"; 3 | @import "./code.css"; 4 | @import "./page-transition-loading-animation.css"; 5 | -------------------------------------------------------------------------------- /examples/nestjs-vike/renderer/css/links.css: -------------------------------------------------------------------------------- 1 | a { 2 | text-decoration: none; 3 | } 4 | -------------------------------------------------------------------------------- /examples/nestjs-vike/renderer/css/page-transition-loading-animation.css: -------------------------------------------------------------------------------- 1 | #page-container { 2 | position: relative; 3 | width: 100%; 4 | } 5 | #page-container::before { 6 | content: ""; 7 | position: absolute; 8 | width: 100%; 9 | height: 100%; 10 | z-index: 999; 11 | background: no-repeat url("./page-transition-loading-animation/loading.svg"); 12 | background-size: 100px; 13 | background-position: center center; 14 | pointer-events: none; 15 | opacity: 0; 16 | } 17 | body.page-is-transitioning #page-container::before { 18 | opacity: 1; 19 | } 20 | #page-content, 21 | #page-container::before { 22 | transition: opacity 0.5s ease-in-out; 23 | } 24 | body.page-is-transitioning #page-content { 25 | opacity: 0.17; 26 | } 27 | -------------------------------------------------------------------------------- /examples/nestjs-vike/renderer/css/page-transition-loading-animation/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/nestjs-vike/renderer/css/reset.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: sans-serif; 4 | } 5 | * { 6 | box-sizing: border-box; 7 | } 8 | -------------------------------------------------------------------------------- /examples/nestjs-vike/renderer/getPageTitle.ts: -------------------------------------------------------------------------------- 1 | export { getPageTitle }; 2 | 3 | import type { PageContext } from "vike/types"; 4 | 5 | function getPageTitle(pageContext: PageContext): string { 6 | const title = 7 | // Title defined dynamically by data() 8 | pageContext.data?.title || 9 | // Title defined statically by /pages/some-page/+title.js (or by `export default { title }` in /pages/some-page/+config.js) 10 | // The setting 'pageContext.config.title' is a custom setting we defined at ./+config.ts 11 | pageContext.config.title || 12 | "Vike Demo"; 13 | return title; 14 | } 15 | -------------------------------------------------------------------------------- /examples/nestjs-vike/renderer/types.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/pageContext#typescript 2 | declare global { 3 | namespace Vike { 4 | interface PageContext { 5 | Page: () => React.ReactElement; 6 | data?: { 7 | /** Value for defined dynmically by by /pages/some-page/+data.js */ 8 | title?: string; 9 | /** Value for <meta name="description"> defined dynmically */ 10 | description?: string; 11 | }; 12 | config: { 13 | /** Value for <title> defined statically by /pages/some-page/+title.js (or by `export default { title }` in /pages/some-page/+config.js) */ 14 | title?: string; 15 | /** Value for <meta name="description"> defined statically */ 16 | description?: string; 17 | }; 18 | /** https://vike.dev/render */ 19 | abortReason?: string; 20 | } 21 | } 22 | } 23 | 24 | // Tell TypeScript this file isn't an ambient module 25 | export {}; 26 | -------------------------------------------------------------------------------- /examples/nestjs-vike/renderer/useData.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/useData 2 | export { useData }; 3 | 4 | import { usePageContext } from "./usePageContext"; 5 | 6 | /** https://vike.dev/useData */ 7 | function useData<Data>() { 8 | const { data } = usePageContext(); 9 | return data as Data; 10 | } 11 | -------------------------------------------------------------------------------- /examples/nestjs-vike/renderer/usePageContext.tsx: -------------------------------------------------------------------------------- 1 | // https://vike.dev/usePageContext 2 | // eslint-disable-next-line react-refresh/only-export-components 3 | export { usePageContext }; 4 | export { PageContextProvider }; 5 | 6 | import React, { useContext } from "react"; 7 | import type { PageContext } from "vike/types"; 8 | 9 | const Context = React.createContext<PageContext>( 10 | undefined as unknown as PageContext, 11 | ); 12 | 13 | function PageContextProvider({ 14 | pageContext, 15 | children, 16 | }: { 17 | pageContext: PageContext; 18 | children: React.ReactNode; 19 | }) { 20 | return <Context.Provider value={pageContext}>{children}</Context.Provider>; 21 | } 22 | 23 | /** https://vike.dev/usePageContext */ 24 | function usePageContext() { 25 | const pageContext = useContext(Context); 26 | return pageContext; 27 | } 28 | -------------------------------------------------------------------------------- /examples/nestjs-vike/server/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { VpsModule } from "./vps.module"; 3 | 4 | @Module({ 5 | imports: [VpsModule.forRoot()], 6 | providers: [], 7 | }) 8 | export class AppModule {} 9 | -------------------------------------------------------------------------------- /examples/nestjs-vike/server/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from "@nestjs/core"; 2 | import type { Express } from "express"; 3 | import { IncomingMessage, ServerResponse } from "node:http"; 4 | import { AppModule } from "./app.module"; 5 | 6 | bootstrap(); 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(AppModule); 10 | await app.init(); 11 | resolveHandler(await app.getHttpAdapter().getInstance()); 12 | } 13 | 14 | let resolveHandler: (value: Express) => void; 15 | let expressHandler: Express | Promise<Express> = new Promise((resolve) => { 16 | resolveHandler = resolve; 17 | }); 18 | 19 | export default async function handler( 20 | request: IncomingMessage, 21 | reply: ServerResponse, 22 | ) { 23 | if (expressHandler instanceof Promise) { 24 | expressHandler = await expressHandler; 25 | } 26 | 27 | expressHandler(request, reply); 28 | } 29 | -------------------------------------------------------------------------------- /examples/nestjs-vike/server/vps.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Inject, Module, OnModuleInit } from "@nestjs/common"; 2 | import { HttpAdapterHost } from "@nestjs/core"; 3 | import type { NextFunction, Request, Response } from "express"; 4 | import { fileURLToPath } from "node:url"; 5 | import { renderPage } from "vike/server"; 6 | import { ServeStaticModule } from "@nestjs/serve-static"; 7 | import devServer from "vavite/http-dev-server"; 8 | 9 | const OPTIONS = Symbol.for("vike.options"); 10 | 11 | interface ViteSsrOptions { 12 | root?: string; 13 | } 14 | 15 | @Module({}) 16 | export class VpsModule implements OnModuleInit { 17 | constructor( 18 | private readonly httpAdapterHost: HttpAdapterHost, 19 | @Inject(OPTIONS) 20 | private readonly viteSsrOptions: ViteSsrOptions, 21 | ) {} 22 | 23 | static forRoot(options?: ViteSsrOptions): DynamicModule { 24 | options ??= { 25 | root: fileURLToPath(new URL("../client", import.meta.url)), 26 | }; 27 | 28 | const imports: DynamicModule[] = []; 29 | if (!devServer) { 30 | imports.push( 31 | ServeStaticModule.forRoot({ 32 | rootPath: options.root, 33 | serveRoot: "/", 34 | }), 35 | ); 36 | } 37 | 38 | return { 39 | module: VpsModule, 40 | imports, 41 | providers: [{ provide: OPTIONS, useValue: options }], 42 | }; 43 | } 44 | 45 | async onModuleInit() { 46 | if (!this.httpAdapterHost) { 47 | throw new Error( 48 | "httpAdapterHost is undefined, no decorator metadata available", 49 | ); 50 | } 51 | const httpAdapter = this.httpAdapterHost.httpAdapter; 52 | if (!httpAdapter) { 53 | return; 54 | } 55 | const app = httpAdapter.getInstance(); 56 | 57 | app.get("*", async (req: Request, res: Response, next: NextFunction) => { 58 | const urlOriginal = req.originalUrl; 59 | const pageContext = await renderPage({ urlOriginal }); 60 | const { httpResponse } = pageContext; 61 | if (!httpResponse) { 62 | next(); 63 | return; 64 | } 65 | const { body, statusCode, headers } = httpResponse; 66 | headers.forEach(([name, value]) => res.header(name, value)); 67 | res.status(statusCode); 68 | res.send(body); 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/nestjs-vike/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "es2017", 5 | "module": "ESNext", 6 | "esModuleInterop": true, 7 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "moduleResolution": "Bundler", 12 | "customConditions": ["import"], 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "jsx": "react-jsx", 16 | "types": ["vite/client"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/nestjs-vike/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { vavite } from "vavite"; 3 | import { swc } from "rollup-plugin-swc3"; 4 | import react from "@vitejs/plugin-react"; 5 | import ssr from "vike/plugin"; 6 | import { join } from "node:path"; 7 | 8 | export default defineConfig({ 9 | buildSteps: [ 10 | { 11 | name: "client", 12 | }, 13 | { 14 | name: "server", 15 | config: { 16 | build: { ssr: true }, 17 | }, 18 | }, 19 | ], 20 | ssr: { 21 | external: ["reflect-metadata"], 22 | }, 23 | esbuild: false, 24 | plugins: [ 25 | { 26 | ...swc({ 27 | jsc: { 28 | baseUrl: join(__dirname, "./src"), 29 | paths: { 30 | "*": ["*"], 31 | }, 32 | transform: { 33 | decoratorMetadata: true, 34 | legacyDecorator: true, 35 | }, 36 | target: "es2017", 37 | }, 38 | }), 39 | enforce: "pre", // Make sure this is applied before anything else 40 | }, 41 | vavite({ 42 | handlerEntry: "/server/main.ts", 43 | serveClientAssetsInDev: true, 44 | }), 45 | react(), 46 | ssr({ disableAutoFullBuild: true }), 47 | ], 48 | }); 49 | -------------------------------------------------------------------------------- /examples/nestjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/example-reloader-nestjs", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "start": "node dist/index", 7 | "dev": "vite", 8 | "build": "vite build --ssr --mode=production" 9 | }, 10 | "files": [ 11 | "dist" 12 | ], 13 | "devDependencies": { 14 | "@swc/core": "^1.9.3", 15 | "@types/express": "^5.0.0", 16 | "@types/node": "^18.19.67", 17 | "rollup-plugin-swc3": "^0.12.1", 18 | "typescript": "^5.7.2", 19 | "vavite": "5.1.0", 20 | "vite": "^5.4.11" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^10.4.12", 24 | "@nestjs/core": "^10.4.12", 25 | "@nestjs/platform-express": "^10.4.12", 26 | "reflect-metadata": "^0.2.2", 27 | "rxjs": "^7.8.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/nestjs/readme.md: -------------------------------------------------------------------------------- 1 | # Vavite NestJS example 2 | 3 | `serverEntry` example that shows how to integrate with [NestJS](https://nestjs.com/). 4 | 5 | Clone with: 6 | 7 | ```bash 8 | npx degit cyco130/vavite/examples/nestjs 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/nestjs/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Req, Request } from "@nestjs/common"; 2 | import viteDevServer from "vavite/vite-dev-server"; 3 | 4 | @Controller() 5 | export class AppController { 6 | @Get("/") 7 | async home(@Req() request: Request) { 8 | let html = "<h1>Hello from Nest.js</h1>" + nav; 9 | 10 | if (viteDevServer) { 11 | html = await viteDevServer.transformIndexHtml(request.url, html); 12 | } 13 | 14 | return html; 15 | } 16 | 17 | @Get("/foo") 18 | async foo(@Req() request: Request) { 19 | let html = "<h1>Hello from page /foo</h1>" + nav; 20 | 21 | if (viteDevServer) { 22 | html = await viteDevServer.transformIndexHtml(request.url, html); 23 | } 24 | 25 | return html; 26 | } 27 | 28 | @Get("/bar") 29 | async bar(@Req() request: Request) { 30 | let html = "<h1>Hello from page /bar</h1>" + nav; 31 | 32 | if (viteDevServer) { 33 | html = await viteDevServer.transformIndexHtml(request.url, html); 34 | } 35 | 36 | return html; 37 | } 38 | } 39 | 40 | const nav = ` 41 | <nav> 42 | <ul> 43 | <li><a href="/">Home</a></li> 44 | <li><a href="/foo">Foo</a></li> 45 | <li><a href="/bar">Bar</a></li> 46 | </ul> 47 | </nav> 48 | `; 49 | -------------------------------------------------------------------------------- /examples/nestjs/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { AppController } from "./app.controller"; 3 | 4 | @Module({ 5 | controllers: [AppController], 6 | imports: [], 7 | providers: [], 8 | }) 9 | export class AppModule {} 10 | -------------------------------------------------------------------------------- /examples/nestjs/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from "@nestjs/core"; 2 | import type { Express } from "express"; 3 | import { IncomingMessage, ServerResponse } from "node:http"; 4 | import { AppModule } from "./app.module"; 5 | 6 | bootstrap(); 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(AppModule); 10 | await app.init(); 11 | resolveHandler(await app.getHttpAdapter().getInstance()); 12 | } 13 | 14 | let resolveHandler: (value: Express) => void; 15 | let expressHandler: Express | Promise<Express> = new Promise((resolve) => { 16 | resolveHandler = resolve; 17 | }); 18 | 19 | export default async function handler( 20 | request: IncomingMessage, 21 | reply: ServerResponse, 22 | ) { 23 | if (expressHandler instanceof Promise) { 24 | expressHandler = await expressHandler; 25 | } 26 | 27 | expressHandler(request, reply); 28 | } 29 | -------------------------------------------------------------------------------- /examples/nestjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "Bundler", 10 | "customConditions": ["import"], 11 | "experimentalDecorators": true, 12 | "types": ["vite/client"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/nestjs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { vavite } from "vavite"; 3 | import { swc } from "rollup-plugin-swc3"; 4 | 5 | export default defineConfig({ 6 | ssr: { 7 | external: ["reflect-metadata"], 8 | }, 9 | esbuild: false, 10 | plugins: [ 11 | { 12 | ...swc({ 13 | jsc: { 14 | transform: { 15 | decoratorMetadata: true, 16 | legacyDecorator: true, 17 | }, 18 | target: "es2021", 19 | }, 20 | }), 21 | enforce: "pre", // Make sure this is applied before anything else 22 | }, 23 | vavite({ 24 | handlerEntry: "/src/main.ts", 25 | serveClientAssetsInDev: true, 26 | }), 27 | ], 28 | }); 29 | -------------------------------------------------------------------------------- /examples/readme.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | - [simple-standalone](simple-standalone): Simple standalone example ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/simple-standalone)) 4 | - [express](express): Integrating with Express ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/express)) 5 | - [koa](koa): Integrating with Koa ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/koa)) 6 | - [fastify](fastify): Integrating with Fastify ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/fastify)) 7 | - [hapi](hapi): Integrating with Hapi ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/hapi)) 8 | - [NestJS](nestjs): Integrating with [NestJS](https://nestjs.com/) 9 | - [NestJS+Vike](nestjs-vike): Vike with React and NestJS 10 | - [ssr-react-express](ssr-react-express): React SSR with Express ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/ssr-react-express)) 11 | - [ssr-vue-express](ssr-vue-express): Vue SSR with Express ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/ssr-vue-express)) 12 | - [vike](vike): Vike with React and Express ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/vike)) 13 | - [fastify-vike](fastify-vike): Vike with React and Fastify ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/fastify-vike)) 14 | - [socket-io](socket-io): [socket.io](https://socket.io/) with Express ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/socket-io)) 15 | -------------------------------------------------------------------------------- /examples/simple-standalone/.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev" 4 | } 5 | -------------------------------------------------------------------------------- /examples/simple-standalone/handler.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "node:http"; 2 | 3 | export default function handler( 4 | req: IncomingMessage, 5 | res: ServerResponse, 6 | next: () => void, 7 | ) { 8 | if (!res.writableEnded && req.url === "/") { 9 | res.setHeader("Content-Type", "text/html; charset=utf-8"); 10 | // This is not Express, res.send is Express-specific. 11 | // So you should use res.write or res.end instead. 12 | res.end("<h1>Hello from standalone!</h1>"); 13 | } 14 | 15 | next(); 16 | } 17 | -------------------------------------------------------------------------------- /examples/simple-standalone/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/example-simple-standalone", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "start": "node dist", 7 | "dev": "vavite serve", 8 | "build": "vite build --ssr --mode=production" 9 | }, 10 | "devDependencies": { 11 | "@types/node": "^18.19.67", 12 | "typescript": "^5.7.2", 13 | "vavite": "5.1.0", 14 | "vite": "^5.4.11" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/simple-standalone/readme.md: -------------------------------------------------------------------------------- 1 | # Vavite Simple Standalone example 2 | 3 | Simplest example that specifies a `handlerEntry` and handles incoming requests. 4 | 5 | > [Try on StackBlitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/simple-standalone) 6 | 7 | Clone with: 8 | 9 | ```bash 10 | npx degit cyco130/vavite/examples/simple-standalone 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/simple-standalone/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "Bundler", 10 | "customConditions": ["import"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/simple-standalone/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { vavite } from "vavite"; 3 | 4 | export default defineConfig({ 5 | plugins: [vavite({ handlerEntry: "/handler.ts" })], 6 | }); 7 | -------------------------------------------------------------------------------- /examples/socket-io/.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev" 4 | } 5 | -------------------------------------------------------------------------------- /examples/socket-io/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/example-socket-io", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "start": "node dist", 7 | "dev": "vite", 8 | "build": "vite build --ssr --mode=production" 9 | }, 10 | "files": [ 11 | "dist" 12 | ], 13 | "devDependencies": { 14 | "@types/express": "^5.0.0", 15 | "@types/node": "^18.19.67", 16 | "typescript": "^5.7.2", 17 | "vavite": "5.1.0", 18 | "vite": "^5.4.11" 19 | }, 20 | "dependencies": { 21 | "express": "^4.21.1", 22 | "socket.io": "^4.8.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/socket-io/readme.md: -------------------------------------------------------------------------------- 1 | # Vavite socket.io example 2 | 3 | `serverEntry` example that shows how to integrate with [socket.io](http://socket.io/). 4 | 5 | The trick is to attach the socket.io server to `viteDevServer.httpServer` (from `"vavite/vite-dev-server"`) and not to `httpDevServer` from `"vavite/http-dev-server"`. The latter is merely a proxy to force server frameworks to behave in a controlled manner, the former is the actual server used by Vite. 6 | 7 | > [Try on StackBlitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/socket-io) 8 | 9 | Clone with: 10 | 11 | ```bash 12 | npx degit cyco130/vavite/examples/socket-io 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/socket-io/server.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="vite/client" /> 2 | 3 | import express from "express"; 4 | import { Server as SocketIOServer } from "socket.io"; 5 | import type {} from "node:http"; 6 | import type {} from "node:net"; 7 | 8 | declare module "node:net" { 9 | interface Socket { 10 | server: import("node:http").Server; 11 | } 12 | } 13 | 14 | declare module "node:http" { 15 | interface Server { 16 | io?: SocketIOServer; 17 | } 18 | } 19 | 20 | const app = express(); 21 | 22 | let io: SocketIOServer; 23 | 24 | app.use((req, res, next) => { 25 | if (!io || io !== req.socket.server.io) { 26 | req.socket.server.io?.close(); 27 | 28 | io = req.socket.server.io = new SocketIOServer(req.socket.server); 29 | 30 | io.on("connection", (socket) => { 31 | socket.on("chat message", (msg) => { 32 | io.emit("chat message", msg); 33 | }); 34 | }); 35 | } 36 | 37 | next(); 38 | }); 39 | 40 | app.get("/", async (req, res) => { 41 | res.sendFile(process.cwd() + "/view.html"); 42 | }); 43 | 44 | export default app; 45 | -------------------------------------------------------------------------------- /examples/socket-io/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "Bundler", 10 | "customConditions": ["import"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/socket-io/view.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html> 3 | <head> 4 | <title>Socket.IO chat 5 | 57 | 58 | 59 |
    60 |
    61 | 62 |
    63 | 64 | 65 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /examples/socket-io/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { vavite } from "vavite"; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | vavite({ 7 | handlerEntry: "/server.ts", 8 | reloadOn: "static-deps-change", 9 | }), 10 | ], 11 | }); 12 | -------------------------------------------------------------------------------- /examples/ssr-react-express/.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev" 4 | } 5 | -------------------------------------------------------------------------------- /examples/ssr-react-express/App.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { Nav } from "./components/Nav"; 3 | 4 | export function App(props: { children: ReactNode }) { 5 | return ( 6 |
    7 | 10 | {props.children} 11 |
    12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/ssr-react-express/client-entry.tsx: -------------------------------------------------------------------------------- 1 | import { hydrate } from "react-dom"; 2 | import { App } from "./App"; 3 | 4 | async function render() { 5 | // Tiny, crappy router 6 | const importer = { 7 | "/": () => import("./pages/Home"), 8 | "/foo": () => import("./pages/Foo"), 9 | "/bar": () => import("./pages/Bar"), 10 | }[window.location.pathname]; 11 | 12 | if (!importer) { 13 | throw new Error(`No page found for ${window.location.pathname}`); 14 | } 15 | 16 | const Page = (await importer()).default; 17 | 18 | const root = document.getElementById("root"); 19 | hydrate( 20 | 21 | 22 | , 23 | root, 24 | ); 25 | } 26 | 27 | render(); 28 | -------------------------------------------------------------------------------- /examples/ssr-react-express/components/Counter.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export function Counter() { 4 | const [count, setCount] = useState(0); 5 | 6 | return ( 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /examples/ssr-react-express/components/Nav.tsx: -------------------------------------------------------------------------------- 1 | export function Nav() { 2 | return ( 3 |
    16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/ssr-react-express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/example-ssr-react-express", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vavite serve", 7 | "build": "vavite build", 8 | "start": "node dist/server" 9 | }, 10 | "devDependencies": { 11 | "@types/express": "^5.0.0", 12 | "@types/node": "^18.19.67", 13 | "@types/react": "^18.3.12", 14 | "@types/react-dom": "^18.3.1", 15 | "@vitejs/plugin-react": "^4.3.4", 16 | "typescript": "^5.7.2", 17 | "vavite": "5.1.0", 18 | "vite": "^5.4.11" 19 | }, 20 | "dependencies": { 21 | "express": "^4.21.1", 22 | "react": "^18.3.1", 23 | "react-dom": "^18.3.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/ssr-react-express/pages/Bar.tsx: -------------------------------------------------------------------------------- 1 | export default function Bar() { 2 | return ( 3 |
    4 |

    Bar

    5 |
    6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /examples/ssr-react-express/pages/Foo.tsx: -------------------------------------------------------------------------------- 1 | export default function Foo() { 2 | return ( 3 |
    4 |

    Foo

    5 |
    6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /examples/ssr-react-express/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import { Counter } from "../components/Counter"; 2 | 3 | export default function Home() { 4 | return ( 5 |
    6 |

    Hello from home page!

    7 | 8 |
    9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /examples/ssr-react-express/readme.md: -------------------------------------------------------------------------------- 1 | # Vavite React SSR example with Express 2 | 3 | Simple example for React SSR with Vavite that shows how to: 4 | 5 | - Integrate Express with Vite's dev server 6 | - Run multiple build steps (for client and server) 7 | - Perform SSR in React 8 | 9 | > [Try on StackBlitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/ssr-react-express) 10 | 11 | Clone with: 12 | 13 | ```bash 14 | npx degit cyco130/vavite/examples/ssr-react-express 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/ssr-react-express/server-entry.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import express, { Request, Response } from "express"; 4 | import httpDevServer from "vavite/http-dev-server"; 5 | import viteDevServer from "vavite/vite-dev-server"; 6 | import { ComponentType } from "react"; 7 | import { renderToString } from "react-dom/server"; 8 | import { App } from "./App"; 9 | 10 | const app = express(); 11 | 12 | if (import.meta.env.PROD) { 13 | // Serve client assets in production 14 | app.use(express.static("dist/client")); 15 | } 16 | 17 | // Page routes 18 | app.get("/", (req, res) => render(req, res, () => import("./pages/Home"))); 19 | app.get("/foo", (req, res) => render(req, res, () => import("./pages/Foo"))); 20 | app.get("/bar", (req, res) => render(req, res, () => import("./pages/Bar"))); 21 | 22 | type PageImporter = () => Promise<{ default: ComponentType }>; 23 | 24 | async function render(req: Request, res: Response, importer: PageImporter) { 25 | const Page = (await importer()).default; 26 | 27 | let clientEntryPath: string; 28 | if (viteDevServer) { 29 | // In development, we can simply refer to the source file name 30 | clientEntryPath = "/client-entry.tsx"; 31 | } else { 32 | // In production we'll figure out the path to the client entry file using the manifest 33 | // @ts-ignore: This only exists after the client build is complete 34 | const manifest = (await import("./dist/client/.vite/manifest.json")) 35 | .default; 36 | clientEntryPath = manifest["client-entry.tsx"].file; 37 | 38 | // In a real application we would also use the manifest to generate 39 | // preload links for assets needed for the rendered page 40 | } 41 | 42 | let html = ` 43 | 44 | 45 | SSR React Express 46 | 47 | 48 |
    ${renderToString( 49 | 50 | 51 | , 52 | )}
    53 | 54 | 55 | `; 56 | 57 | if (viteDevServer) { 58 | // This will inject the Vite client and React fast refresh in development 59 | html = await viteDevServer.transformIndexHtml(req.url, html); 60 | } 61 | 62 | res.send(html); 63 | } 64 | 65 | if (viteDevServer) { 66 | httpDevServer!.on("request", app); 67 | } else { 68 | console.log("Starting production server"); 69 | app.listen(3000); 70 | } 71 | -------------------------------------------------------------------------------- /examples/ssr-react-express/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "Bundler", 10 | "customConditions": ["import"], 11 | "jsx": "react-jsx" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/ssr-react-express/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { vavite } from "vavite"; 3 | import react from "@vitejs/plugin-react"; 4 | 5 | export default defineConfig({ 6 | buildSteps: [ 7 | { 8 | name: "client", 9 | config: { 10 | build: { 11 | outDir: "dist/client", 12 | manifest: true, 13 | rollupOptions: { input: "client-entry.tsx" }, 14 | }, 15 | }, 16 | }, 17 | { 18 | name: "server", 19 | config: { 20 | build: { 21 | ssr: true, 22 | outDir: "dist/server", 23 | }, 24 | }, 25 | }, 26 | ], 27 | 28 | plugins: [ 29 | react(), 30 | vavite({ 31 | serverEntry: "/server-entry.tsx", 32 | serveClientAssetsInDev: true, 33 | // Don't reload when dynamically imported dependencies change 34 | reloadOn: "static-deps-change", 35 | }), 36 | ], 37 | }); 38 | -------------------------------------------------------------------------------- /examples/ssr-vue-express/.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev" 4 | } 5 | -------------------------------------------------------------------------------- /examples/ssr-vue-express/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /examples/ssr-vue-express/client-entry.ts: -------------------------------------------------------------------------------- 1 | import { createSSRApp, h } from "vue"; 2 | import App from "./App.vue"; 3 | 4 | async function render() { 5 | // Tiny, crappy router 6 | const importer = { 7 | "/": () => import("./pages/Home.vue"), 8 | "/foo": () => import("./pages/Foo.vue"), 9 | "/bar": () => import("./pages/Bar.vue"), 10 | }[window.location.pathname]; 11 | 12 | if (!importer) { 13 | throw new Error(`No page found for ${window.location.pathname}`); 14 | } 15 | 16 | const Page = (await importer()).default; 17 | 18 | const app = createSSRApp({ 19 | render() { 20 | return h( 21 | App, 22 | {}, 23 | { 24 | default() { 25 | return h(Page, {}); 26 | }, 27 | }, 28 | ); 29 | }, 30 | }); 31 | 32 | app.mount("#root"); 33 | } 34 | 35 | render(); 36 | -------------------------------------------------------------------------------- /examples/ssr-vue-express/components/Counter.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /examples/ssr-vue-express/components/Nav.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /examples/ssr-vue-express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/example-ssr-vue-express", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vavite serve", 7 | "build": "vavite build", 8 | "start": "node dist/server" 9 | }, 10 | "devDependencies": { 11 | "@types/express": "^5.0.0", 12 | "@types/node": "^18.19.67", 13 | "@vitejs/plugin-vue": "^5.2.1", 14 | "typescript": "^5.7.2", 15 | "vavite": "5.1.0", 16 | "vite": "^5.4.11" 17 | }, 18 | "dependencies": { 19 | "express": "^4.21.1", 20 | "vue": "^3.5.13" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/ssr-vue-express/pages/Bar.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /examples/ssr-vue-express/pages/Foo.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /examples/ssr-vue-express/pages/Home.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /examples/ssr-vue-express/readme.md: -------------------------------------------------------------------------------- 1 | # Vavite Vue SSR example with Express 2 | 3 | Simple example for Vue SSR with Vavite that shows how to: 4 | 5 | - Integrate Express with Vite's dev server 6 | - Run multiple build steps (for client and server) 7 | - Perform SSR in Vue 8 | 9 | > [Try on StackBlitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/ssr-vue-express) 10 | 11 | Clone with: 12 | 13 | ```bash 14 | npx degit cyco130/vavite/examples/ssr-vue-express 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/ssr-vue-express/server-entry.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import express, { Request, Response } from "express"; 4 | import httpDevServer from "vavite/http-dev-server"; 5 | import viteDevServer from "vavite/vite-dev-server"; 6 | import Vue, { createSSRApp, h, defineComponent } from "vue"; 7 | import { renderToString } from "vue/server-renderer"; 8 | import App from "./App.vue"; 9 | 10 | const app = express(); 11 | 12 | if (import.meta.env.PROD) { 13 | // Serve client assets in production 14 | app.use(express.static("dist/client")); 15 | } 16 | 17 | // Page routes 18 | app.get("/", (req, res) => render(req, res, () => import("./pages/Home.vue"))); 19 | app.get("/foo", (req, res) => 20 | render(req, res, () => import("./pages/Foo.vue")), 21 | ); 22 | app.get("/bar", (req, res) => 23 | render(req, res, () => import("./pages/Bar.vue")), 24 | ); 25 | 26 | type PageImporter = () => Promise<{ default: any }>; 27 | 28 | async function render(req: Request, res: Response, importer: PageImporter) { 29 | const Page = (await importer()).default; 30 | 31 | let clientEntryPath: string; 32 | if (viteDevServer) { 33 | // In development, we can simply refer to the source file name 34 | clientEntryPath = "/client-entry.ts"; 35 | } else { 36 | // In production we'll figure out the path to the client entry file using the manifest 37 | // @ts-ignore: This only exists after the client build is complete 38 | const manifest = (await import("./dist/client/.vite/manifest.json")) 39 | .default; 40 | clientEntryPath = manifest["client-entry.ts"].file; 41 | 42 | // In a real application we would also use the manifest to generate 43 | // preload links for assets needed for the rendered page 44 | } 45 | 46 | const Content = defineComponent({ 47 | render() { 48 | return h( 49 | App, 50 | {}, 51 | { 52 | default() { 53 | return h(Page, {}); 54 | }, 55 | }, 56 | ); 57 | }, 58 | }); 59 | 60 | const content = await renderToString(createSSRApp(Content)); 61 | 62 | let html = ` 63 | 64 | 65 | SSR Vue Express 66 | 67 | 68 |
    ${content}
    69 | 70 | 71 | `; 72 | 73 | if (viteDevServer) { 74 | // This will inject the Vite client and React fast refresh in development 75 | html = await viteDevServer.transformIndexHtml(req.url, html); 76 | } 77 | 78 | res.send(html); 79 | } 80 | 81 | if (viteDevServer) { 82 | httpDevServer!.on("request", app); 83 | } else { 84 | console.log("Starting production server"); 85 | app.listen(3000); 86 | } 87 | -------------------------------------------------------------------------------- /examples/ssr-vue-express/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "Bundler", 10 | "customConditions": ["import"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/ssr-vue-express/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { vavite } from "vavite"; 3 | import vue from "@vitejs/plugin-vue"; 4 | 5 | export default defineConfig({ 6 | buildSteps: [ 7 | { 8 | name: "client", 9 | config: { 10 | build: { 11 | outDir: "dist/client", 12 | manifest: true, 13 | rollupOptions: { input: "client-entry.ts" }, 14 | }, 15 | }, 16 | }, 17 | { 18 | name: "server", 19 | config: { 20 | build: { 21 | ssr: true, 22 | outDir: "dist/server", 23 | }, 24 | }, 25 | }, 26 | ], 27 | 28 | plugins: [ 29 | vue(), 30 | vavite({ 31 | serverEntry: "/server-entry.ts", 32 | serveClientAssetsInDev: true, 33 | // Don't reload when dynamically imported dependencies change 34 | reloadOn: "static-deps-change", 35 | }), 36 | ], 37 | }); 38 | -------------------------------------------------------------------------------- /examples/ssr-vue-express/vue-sfc.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | const Component: any; 3 | export default Component; 4 | } 5 | -------------------------------------------------------------------------------- /examples/vike/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | .env.production 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # yarn v2 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .yarn/install-state.gz 118 | .pnp.* 119 | 120 | # Local files 121 | *.local 122 | -------------------------------------------------------------------------------- /examples/vike/.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev" 4 | } 5 | -------------------------------------------------------------------------------- /examples/vike/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/example-vike", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vavite serve", 7 | "build": "vavite build", 8 | "start": "cross-env NODE_ENV=production node dist/server/index.mjs" 9 | }, 10 | "dependencies": { 11 | "@types/express": "^5.0.0", 12 | "@types/node": "^18.19.67", 13 | "@types/react": "^18.3.12", 14 | "@types/react-dom": "^18.3.1", 15 | "@vitejs/plugin-react": "^4.3.4", 16 | "cross-env": "^7.0.3", 17 | "express": "^4.21.1", 18 | "react": "^18.3.1", 19 | "react-dom": "^18.3.1", 20 | "ts-node": "^10.9.2", 21 | "typescript": "^5.7.2", 22 | "vavite": "5.1.0", 23 | "vike": "^0.4.205", 24 | "vite": "^5.4.11" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/vike/pages/_error/+Page.tsx: -------------------------------------------------------------------------------- 1 | export { Page }; 2 | 3 | import { usePageContext } from "../../renderer/usePageContext"; 4 | 5 | function Page() { 6 | const pageContext = usePageContext(); 7 | let { abortReason } = pageContext; 8 | if (!abortReason) { 9 | abortReason = pageContext.is404 10 | ? "Page not found." 11 | : "Something went wrong."; 12 | } 13 | return ( 14 |
    15 |

    {abortReason}

    16 |
    17 | ); 18 | } 19 | 20 | function Center({ children }: { children: React.ReactNode }) { 21 | return ( 22 |
    30 | {children} 31 |
    32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /examples/vike/pages/about/+Page.tsx: -------------------------------------------------------------------------------- 1 | import "./code.css"; 2 | 3 | export { Page }; 4 | 5 | function Page() { 6 | return ( 7 | <> 8 |

    About

    9 |

    Example of using Vike.

    10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/vike/pages/about/code.css: -------------------------------------------------------------------------------- 1 | code { 2 | font-family: monospace; 3 | background-color: #eaeaea; 4 | padding: 3px 5px; 5 | border-radius: 4px; 6 | } 7 | -------------------------------------------------------------------------------- /examples/vike/pages/index/+Page.tsx: -------------------------------------------------------------------------------- 1 | export { Page }; 2 | 3 | import { Counter } from "./Counter"; 4 | 5 | function Page() { 6 | return ( 7 | <> 8 |

    Hello from Vike on Express!

    9 | This page is: 10 |
      11 |
    • Rendered to HTML.
    • 12 |
    • 13 | Interactive. 14 |
    • 15 |
    16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/vike/pages/index/Counter.tsx: -------------------------------------------------------------------------------- 1 | export { Counter }; 2 | 3 | import { useState } from "react"; 4 | 5 | function Counter() { 6 | const [count, setCount] = useState(0); 7 | return ( 8 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/vike/pages/star-wars/@id/+Page.tsx: -------------------------------------------------------------------------------- 1 | export { Page }; 2 | 3 | import { useData } from "../../../renderer/useData"; 4 | import type { Data } from "./+data"; 5 | 6 | function Page() { 7 | const { movie } = useData(); 8 | return ( 9 | <> 10 |

    {movie.title}

    11 | Release Date: {movie.release_date} 12 |
    13 | Director: {movie.director} 14 |
    15 | Producer: {movie.producer} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/vike/pages/star-wars/@id/+data.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/data 2 | export { data }; 3 | export type Data = Awaited>; 4 | 5 | import type { MovieDetails } from "../types"; 6 | import type { PageContextServer } from "vike/types"; 7 | 8 | const data = async (pageContext: PageContextServer) => { 9 | await sleep(300); // Simulate slow network 10 | 11 | const response = await fetch( 12 | `https://brillout.github.io/star-wars/api/films/${pageContext.routeParams!.id}.json`, 13 | ); 14 | let movie = (await response.json()) as MovieDetails; 15 | 16 | // We remove data we don't need because the data is passed to the client; we should 17 | // minimize what is sent over the network. 18 | movie = minimize(movie); 19 | 20 | return { 21 | movie, 22 | // The page's 23 | title: movie.title, 24 | }; 25 | }; 26 | 27 | function minimize(movie: MovieDetails & Record<string, unknown>): MovieDetails { 28 | const { id, title, release_date, director, producer } = movie; 29 | movie = { id, title, release_date, director, producer }; 30 | return movie; 31 | } 32 | 33 | function sleep(milliseconds: number) { 34 | return new Promise((r) => setTimeout(r, milliseconds)); 35 | } 36 | -------------------------------------------------------------------------------- /examples/vike/pages/star-wars/index/+Page.tsx: -------------------------------------------------------------------------------- 1 | export default Page; 2 | 3 | import { useData } from "../../../renderer/useData"; 4 | import type { Data } from "./+data"; 5 | 6 | function Page() { 7 | const { movies } = useData<Data>(); 8 | return ( 9 | <> 10 | <h1>Star Wars Movies</h1> 11 | <ol> 12 | {movies.map(({ id, title, release_date }) => ( 13 | <li key={id}> 14 | <a href={`/star-wars/${id}`}>{title}</a> ({release_date}) 15 | </li> 16 | ))} 17 | </ol> 18 | <p> 19 | Source:{" "} 20 | <a href="https://brillout.github.io/star-wars/"> 21 | brillout.github.io/star-wars 22 | </a> 23 | . 24 | </p> 25 | <p> 26 | Data can be fetched by using the <code>data()</code> hook. 27 | </p> 28 | </> 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /examples/vike/pages/star-wars/index/+data.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/data 2 | export { data }; 3 | export type Data = Awaited<ReturnType<typeof data>>; 4 | 5 | import type { MovieDetails, Movie } from "../types"; 6 | import type { PageContextServer } from "vike/types"; 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 9 | const data = async (pageContext: PageContextServer) => { 10 | await sleep(700); // Simulate slow network 11 | 12 | const response = await fetch( 13 | "https://brillout.github.io/star-wars/api/films.json", 14 | ); 15 | const moviesData = (await response.json()) as MovieDetails[]; 16 | 17 | // We remove data we don't need because the data is passed to the client; we should 18 | // minimize what is sent over the network. 19 | const movies = minimize(moviesData); 20 | 21 | return { 22 | movies, 23 | // The page's <title> 24 | title: `${movies.length} Star Wars Movies`, 25 | }; 26 | }; 27 | 28 | function minimize(movies: MovieDetails[]): Movie[] { 29 | return movies.map((movie) => { 30 | const { title, release_date, id } = movie; 31 | return { title, release_date, id }; 32 | }); 33 | } 34 | 35 | function sleep(milliseconds: number) { 36 | return new Promise((r) => setTimeout(r, milliseconds)); 37 | } 38 | -------------------------------------------------------------------------------- /examples/vike/pages/star-wars/types.ts: -------------------------------------------------------------------------------- 1 | export type Movie = { 2 | id: string; 3 | title: string; 4 | release_date: string; 5 | }; 6 | export type MovieDetails = Movie & { 7 | director: string; 8 | producer: string; 9 | }; 10 | -------------------------------------------------------------------------------- /examples/vike/readme.md: -------------------------------------------------------------------------------- 1 | # Vavite Vike example with Express 2 | 3 | Simple example of using [Vike](https://vike.dev/) with Vavite and Express that shows how to: 4 | 5 | - Integrate Express with Vite's dev server 6 | - Run multiple build steps (for client and server) 7 | - Perform React SSR with Vike 8 | 9 | > [Try on StackBlitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/vike) 10 | 11 | Clone with: 12 | 13 | ```bash 14 | npx degit cyco130/vavite/examples/vike 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/vike/renderer/+config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "vike/types"; 2 | 3 | // https://vike.dev/config 4 | export default { 5 | // https://vike.dev/clientRouting 6 | clientRouting: true, 7 | // https://vike.dev/meta 8 | meta: { 9 | // Define new setting 'title' 10 | title: { 11 | env: { server: true, client: true }, 12 | }, 13 | // Define new setting 'description' 14 | description: { 15 | env: { server: true }, 16 | }, 17 | }, 18 | hydrationCanBeAborted: true, 19 | } satisfies Config; 20 | -------------------------------------------------------------------------------- /examples/vike/renderer/+onPageTransitionEnd.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/onPageTransitionEnd 2 | export { onPageTransitionEnd }; 3 | 4 | import type { OnPageTransitionEndAsync } from "vike/types"; 5 | 6 | const onPageTransitionEnd: OnPageTransitionEndAsync = 7 | async (): ReturnType<OnPageTransitionEndAsync> => { 8 | console.log("Page transition end"); 9 | document.querySelector("body")!.classList.remove("page-is-transitioning"); 10 | }; 11 | -------------------------------------------------------------------------------- /examples/vike/renderer/+onPageTransitionStart.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/onPageTransitionStart 2 | export { onPageTransitionStart }; 3 | 4 | import type { OnPageTransitionStartAsync } from "vike/types"; 5 | 6 | const onPageTransitionStart: OnPageTransitionStartAsync = 7 | async (): ReturnType<OnPageTransitionStartAsync> => { 8 | console.log("Page transition start"); 9 | document.querySelector("body")!.classList.add("page-is-transitioning"); 10 | }; 11 | -------------------------------------------------------------------------------- /examples/vike/renderer/+onRenderClient.tsx: -------------------------------------------------------------------------------- 1 | // https://vike.dev/onRenderClient 2 | export { onRenderClient }; 3 | 4 | import ReactDOM from "react-dom/client"; 5 | import { Layout } from "./Layout"; 6 | import { getPageTitle } from "./getPageTitle"; 7 | import type { OnRenderClientAsync } from "vike/types"; 8 | 9 | let root: ReactDOM.Root; 10 | const onRenderClient: OnRenderClientAsync = async ( 11 | pageContext, 12 | ): ReturnType<OnRenderClientAsync> => { 13 | const { Page } = pageContext; 14 | 15 | // This onRenderClient() hook only supports SSR, see https://vike.dev/render-modes for how to modify onRenderClient() 16 | // to support SPA 17 | if (!Page) 18 | throw new Error( 19 | "My onRenderClient() hook expects pageContext.Page to be defined", 20 | ); 21 | 22 | const container = document.getElementById("react-root"); 23 | if (!container) throw new Error("DOM element #react-root not found"); 24 | 25 | const page = ( 26 | <Layout pageContext={pageContext}> 27 | <Page /> 28 | </Layout> 29 | ); 30 | if (pageContext.isHydration) { 31 | root = ReactDOM.hydrateRoot(container, page); 32 | } else { 33 | if (!root) { 34 | root = ReactDOM.createRoot(container); 35 | } 36 | root.render(page); 37 | } 38 | document.title = getPageTitle(pageContext); 39 | }; 40 | -------------------------------------------------------------------------------- /examples/vike/renderer/+onRenderHtml.tsx: -------------------------------------------------------------------------------- 1 | // https://vike.dev/onRenderHtml 2 | export { onRenderHtml }; 3 | 4 | import ReactDOMServer from "react-dom/server"; 5 | import { Layout } from "./Layout"; 6 | import { escapeInject, dangerouslySkipEscape } from "vike/server"; 7 | import logoUrl from "./logo.svg"; 8 | import type { OnRenderHtmlAsync } from "vike/types"; 9 | import { getPageTitle } from "./getPageTitle"; 10 | 11 | const onRenderHtml: OnRenderHtmlAsync = async ( 12 | pageContext, 13 | ): ReturnType<OnRenderHtmlAsync> => { 14 | const { Page } = pageContext; 15 | 16 | // This onRenderHtml() hook only supports SSR, see https://vike.dev/render-modes for how to modify 17 | // onRenderHtml() to support SPA 18 | if (!Page) 19 | throw new Error( 20 | "My onRenderHtml() hook expects pageContext.Page to be defined", 21 | ); 22 | 23 | // Alternativly, we can use an HTML stream, see https://vike.dev/streaming 24 | const pageHtml = ReactDOMServer.renderToString( 25 | <Layout pageContext={pageContext}> 26 | <Page /> 27 | </Layout>, 28 | ); 29 | 30 | const title = getPageTitle(pageContext); 31 | const desc = 32 | pageContext.data?.description || 33 | pageContext.config.description || 34 | "Demo of using Vike"; 35 | 36 | const documentHtml = escapeInject`<!DOCTYPE html> 37 | <html lang="en"> 38 | <head> 39 | <meta charset="UTF-8" /> 40 | <link rel="icon" href="${logoUrl}" /> 41 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 42 | <meta name="description" content="${desc}" /> 43 | <title>${title} 44 | 45 | 46 |
    ${dangerouslySkipEscape(pageHtml)}
    47 | 48 | `; 49 | 50 | return { 51 | documentHtml, 52 | pageContext: { 53 | // We can add custom pageContext properties here, see https://vike.dev/pageContext#custom 54 | }, 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /examples/vike/renderer/Layout.css: -------------------------------------------------------------------------------- 1 | #sidebar a { 2 | padding: 2px 10px; 3 | margin-left: -10px; 4 | } 5 | #sidebar a.is-active { 6 | background-color: #eee; 7 | } 8 | -------------------------------------------------------------------------------- /examples/vike/renderer/Layout.tsx: -------------------------------------------------------------------------------- 1 | export { Layout }; 2 | 3 | import React from "react"; 4 | import logoUrl from "./logo.svg"; 5 | import { PageContextProvider } from "./usePageContext"; 6 | import { Link } from "./Link"; 7 | import type { PageContext } from "vike/types"; 8 | import "./css/index.css"; 9 | import "./Layout.css"; 10 | 11 | function Layout({ 12 | children, 13 | pageContext, 14 | }: { 15 | children: React.ReactNode; 16 | pageContext: PageContext; 17 | }) { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | Welcome 25 | About 26 | Data Fetching 27 | 28 | {children} 29 | 30 | 31 | 32 | ); 33 | } 34 | 35 | function Frame({ children }: { children: React.ReactNode }) { 36 | return ( 37 |
    44 | {children} 45 |
    46 | ); 47 | } 48 | 49 | function Sidebar({ children }: { children: React.ReactNode }) { 50 | return ( 51 | 64 | ); 65 | } 66 | 67 | function Content({ children }: { children: React.ReactNode }) { 68 | return ( 69 |
    70 |
    78 | {children} 79 |
    80 |
    81 | ); 82 | } 83 | 84 | function Logo() { 85 | return ( 86 |
    92 | 93 | logo 94 | 95 |
    96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /examples/vike/renderer/Link.tsx: -------------------------------------------------------------------------------- 1 | import { usePageContext } from "./usePageContext"; 2 | 3 | export { Link }; 4 | 5 | function Link(props: { 6 | href: string; 7 | className?: string; 8 | children: React.ReactNode; 9 | }) { 10 | const pageContext = usePageContext(); 11 | const { urlPathname } = pageContext; 12 | const { href } = props; 13 | const isActive = 14 | href === "/" ? urlPathname === href : urlPathname.startsWith(href); 15 | const className = [props.className, isActive && "is-active"] 16 | .filter(Boolean) 17 | .join(" "); 18 | return ; 19 | } 20 | -------------------------------------------------------------------------------- /examples/vike/renderer/css/code.css: -------------------------------------------------------------------------------- 1 | code { 2 | font-family: monospace; 3 | background-color: #eaeaea; 4 | padding: 3px 5px; 5 | border-radius: 4px; 6 | } 7 | -------------------------------------------------------------------------------- /examples/vike/renderer/css/index.css: -------------------------------------------------------------------------------- 1 | @import "./reset.css"; 2 | @import "./links.css"; 3 | @import "./code.css"; 4 | @import "./page-transition-loading-animation.css"; 5 | -------------------------------------------------------------------------------- /examples/vike/renderer/css/links.css: -------------------------------------------------------------------------------- 1 | a { 2 | text-decoration: none; 3 | } 4 | -------------------------------------------------------------------------------- /examples/vike/renderer/css/page-transition-loading-animation.css: -------------------------------------------------------------------------------- 1 | #page-container { 2 | position: relative; 3 | width: 100%; 4 | } 5 | #page-container::before { 6 | content: ""; 7 | position: absolute; 8 | width: 100%; 9 | height: 100%; 10 | z-index: 999; 11 | background: no-repeat url("./page-transition-loading-animation/loading.svg"); 12 | background-size: 100px; 13 | background-position: center center; 14 | pointer-events: none; 15 | opacity: 0; 16 | } 17 | body.page-is-transitioning #page-container::before { 18 | opacity: 1; 19 | } 20 | #page-content, 21 | #page-container::before { 22 | transition: opacity 0.5s ease-in-out; 23 | } 24 | body.page-is-transitioning #page-content { 25 | opacity: 0.17; 26 | } 27 | -------------------------------------------------------------------------------- /examples/vike/renderer/css/page-transition-loading-animation/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/vike/renderer/css/reset.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: sans-serif; 4 | } 5 | * { 6 | box-sizing: border-box; 7 | } 8 | -------------------------------------------------------------------------------- /examples/vike/renderer/getPageTitle.ts: -------------------------------------------------------------------------------- 1 | export { getPageTitle }; 2 | 3 | import type { PageContext } from "vike/types"; 4 | 5 | function getPageTitle(pageContext: PageContext): string { 6 | const title = 7 | // Title defined dynamically by data() 8 | pageContext.data?.title || 9 | // Title defined statically by /pages/some-page/+title.js (or by `export default { title }` in /pages/some-page/+config.js) 10 | // The setting 'pageContext.config.title' is a custom setting we defined at ./+config.ts 11 | pageContext.config.title || 12 | "Vike Demo"; 13 | return title; 14 | } 15 | -------------------------------------------------------------------------------- /examples/vike/renderer/types.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/pageContext#typescript 2 | declare global { 3 | namespace Vike { 4 | interface PageContext { 5 | Page: () => React.ReactElement; 6 | data?: { 7 | /** Value for defined dynmically by by /pages/some-page/+data.js */ 8 | title?: string; 9 | /** Value for <meta name="description"> defined dynmically */ 10 | description?: string; 11 | }; 12 | config: { 13 | /** Value for <title> defined statically by /pages/some-page/+title.js (or by `export default { title }` in /pages/some-page/+config.js) */ 14 | title?: string; 15 | /** Value for <meta name="description"> defined statically */ 16 | description?: string; 17 | }; 18 | /** https://vike.dev/render */ 19 | abortReason?: string; 20 | } 21 | } 22 | } 23 | 24 | // Tell TypeScript this file isn't an ambient module 25 | export {}; 26 | -------------------------------------------------------------------------------- /examples/vike/renderer/useData.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/useData 2 | export { useData }; 3 | 4 | import { usePageContext } from "./usePageContext"; 5 | 6 | /** https://vike.dev/useData */ 7 | function useData<Data>() { 8 | const { data } = usePageContext(); 9 | return data as Data; 10 | } 11 | -------------------------------------------------------------------------------- /examples/vike/renderer/usePageContext.tsx: -------------------------------------------------------------------------------- 1 | // https://vike.dev/usePageContext 2 | // eslint-disable-next-line react-refresh/only-export-components 3 | export { usePageContext }; 4 | export { PageContextProvider }; 5 | 6 | import React, { useContext } from "react"; 7 | import type { PageContext } from "vike/types"; 8 | 9 | const Context = React.createContext<PageContext>( 10 | undefined as unknown as PageContext, 11 | ); 12 | 13 | function PageContextProvider({ 14 | pageContext, 15 | children, 16 | }: { 17 | pageContext: PageContext; 18 | children: React.ReactNode; 19 | }) { 20 | return <Context.Provider value={pageContext}>{children}</Context.Provider>; 21 | } 22 | 23 | /** https://vike.dev/usePageContext */ 24 | function usePageContext() { 25 | const pageContext = useContext(Context); 26 | return pageContext; 27 | } 28 | -------------------------------------------------------------------------------- /examples/vike/server/index.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="vite/client" /> 2 | 3 | import express from "express"; 4 | import { renderPage } from "vike/server"; 5 | import viteDevServer from "vavite/vite-dev-server"; 6 | 7 | const app = express(); 8 | 9 | if (!viteDevServer) { 10 | // Serve static files in production 11 | app.use(express.static("dist/client")); 12 | } 13 | 14 | // Vike middleware. It should always be our last middleware (because it's a 15 | // catch-all middleware superseding any middleware placed after it). 16 | app.get("*", async (req, res, next) => { 17 | const pageContextInit = { 18 | urlOriginal: req.originalUrl, 19 | headersOriginal: req.headers, 20 | }; 21 | const pageContext = await renderPage(pageContextInit); 22 | if (pageContext.errorWhileRendering) { 23 | // Install error tracking here, see https://vike.dev/errors 24 | } 25 | const { httpResponse } = pageContext; 26 | if (!httpResponse) { 27 | return next(); 28 | } else { 29 | const { body, statusCode, headers, earlyHints } = httpResponse; 30 | if (res.writeEarlyHints) 31 | res.writeEarlyHints({ link: earlyHints.map((e) => e.earlyHintLink) }); 32 | headers.forEach(([name, value]) => res.setHeader(name, value)); 33 | res.status(statusCode); 34 | // For HTTP streams use httpResponse.pipe() instead, see https://vike.dev/streaming 35 | res.send(body); 36 | } 37 | }); 38 | 39 | export default app; 40 | -------------------------------------------------------------------------------- /examples/vike/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "ES2020", 5 | "moduleResolution": "Bundler", 6 | "customConditions": ["import"], 7 | "target": "ES2017", 8 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 9 | "jsx": "react-jsx", 10 | "esModuleInterop": true, 11 | "skipLibCheck": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/vike/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import ssr from "vike/plugin"; 4 | import { vavite } from "vavite"; 5 | 6 | export default defineConfig({ 7 | buildSteps: [ 8 | { 9 | name: "client", 10 | }, 11 | { 12 | name: "server", 13 | config: { 14 | build: { ssr: true }, 15 | }, 16 | }, 17 | ], 18 | 19 | plugins: [ 20 | vavite({ 21 | handlerEntry: "/server/index.ts", 22 | serveClientAssetsInDev: true, 23 | }), 24 | react(), 25 | ssr({ disableAutoFullBuild: true }), 26 | ], 27 | }); 28 | -------------------------------------------------------------------------------- /examples/ws/.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev" 4 | } 5 | -------------------------------------------------------------------------------- /examples/ws/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8" /> 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 | <title>Websocket example 7 | 8 | 9 |

    Hello

    10 | 11 |
    Test route 12 | 13 |

    14 | 15 |

    16 | 17 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/ws/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/example-reloader-ws", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "start": "node dist", 7 | "dev": "vite", 8 | "build": "vite build --ssr --mode=production" 9 | }, 10 | "files": [ 11 | "dist" 12 | ], 13 | "devDependencies": { 14 | "@types/express": "^5.0.0", 15 | "@types/express-ws": "^3.0.5", 16 | "@types/node": "^18.19.67", 17 | "@types/ws": "^8.5.13", 18 | "typescript": "^5.7.2", 19 | "vavite": "5.1.0", 20 | "vite": "^5.4.11" 21 | }, 22 | "dependencies": { 23 | "express": "^4.21.1", 24 | "express-ws": "^5.0.2", 25 | "ws": "^8.18.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/ws/readme.md: -------------------------------------------------------------------------------- 1 | # Vavite WebSocket example 2 | 3 | WebSocket example. 4 | 5 | > [Try on StackBlitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/ws) 6 | 7 | Clone with: 8 | 9 | ```bash 10 | npx degit cyco130/vavite/examples/ws 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/ws/server.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import express from "express"; 4 | import { WebSocketServer } from "ws"; 5 | import type { Server, IncomingMessage } from "node:http"; 6 | import type { Duplex } from "node:stream"; 7 | import html from "./index.html?raw"; 8 | 9 | const app = express(); 10 | 11 | // Since we don't have access to the server instance, we will lazily add the 12 | // upgrade listener on the first request in the following middleware. 13 | // This is fine for dev since our server code won't run until the first request. 14 | // If it is unacceptable for prod, you can provide a custom server entry and 15 | // add the upgrade listener there (and guard this code with an environment check). 16 | let isFirstRequest = true; 17 | app.use((req, _res, next) => { 18 | if (!isFirstRequest) { 19 | next(); 20 | return; 21 | } 22 | isFirstRequest = false; 23 | 24 | // Types are missing but the server instance can be accessed via the socket. 25 | const server: Server = (req.socket as any).server; 26 | 27 | // Vite can reload the server file on changes, so we need to remove the 28 | // previous upgrade listener before adding a new one to avoid multiple 29 | // listeners. We save the previous listener on the global object so that 30 | // it survives reloads. 31 | if ((global as any).__previousUpgradeListener) { 32 | app.off("upgrade", (global as any).__previousUpgradeListener); 33 | } 34 | 35 | // Reuse the WebSocket server instance after reload if it already exists. 36 | const wss: WebSocketServer = 37 | (global as any).__previousWss || new WebSocketServer({ noServer: true }); 38 | (global as any).__previousWss = wss; 39 | 40 | function upgradeListener(req: IncomingMessage, socket: Duplex, head: Buffer) { 41 | // Vite uses the / path for HMR so we need to use some other path for our 42 | // WebSocket server. 43 | if (req.url !== "/ws") return; 44 | 45 | wss.handleUpgrade(req, socket, head, (ws) => { 46 | ws.emit("connection", ws, req); 47 | 48 | ws.on("message", (message) => { 49 | console.log(`Received message: ${message}`); 50 | // Echo the message back to the client. 51 | ws.send(`Hello, you sent: ${message}`); 52 | }); 53 | }); 54 | } 55 | 56 | // Add the upgrade listener to the server. 57 | server.on("upgrade", upgradeListener); 58 | 59 | // Save the upgrade listener so we can remove it later. 60 | (global as any).__previousUpgradeListener = upgradeListener; 61 | (global as any).__previousWss = wss; 62 | 63 | next(); 64 | }); 65 | 66 | app.get("/", (_req, res) => { 67 | res.send(html); 68 | }); 69 | 70 | export default app; 71 | -------------------------------------------------------------------------------- /examples/ws/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "Bundler", 10 | "customConditions": ["import"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/ws/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { vavite } from "vavite"; 3 | 4 | export default defineConfig({ 5 | appType: "custom", 6 | plugins: [ 7 | vavite({ 8 | handlerEntry: "/server.ts", 9 | serveClientAssetsInDev: true, 10 | }), 11 | ], 12 | }); 13 | -------------------------------------------------------------------------------- /lint-staged.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | "*": "prettier --ignore-unknown --write", 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vavite-workspace-root", 3 | "private": "true", 4 | "scripts": { 5 | "dev": "pnpm -r --parallel --filter \"./packages/*\" run dev", 6 | "build": "pnpm -r --filter \"./packages/*\" run build", 7 | "prepare": "husky", 8 | "precommit": "lint-staged", 9 | "test": "pnpm run \"/^(cq|ci)$/\"", 10 | "cq": "pnpm run /^test:/", 11 | "ci": "pnpm -r --stream --workspace-concurrency=1 run ci", 12 | "test:packages": "pnpm -r --stream run test", 13 | "test:prettier": "prettier --check --ignore-path .gitignore --ignore-unknown . '!pnpm-lock.yaml'", 14 | "format": "prettier --ignore-path .gitignore --ignore-unknown . '!pnpm-lock.yaml' --write" 15 | }, 16 | "devDependencies": { 17 | "husky": "^9.1.7", 18 | "lint-staged": "^15.2.10", 19 | "prettier": "^3.4.1", 20 | "publint": "^0.2.12", 21 | "typescript": "^5.7.2" 22 | }, 23 | "pnpm": { 24 | "peerDependencyRules": { 25 | "ignoreMissing": [ 26 | "rollup" 27 | ] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/connect/eslint.config.js: -------------------------------------------------------------------------------- 1 | import config from "@cyco130/eslint-config/node"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | 5 | const tsconfigRootDir = 6 | // @ts-expect-error: import.meta.dirname requires v20.11.0 or v21.2.0 7 | import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url)); 8 | 9 | /** @type {typeof config} */ 10 | export default [ 11 | ...config, 12 | { 13 | ignores: ["dist/", "node_modules/"], 14 | }, 15 | { 16 | languageOptions: { 17 | parserOptions: { 18 | projectService: true, 19 | tsconfigRootDir, 20 | }, 21 | }, 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /packages/connect/lint-staged.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | "**/*.ts?(x)": [ 3 | () => "tsc -p tsconfig.json --noEmit", 4 | "eslint --max-warnings 0 --ignore-pattern dist", 5 | ], 6 | "*": "prettier --ignore-unknown --write", 7 | }; 8 | -------------------------------------------------------------------------------- /packages/connect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/connect", 3 | "version": "5.1.0", 4 | "type": "module", 5 | "exports": { 6 | ".": { 7 | "import": "./dist/index.js" 8 | }, 9 | "./entry-standalone": { 10 | "import": "./dist/entry-standalone.js" 11 | }, 12 | "./entry-standalone-bundled-sirv": { 13 | "import": "./dist/entry-standalone-bundled-sirv.js" 14 | }, 15 | "./entry-standalone-imported-sirv": { 16 | "import": "./dist/entry-standalone-imported-sirv.js" 17 | } 18 | }, 19 | "typesVersions": { 20 | "*": { 21 | "*": [ 22 | "dist/*.d.ts" 23 | ] 24 | } 25 | }, 26 | "files": [ 27 | "dist", 28 | "*.d.ts" 29 | ], 30 | "description": "Vite plugin for developing Node.js server applications", 31 | "author": "Fatih Aygün ", 32 | "repository": "github:cyco130/vavite", 33 | "license": "MIT", 34 | "scripts": { 35 | "build": "tsup", 36 | "dev": "tsup --watch", 37 | "prepack": "rm -rf dist && pnpm build", 38 | "test": "pnpm run test:typecheck && pnpm run test:lint && pnpm run test:package", 39 | "test:typecheck": "tsc -p tsconfig.json --noEmit", 40 | "test:lint": "eslint . --max-warnings 0 --ignore-pattern dist", 41 | "test:package": "publint --strict" 42 | }, 43 | "peerDependencies": { 44 | "vite": "^2.8.1 || 3 || 4 || 5 || 6" 45 | }, 46 | "devDependencies": { 47 | "@cyco130/eslint-config": "^5.0.1", 48 | "eslint": "^9.16.0", 49 | "sirv": "^3.0.0", 50 | "tsup": "^8.3.5", 51 | "typescript": "^5.7.2", 52 | "vite": "^5.4.11" 53 | }, 54 | "dependencies": { 55 | "@types/node": "^18.19.67" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/connect/readme.md: -------------------------------------------------------------------------------- 1 | # @vavite/connect 2 | 3 | `@vavite/connect` is a [Vite](https://vitejs.dev) plugin for developing and building server-side Node.js applications in the form of a [Connect](https://github.com/senchalabs/connect)-compatible middleware function `(req, res, next)`. For development, the middleware is plugged into Vite's development server. For production, `@vavite/connect` can build a middleware, or a standalone server. You can also provide a custom server entry to be processed with Vite so that you can use Vite-specific features such as `import.meta.env`. 4 | 5 | `@vavite/connect` does _not_ support custom servers during development, check out the [`@vavite/reloader`](../reloader) package if that's your requirement. 6 | 7 | ## Installation and usage 8 | 9 | Install `vite` and `@vavite/connect` as development dependencies (`npm install --save-dev vite @vavite/connect`) and add `@vavite/connect` to your Vite config: 10 | 11 | ```ts 12 | import { defineConfig } from "vite"; 13 | import { vaviteConnect } from "@vavite/connect"; 14 | 15 | export default defineConfig({ 16 | plugins: [ 17 | vaviteConnect({ 18 | // Options, see below 19 | }), 20 | ], 21 | }); 22 | ``` 23 | 24 | Then create an `handler.ts` (or `.js`) file in the root of your project that default exports a function that takes `(req, res, next)` and handles the request. For example: 25 | 26 | ```ts 27 | import type { IncomingMessage, ServerResponse } from "node:http"; 28 | import type { SirvOptions } from "@vavite/connect"; 29 | 30 | export default function handler( 31 | req: IncomingMessage, 32 | res: ServerResponse, 33 | next: () => void, 34 | ) { 35 | if (req.url === "/") { 36 | res.setHeader("Content-Type", "text/html; charset=utf-8"); 37 | // This is plain http.Server, not Express, so res.send() is 38 | // not available, use res.write() and res.end() 39 | res.end("

    Hello, world!

    "); 40 | } else { 41 | next(); 42 | } 43 | } 44 | 45 | export const sirvOptions: SirvOptions = { 46 | // sirv options, optional, see below. 47 | }; 48 | ``` 49 | 50 | Now you can start the development server with `npx vite` and visit `http://localhost:3000/` to see the result. 51 | 52 | You can build a standalone application with the entry point in `dist/server.js` with `npx vite build --ssr`. The application will listen to the host and port specified in the `HOST` and `PORT` environment variables, or `0.0.0.0:3000` by default. 53 | 54 | ## Options 55 | 56 | ### `handlerEntry: string = "/handler"` 57 | 58 | Handler entry module. The default value `"/handler"` will resolve to `handler.js`, `handler.ts` etc. in your project root. 59 | 60 | ### `serveClientAssetsInDev: boolean = false` 61 | 62 | Whether to serve client assets in development mode. Enable when developing full-stack or server-side rendering applications. 63 | 64 | > TODO: Currently HTML files are _not_ served regardless of this setting. An option to enable it is being considered. 65 | 66 | ### `standalone: boolean = true` 67 | 68 | Whether to build a standalone application instead of a Connect-compatible middleware function to be used with a custom server. 69 | 70 | ### `clientAssetsDir: string | null = null` 71 | 72 | Directory that contains client assets to be served using the [`sirv`](https://github.com/lukeed/sirv) package. By default it's set to `null` to exclude `sirv` from the build. 73 | 74 | If you do provide a client assets directory, you can export `sirvOptions` from your handler entry to customize the options. The types for the options are exported from `@vavite/connect` as `SirvOptions` so you don't have to install `sirv` yourself. 75 | 76 | ### `bundleSirv: boolean = true` 77 | 78 | Whether `sirv` should be bundled with the application or simply be imported. You must install `sirv` as a production dependency if you set this to `false`. 79 | 80 | ## Custom server entry 81 | 82 | > Reminder: This section applies to production build only. `@vavite/connect` does _not_ support custom servers during development, check out the [`@vavite/reloader`](../reloader) package for that use case. 83 | 84 | You can provide a custom server for production with or without processing it with Vite. In this case, you will import the handler in the appropriate way (see below) and add it to your application as a middleware function. For example, in Express it would be something like `app.use(handler)`. 85 | 86 | For a custom server entry processed with Vite, build your application with `npx vite build --ssr server.js` where `server.js` is your custom server entry. The `standalone` option has no effect when using a custom server entry like this and `sirv` middleware will not be injected, you will have to handle serving client assets yourself. 87 | 88 | If you don't want to process your custom server with Vite -maybe because you have an existing application with separate tooling- you should set `standalone` to `false` and import the handler from `./dist/handler.js` (or whatever your build output path is). 89 | -------------------------------------------------------------------------------- /packages/connect/src/entry-standalone-with-sirv.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from "node:http"; 2 | import sirv, { RequestHandler, Options } from "sirv"; 3 | 4 | let handleExports: { 5 | default: RequestHandler; 6 | sirvOptions?: Options; 7 | }; 8 | 9 | let sirvHandler: RequestHandler; 10 | 11 | async function init() { 12 | // @ts-expect-error: This is a virtual module 13 | handleExports = await import("/virtual:vavite-connect-handler"); 14 | 15 | sirvHandler = sirv( 16 | // @ts-expect-error: This will be defined by the plugin 17 | __VAVITE_CLIENT_BUILD_OUTPUT_DIR, 18 | handleExports.sirvOptions, 19 | ); 20 | 21 | const PORT = Number(process.env.PORT) || 3000; 22 | const HOST = process.env.HOST || "localhost"; 23 | 24 | createServer((req, res) => 25 | sirvHandler(req, res, () => { 26 | handleExports.default(req, res, () => { 27 | if (!res.writableEnded) { 28 | res.statusCode = 404; 29 | res.end(); 30 | } 31 | }); 32 | }), 33 | ).listen(PORT, HOST, () => { 34 | // eslint-disable-next-line no-console 35 | console.log(`Server listening on http://${HOST}:${PORT}`); 36 | }); 37 | } 38 | 39 | init().catch((error) => { 40 | console.error(error); 41 | process.exit(1); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/connect/src/entry-standalone.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from "node:http"; 2 | // @ts-expect-error: This is a virtual module 3 | import handler from "/virtual:vavite-connect-handler"; 4 | 5 | const PORT = Number(process.env.PORT) || 3000; 6 | const HOST = process.env.HOST || "localhost"; 7 | 8 | createServer((req, res) => 9 | handler(req, res, () => { 10 | if (!res.writableEnded) { 11 | res.statusCode = 404; 12 | res.end(); 13 | } 14 | }), 15 | ).listen(PORT, HOST, () => { 16 | // eslint-disable-next-line no-console 17 | console.log(`Server listening on http://${HOST}:${PORT}`); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/connect/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "es2020", 5 | "module": "ESNext", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "moduleResolution": "Bundler", 11 | "customConditions": ["import"], 12 | "checkJs": true 13 | }, 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/connect/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig([ 4 | // Plugin 5 | { 6 | entry: ["./src/index.ts"], 7 | format: ["esm"], 8 | platform: "node", 9 | target: "node18", 10 | dts: true, 11 | }, 12 | // Standalone entry 13 | { 14 | entry: { 15 | "entry-standalone": "./src/entry-standalone.ts", 16 | "entry-standalone-imported-sirv": "./src/entry-standalone-with-sirv.ts", 17 | }, 18 | format: ["esm"], 19 | platform: "node", 20 | target: "esnext", 21 | shims: false, 22 | external: ["sirv", "/virtual:vavite-connect-handler"], 23 | }, 24 | { 25 | entry: { 26 | "entry-standalone-bundled-sirv": "./src/entry-standalone-with-sirv.ts", 27 | }, 28 | format: ["esm"], 29 | platform: "node", 30 | target: "esnext", 31 | shims: false, 32 | external: ["/virtual:vavite-connect-handler"], 33 | noExternal: ["sirv"], 34 | }, 35 | ]); 36 | -------------------------------------------------------------------------------- /packages/expose-vite-dev-server/eslint.config.js: -------------------------------------------------------------------------------- 1 | import config from "@cyco130/eslint-config/node"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | 5 | const tsconfigRootDir = 6 | // @ts-expect-error: import.meta.dirname requires v20.11.0 or v21.2.0 7 | import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url)); 8 | 9 | /** @type {typeof config} */ 10 | export default [ 11 | ...config, 12 | { 13 | ignores: ["dist/", "node_modules/"], 14 | }, 15 | { 16 | languageOptions: { 17 | parserOptions: { 18 | projectService: true, 19 | tsconfigRootDir, 20 | }, 21 | }, 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /packages/expose-vite-dev-server/lint-staged.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | "**/*.ts?(x)": [ 3 | () => "tsc -p tsconfig.json --noEmit", 4 | "eslint --max-warnings 0 --ignore-pattern dist", 5 | ], 6 | "*": "prettier --ignore-unknown --write", 7 | }; 8 | -------------------------------------------------------------------------------- /packages/expose-vite-dev-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/expose-vite-dev-server", 3 | "version": "5.1.0", 4 | "type": "module", 5 | "files": [ 6 | "dist", 7 | "*.d.ts" 8 | ], 9 | "exports": { 10 | ".": { 11 | "import": "./dist/index.js" 12 | }, 13 | "./vite-dev-server": { 14 | "import": "./dist/vite-dev-server.js" 15 | } 16 | }, 17 | "typesVersions": { 18 | "*": { 19 | "*": [ 20 | "dist/*.d.ts" 21 | ] 22 | } 23 | }, 24 | "description": "Vite plugin for exposing Vite's development server to user code", 25 | "author": "Fatih Aygün ", 26 | "repository": "github:cyco130/vavite", 27 | "license": "MIT", 28 | "scripts": { 29 | "build": "tsup", 30 | "dev": "tsup --watch", 31 | "prepack": "rm -rf dist && pnpm build", 32 | "test": "pnpm run test:typecheck && pnpm run test:lint && pnpm run test:package", 33 | "test:typecheck": "tsc -p tsconfig.json --noEmit", 34 | "test:lint": "eslint . --max-warnings 0 --ignore-pattern dist", 35 | "test:package": "publint --strict" 36 | }, 37 | "peerDependencies": { 38 | "vite": "^2.8.1 || 3 || 4 || 5 || 6" 39 | }, 40 | "devDependencies": { 41 | "@cyco130/eslint-config": "^5.0.1", 42 | "@types/node": "^18.19.67", 43 | "eslint": "^9.16.0", 44 | "tsup": "^8.3.5", 45 | "typescript": "^5.7.2", 46 | "vite": "^5.4.11" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/expose-vite-dev-server/readme.md: -------------------------------------------------------------------------------- 1 | # @vavite/expose-vite-dev-server 2 | 3 | `@vavite/expose-vite-dev-server` is a [Vite](https://vitejs.dev) plugin for exposing the Vite development server to user code during development. It can be used for accessing Vite development server methods such as `ssrFixStacktrace` and `transformIndexHtml` in [`vavite`](../vavite), [`@vavite/connect`](../connect), and [`@vavite/reloader`](../reloader) applications. 4 | 5 | ## Installation and usage 6 | 7 | Install `vite` and `@vavite/expose-vite-dev-server` as development dependencies (`npm install --save-dev vite @vavite/expose-vite-dev-server`) and add `@vavite/expose-vite-dev-server` plugin to your Vite config: 8 | 9 | ```ts 10 | import { defineConfig } from "vite"; 11 | import exposeViteDevServer from "@vavite/expose-vite-dev-server"; 12 | 13 | export default defineConfig({ 14 | plugins: [exposeViteDevServer()], 15 | }); 16 | ``` 17 | 18 | Now you can import the dev server in your application with `import viteDevServer from @vavite/expose-vite-dev-server/vite-dev-server` during development. In production, it will be available but its value will be `undefined`. 19 | -------------------------------------------------------------------------------- /packages/expose-vite-dev-server/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin, SSROptions, UserConfig, ViteDevServer } from "vite"; 2 | import crypto from "node:crypto"; 3 | 4 | export default function vaviteDevServerPlugin(): Plugin { 5 | let dev: boolean; 6 | let viteDevServer: ViteDevServer | undefined; 7 | let globalSymbol: string; 8 | 9 | function getModuleContents() { 10 | return `export default ${globalSymbol}`; 11 | } 12 | 13 | return { 14 | name: "@vavite/expose-vite-dev-server", 15 | 16 | enforce: "pre", 17 | 18 | buildStart() { 19 | globalSymbol = 20 | "VAVITE_VITE_DEV_SERVER_" + crypto.randomBytes(20).toString("hex"); 21 | (global as any)[globalSymbol] = viteDevServer; 22 | }, 23 | 24 | closeBundle() { 25 | delete (global as any)[globalSymbol]; 26 | }, 27 | 28 | resolveId(source, _importer, options) { 29 | if ( 30 | (source === "@vavite/expose-vite-dev-server/vite-dev-server" || 31 | source === "vavite/vite-dev-server" || 32 | source === "virtual:vavite-vite-dev-server") && 33 | dev && 34 | options.ssr 35 | ) { 36 | return "virtual:vavite-vite-dev-server"; 37 | } 38 | }, 39 | 40 | load(id, options) { 41 | if (id === "virtual:vavite-vite-dev-server" && dev && options?.ssr) { 42 | return getModuleContents(); 43 | } 44 | }, 45 | 46 | config(_config, env) { 47 | dev = env.command === "serve"; 48 | 49 | const out: UserConfig & { ssr: SSROptions } = { 50 | ssr: { 51 | noExternal: ["vavite"], 52 | optimizeDeps: { 53 | exclude: [ 54 | "@vavite/expose-vite-dev-server", 55 | "virtual:vavite-vite-dev-server", 56 | ], 57 | }, 58 | }, 59 | optimizeDeps: { 60 | exclude: [ 61 | "@vavite/expose-vite-dev-server", 62 | "virtual:vavite-vite-dev-server", 63 | ], 64 | }, 65 | }; 66 | 67 | return out; 68 | }, 69 | 70 | configureServer(server) { 71 | viteDevServer = server; 72 | }, 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /packages/expose-vite-dev-server/src/vite-dev-server.ts: -------------------------------------------------------------------------------- 1 | import type { ViteDevServer } from "vite"; 2 | 3 | const viteDevServer: ViteDevServer | undefined = undefined; 4 | 5 | export default viteDevServer; 6 | -------------------------------------------------------------------------------- /packages/expose-vite-dev-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "es2020", 5 | "module": "ESNext", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "moduleResolution": "Bundler", 11 | "customConditions": ["import"], 12 | "checkJs": true 13 | }, 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/expose-vite-dev-server/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ["./src/index.ts"], 6 | format: ["esm"], 7 | platform: "node", 8 | target: "node18", 9 | dts: true, 10 | }, 11 | { 12 | entry: ["./src/vite-dev-server.ts"], 13 | format: ["esm"], 14 | platform: "node", 15 | target: "node18", 16 | dts: true, 17 | }, 18 | ]); 19 | -------------------------------------------------------------------------------- /packages/multibuild-cli/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "./dist/index.js"; 3 | -------------------------------------------------------------------------------- /packages/multibuild-cli/eslint.config.js: -------------------------------------------------------------------------------- 1 | import config from "@cyco130/eslint-config/node"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | 5 | const tsconfigRootDir = 6 | // @ts-expect-error: import.meta.dirname requires v20.11.0 or v21.2.0 7 | import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url)); 8 | 9 | /** @type {typeof config} */ 10 | export default [ 11 | ...config, 12 | { 13 | ignores: ["dist/", "node_modules/", "cli.js"], 14 | }, 15 | { 16 | languageOptions: { 17 | parserOptions: { 18 | projectService: true, 19 | tsconfigRootDir, 20 | }, 21 | }, 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /packages/multibuild-cli/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | export {}; 4 | -------------------------------------------------------------------------------- /packages/multibuild-cli/lint-staged.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | "**/*.ts?(x)": [ 3 | () => "tsc -p tsconfig.json --noEmit", 4 | "eslint --max-warnings 0 --ignore-pattern dist", 5 | ], 6 | "*": "prettier --ignore-unknown --write", 7 | }; 8 | -------------------------------------------------------------------------------- /packages/multibuild-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/multibuild-cli", 3 | "version": "5.1.0", 4 | "type": "module", 5 | "bin": { 6 | "vavite-multibuild": "./cli.js" 7 | }, 8 | "files": [ 9 | "dist", 10 | "cli.js", 11 | "index.d.ts" 12 | ], 13 | "description": "CLI command for orchestrating multiple Vite builds", 14 | "author": "Fatih Aygün ", 15 | "repository": "github:cyco130/vavite", 16 | "license": "MIT", 17 | "scripts": { 18 | "build": "tsup", 19 | "dev": "tsup --watch", 20 | "prepack": "rm -rf dist && pnpm build", 21 | "test": "pnpm run test:typecheck && pnpm run test:lint && pnpm run test:package", 22 | "test:typecheck": "tsc -p tsconfig.json --noEmit", 23 | "test:lint": "eslint . --max-warnings 0 --ignore-pattern dist", 24 | "test:package": "publint --strict" 25 | }, 26 | "peerDependencies": { 27 | "vite": "^2.8.1 || 3 || 4 || 5 || 6" 28 | }, 29 | "devDependencies": { 30 | "@cyco130/eslint-config": "^5.0.1", 31 | "eslint": "^9.16.0", 32 | "tsup": "^8.3.5", 33 | "typescript": "^5.7.2", 34 | "vite": "^5.4.11" 35 | }, 36 | "dependencies": { 37 | "@types/node": "^18.19.67", 38 | "@vavite/multibuild": "workspace:*", 39 | "cac": "^6.7.14", 40 | "picocolors": "^1.1.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/multibuild-cli/readme.md: -------------------------------------------------------------------------------- 1 | # @vavite/multibuild-cli 2 | 3 | `@vavite/multibuild-cli` is a tool for orchestrating multiple [Vite](https://vitejs.dev) builds. This package is the CLI binary, check out [`@vavite/multibuild`](../multibuild) for the JavaScript API. 4 | 5 | Developing applications that perform server-side rendering (SSR) with Vite requires two separate build steps: one for the client and one for the server. This library allows you to run both builds with a single command and customize the configuration for each build. 6 | 7 | We're hoping that, eventually, [this feature will be implemented in Vite itself](https://github.com/vitejs/vite/issues/5936). This package exists to provide a workaround until then. 8 | 9 | ## Usage 10 | 11 | `@vavite/multibuild` extends the Vite configuration with a `buildSteps` property, which is an array of build step definitions. A build step definition is an object with a `name` property (which is simply a string naming the build step), and an optional `config` property which will be merged into the Vite configuration for the build step. For example, a client build followed by a server build can be defined like this: 12 | 13 | ```ts 14 | import { defineConfig } from "vite"; 15 | 16 | export default defineConfig({ 17 | buildSteps: [ 18 | { 19 | name: "client", 20 | config: { 21 | build: { 22 | outDir: "dist/client", 23 | rollupOptions: { 24 | // Client entry 25 | input: "/client", 26 | }, 27 | }, 28 | }, 29 | }, 30 | { 31 | name: "server", 32 | config: { 33 | build: { 34 | // Server entry 35 | ssr: "/server", 36 | outDir: "dist/server", 37 | }, 38 | }, 39 | }, 40 | ], 41 | }); 42 | ``` 43 | 44 | You can then run the `vavite-multibuild` command as a drop-in replacement for `vite build`. This will call `resolveConfig` with the `mode` parameter set to `"multibuild"` to extract the build steps. Setting `buildSteps` in subsequent steps has no effect. 45 | 46 | ## Sharing information between builds 47 | 48 | `@vavite/multibuild-cli` will call the `buildStepStart` hook on each plugin when a build step starts and pass it information about the current step and data forwarded from the previous step. The `buildStepEnd` hook will be called when the build step ends and its return value will be forwarded to the next step. If a promise is returned, it will be awaited first. 49 | 50 | If no build steps are defined, `buildStepStart` and `buildStepEnd` will not be called. 51 | -------------------------------------------------------------------------------- /packages/multibuild-cli/src/index.ts: -------------------------------------------------------------------------------- 1 | import { BuildOptions, LogLevel, ResolvedConfig } from "vite"; 2 | import { cac } from "cac"; 3 | import { multibuild } from "@vavite/multibuild"; 4 | import { version } from "../package.json"; 5 | import colors from "picocolors"; 6 | 7 | interface GlobalCLIOptions { 8 | "--"?: string[]; 9 | c?: boolean | string; 10 | config?: string; 11 | base?: string; 12 | l?: LogLevel; 13 | logLevel?: LogLevel; 14 | clearScreen?: boolean; 15 | d?: boolean | string; 16 | debug?: boolean | string; 17 | f?: string; 18 | filter?: string; 19 | m?: string; 20 | mode?: string; 21 | } 22 | 23 | const cli = cac("vavite"); 24 | 25 | /** 26 | * removing global flags before passing as command specific sub-configs 27 | */ 28 | function cleanOptions( 29 | options: Options, 30 | ): Omit { 31 | const ret = { ...options }; 32 | delete ret["--"]; 33 | delete ret.c; 34 | delete ret.config; 35 | delete ret.base; 36 | delete ret.l; 37 | delete ret.logLevel; 38 | delete ret.clearScreen; 39 | delete ret.d; 40 | delete ret.debug; 41 | delete ret.f; 42 | delete ret.filter; 43 | delete ret.m; 44 | delete ret.mode; 45 | return ret; 46 | } 47 | 48 | cli 49 | .command("[root]") 50 | .option("-c, --config ", `[string] use specified config file`) 51 | .option("--base ", `[string] public base path (default: /)`) 52 | .option("-l, --logLevel ", `[string] info | warn | error | silent`) 53 | .option("--clearScreen", `[boolean] allow/disable clear screen when logging`) 54 | .option("-d, --debug [feat]", `[string | boolean] show debug logs`) 55 | .option("-f, --filter ", `[string] filter debug logs`) 56 | .option("-m, --mode ", `[string] set env mode`) 57 | .option("--target ", `[string] transpile target (default: 'modules')`) 58 | .option("--outDir ", `[string] output directory (default: dist)`) 59 | .option( 60 | "--assetsDir ", 61 | `[string] directory under outDir to place assets in (default: _assets)`, 62 | ) 63 | .option( 64 | "--assetsInlineLimit ", 65 | `[number] static asset base64 inline threshold in bytes (default: 4096)`, 66 | ) 67 | .option( 68 | "--ssr [entry]", 69 | `[string] build specified entry for server-side rendering`, 70 | ) 71 | .option( 72 | "--sourcemap", 73 | `[boolean] output source maps for build (default: false)`, 74 | ) 75 | .option( 76 | "--minify [minifier]", 77 | `[boolean | "terser" | "esbuild"] enable/disable minification, ` + 78 | `or specify minifier to use (default: esbuild)`, 79 | ) 80 | .option("--manifest", `[boolean] emit build manifest json`) 81 | .option("--ssrManifest", `[boolean] emit ssr manifest json`) 82 | .option( 83 | "--emptyOutDir", 84 | `[boolean] force empty outDir when it's outside of root`, 85 | ) 86 | .option("-w, --watch", `[boolean] rebuilds when modules have changed on disk`) 87 | .action(async (root: string, options: BuildOptions & GlobalCLIOptions) => { 88 | const buildOptions: BuildOptions = cleanOptions(options); 89 | 90 | let initialConfig: ResolvedConfig; 91 | 92 | process.env.NODE_ENV = options.mode || "production"; 93 | 94 | await multibuild( 95 | { 96 | root, 97 | base: options.base, 98 | mode: options.mode, 99 | configFile: options.config, 100 | logLevel: options.logLevel, 101 | clearScreen: options.clearScreen, 102 | build: buildOptions, 103 | }, 104 | { 105 | onInitialConfigResolved(config) { 106 | initialConfig = config; 107 | // eslint-disable-next-line no-console 108 | console.log(initialConfig.buildSteps); 109 | }, 110 | 111 | onStartBuildStep(info) { 112 | initialConfig.logger.info( 113 | (info.currentStepIndex ? "\n" : "") + 114 | colors.cyan("vavite: ") + 115 | (info.currentStep.description || 116 | colors.white("running build step") + 117 | " " + 118 | colors.blue(info.currentStep.name)) + 119 | " (" + 120 | colors.green( 121 | info.currentStepIndex + 1 + "/" + info.buildSteps.length, 122 | ) + 123 | ")", 124 | ); 125 | }, 126 | }, 127 | ); 128 | }); 129 | 130 | cli.help(); 131 | cli.version(version); 132 | 133 | cli.parse(); 134 | -------------------------------------------------------------------------------- /packages/multibuild-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "es2020", 5 | "module": "ESNext", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "moduleResolution": "Bundler", 11 | "customConditions": ["import"], 12 | "checkJs": true 13 | }, 14 | "exclude": ["node_modules", "dist", "cli.js"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/multibuild-cli/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ["./src/index.ts"], 6 | format: ["esm"], 7 | platform: "node", 8 | target: "node18", 9 | }, 10 | ]); 11 | -------------------------------------------------------------------------------- /packages/multibuild/eslint.config.js: -------------------------------------------------------------------------------- 1 | import config from "@cyco130/eslint-config/node"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | 5 | const tsconfigRootDir = 6 | // @ts-expect-error: import.meta.dirname requires v20.11.0 or v21.2.0 7 | import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url)); 8 | 9 | /** @type {typeof config} */ 10 | export default [ 11 | ...config, 12 | { 13 | ignores: ["dist/", "node_modules/"], 14 | }, 15 | { 16 | languageOptions: { 17 | parserOptions: { 18 | projectService: true, 19 | tsconfigRootDir, 20 | }, 21 | }, 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /packages/multibuild/lint-staged.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | "**/*.ts?(x)": [ 3 | () => "tsc -p tsconfig.json --noEmit", 4 | "eslint --max-warnings 0 --ignore-pattern dist", 5 | ], 6 | "*": "prettier --ignore-unknown --write", 7 | }; 8 | -------------------------------------------------------------------------------- /packages/multibuild/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/multibuild", 3 | "version": "5.1.0", 4 | "type": "module", 5 | "exports": { 6 | ".": { 7 | "import": "./dist/index.js" 8 | } 9 | }, 10 | "typesVersions": { 11 | "*": { 12 | "*": [ 13 | "dist/*.d.ts" 14 | ] 15 | } 16 | }, 17 | "files": [ 18 | "dist", 19 | "*.d.ts" 20 | ], 21 | "description": "Tool for orchestrating multiple Vite builds", 22 | "author": "Fatih Aygün ", 23 | "repository": "github:cyco130/vavite", 24 | "license": "MIT", 25 | "scripts": { 26 | "build": "tsup", 27 | "dev": "tsup --watch", 28 | "prepack": "rm -rf dist && pnpm build", 29 | "test": "pnpm run test:typecheck && pnpm run test:lint && pnpm run test:package", 30 | "test:typecheck": "tsc -p tsconfig.json --noEmit", 31 | "test:lint": "eslint . --max-warnings 0 --ignore-pattern dist", 32 | "test:package": "publint --strict" 33 | }, 34 | "peerDependencies": { 35 | "vite": "^2.8.1 || 3 || 4 || 5 || 6" 36 | }, 37 | "devDependencies": { 38 | "@cyco130/eslint-config": "^5.0.1", 39 | "eslint": "^9.16.0", 40 | "tsup": "^8.3.5", 41 | "typescript": "^5.7.2", 42 | "vite": "^5.4.11" 43 | }, 44 | "dependencies": { 45 | "@types/node": "^18.19.67", 46 | "cac": "^6.7.14", 47 | "picocolors": "^1.1.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/multibuild/readme.md: -------------------------------------------------------------------------------- 1 | # @vavite/multibuild 2 | 3 | `@vavite/multibuild` is a tool for orchestrating multiple [Vite](https://vitejs.dev) builds. This package is the JavaScript API, check out [`@vavite/multibuild-cli`](../multibuild-cli) for the command line interface. 4 | 5 | Developing applications that perform server-side rendering (SSR) with Vite requires two separate build steps: one for the client and one for the server. This library allows you to run both builds in a and customize the configuration for each build. 6 | 7 | We're hoping that, eventually, [this feature will be implemented in Vite itself](https://github.com/vitejs/vite/issues/5936). This package exists to provide workaround until then. 8 | 9 | ## Usage 10 | 11 | `@vavite/multibuild` extends the Vite configuration with a `buildSteps` property, which is an array of build step definitions. A build step definition is an object with a `name` property (which is simply a string naming the build step), and an optional `config` property which will be merged into the Vite configuration for the build step. For example, a client build followed by a server build can be defined like this: 12 | 13 | ```ts 14 | import { defineConfig } from "vite"; 15 | 16 | export default defineConfig({ 17 | buildSteps: [ 18 | { 19 | name: "client", 20 | config: { 21 | build: { 22 | outDir: "dist/client", 23 | rollupOptions: { 24 | // Client entry 25 | input: "/client", 26 | }, 27 | }, 28 | }, 29 | }, 30 | { 31 | name: "server", 32 | config: { 33 | build: { 34 | // Server entry 35 | ssr: "/server", 36 | outDir: "dist/server", 37 | }, 38 | }, 39 | }, 40 | ], 41 | }); 42 | ``` 43 | 44 | You can then `import { multibuild } from "@vavite/multibuild"` and use it instead of Vite's `build` function to run a multi-step build. 45 | 46 | `@vavite/multibuild` will call `resolveConfig` with the `mode` parameter set to `"multibuild"` to extract the build steps. Setting `buildSteps` in subsequent steps has no effect. 47 | 48 | ## Sharing information between builds 49 | 50 | `@vavite/multibuild` will call the `buildStepStart` hook on each plugin when a build step starts and pass it information about the current step and data forwarded from the previous step. The `buildStepEnd` hook will be called when the build step ends and its return value will be forwarded to the next step. If a promise is returned, it will be awaited first. 51 | 52 | If no build steps are defined, `buildStepStart` and `buildStepEnd` will not be called. 53 | -------------------------------------------------------------------------------- /packages/multibuild/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "es2020", 5 | "module": "ESNext", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "moduleResolution": "Bundler", 11 | "customConditions": ["import"], 12 | "checkJs": true 13 | }, 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/multibuild/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ["./src/index.ts"], 6 | format: ["esm"], 7 | platform: "node", 8 | target: "node18", 9 | dts: true, 10 | }, 11 | ]); 12 | -------------------------------------------------------------------------------- /packages/node-loader/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "./dist/cli.js"; 3 | -------------------------------------------------------------------------------- /packages/node-loader/eslint.config.js: -------------------------------------------------------------------------------- 1 | import config from "@cyco130/eslint-config/node"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | 5 | const tsconfigRootDir = 6 | // @ts-expect-error: import.meta.dirname requires v20.11.0 or v21.2.0 7 | import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url)); 8 | 9 | /** @type {typeof config} */ 10 | export default [ 11 | ...config, 12 | { 13 | ignores: ["dist/", "node_modules/", "cli.js"], 14 | }, 15 | { 16 | languageOptions: { 17 | parserOptions: { 18 | projectService: true, 19 | tsconfigRootDir, 20 | }, 21 | }, 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /packages/node-loader/lint-staged.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | "**/*.ts?(x)": [ 3 | () => "tsc -p tsconfig.json --noEmit", 4 | "eslint --max-warnings 0 --ignore-pattern dist", 5 | ], 6 | "*": "prettier --ignore-unknown --write", 7 | }; 8 | -------------------------------------------------------------------------------- /packages/node-loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/node-loader", 3 | "version": "5.1.0", 4 | "type": "module", 5 | "exports": { 6 | ".": "./dist/index.js", 7 | "./plugin": { 8 | "import": "./dist/plugin.js" 9 | }, 10 | "./suppress-warnings": "./dist/suppress-warnings.cjs" 11 | }, 12 | "typesVersions": { 13 | "*": { 14 | "*": [ 15 | "dist/*.d.ts" 16 | ] 17 | } 18 | }, 19 | "bin": { 20 | "vavite-loader": "cli.js" 21 | }, 22 | "files": [ 23 | "dist", 24 | "cli.js" 25 | ], 26 | "description": "ESM loader for transpiling modules with Vite", 27 | "author": "Fatih Aygün ", 28 | "repository": "github:cyco130/vavite", 29 | "license": "MIT", 30 | "scripts": { 31 | "build": "tsup", 32 | "dev": "tsup --watch", 33 | "prepack": "rm -rf dist && pnpm build", 34 | "test": "pnpm run test:typecheck && pnpm run test:lint && pnpm run test:package", 35 | "test:typecheck": "tsc -p tsconfig.json --noEmit", 36 | "test:lint": "eslint . --max-warnings 0 --ignore-pattern dist", 37 | "test:package": "publint --strict" 38 | }, 39 | "peerDependencies": { 40 | "vite": "^2.8.1 || 3 || 4 || 5 || 6" 41 | }, 42 | "devDependencies": { 43 | "@cyco130/eslint-config": "^5.0.1", 44 | "@types/node": "^18.19.67", 45 | "eslint": "^9.16.0", 46 | "sirv": "^3.0.0", 47 | "tsup": "^8.3.5", 48 | "typescript": "^5.7.2", 49 | "vite": "^5.4.11" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/node-loader/readme.md: -------------------------------------------------------------------------------- 1 | # @vavite/node-loader 2 | 3 | `@vavite/node-loader` is [Node ESM loader](https://nodejs.org/api/esm.html#loaders) that uses [Vite](https://vitejs.dev) to transpile modules. It is part of the `vavite` project but it can be used in any Vite SSR project to enable sourcemap and breakpoints support. 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install --save-dev @vavite/node-loader 9 | ``` 10 | 11 | ## Usage 12 | 13 | Add the following to your Vite config: 14 | 15 | ```ts 16 | import { defineConfig } from "vite"; 17 | import { nodeLoaderPlugin } from "@vavite/node-loader/plugin"; 18 | 19 | export default defineConfig({ 20 | plugins: [ 21 | nodeLoaderPlugin(), 22 | // ... 23 | ], 24 | }); 25 | ``` 26 | 27 | And run your project with `vavite-loader vite dev`. 28 | -------------------------------------------------------------------------------- /packages/node-loader/src/cli.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from "node:child_process"; 2 | import { fileURLToPath } from "node:url"; 3 | 4 | const suppressPath = fileURLToPath( 5 | new URL("./suppress-warnings.cjs", import.meta.url).href, 6 | ); 7 | const loaderPath = new URL("./index.js", import.meta.url).href; 8 | 9 | const options = 10 | (process.env.NODE_OPTIONS ? process.env.NODE_OPTIONS + " " : "") + 11 | `-r ${JSON.stringify(suppressPath)} --loader ${loaderPath}`; 12 | 13 | const command = process.argv[2]; 14 | const args = process.argv.slice(3); 15 | 16 | // Run the command with the options 17 | const cp = spawn(command, args, { 18 | shell: true, 19 | stdio: "inherit", 20 | env: { 21 | ...process.env, 22 | NODE_OPTIONS: options, 23 | }, 24 | }); 25 | 26 | cp.on("error", (err) => { 27 | console.error(err); 28 | process.exit(1); 29 | }); 30 | 31 | cp.on("exit", (code) => { 32 | process.exit(code ?? 0); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/node-loader/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin, ViteDevServer } from "vite"; 2 | 3 | declare global { 4 | // eslint-disable-next-line no-var 5 | var __vite_dev_server__: ViteDevServer | undefined; 6 | // eslint-disable-next-line no-var 7 | var __vavite_loader__: boolean; 8 | } 9 | 10 | const hasLoader = global.__vavite_loader__; 11 | 12 | export function nodeLoaderPlugin(): Plugin { 13 | return { 14 | name: "@vavite/node-loader", 15 | enforce: "pre", 16 | apply: "serve", 17 | config() { 18 | if (hasLoader) { 19 | return { 20 | experimental: { 21 | skipSsrTransform: true, 22 | }, 23 | }; 24 | } 25 | }, 26 | configResolved(config) { 27 | if (!hasLoader) { 28 | config.logger.warn( 29 | "@vavite/node-loader/plugin: @vavite/node-loader is not enabled. " + 30 | "Please run with `node --experimental-loader=@vavite/node-loader`.", 31 | ); 32 | } 33 | }, 34 | configureServer(server) { 35 | if (hasLoader) { 36 | global.__vite_dev_server__ = server; 37 | server.ssrLoadModule = (id) => import(id + "?ssrLoadModuleEntry"); 38 | server.ssrFixStacktrace = () => { 39 | /* noop */ 40 | }; 41 | server.ssrRewriteStacktrace = (s) => s; 42 | } 43 | }, 44 | buildEnd() { 45 | global.__vite_dev_server__ = undefined; 46 | }, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /packages/node-loader/src/suppress-warnings.ts: -------------------------------------------------------------------------------- 1 | // Based on following code: 2 | // https://github.com/esbuild-kit/tsx/blob/bd3ec4a5ed67545c044d6780fcd3187f0ff2f8a8/src/suppress-warnings.cts 3 | // Copyright (c) Hiroki Osame 4 | // Under MIT License 5 | 6 | const ignoreWarnings = new Set([ 7 | "--experimental-loader is an experimental feature. This feature could change at any time", 8 | "Custom ESM Loaders is an experimental feature. This feature could change at any time", 9 | "Custom ESM Loaders is an experimental feature and might change at any time", 10 | ]); 11 | 12 | const { emit } = process; 13 | 14 | process.emit = function (this: any, event: string, warning: Error) { 15 | if (event === "warning" && ignoreWarnings.has(warning.message)) { 16 | return; 17 | } 18 | 19 | // eslint-disable-next-line prefer-rest-params 20 | return Reflect.apply(emit, this, arguments); 21 | } as any; 22 | -------------------------------------------------------------------------------- /packages/node-loader/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "es2020", 5 | "module": "ESNext", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "moduleResolution": "Bundler", 11 | "customConditions": ["import"], 12 | "checkJs": true 13 | }, 14 | "exclude": ["node_modules", "dist", "cli.js"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/node-loader/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig([ 4 | // Plugin 5 | { 6 | entry: ["./src/index.ts", "./src/cli.ts"], 7 | format: ["esm"], 8 | platform: "node", 9 | target: "node18", 10 | }, 11 | { 12 | entry: ["./src/plugin.ts"], 13 | format: ["esm"], 14 | platform: "node", 15 | target: "node18", 16 | dts: true, 17 | }, 18 | { 19 | entry: ["./src/suppress-warnings.ts"], 20 | format: ["cjs"], 21 | platform: "node", 22 | target: "node18", 23 | }, 24 | ]); 25 | -------------------------------------------------------------------------------- /packages/reloader/eslint.config.js: -------------------------------------------------------------------------------- 1 | import config from "@cyco130/eslint-config/node"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | 5 | const tsconfigRootDir = 6 | // @ts-expect-error: import.meta.dirname requires v20.11.0 or v21.2.0 7 | import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url)); 8 | 9 | /** @type {typeof config} */ 10 | export default [ 11 | ...config, 12 | { 13 | ignores: ["dist/", "node_modules/"], 14 | }, 15 | { 16 | languageOptions: { 17 | parserOptions: { 18 | projectService: true, 19 | tsconfigRootDir, 20 | }, 21 | }, 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /packages/reloader/lint-staged.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | "**/*.ts?(x)": [ 3 | () => "tsc -p tsconfig.json --noEmit", 4 | "eslint --max-warnings 0 --ignore-pattern dist", 5 | ], 6 | "*": "prettier --ignore-unknown --write", 7 | }; 8 | -------------------------------------------------------------------------------- /packages/reloader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vavite/reloader", 3 | "version": "5.1.0", 4 | "type": "module", 5 | "files": [ 6 | "dist", 7 | "*.d.ts" 8 | ], 9 | "exports": { 10 | ".": { 11 | "import": "./dist/index.js" 12 | }, 13 | "./http-dev-server": { 14 | "import": "./dist/http-dev-server.js" 15 | } 16 | }, 17 | "typesVersions": { 18 | "*": { 19 | "*": [ 20 | "dist/*.d.ts" 21 | ] 22 | } 23 | }, 24 | "description": "Vite plugin for hot hot reloading server applications", 25 | "author": "Fatih Aygün ", 26 | "repository": "github:cyco130/vavite", 27 | "license": "MIT", 28 | "scripts": { 29 | "build": "tsup", 30 | "dev": "tsup --watch", 31 | "prepack": "rm -rf dist && pnpm build", 32 | "test": "pnpm run test:typecheck && pnpm run test:lint && pnpm run test:package", 33 | "test:typecheck": "tsc -p tsconfig.json --noEmit", 34 | "test:lint": "eslint . --max-warnings 0 --ignore-pattern dist", 35 | "test:package": "publint --strict" 36 | }, 37 | "peerDependencies": { 38 | "vite": "^2.8.1 || 3 || 4 || 5 || 6" 39 | }, 40 | "devDependencies": { 41 | "@cyco130/eslint-config": "^5.0.1", 42 | "@types/node": "^18.19.67", 43 | "eslint": "^9.16.0", 44 | "tsup": "^8.3.5", 45 | "typescript": "^5.7.2", 46 | "vite": "^5.4.11" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/reloader/src/http-dev-server.ts: -------------------------------------------------------------------------------- 1 | import type { Server } from "node:http"; 2 | 3 | const httpDevServer: Server | undefined = undefined; 4 | 5 | export default httpDevServer; 6 | -------------------------------------------------------------------------------- /packages/reloader/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "es2020", 5 | "module": "ESNext", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "moduleResolution": "Bundler", 11 | "customConditions": ["import"], 12 | "checkJs": true 13 | }, 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/reloader/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ["./src/index.ts", "./src/http-dev-server.ts"], 6 | format: ["esm"], 7 | platform: "node", 8 | target: "node18", 9 | dts: true, 10 | }, 11 | ]); 12 | -------------------------------------------------------------------------------- /packages/vavite/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "./dist/cli.js"; 3 | -------------------------------------------------------------------------------- /packages/vavite/eslint.config.js: -------------------------------------------------------------------------------- 1 | import config from "@cyco130/eslint-config/node"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | 5 | const tsconfigRootDir = 6 | // @ts-expect-error: import.meta.dirname requires v20.11.0 or v21.2.0 7 | import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url)); 8 | 9 | /** @type {typeof config} */ 10 | export default [ 11 | ...config, 12 | { 13 | ignores: [ 14 | "dist/", 15 | "node_modules/", 16 | "cli.js", 17 | "node-loader.mjs", 18 | "suppress-loader-warnings.cjs", 19 | ], 20 | }, 21 | { 22 | languageOptions: { 23 | parserOptions: { 24 | projectService: true, 25 | tsconfigRootDir, 26 | }, 27 | }, 28 | }, 29 | ]; 30 | -------------------------------------------------------------------------------- /packages/vavite/lint-staged.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | "**/*.ts?(x)": [ 3 | () => "tsc -p tsconfig.json --noEmit", 4 | "eslint --max-warnings 0 --ignore-pattern dist", 5 | ], 6 | "*": "prettier --ignore-unknown --write", 7 | }; 8 | -------------------------------------------------------------------------------- /packages/vavite/node-loader.mjs: -------------------------------------------------------------------------------- 1 | export * from "@vavite/node-loader"; 2 | -------------------------------------------------------------------------------- /packages/vavite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vavite", 3 | "version": "5.1.0", 4 | "description": "A Vite plugin for develoing server-side applications", 5 | "type": "module", 6 | "files": [ 7 | "dist", 8 | "*.d.ts", 9 | "node-loader.mjs", 10 | "suppress-loader-warnings.cjs" 11 | ], 12 | "exports": { 13 | ".": { 14 | "import": "./dist/index.js" 15 | }, 16 | "./vite-dev-server": { 17 | "import": "./dist/vite-dev-server.js" 18 | }, 19 | "./http-dev-server": { 20 | "import": "./dist/http-dev-server.js" 21 | }, 22 | "./node-loader": "./node-loader.mjs", 23 | "./suppress-loader-warnings": "./suppress-loader-warnings.cjs", 24 | "./package.json": "./package.json" 25 | }, 26 | "typesVersions": { 27 | "*": { 28 | "*": [ 29 | "dist/*.d.ts" 30 | ] 31 | } 32 | }, 33 | "author": "Fatih Aygün ", 34 | "repository": "github:cyco130/vavite", 35 | "license": "MIT", 36 | "scripts": { 37 | "build": "tsup", 38 | "dev": "tsup --watch", 39 | "prepack": "rm -rf dist && pnpm build", 40 | "test": "pnpm run test:typecheck && pnpm run test:lint && pnpm run test:package", 41 | "test:typecheck": "tsc -p tsconfig.json --noEmit", 42 | "test:lint": "eslint . --max-warnings 0 --ignore-pattern dist", 43 | "test:package": "publint --strict" 44 | }, 45 | "bin": { 46 | "vavite": "./cli.js" 47 | }, 48 | "peerDependencies": { 49 | "vite": "^2.8.1 || 3 || 4 || 5 || 6" 50 | }, 51 | "devDependencies": { 52 | "@cyco130/eslint-config": "^5.0.1", 53 | "@types/node": "^18.19.67", 54 | "eslint": "^9.16.0", 55 | "sirv": "^3.0.0", 56 | "tsup": "^8.3.5", 57 | "typescript": "^5.7.2", 58 | "vite": "^5.4.11" 59 | }, 60 | "dependencies": { 61 | "@vavite/connect": "workspace:*", 62 | "@vavite/expose-vite-dev-server": "workspace:*", 63 | "@vavite/multibuild": "workspace:*", 64 | "@vavite/multibuild-cli": "workspace:*", 65 | "@vavite/node-loader": "workspace:*", 66 | "@vavite/reloader": "workspace:*", 67 | "cac": "^6.7.14", 68 | "picocolors": "^1.1.1" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/vavite/src/http-dev-server.ts: -------------------------------------------------------------------------------- 1 | import type { Server } from "node:http"; 2 | 3 | const httpDevServer: Server | undefined = undefined; 4 | 5 | export default httpDevServer; 6 | -------------------------------------------------------------------------------- /packages/vavite/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, UserConfig } from "vite"; 2 | import { vaviteConnect } from "@vavite/connect"; 3 | import { reloader } from "@vavite/reloader"; 4 | import vaviteExposeViteDevServer from "@vavite/expose-vite-dev-server"; 5 | import { nodeLoaderPlugin } from "@vavite/node-loader/plugin"; 6 | 7 | declare global { 8 | // eslint-disable-next-line no-var 9 | var __vavite_loader__: boolean; 10 | } 11 | 12 | const hasLoader = global.__vavite_loader__; 13 | 14 | export interface VaviteOptions { 15 | /** Entry module that default exports a middleware function. 16 | * You have to provide either a handler entry or a server entry. 17 | * If you provide both, the server entry will only be used in the 18 | * production build. 19 | */ 20 | handlerEntry?: string; 21 | /** Server entry point. You have to provide either a handler entry 22 | * or a server entry. If you provide both, the server entry will only 23 | * be used in the production build. 24 | */ 25 | serverEntry?: string; 26 | /** Whether to serve client-side assets in development. 27 | * @default false 28 | */ 29 | serveClientAssetsInDev?: boolean; 30 | /** If you only provide a handler entry, this option controls whether 31 | * to build a standalone server application or a middleware function. 32 | * @default true 33 | */ 34 | standalone?: boolean; 35 | /** Directory where the client-side assets are located. Set to null to disable 36 | * static file serving in production. 37 | * @default null 38 | */ 39 | clientAssetsDir?: string | null; 40 | /** Whether to bundle the sirv package or to import it when building in standalone 41 | * mode. You have to install it as a production dependency if this is set to false. 42 | * @default true 43 | */ 44 | bundleSirv?: boolean; 45 | /** 46 | * When to reload the server. "any-change" reloads every time any of the dependencies of the 47 | * server entry changes. "static-deps-change" only reloads when statically imported dependencies 48 | * change, dynamically imported dependencies are not tracked. 49 | * @default "any-change" 50 | */ 51 | reloadOn?: "any-change" | "static-deps-change"; 52 | /** Whether to use the vite runtime to execute the server entry. 53 | * @experimental 54 | * @default false (can be overriden by the USE_VITE_RUNTIME environment variable to "true") 55 | */ 56 | useViteRuntime?: boolean; 57 | } 58 | 59 | export function vavite(options: VaviteOptions): Plugin[] { 60 | const { 61 | serverEntry, 62 | handlerEntry, 63 | serveClientAssetsInDev, 64 | standalone, 65 | clientAssetsDir, 66 | bundleSirv, 67 | reloadOn, 68 | useViteRuntime, 69 | } = options; 70 | 71 | if (!serverEntry && !handlerEntry) { 72 | throw new Error( 73 | "vavite: either serverEntry or handlerEntry must be specified", 74 | ); 75 | } 76 | 77 | let buildStepStartCalled = false; 78 | 79 | const plugins: (Plugin | false)[] = [ 80 | { 81 | name: "vavite:check-multibuild", 82 | 83 | buildStepStart() { 84 | buildStepStartCalled = true; 85 | }, 86 | 87 | config() { 88 | return { 89 | ssr: { 90 | optimizeDeps: { 91 | exclude: ["vavite"], 92 | }, 93 | }, 94 | optimizeDeps: { 95 | exclude: ["vavite"], 96 | }, 97 | } as UserConfig; 98 | }, 99 | 100 | configResolved(config) { 101 | if ( 102 | config.buildSteps && 103 | config.command === "build" && 104 | config.mode !== "multibuild" && 105 | !buildStepStartCalled 106 | ) { 107 | throw new Error( 108 | "vavite: You have multiple build steps defined in your Vite config, please use the 'vavite' command instead of 'vite build' to build.", 109 | ); 110 | } 111 | }, 112 | }, 113 | hasLoader && nodeLoaderPlugin(), 114 | ]; 115 | 116 | if (handlerEntry) { 117 | plugins.push( 118 | ...vaviteConnect({ 119 | handlerEntry, 120 | customServerEntry: serverEntry, 121 | serveClientAssetsInDev, 122 | standalone, 123 | clientAssetsDir, 124 | bundleSirv, 125 | useViteRuntime, 126 | }), 127 | ); 128 | } else { 129 | plugins.push( 130 | reloader({ 131 | entry: serverEntry, 132 | serveClientAssetsInDev, 133 | reloadOn, 134 | useViteRuntime, 135 | }), 136 | ); 137 | } 138 | 139 | plugins.push(vaviteExposeViteDevServer()); 140 | 141 | return plugins.filter(Boolean) as Plugin[]; 142 | } 143 | 144 | export type { 145 | BuildStep, 146 | CustomBuildStep, 147 | VaviteMultiBuildInfo, 148 | ViteBuildStep, 149 | } from "@vavite/multibuild"; 150 | -------------------------------------------------------------------------------- /packages/vavite/src/vite-dev-server.ts: -------------------------------------------------------------------------------- 1 | import type { ViteDevServer } from "vite"; 2 | 3 | const viteDevServer: ViteDevServer | undefined = undefined; 4 | 5 | export default viteDevServer; 6 | -------------------------------------------------------------------------------- /packages/vavite/suppress-loader-warnings.cjs: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-require-imports 2 | require("@vavite/node-loader/suppress-warnings"); 3 | -------------------------------------------------------------------------------- /packages/vavite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "es2020", 5 | "module": "ESNext", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "moduleResolution": "Bundler", 11 | "customConditions": ["import"], 12 | "checkJs": true 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | "dist", 17 | "cli.js", 18 | "node-loader.mjs", 19 | "suppress-loader-warnings.cjs" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /packages/vavite/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ["./src/index.ts"], 6 | format: ["esm"], 7 | platform: "node", 8 | target: "node18", 9 | dts: true, 10 | }, 11 | { 12 | entry: ["./src/vite-dev-server.ts", "./src/http-dev-server.ts"], 13 | format: ["esm"], 14 | platform: "node", 15 | dts: true, 16 | }, 17 | { 18 | entry: ["./src/cli.ts"], 19 | format: ["esm"], 20 | platform: "node", 21 | }, 22 | ]); 23 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "ci" 4 | - "examples/*" 5 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Va vite! 2 | 3 | `vavite` is a set of tools for developing and building server-side applications with [Vite](https://vitejs.dev). 4 | 5 | Vite, despite being a frontend tool, has support for transpiling server-side code. The feature is intended for building [server-side rendering (SSR)](https://vitejs.dev/guide/ssr.html) applications. But there's no reason why it can't be leveraged for building server-side applications that are not necessarily related to SSR. `vavite` lets you do that, but also vastly simplifies the SSR workflow. 6 | 7 | Vite's official SSR guide describes a workflow where Vite's development server is used as a middleware function in a server application made with a [Connect](https://github.com/senchalabs/connect) compatible Node.js framework (like [Express](https://expressjs.com)). If your server-side code needs transpilation (e.g. for TypeScript), you're required to use another set of tools (say [`ts-node`](https://typestrong.org/ts-node/) and [`nodemon`](https://nodemon.io/)) for development and building. `vavite` enables you to use Vite itself to transpile your server-side code. 8 | 9 | ## Examples 10 | 11 | - [simple-standalone](examples/simple-standalone): Simple standalone example ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/simple-standalone)) 12 | - [express](examples/express): Integrating with Express ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/express)) 13 | - [koa](examples/koa): Integrating with Koa ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/koa)) 14 | - [fastify](examples/fastify): Integrating with Fastify ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/fastify)) 15 | - [hapi](examples/hapi): Integrating with Hapi ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/hapi)) 16 | - [ssr-react-express](examples/ssr-react-express): React SSR with Express ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/ssr-react-express)) 17 | - [ssr-vue-express](examples/ssr-vue-express): Vue SSR with Express ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/ssr-vue-express)) 18 | - [vike](examples/vike): Vike with React and Express ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/vike)) 19 | - [socket-io](examples/socket-io): [socket.io](https://socket.io/) with Express ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/socket-io)) 20 | - [Nest.js](examples/nestjs): [Nest.js](https://nestjs.com/) with Express ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/nestjs)) 21 | - [Nest.js with vike](examples/nestjs-vike): [Nest.js](https://nestjs.com/) with Vike ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/nestjs-vike)) 22 | 23 | ## Packages 24 | 25 | [`vavite`](packages/vavite) is the main package that should work for most workflows but it is built on a set of lower level tools that you can use independently: 26 | 27 | - [`@vavite/connect`](packages/connect) is a Vite plugin that turns the official SSR workflow around: Instead of mounting Vite's dev server into your application as a middleware function, you write your application in the form of a middleware function (with the `(req, res, next)` signature) and mount it into Vite's dev server. For production, you can provide a custom server entry or it can build a standalone Node.js server application for you. This workflow is best if you're only interested in handling requests and you don't need control over the server entry during development. 28 | 29 | - If you do need control over your server entry even during development, [`@vavite/reloader`](packages/reloader) is a Vite plugin that provides live reloading capabilities for applications written with _any_ Node.js server framework. It should be usable with any framework that allows you to provide your own `http.Server` instance. **Note that this is a less reliable method and some things don't work on some operating systems.** 30 | 31 | - [`@vavite/expose-vite-dev-server`](packages/expose-vite-dev-server) is a plugin that provides access to Vite's dev server by simply importing it. It's useful for accessing server methods like `ssrFixStacktrace` and `transformIndexHtml` during development using either `@vavite/connect` or `@vavite/reloader`. 32 | 33 | - Building an SSR application with Vite involves at least two invocations of Vite's build command: once for the client and once for the server. [`@vavite/multibuild`](packages/multibuild) provides a JavaScript API for orchestrating multiple Vite builds and [`@vavite/multibuild-cli`](packages/multibuild-cli) is a drop-in replacement for the `vite build` CLI command for invoking multiple builds. 34 | 35 | - [`@vavite/node-loader`](packages/node-loader) is a Vite plugin that makes it possible to debug SSR code with full support for sourcemaps and breakpoints. It uses a Node ESM loader behind the scenes. 36 | -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | WHITE='\033[1;37m' 3 | GREEN='\033[0;32m' 4 | BLUE='\033[1;34m' 5 | NC='\033[0m' 6 | 7 | echo -e ${WHITE}Updating package versions${NC} 8 | pnpm -r --filter=./packages/* exec -- bash -c "echo -e ${BLUE@Q}\$PNPM_PACKAGE_NAME${NC@Q}@${GREEN@Q}\`npm version --allow-same-version --no-git-tag-version $@\`${NC@Q}" 9 | echo -e ${WHITE}Updating dependency versions in examples${NC} 10 | pnpm -r --filter=./examples/* update --workspace --save-workspace-protocol=false 'vavite' '@vavite/*' 11 | --------------------------------------------------------------------------------