├── .gitignore ├── README.md ├── pnpm-workspace.yaml ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── examples ├── basic │ ├── src │ │ ├── vite-env.d.ts │ │ ├── counter.ts │ │ ├── main.ts │ │ ├── typescript.svg │ │ └── style.css │ ├── wrangler.toml │ ├── .gitignore │ ├── index.html │ ├── vite.config.ts │ ├── tsconfig.json │ ├── package.json │ ├── worker │ │ ├── index.ts │ │ └── static-assets.js │ ├── basic.test.ts │ ├── public │ │ └── vite.svg │ └── favicon.svg └── vite-plugin-ssr │ ├── .gitignore │ ├── pages │ ├── about │ │ ├── index.css │ │ └── index.page.jsx │ ├── index │ │ ├── Counter.jsx │ │ └── index.page.jsx │ └── star-wars │ │ ├── index.page.jsx │ │ └── index.page.server.js │ ├── vitest.config.ts │ ├── wrangler.toml │ ├── renderer │ ├── PageLayout.css │ ├── _default.page.client.jsx │ ├── _default.page.server.jsx │ └── PageLayout.jsx │ ├── vite.config.mjs │ ├── worker │ ├── ssr.js │ ├── index.js │ └── static-assets.js │ ├── package.json │ ├── render.test.ts │ └── render.dev.test.ts ├── packages └── vite-plugin-cloudflare │ ├── pnpm-global │ └── 5 │ │ └── pnpm-lock.yaml │ ├── bin │ └── vite-plugin-cloudflare.mjs │ ├── tsconfig.json │ ├── rollup.config.js │ ├── src │ ├── plugin.ts │ ├── build.ts │ ├── utils.ts │ └── vite.ts │ ├── package.json │ └── README.md ├── tsconfig.json ├── LICENSE └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist 3 | **/.vpc/** 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ./packages/vite-plugin-cloudflare/README.md -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - examples/* 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: aslemammad 2 | github: [aslemammad] 3 | -------------------------------------------------------------------------------- /examples/basic/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /worker/worker/ 3 | /dist/ 4 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/pages/about/index.css: -------------------------------------------------------------------------------- 1 | h1, 2 | p { 3 | color: green; 4 | } 5 | -------------------------------------------------------------------------------- /packages/vite-plugin-cloudflare/pnpm-global/5/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.3 2 | 3 | specifiers: 4 | vite-plugin-cloudflare: ^0.0.0 5 | -------------------------------------------------------------------------------- /packages/vite-plugin-cloudflare/bin/vite-plugin-cloudflare.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | (async () => { 4 | await import('../dist/cli.js') 5 | })() 6 | 7 | -------------------------------------------------------------------------------- /packages/vite-plugin-cloudflare/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src/**/*.ts"], 4 | "exclude": ["./dist"] 5 | } 6 | -------------------------------------------------------------------------------- /examples/basic/wrangler.toml: -------------------------------------------------------------------------------- 1 | # wrangler.toml (https://simple.zorofight94.workers.dev) 2 | name = "simple" 3 | main = "./dist/worker.js" 4 | compatibility_date = "2022-08-10" 5 | 6 | [site] 7 | bucket = "./dist" 8 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | 3 | export default defineConfig({ 4 | test: { 5 | threads: false, 6 | testTimeout: 30_000, 7 | hookTimeout: 30_000, 8 | }, 9 | }) 10 | 11 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/wrangler.toml: -------------------------------------------------------------------------------- 1 | # wrangler.toml 2 | name = "vite-ssr-worker" 3 | # node_compat = true 4 | main = "./dist/server/worker.js" 5 | compatibility_date = "2022-08-10" 6 | 7 | [site] 8 | bucket = "./dist/client" 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/pages/about/index.page.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './index.css' 3 | 4 | export { Page } 5 | 6 | function Page() { 7 | return ( 8 | <> 9 |

About

10 |

A colored page.

11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/renderer/PageLayout.css: -------------------------------------------------------------------------------- 1 | /* This CSS is common to all pages */ 2 | 3 | body { 4 | margin: 0; 5 | font-family: sans-serif; 6 | } 7 | * { 8 | box-sizing: border-box; 9 | } 10 | a { 11 | text-decoration: none; 12 | } 13 | 14 | .navitem { 15 | padding: 3px; 16 | } 17 | -------------------------------------------------------------------------------- /examples/basic/src/counter.ts: -------------------------------------------------------------------------------- 1 | export function setupCounter(element: HTMLButtonElement) { 2 | let counter = 0 3 | const setCounter = (count: number) => { 4 | counter = count 5 | element.innerHTML = `count is ${counter}` 6 | } 7 | element.addEventListener('click', () => setCounter(++counter)) 8 | setCounter(0) 9 | } 10 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/pages/index/Counter.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | export { Counter } 4 | 5 | function Counter() { 6 | const [count, setCount] = useState(0) 7 | return ( 8 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/renderer/_default.page.client.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | import React from "react"; 3 | 4 | export { render }; 5 | 6 | async function render(pageContext) { 7 | const { Page, pageProps } = pageContext 8 | ReactDOM.hydrate( 9 | , 10 | document.getElementById("page-view") 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import ssr from "vite-plugin-ssr/plugin"; 4 | import vpc from "vite-plugin-cloudflare"; 5 | 6 | export default defineConfig({ 7 | plugins: [react(), ssr(), vpc({ scriptPath: "./worker/index.js" })], 8 | esbuild: { minify: true }, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/basic/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/pages/index/index.page.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Counter } from './Counter' 3 | 4 | export { Page } 5 | 6 | function Page() { 7 | return ( 8 | <> 9 |

Welcome

10 | This page is: 11 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/pages/star-wars/index.page.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export { Page } 4 | 5 | function Page({ movies }) { 6 | return ( 7 | <> 8 |

Star Wars Movies

9 |
    10 | {movies.map(({ id, title, release_date }) => ( 11 |
  1. 12 | {title} ({release_date}) 13 |
  2. 14 | ))} 15 |
16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /examples/basic/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vpc from "vite-plugin-cloudflare"; 3 | 4 | export default defineConfig({ 5 | esbuild: { 6 | define: { 7 | DEBUG: `${process.env.NODE_ENV === "development"}`, 8 | }, 9 | }, 10 | plugins: [ 11 | vpc({ 12 | scriptPath: "./worker/index.ts", 13 | polyfilledGlobals: { process: "process/browser", Buffer: null }, 14 | polyfilledModules: { util: require.resolve("util/") }, 15 | }), 16 | ], 17 | }); 18 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/worker/ssr.js: -------------------------------------------------------------------------------- 1 | export { handleSsr } 2 | 3 | import { renderPage } from 'vite-plugin-ssr' 4 | 5 | async function handleSsr(url) { 6 | const pageContextInit = { url } 7 | const pageContext = await renderPage(pageContextInit) 8 | const { httpResponse } = pageContext 9 | if (!httpResponse) { 10 | return null 11 | } else { 12 | const { body, statusCode, contentType } = httpResponse 13 | return new Response(body, { 14 | headers: { 'content-type': contentType }, 15 | status: statusCode, 16 | }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "noEmit": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "skipLibCheck": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "test": "vitest", 8 | "dev": "NODE_ENV=development vite", 9 | "build": "NODE_ENV=production vite build", 10 | "preview": "vite preview" 11 | }, 12 | "devDependencies": { 13 | "miniflare": "^2.6.0", 14 | "process": "^0.11.10", 15 | "typescript": "^4.6.4", 16 | "util": "0.12.5", 17 | "vite": "^4.0.3" 18 | }, 19 | "dependencies": { 20 | "@cloudflare/kv-asset-handler": "~0.2.0", 21 | "modern-node-polyfills": "^0.0.9" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": ["esnext", "dom"], 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "resolveJsonModule": true, 11 | "skipDefaultLibCheck": true, 12 | "skipLibCheck": true, 13 | "outDir": "./dist", 14 | "declaration": true, 15 | "inlineSourceMap": true, 16 | "paths": { 17 | "vitest": ["./packages/vitest/src/index.ts"], 18 | "vitest/global.d.ts": ["./packages/vitest/global.d.ts"] 19 | } 20 | }, 21 | "exclude": [ 22 | "**/dist/**" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/renderer/_default.page.server.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOMServer from 'react-dom/server' 2 | import React from 'react' 3 | import { escapeInject, dangerouslySkipEscape } from 'vite-plugin-ssr' 4 | import { PageLayout } from './PageLayout' 5 | 6 | export { render } 7 | export { passToClient } 8 | 9 | // See https://vite-plugin-ssr.com/data-fetching 10 | const passToClient = ['pageProps'] 11 | 12 | function render(pageContext) { 13 | const { Page, pageProps } = pageContext 14 | const pageHtml = ReactDOMServer.renderToString( 15 | 16 | 17 | , 18 | ) 19 | 20 | return escapeInject` 21 | 22 | 23 |
${dangerouslySkipEscape(pageHtml)}
24 | 25 | ` 26 | } 27 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/pages/star-wars/index.page.server.js: -------------------------------------------------------------------------------- 1 | export { onBeforeRender } 2 | 3 | async function onBeforeRender(pageContext) { 4 | const movies = await getStarWarsMovies(pageContext) 5 | return { 6 | pageContext: { 7 | pageProps: { 8 | movies: filterMoviesData(movies), 9 | }, 10 | }, 11 | } 12 | } 13 | 14 | async function getStarWarsMovies(pageContext) { 15 | const response = await pageContext.fetch('https://star-wars.brillout.com/api/films.json') 16 | let movies = (await response.json()).results 17 | movies = movies.map((movie, i) => ({ 18 | ...movie, 19 | id: String(i + 1), 20 | })) 21 | return movies 22 | } 23 | 24 | function filterMoviesData(movies) { 25 | return movies.map((movie) => { 26 | const { title, release_date, id } = movie 27 | return { title, release_date, id } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /examples/basic/src/main.ts: -------------------------------------------------------------------------------- 1 | import './style.css' 2 | import typescriptLogo from './typescript.svg' 3 | import { setupCounter } from './counter' 4 | 5 | document.querySelector('#app')!.innerHTML = ` 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 |

Vite + TypeScript with vite-plugin-cloudflare

14 |
15 | 16 |
17 |

18 | Click on the Vite and TypeScript logos to learn more 19 |

20 |
21 | ` 22 | 23 | setupCounter(document.querySelector('#counter')!) 24 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/worker/index.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'development' 2 | addEventListener("fetch", (event) => { 3 | try { 4 | event.respondWith( 5 | handleFetchEvent(event).catch((err) => { 6 | console.error(err.stack); 7 | }) 8 | ); 9 | } catch (err) { 10 | console.error(err.stack); 11 | event.respondWith(new Response("Internal Error", { status: 500 })); 12 | } 13 | }); 14 | 15 | async function handleFetchEvent(event) { 16 | const { handleSsr } = await import("./ssr"); 17 | const { handleStaticAssets } = await import("./static-assets"); 18 | if (!isAssetUrl(event.request.url)) { 19 | const response = await handleSsr(event.request.url); 20 | if (response !== null) return response; 21 | } 22 | const response = await handleStaticAssets(event); 23 | return response; 24 | } 25 | 26 | function isAssetUrl(url) { 27 | const { pathname } = new URL(url); 28 | return pathname.startsWith("/assets/"); 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.os }} 15 | 16 | timeout-minutes: 6 17 | 18 | strategy: 19 | matrix: 20 | node-version: [16.7.x] 21 | os: [ubuntu-latest, windows-latest, macos-latest] # mac 22 | fail-fast: false 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | 27 | - name: Install pnpm 28 | uses: pnpm/action-setup@v2.0.1 29 | with: 30 | version: 7.26.3 31 | 32 | - name: Set node version to ${{ matrix.node_version }} 33 | uses: actions/setup-node@v2 34 | with: 35 | node-version: ${{ matrix.node_version }} 36 | cache: "pnpm" 37 | 38 | - name: Install 39 | run: pnpm i 40 | 41 | - name: Build 42 | run: pnpm run build 43 | 44 | - name: Lint 45 | run: pnpm run lint --if-present 46 | 47 | - name: TypeCheck 48 | run: pnpm run typecheck 49 | -------------------------------------------------------------------------------- /examples/basic/worker/index.ts: -------------------------------------------------------------------------------- 1 | import { endianness } from "os"; 2 | // just to check if polyfilledModules work 3 | import util from 'util' 4 | import { handleStaticAssets } from "./static-assets"; 5 | 6 | declare const DEBUG: boolean; 7 | 8 | addEventListener("fetch", async (event) => { 9 | const { pathname } = new URL(event.request.url); 10 | if (pathname.startsWith("/api")) { 11 | event.respondWith(handleRequest(event.request)); 12 | return; 13 | } 14 | 15 | if (DEBUG) { 16 | // we skip miniflare and let vite handle the url 17 | event.respondWith(new Response("", { headers: { "x-skip-request": "" } })); 18 | } else { 19 | // this will disable HMR in vite, so only for production 20 | event.respondWith(handleStaticAssets(event)); 21 | } 22 | }); 23 | 24 | async function handleRequest() { 25 | util; 26 | const obj = { 27 | "__dirname": __dirname, 28 | "__filename": __filename, 29 | cwd: process.cwd(), 30 | global: !!global, 31 | Buffer: !!globalThis.Buffer, 32 | process: !!process, 33 | endianness: !!endianness 34 | }; 35 | return new Response(JSON.stringify(obj)); 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 M. Bagher Abiat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "./dist/server/worker.js", 3 | "scripts": { 4 | "test": "vitest", 5 | "// For increased dev speed we use an Express.js dev server instead of wrangler": "", 6 | "dev": "vite dev", 7 | "// Build and try the worker locally": "", 8 | "prod": "npm run build && wrangler dev --port 3000", 9 | "// Build and deploy the worker to Cloudflare Workers": "", 10 | "deploy": "npm run build && wrangler publish", 11 | "// Build scripts": "", 12 | "build": "vite build" 13 | }, 14 | "dependencies": { 15 | "@cloudflare/kv-asset-handler": "~0.2.0", 16 | "@cloudflare/wrangler": "^1.19.5", 17 | "@vitejs/plugin-react": "^1.1.3", 18 | "buffer-es6": "^4.9.3", 19 | "esbuild": "^0.14.6", 20 | "express": "^4.17.1", 21 | "miniflare": "^2.6.0", 22 | "node-fetch": "^2.6.1", 23 | "process-es6": "^0.11.6", 24 | "react": "^17.0.2", 25 | "react-dom": "^17.0.2", 26 | "vite": "^3.0.4", 27 | "vite-plugin-ssr": "0.4.18" 28 | }, 29 | "// Needed for Yarn workspaces": "", 30 | "name": "vite-plugin-ssr", 31 | "version": "0.0.0", 32 | "devDependencies": { 33 | "wrangler": "^2.0.25" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vite-plugin-cloudflare/monorepo", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "pnpm -r run --filter=./packages/** build", 8 | "test:core": "vitest -r examples/basic", 9 | "test:all": "cross-env pnpm -r --stream run test --", 10 | "typecheck": "pnpm -r --parallel run typecheck", 11 | "lint": "echo TODO" 12 | }, 13 | "devDependencies": { 14 | "@antfu/install-pkg": "^0.1.0", 15 | "@rollup/plugin-alias": "^3.1.8", 16 | "@rollup/plugin-commonjs": "^21.0.1", 17 | "@rollup/plugin-json": "^4.1.0", 18 | "@rollup/plugin-node-resolve": "^13.1.1", 19 | "@types/shelljs": "^0.8.10", 20 | "buffer-es6": "^4.9.3", 21 | "cross-env": "^7.0.3", 22 | "esno": "^0.14.1", 23 | "execa": "^6.0.0", 24 | "fast-glob": "^3.2.11", 25 | "kill-port": "^1.6.1", 26 | "process-es6": "^0.11.6", 27 | "puppeteer": "^19.4.1", 28 | "rimraf": "^3.0.2", 29 | "rollup": "^2.61.1", 30 | "rollup-plugin-dts": "^4.2.2", 31 | "rollup-plugin-esbuild": "^4.7.2", 32 | "shelljs": "^0.8.4", 33 | "typescript": "^4.7.4", 34 | "utility-types": "^3.10.0", 35 | "vite": "^4.0.4", 36 | "vite-plugin-cloudflare": "workspace:latest", 37 | "vitest": "^0.28.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/basic/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { endianness } from "os"; 2 | import { dirname } from "path"; 3 | import { fileURLToPath } from "url"; 4 | import fs from 'fs/promises' 5 | import { beforeEach, expect, test } from "vitest"; 6 | import { execaSync as execa } from "execa"; 7 | import { Miniflare } from "miniflare"; 8 | 9 | const __dirname = dirname(fileURLToPath(import.meta.url)); 10 | 11 | execa("npm", ["run", "build"], { cwd: __dirname, stdio: "inherit" }); 12 | 13 | let mf: Miniflare; 14 | 15 | beforeEach(() => { 16 | mf = new Miniflare({ 17 | scriptPath: "./dist/worker.js", 18 | }); 19 | }); 20 | 21 | test("basic", async () => { 22 | const res = await mf.dispatchFetch("http://localhost:8787/api"); 23 | const body = await res.text(); 24 | 25 | const obj = { 26 | __dirname: expect.any(String), 27 | __filename: expect.any(String), 28 | cwd: expect.any(String), 29 | global: !!global, 30 | Buffer: false, // disabled in vite.config.ts 31 | process: !!process, 32 | endianness: !!endianness, 33 | /* XMLHttpRequest: true, 34 | XMLHttpRequestUpload: true, 35 | XMLHttpRequestEventTarget: true, */ 36 | }; 37 | 38 | expect(JSON.parse(body)).toStrictEqual(obj); 39 | 40 | // custom util polyfill 41 | expect(await fs.readFile('./dist/worker.js', 'utf-8')).toContain('util/util.js') 42 | }); 43 | -------------------------------------------------------------------------------- /examples/basic/src/typescript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/vite-plugin-cloudflare/rollup.config.js: -------------------------------------------------------------------------------- 1 | import esbuild from 'rollup-plugin-esbuild' 2 | import dts from 'rollup-plugin-dts' 3 | import resolve from '@rollup/plugin-node-resolve' 4 | import commonjs from '@rollup/plugin-commonjs' 5 | import json from '@rollup/plugin-json' 6 | import alias from '@rollup/plugin-alias' 7 | import pkg from './package.json' 8 | 9 | const entry = [ 10 | 'src/vite.ts' 11 | ] 12 | 13 | const external = [ 14 | ...Object.keys(pkg.dependencies || []), 15 | ...Object.keys(pkg.peerDependencies || []), 16 | 'worker_threads', 17 | 'esbuild', 18 | 'rollup-plugin-node-builtins', 19 | 'rollup-plugin-node-globals', 20 | 'fs/promises', 21 | 'miniflare' 22 | ] 23 | 24 | export default [ 25 | { 26 | input: entry, 27 | output: { 28 | dir: 'dist', 29 | format: 'esm', 30 | }, 31 | external, 32 | plugins: [ 33 | alias({ 34 | entries: [ 35 | { find: /^node:(.+)$/, replacement: '$1' }, 36 | ], 37 | }), 38 | resolve({ 39 | preferBuiltins: true, 40 | }), 41 | json(), 42 | commonjs(), 43 | esbuild({ 44 | target: 'node14', 45 | }), 46 | ], 47 | }, 48 | { 49 | input: [ 50 | 'src/vite.ts', 51 | ], 52 | output: { 53 | file: 'dist/vite.d.ts', 54 | format: 'esm', 55 | }, 56 | external, 57 | plugins: [ 58 | dts(), 59 | ], 60 | }, 61 | ] 62 | -------------------------------------------------------------------------------- /examples/basic/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/vite-plugin-cloudflare/src/plugin.ts: -------------------------------------------------------------------------------- 1 | // import builtins from "rollup-plugin-node-builtins"; 2 | import { readFile } from "fs/promises"; 3 | import { builtinModules } from "module"; 4 | import { polyfillPath, polyfillGlobals } from "modern-node-polyfills"; 5 | import { Plugin, transform } from "esbuild"; 6 | import { dirname } from "path"; 7 | 8 | export type PolyfilledGlobals = Parameters[2] 9 | export type PolyfilledModules = Record 10 | 11 | const isTS = (filename: string): boolean => /\.[cm]?ts$/.test(filename); 12 | 13 | export const plugin = (polyfilledModules?: PolyfilledModules, polyfilledGlobals?: PolyfilledGlobals): Plugin => ({ 14 | name: "vite-plugin-cloudflare", 15 | async setup(build) { 16 | build.onResolve({ filter: /.*/ }, async ({ path }) => { 17 | if (builtinModules.includes(path)) { 18 | return { path: polyfilledModules?.[path] || await polyfillPath(path), sideEffects: false }; 19 | } 20 | }); 21 | 22 | build.onLoad({ filter: /\.[cm]?[jt]s$/ }, async ({ path }) => { 23 | const isTSFile = isTS(path); 24 | let code = await readFile(path, "utf8"); 25 | if (isTSFile) { 26 | code = (await transform(code, { 27 | loader: "ts", 28 | })).code; 29 | } 30 | return { 31 | contents: await polyfillGlobals(code, { 32 | __dirname: dirname(path), 33 | __filename: path, 34 | }, polyfilledGlobals), 35 | loader: isTSFile ? "ts" : "js", 36 | }; 37 | }); 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /examples/basic/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/renderer/PageLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './PageLayout.css' 3 | 4 | export { PageLayout } 5 | 6 | function PageLayout({ children }) { 7 | return ( 8 | 9 | 10 | 11 | 12 | Home 13 | 14 | 15 | About 16 | 17 | 18 | Star Wars 19 | 20 | 21 | {children} 22 | 23 | 24 | ) 25 | } 26 | 27 | function Layout({ children }) { 28 | return ( 29 |
36 | {children} 37 |
38 | ) 39 | } 40 | 41 | function Sidebar({ children }) { 42 | return ( 43 |
54 | {children} 55 |
56 | ) 57 | } 58 | 59 | function Content({ children }) { 60 | return ( 61 |
69 | {children} 70 |
71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /examples/basic/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | #app { 41 | max-width: 1280px; 42 | margin: 0 auto; 43 | padding: 2rem; 44 | text-align: center; 45 | } 46 | 47 | .logo { 48 | height: 6em; 49 | padding: 1.5em; 50 | will-change: filter; 51 | } 52 | .logo:hover { 53 | filter: drop-shadow(0 0 2em #646cffaa); 54 | } 55 | .logo.vanilla:hover { 56 | filter: drop-shadow(0 0 2em #f7df1eaa); 57 | } 58 | 59 | .card { 60 | padding: 2em; 61 | } 62 | 63 | .read-the-docs { 64 | color: #888; 65 | } 66 | 67 | button { 68 | border-radius: 8px; 69 | border: 1px solid transparent; 70 | padding: 0.6em 1.2em; 71 | font-size: 1em; 72 | font-weight: 500; 73 | font-family: inherit; 74 | background-color: #1a1a1a; 75 | cursor: pointer; 76 | transition: border-color 0.25s; 77 | } 78 | button:hover { 79 | border-color: #646cff; 80 | } 81 | button:focus, 82 | button:focus-visible { 83 | outline: 4px auto -webkit-focus-ring-color; 84 | } 85 | 86 | @media (prefers-color-scheme: light) { 87 | :root { 88 | color: #213547; 89 | background-color: #ffffff; 90 | } 91 | a:hover { 92 | color: #747bff; 93 | } 94 | button { 95 | background-color: #f9f9f9; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /examples/basic/worker/static-assets.js: -------------------------------------------------------------------------------- 1 | // ******************************************** 2 | // This code was provided by Cloudflare Workers 3 | // ******************************************** 4 | 5 | import { getAssetFromKV } from '@cloudflare/kv-asset-handler' 6 | 7 | export { handleStaticAssets } 8 | 9 | /** 10 | * The DEBUG flag will do two things that help during development: 11 | * 1. we will skip caching on the edge, which makes it easier to 12 | * debug. 13 | * 2. we will return an error message on exception in your Response rather 14 | * than the default 404.html page. 15 | */ 16 | // const DEBUG = false 17 | 18 | async function handleStaticAssets(event) { 19 | let options = {} 20 | 21 | /** 22 | * You can add custom logic to how we fetch your assets 23 | * by configuring the function `mapRequestToAsset` 24 | */ 25 | // options.mapRequestToAsset = handlePrefix(/^\/docs/) 26 | 27 | try { 28 | if (DEBUG) { 29 | // customize caching 30 | options.cacheControl = { 31 | bypassCache: true, 32 | } 33 | } 34 | const page = await getAssetFromKV(event, options) 35 | 36 | // allow headers to be altered 37 | const response = new Response(page.body, page) 38 | 39 | response.headers.set('X-XSS-Protection', '1; mode=block') 40 | response.headers.set('X-Content-Type-Options', 'nosniff') 41 | response.headers.set('X-Frame-Options', 'DENY') 42 | response.headers.set('Referrer-Policy', 'unsafe-url') 43 | response.headers.set('Feature-Policy', 'none') 44 | 45 | return response 46 | } catch (e) { 47 | // if an error is thrown try to serve the asset at 404.html 48 | if (!DEBUG) { 49 | try { 50 | let notFoundResponse = await getAssetFromKV(event, { 51 | mapRequestToAsset: (req) => new Request(`${new URL(req.url).origin}/404.html`, req), 52 | }) 53 | 54 | return new Response(notFoundResponse.body, { 55 | ...notFoundResponse, 56 | status: 404, 57 | }) 58 | } catch (e) {} 59 | } 60 | 61 | return new Response(e.message || e.toString(), { status: 500 }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/worker/static-assets.js: -------------------------------------------------------------------------------- 1 | // ******************************************** 2 | // This code was provided by Cloudflare Workers 3 | // ******************************************** 4 | 5 | import { getAssetFromKV } from '@cloudflare/kv-asset-handler' 6 | 7 | export { handleStaticAssets } 8 | 9 | /** 10 | * The DEBUG flag will do two things that help during development: 11 | * 1. we will skip caching on the edge, which makes it easier to 12 | * debug. 13 | * 2. we will return an error message on exception in your Response rather 14 | * than the default 404.html page. 15 | */ 16 | const DEBUG = false 17 | 18 | async function handleStaticAssets(event) { 19 | let options = {} 20 | 21 | /** 22 | * You can add custom logic to how we fetch your assets 23 | * by configuring the function `mapRequestToAsset` 24 | */ 25 | // options.mapRequestToAsset = handlePrefix(/^\/docs/) 26 | 27 | try { 28 | if (DEBUG) { 29 | // customize caching 30 | options.cacheControl = { 31 | bypassCache: true, 32 | } 33 | } 34 | const page = await getAssetFromKV(event, options) 35 | 36 | // allow headers to be altered 37 | const response = new Response(page.body, page) 38 | 39 | response.headers.set('X-XSS-Protection', '1; mode=block') 40 | response.headers.set('X-Content-Type-Options', 'nosniff') 41 | response.headers.set('X-Frame-Options', 'DENY') 42 | response.headers.set('Referrer-Policy', 'unsafe-url') 43 | response.headers.set('Feature-Policy', 'none') 44 | 45 | return response 46 | } catch (e) { 47 | // if an error is thrown try to serve the asset at 404.html 48 | if (!DEBUG) { 49 | try { 50 | let notFoundResponse = await getAssetFromKV(event, { 51 | mapRequestToAsset: (req) => new Request(`${new URL(req.url).origin}/404.html`, req), 52 | }) 53 | 54 | return new Response(notFoundResponse.body, { 55 | ...notFoundResponse, 56 | status: 404, 57 | }) 58 | } catch (e) {} 59 | } 60 | 61 | return new Response(e.message || e.toString(), { status: 500 }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/vite-plugin-cloudflare/src/build.ts: -------------------------------------------------------------------------------- 1 | import type { BuildOptions, BuildContext } from "esbuild"; 2 | import esbuild from "esbuild"; 3 | import { ResolvedConfig } from "vite"; 4 | import { plugin } from "./plugin"; 5 | import type { Options } from "./vite"; 6 | 7 | export async function build( 8 | workerFile: string, 9 | dev: true, 10 | config: ResolvedConfig, 11 | options: Options 12 | ): Promise<{ 13 | outfile: string; 14 | content: string; 15 | dispose: () => Promise; 16 | rebuild: BuildContext["rebuild"]; 17 | }>; 18 | 19 | export async function build( 20 | workerFile: string, 21 | dev: false, 22 | config: ResolvedConfig, 23 | options: Options 24 | ): Promise<{ outfile: string }>; 25 | 26 | export async function build( 27 | workerFile: string, 28 | dev: boolean, 29 | config: ResolvedConfig, 30 | options: Options 31 | ): Promise< 32 | | { 33 | outfile: string; 34 | content: string; 35 | dispose: () => Promise; 36 | rebuild: BuildContext["rebuild"]; 37 | } 38 | | { outfile: string } 39 | > { 40 | const outFile = config.build.outDir + "/worker.js"; 41 | const esbuildConfig: BuildOptions = { 42 | banner: { 43 | js: ` 44 | (() => { 45 | globalThis.navigator = { userAgent: "Cloudflare-Workers" }; 46 | })(); 47 | `, 48 | }, 49 | external: ["__STATIC_CONTENT_MANIFEST"], 50 | ...(config.esbuild as BuildOptions), 51 | sourcemap: 'inline', 52 | plugins: [plugin(options?.polyfilledModules, options?.polyfilledGlobals)], 53 | entryPoints: [workerFile], 54 | write: !dev, 55 | bundle: true, 56 | allowOverwrite: true, 57 | platform: "node", 58 | format: "esm", 59 | target: "es2020", 60 | outfile: outFile, 61 | }; 62 | if (dev) { 63 | const { rebuild, dispose } = await esbuild.context(esbuildConfig); 64 | const { outputFiles } = await rebuild(); 65 | return { 66 | content: outputFiles![0].text, 67 | dispose, 68 | rebuild, 69 | outfile: outFile, 70 | }; 71 | } else { 72 | await esbuild.build(esbuildConfig); 73 | return { 74 | outfile: outFile, 75 | }; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/vite-plugin-cloudflare/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-cloudflare", 3 | "version": "0.4.1", 4 | "type": "module", 5 | "scripts": { 6 | "build": "rimraf dist && rollup -c", 7 | "dev": "rollup -c --watch src", 8 | "typecheck": "tsc --noEmit" 9 | }, 10 | "main": "./dist/vite.js", 11 | "module": "./dist/vite.js", 12 | "types": "./dist/vite.d.ts", 13 | "exports": { 14 | ".": { 15 | "import": "./dist/vite.js", 16 | "node": "./dist/vite.js", 17 | "types": "./dist/vite.d.ts" 18 | }, 19 | "./shimmed": { 20 | "import": "./dist/shimmed.js" 21 | }, 22 | "./process": { 23 | "import": "./dist/process.js" 24 | }, 25 | "./inherits": { 26 | "import": "./dist/inherits.js" 27 | }, 28 | "./Buffer": { 29 | "import": "./dist/Buffer.js" 30 | } 31 | }, 32 | "files": [ 33 | "dist", 34 | "bin", 35 | "*.d.ts" 36 | ], 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/aslemammad/tinyspy.git" 40 | }, 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/aslemammad/vite-plugin-cloudflare/issues" 44 | }, 45 | "homepage": "https://github.com/aslemammad/vite-plugin-cloudflare#readme", 46 | "devDependencies": { 47 | "@types/node": "^18.6.3", 48 | "@types/node-fetch": "^2.6.2", 49 | "@types/rollup-plugin-node-builtins": "^2.1.2", 50 | "@types/rollup-plugin-node-globals": "^1.4.1", 51 | "esbuild": "^0.17.5", 52 | "miniflare": "^2.6.0", 53 | "setimmediate": "^1.0.5", 54 | "vite": "4.0.4" 55 | }, 56 | "peerDependencies": { 57 | "esbuild": "*", 58 | "miniflare": "*", 59 | "vite": "*" 60 | }, 61 | "engines": { 62 | "node": ">=14.0.0" 63 | }, 64 | "dependencies": { 65 | "buffer-es6": "^4.9.3", 66 | "cac": "^6.7.12", 67 | "fast-glob": "^3.2.11", 68 | "modern-node-polyfills": "^0.1.0", 69 | "node-fetch": "^2.6.1", 70 | "picocolors": "^1.0.0", 71 | "process-es6": "^0.11.6", 72 | "rollup-plugin-node-builtins": "^2.1.2", 73 | "rollup-plugin-node-globals": "^1.4.0", 74 | "stream-http": "^3.2.0", 75 | "undici": "^5.8.0", 76 | "xhr2": "^0.2.1" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/render.test.ts: -------------------------------------------------------------------------------- 1 | import type { Server } from "http"; 2 | import { describe, afterAll, beforeAll, expect, test } from "vitest"; 3 | import puppeteer from "puppeteer"; 4 | import { execaSync as execa } from "execa"; 5 | import { Miniflare } from "miniflare"; 6 | 7 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 8 | 9 | async function autoRetry(test: () => void | Promise): Promise { 10 | const period = 100; 11 | const numberOfTries = 5000 / period; 12 | let i = 0; 13 | while (true) { 14 | try { 15 | await test(); 16 | return; 17 | } catch (err) { 18 | i = i + 1; 19 | if (i > numberOfTries) { 20 | throw err; 21 | } 22 | } 23 | await sleep(period); 24 | } 25 | } 26 | 27 | describe("render", async () => { 28 | const url = "http://localhost:8787"; 29 | let mf: Miniflare; 30 | 31 | let browser: puppeteer.Browser; 32 | let page: puppeteer.Page; 33 | let server: Server; 34 | beforeAll(async () => { 35 | execa("npm", ["run", "build"], { cwd: __dirname, stdio: "inherit" }); 36 | mf = new Miniflare({ 37 | scriptPath: "./dist/server/worker.js", 38 | wranglerConfigPath: true, 39 | packagePath: true, 40 | envPath: true, 41 | }); 42 | browser = await puppeteer.launch(); 43 | page = await browser.newPage(); 44 | server = await mf.createServer(); 45 | server.listen(8787); 46 | }); 47 | afterAll(async () => { 48 | await browser.close(); 49 | server.close(); 50 | }); 51 | 52 | test("page content is rendered to HTML", async () => { 53 | await page.goto(url); 54 | expect(await page.content()).toContain("

Welcome

"); 55 | }); 56 | 57 | test("page is rendered to the DOM and interactive", async () => { 58 | try { 59 | await page.goto(url); 60 | const h1Text = await ( 61 | await page.$("h1") 62 | )?.evaluate((el) => el.textContent); 63 | expect(h1Text).toBe("Welcome"); 64 | { 65 | const buttonText = await ( 66 | await page.$("button") 67 | )?.evaluate((el) => el.textContent); 68 | expect(buttonText).toBe("Counter 0"); 69 | } 70 | { 71 | await page.click("button"); 72 | await sleep(100); 73 | const buttonText = await ( 74 | await page.$("button") 75 | )?.evaluate((el) => el.textContent); 76 | expect(buttonText).toBe("Counter 1"); 77 | } 78 | } catch (e) { 79 | console.error(e); 80 | expect(e).toBeUndefined(); 81 | } 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /examples/vite-plugin-ssr/render.dev.test.ts: -------------------------------------------------------------------------------- 1 | import type { Server } from "http"; 2 | import { describe, afterAll, beforeAll, expect, test } from "vitest"; 3 | import {createServer} from 'vite' 4 | import puppeteer from "puppeteer"; 5 | import { execaSync as execa } from "execa"; 6 | import { Miniflare } from "miniflare"; 7 | import { ViteDevServer } from "vite"; 8 | 9 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 10 | 11 | async function autoRetry(test: () => void | Promise): Promise { 12 | const period = 100; 13 | const numberOfTries = 5000 / period; 14 | let i = 0; 15 | while (true) { 16 | try { 17 | await test(); 18 | return; 19 | } catch (err) { 20 | i = i + 1; 21 | if (i > numberOfTries) { 22 | throw err; 23 | } 24 | } 25 | await sleep(period); 26 | } 27 | } 28 | 29 | describe("render dev", async () => { 30 | const url = "http://localhost:8787"; 31 | // let mf: Miniflare; 32 | 33 | let browser: puppeteer.Browser; 34 | let page: puppeteer.Page; 35 | let server: ViteDevServer; 36 | beforeAll(async () => { 37 | // execa("npm", ["run", "build"], { cwd: __dirname, stdio: "inherit" }); 38 | /* mf = new Miniflare({ 39 | scriptPath: "./dist/server/worker.js", 40 | wranglerConfigPath: true, 41 | packagePath: true, 42 | envPath: true, 43 | }); */ 44 | 45 | browser = await puppeteer.launch(); 46 | page = await browser.newPage(); 47 | server = await createServer(); 48 | await server.listen(8787); 49 | }); 50 | afterAll(async () => { 51 | await browser.close(); 52 | server.close(); 53 | }); 54 | 55 | test("page content is rendered to HTML", async () => { 56 | await page.goto(url); 57 | expect(await page.content()).toContain("

Welcome

"); 58 | }); 59 | 60 | test("page is rendered to the DOM and interactive", async () => { 61 | try { 62 | await page.goto(url); 63 | const h1Text = await ( 64 | await page.$("h1") 65 | )?.evaluate((el) => el.textContent); 66 | expect(h1Text).toBe("Welcome"); 67 | { 68 | const buttonText = await ( 69 | await page.$("button") 70 | )?.evaluate((el) => el.textContent); 71 | expect(buttonText).toBe("Counter 0"); 72 | } 73 | { 74 | await page.click("button"); 75 | await sleep(100); 76 | const buttonText = await ( 77 | await page.$("button") 78 | )?.evaluate((el) => el.textContent); 79 | expect(buttonText).toBe("Counter 1"); 80 | } 81 | } catch (e) { 82 | console.error(e); 83 | expect(e).toBeUndefined(); 84 | } 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /packages/vite-plugin-cloudflare/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "undici"; 2 | import { ServerResponse } from "http"; 3 | import { Connect } from "vite"; 4 | 5 | export function toRequest(req: Connect.IncomingMessage): Request { 6 | const url = new URL(req.url || req.originalUrl!, `http://localhost:8787`); 7 | 8 | return new Request(url.href, { 9 | headers: req.headers as Record, 10 | method: req.method, 11 | body: req.method === "GET" || req.method === "HEAD" ? undefined : req as any, 12 | // @ts-ignore 13 | duplex: "half" 14 | }); 15 | } 16 | 17 | export async function fromResponse(response: Response, res: ServerResponse) { 18 | /* 19 | Copyright 2021 Fatih Aygün and contributors 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | */ 27 | res.statusCode = response.status; 28 | for (const [key, value] of response.headers) { 29 | if (key === "set-cookie") { 30 | const setCookie = response.headers.get("set-cookie") as string; 31 | res.setHeader("set-cookie", setCookie); 32 | } else { 33 | res.setHeader(key, value); 34 | } 35 | } 36 | 37 | const contentLengthSet = response.headers.get("content-length"); 38 | if (response.body) { 39 | if (contentLengthSet) { 40 | for await (let chunk of response.body as any) { 41 | chunk = Buffer.from(chunk); 42 | res.write(chunk); 43 | } 44 | } else { 45 | const reader = (response.body as any as AsyncIterable)[ 46 | Symbol.asyncIterator 47 | ](); 48 | 49 | const first = await reader.next(); 50 | if (first.done) { 51 | res.setHeader("content-length", "0"); 52 | } else { 53 | const secondPromise = reader.next(); 54 | let second = await Promise.race([secondPromise, Promise.resolve(null)]); 55 | 56 | if (second && second.done) { 57 | res.setHeader("content-length", first.value.length); 58 | res.write(first.value); 59 | } else { 60 | res.write(first.value); 61 | second = await secondPromise; 62 | for (; !second.done; second = await reader.next()) { 63 | res.write(Buffer.from(second.value)); 64 | } 65 | } 66 | } 67 | } 68 | } else if (!contentLengthSet) { 69 | res.setHeader("content-length", "0"); 70 | } 71 | 72 | res.end(); 73 | } 74 | -------------------------------------------------------------------------------- /packages/vite-plugin-cloudflare/src/vite.ts: -------------------------------------------------------------------------------- 1 | // ignore in typescript 2 | // @ts-nocheck 3 | import type { Connect, ResolvedConfig, PluginOption } from "vite"; 4 | import { 5 | Log, 6 | LogLevel, 7 | Miniflare, 8 | MiniflareOptions, 9 | RequestInit, 10 | } from "miniflare"; 11 | import colors from "picocolors"; 12 | import path from "path"; 13 | import { fromResponse, toRequest } from "./utils"; 14 | import { build } from "./build"; 15 | import type { BuildContext } from "esbuild"; 16 | import { PolyfilledGlobals, PolyfilledModules } from "./plugin"; 17 | import fg from "fast-glob"; 18 | 19 | export type Options = { 20 | // miniflare specific options for development (optional) 21 | miniflare?: Omit; 22 | // the worker file (required) 23 | scriptPath: string; 24 | // customize globals that need to polyfilled (process, setTimeout, ...) 25 | polyfilledGlobals?: PolyfilledGlobals; 26 | // customize mods (node's builtinModules) that need to polyfilled (utils, http, ...) 27 | polyfilledModules?: PolyfilledModules; 28 | // a fast-glob pattern for files who's changes should reload the worker (optional) 29 | workerFilesPattern?: string | string[]; 30 | // enable modules (esm) 31 | modules?: boolean; 32 | }; 33 | 34 | export default function vitePlugin(options: Options): PluginOption { 35 | let mf: Miniflare; 36 | let resolvedConfig: ResolvedConfig; 37 | let workerFile: string; 38 | let otherWorkerFiles: string[] 39 | let esbuildRebuild: BuildContext['rebuild']; 40 | return { 41 | enforce: "post", 42 | name: "cloudflare", 43 | configResolved(config) { 44 | resolvedConfig = config; 45 | workerFile = path.resolve(config.root, options.scriptPath); 46 | otherWorkerFiles = options.workerFilesPattern 47 | ? fg.sync(options.workerFilesPattern, { 48 | cwd: resolvedConfig.root, 49 | absolute: true, 50 | }) 51 | : [] 52 | }, 53 | async configureServer(server) { 54 | const { rebuild, content, dispose } = await build( 55 | workerFile, 56 | true, 57 | resolvedConfig, 58 | options 59 | ); 60 | esbuildRebuild = rebuild!; 61 | 62 | mf = new Miniflare({ 63 | log: new Log(LogLevel.DEBUG), 64 | sourceMap: true, 65 | wranglerConfigPath: true, 66 | packagePath: false, 67 | modules: options.modules, 68 | ...options.miniflare, 69 | script: content, 70 | watch: true, 71 | }); 72 | 73 | process.on("beforeExit", async () => { 74 | await mf.dispose(); 75 | dispose(); 76 | }); 77 | 78 | const mfMiddleware: Connect.NextHandleFunction = async ( 79 | req, 80 | res, 81 | next 82 | ) => { 83 | try { 84 | const mfRequest = toRequest(req); 85 | 86 | // @ts-ignore 87 | const mfResponse = await mf.dispatchFetch( 88 | mfRequest.url, 89 | mfRequest as RequestInit 90 | ); 91 | 92 | if (mfResponse.headers.has("x-skip-request")) { 93 | throw undefined; // skip miniflare and pass to next middleware 94 | } 95 | 96 | await fromResponse(mfResponse, res); 97 | } catch (e) { 98 | if (e) { 99 | console.error(e); 100 | } 101 | next(e); 102 | } 103 | }; 104 | 105 | server.middlewares.use(mfMiddleware); 106 | 107 | return async () => { 108 | // enable HMR analyzing by vite, so we have better track of the worker 109 | // file (deps, importers, ...) 110 | try { 111 | // this may fail in custom server mode 112 | await server.transformRequest(workerFile); 113 | } catch {} 114 | }; 115 | }, 116 | async handleHotUpdate({ file, server }) { 117 | const module = server.moduleGraph.getModuleById(file); 118 | const isImportedByWorkerFile = [...(module?.importers || [])].some( 119 | (importer) => importer.file === workerFile 120 | ); 121 | const isOtherWorkerFile = otherWorkerFiles.includes(file) 122 | 123 | if (module?.file === workerFile || isImportedByWorkerFile || isOtherWorkerFile) { 124 | const { outputFiles } = await esbuildRebuild(); 125 | // @ts-ignore 126 | await mf.setOptions({ script: outputFiles![0].text }); 127 | server.ws.send({ type: "full-reload" }); 128 | server.config.logger.info(colors.cyan(`🔥 [cloudflare] hot reloaded`)); 129 | // we already handle the reload, so we skip the Vite's HMR handling here 130 | return []; 131 | } 132 | }, 133 | async closeBundle() { 134 | if (resolvedConfig.env.DEV) { 135 | return 136 | } 137 | const { outfile } = await build(workerFile, false, resolvedConfig, options); 138 | 139 | resolvedConfig.logger.info( 140 | colors.cyan( 141 | `🔥 [cloudflare] bundled worker file in '${path.relative( 142 | resolvedConfig.root, 143 | outfile 144 | )}'` 145 | ) 146 | ); 147 | }, 148 | }; 149 | } 150 | -------------------------------------------------------------------------------- /packages/vite-plugin-cloudflare/README.md: -------------------------------------------------------------------------------- 1 | # vite-plugin-cloudflare 🔥 2 | 3 | Vite-plugin-cloudflare is a plugin for transforming & bundling cloudflare 4 | workers with shimming [modern node 5 | polyfills](https://github.com/Aslemammad/modern-node-polyfills) like `process`, 6 | `os`, `stream` and other node global functions and modules using **Esbuild** and **Vite**! 7 | 8 | - Universal Vite plugin 9 | - Lightning builds 10 | - Workers compatible build using shimming 11 | - Fast development and HMR compatible reloads 12 | - Builtin [Miniflare](https://miniflare.dev/) support 13 | 14 | ## Install 15 | 16 | ``` 17 | npm i --save-dev vite-plugin-cloudflare esbuild@latest 18 | ``` 19 | 20 | ## Plugin 21 | 22 | ```ts 23 | // vite.config.js 24 | import { defineConfig } from "vite"; 25 | import vpc from "vite-plugin-cloudflare"; 26 | 27 | export default defineConfig({ 28 | plugins: [vpc({ scriptPath: "./worker/index.ts" })], 29 | }); 30 | ``` 31 | 32 | The plugin gets an options object with this type signature. 33 | 34 | ```ts 35 | export type Options = { 36 | // miniflare specific options for development (optional) 37 | miniflare?: Omit; 38 | // the worker file (required) 39 | scriptPath: string; 40 | // customize globals that need to polyfilled (process, setTimeout, ...) 41 | polyfilledGlobals?: PolyfilledGlobals; 42 | // customize mods (node's builtinModules) that need to polyfilled (utils, http, ...) 43 | polyfilledModules?: PolyfilledModules; 44 | // a fast-glob pattern for files who's changes should reload the worker (optional) 45 | workerFilesPattern?: string | string[]; 46 | // enable modules (esm) 47 | modules?: boolean; 48 | }; 49 | ``` 50 | 51 | Since this plugin works with Esbuild, options passed to the `esbuild` field of 52 | your vite plugin will affect the worker result, unless they are not compatible 53 | with the `BuildOptions` type of Esbuild. 54 | 55 | ## Development 56 | 57 | You can start your Vite dev server and continue developing your applications. As 58 | previously mentioned, this plugin integrates Miniflare with Vite, so you'd have 59 | a nice experience writing your workers. 60 | 61 | ``` 62 | vite dev 63 | ``` 64 | 65 | ## Build 66 | 67 | When building, the plugin is going to start bundling your worker at the end of 68 | the vite bundling phase and generates it into the `config.outDir` with the 69 | `worker.js` file name. 70 | 71 | ``` 72 | vite build 73 | ``` 74 | 75 | Output: 76 | 77 | ``` 78 | vite v3.0.4 building for production... 79 | ✓ 6 modules transformed. 80 | dist/assets/typescript.f6ead1af.svg 1.40 KiB 81 | dist/index.html 0.44 KiB 82 | dist/assets/index.2547d205.js 1.41 KiB / gzip: 0.72 KiB 83 | dist/assets/index.d0964974.css 1.19 KiB / gzip: 0.62 KiB 84 | 🔥 [cloudflare] bundled worker file in 'dist/worker.js' 85 | ``` 86 | 87 | ## Wrangler 88 | 89 | Update your wrangler config to be compatible with the build, for instance, 90 | here's a config that uses the `dist/worker.js` bundled worker file generated by 91 | vite-plugin-cloudflare and serves the assets from the vite build: 92 | 93 | ```toml 94 | # wrangler.toml 95 | name = "vite-ssr-worker" 96 | main = "./dist/worker.js" 97 | compatibility_date = "2022-08-10" 98 | 99 | [site] 100 | bucket = "./dist/client" 101 | ``` 102 | 103 | > The values may change based on your build 104 | 105 | ## Skip Requests 106 | 107 | Vite has some builtin middlewares that handle different types of requests from the 108 | client, and in a Vite plugin, we can inject our middlewares along 109 | vite ones. 110 | 111 | Vite-plugin-cloudflare injects a middleware, that is responsible for handling 112 | the worker, So every request from the client (browser) may come to your worker 113 | first, before vite native middlewares. These requests can be assets, 114 | transforms and other types of vite-related requests that should not be handled by 115 | vite-plugin-cloudflare and instead, they should be handled by vite. 116 | 117 | > This concern only occurs in dev mode, so no worries when building for production 118 | 119 | Here's how we handle these type of requests in vite-plugin-cloudflare. 120 | 121 | ```ts 122 | addEventListener("fetch", (event) => { 123 | const { pathname } = new URL(url); 124 | if (pathname.startsWith("/api")) { 125 | event.respondWith(handleFetchEvent(event)); 126 | return; 127 | } 128 | event.respondWith( 129 | new Response("", { 130 | headers: { 131 | "x-skip-request": "", 132 | }, 133 | }) 134 | ); 135 | }); 136 | ``` 137 | 138 | The `x-skip-request` header enforces vite-plugin-cloudflare to skip the response of the worker and passes the 139 | request to the next vite middleware, so Vite would handle the request instead. 140 | 141 | ## Authors 142 | 143 | |
Mohammad Bagher
| 144 | | ------------------------------------------------------------------------------------------------------------------------------------------------ | 145 | 146 | ## Contributing 147 | 148 | Feel free to create issues/discussions and then PRs for the project! 149 | 150 | ## Sponsors 151 | 152 | Your sponsorship can make a huge difference in continuing our work in open source! 153 | 154 |

155 | 156 | 157 | 158 |

159 | --------------------------------------------------------------------------------