├── .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 |
12 | - Rendered to HTML.
13 | -
14 | Interactive.
15 |
16 |
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 | -
12 | {title} ({release_date})
13 |
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 |
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 |
--------------------------------------------------------------------------------