├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── release.yml ├── pnpm-workspace.yaml ├── examples └── react-with-vite-on-cloudflare │ ├── src │ ├── components │ │ ├── Page │ │ │ ├── index.tsx │ │ │ └── Page.tsx │ │ ├── Task │ │ │ ├── index.tsx │ │ │ └── Task.tsx │ │ ├── Button │ │ │ ├── index.tsx │ │ │ └── Button.tsx │ │ └── FormField │ │ │ ├── index.tsx │ │ │ ├── FormField.tsx │ │ │ └── InputTextField.tsx │ ├── index.css │ ├── model │ │ ├── session.ts │ │ ├── task.ts │ │ └── github.ts │ ├── vite-env.d.ts │ ├── main.tsx │ ├── trpcUtil.ts │ ├── router │ │ ├── AuthGuard.tsx │ │ └── index.tsx │ ├── App.tsx │ ├── pages │ │ ├── Auth.tsx │ │ ├── Home.tsx │ │ ├── AddTask.tsx │ │ └── Login.tsx │ └── hooks │ │ └── useForm.ts │ ├── postcss.config.cjs │ ├── tsconfig.node.json │ ├── vite.config.ts │ ├── tailwind.config.cjs │ ├── functions │ └── api │ │ ├── oauth │ │ ├── github.ts │ │ └── auth.ts │ │ └── [[trpc]].ts │ ├── wrangler.toml │ ├── .gitignore │ ├── index.html │ ├── .eslintrc.cjs │ ├── README.md │ ├── tsconfig.json │ ├── schema.sql │ ├── package.json │ └── public │ └── vite.svg ├── .gitignore ├── vite.config.ts ├── tsconfig.json ├── .changeset ├── config.json └── README.md ├── packages └── pages-plugin-trpc │ ├── index.d.ts │ ├── package.json │ ├── CHANGELOG.md │ └── functions │ ├── [[path]].test.ts │ └── [[path]].ts ├── package.json ├── README.md └── pnpm-lock.yaml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: toyamarinyon 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/components/Page/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./Page"; 2 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/components/Task/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./Task"; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | examples/dist 3 | node_modules 4 | memo 5 | *.log 6 | *.sqlite3 7 | .wrangler 8 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./Button"; 2 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/components/FormField/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./InputTextField"; 2 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | dir: "functions", 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/model/session.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const sessionScheme = z.object({ 4 | id: z.string(), 5 | }); 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "lib": ["ES2020"], 6 | "types": ["@cloudflare/workers-types"], 7 | "strict": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly CF_PAGES_URL: string?; 5 | } 6 | 7 | interface ImportMeta { 8 | readonly env: ImportMetaEnv; 9 | } 10 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require("@tailwindcss/forms")], 8 | }; 9 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/functions/api/oauth/github.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | const CLIENT_ID: string; 3 | const CLIENT_SECRET: string; 4 | } 5 | 6 | export const onRequestGet: PagesFunction = async ({ request, env }) => { 7 | console.log(JSON.stringify(env, null, 2)) 8 | return new Response("hello"); 9 | }; 10 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "react-with-vite-on-cloudflare" 2 | compatibility_date = "2022-12-26" 3 | 4 | [[ d1_databases ]] 5 | binding = "DB" # i.e. available in your Worker on env.DB 6 | database_name = "example001" 7 | database_id = "eb5a523d-6956-4e7f-942a-7a8d3b946856" # Replace with your ID from `wrangler d1 create` 8 | 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog":["@changesets/changelog-github", { "repo": "toyamarinyon/cloudflare-pages-plugin-trpc" }], 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/.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 | .dev.vars 15 | wrangler.prod.toml 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/model/task.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const taskScheme = z.object({ 4 | id: z.number(), 5 | title: z.string(), 6 | description: z.string(), 7 | completion_datetime: z.number().optional(), 8 | }); 9 | export type Task = z.infer; 10 | export const createTaskScheme = taskScheme.omit({ 11 | id: true, 12 | completion_datetime: true, 13 | }); 14 | export type CreateTaskPayload = z.infer; 15 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/trpcUtil.ts: -------------------------------------------------------------------------------- 1 | // utils/trpc.ts 2 | import { 3 | createTRPCProxyClient, 4 | createTRPCReact, 5 | httpBatchLink, 6 | } from "@trpc/react-query"; 7 | import type { AppRouter } from "../functions/api/[[trpc]]"; 8 | 9 | export const trpc = createTRPCReact(); 10 | export const links = [ 11 | httpBatchLink({ 12 | url: `${window.location.protocol}//${window.location.host}/api/trpc`, 13 | }), 14 | ]; 15 | export const trpcClient = createTRPCProxyClient({ 16 | links, 17 | }); 18 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | const config = { 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:react-hooks/recommended", 7 | ], 8 | parser: "@typescript-eslint/parser", 9 | plugins: ["@typescript-eslint"], 10 | rules: { 11 | "@typescript-eslint/no-unused-vars": [ 12 | "error", 13 | { 14 | argsIgnorePattern: "^_", 15 | varsIgnorePattern: "^_", 16 | }, 17 | ], 18 | }, 19 | root: true, 20 | }; 21 | module.exports = config; 22 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/router/AuthGuard.tsx: -------------------------------------------------------------------------------- 1 | import { trpc } from "../trpcUtil"; 2 | import { useRouter } from "../router"; 3 | import { ReactNode, useEffect } from "react"; 4 | 5 | export const AuthGuard = ({ 6 | children, 7 | }: { 8 | children: ReactNode; 9 | }): JSX.Element => { 10 | const { router } = useRouter(); 11 | const { isLoading, data } = trpc.auth.currentUser.useQuery(); 12 | useEffect(() => { 13 | if (!isLoading && data?.currentUser == null) { 14 | router.push("/login"); 15 | } 16 | }, [isLoading, data?.currentUser, router]); 17 | if (isLoading || data?.currentUser == null) { 18 | return <>; 19 | } 20 | return <>{children}; 21 | }; 22 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 2 | import { useState } from "react"; 3 | import { Router } from "./router"; 4 | import { links, trpc } from "./trpcUtil"; 5 | 6 | export function App() { 7 | const [queryClient] = useState(() => new QueryClient()); 8 | const [trpcClient] = useState(() => 9 | trpc.createClient({ 10 | links, 11 | }) 12 | ); 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/README.md: -------------------------------------------------------------------------------- 1 | # React with Vite on Cloudflare Pages 2 | 3 | # Getting started 4 | 5 | ## Install and authenticate 6 | 7 | ```bash 8 | # npm 9 | npm install -g wrangler 10 | 11 | # yarn 12 | yarn global add wrangler 13 | ``` 14 | 15 | ## Set up Project 16 | ```bash 17 | # Clone the template 18 | npx digit toyamarinyon/cloudflare-pages-plugin-trpc/examples/react-with-vite-on-cloudflare 19 | 20 | cd react-with-vite-on-cloudflare 21 | 22 | # Install dependencies 23 | npm install 24 | ``` 25 | 26 | ## Create your database 27 | ```bash 28 | wrangler d1 create DB 29 | ``` 30 | 31 | ## Create table 32 | wrangler d1 execute DB --local --file=./schema.query 33 | 34 | ## Run 35 | npm run dev 36 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "types": ["@cloudflare/workers-types"] 19 | }, 20 | "include": ["src", "functions"], 21 | "references": [{ "path": "./tsconfig.node.json" }] 22 | } 23 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/components/FormField/FormField.tsx: -------------------------------------------------------------------------------- 1 | import { ZodIssue } from "zod"; 2 | 3 | export interface FormFieldProps { 4 | name: string; 5 | label: string; 6 | children: React.ReactNode; 7 | helperText?: string; 8 | error?: ZodIssue; 9 | } 10 | export const FormField = ({ 11 | name, 12 | label, 13 | children, 14 | error, 15 | }: FormFieldProps): JSX.Element => { 16 | return ( 17 |
18 | 21 |
{children}
22 | {error &&

{error.message}

} 23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/pages-plugin-trpc/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { FetchHandlerRequestOptions } from "@trpc/server/adapters/fetch"; 2 | import type { AnyRouter, inferRouterContext } from "@trpc/server"; 3 | 4 | export type FetchCreateContextWithCloudflareEnvFnOptions = { 5 | req: Request; 6 | env: Env; 7 | }; 8 | 9 | type FetchCreateContextWithCloudflareEnvFn = ( 10 | opts: FetchCreateContextWithCloudflareEnvFnOptions 11 | ) => inferRouterContext | Promise>; 12 | 13 | export type PluginArgs = Omit< 14 | FetchHandlerRequestOptions, 15 | "req" | "createContext" 16 | > & { 17 | createContext?: FetchCreateContextWithCloudflareEnvFn; 18 | }; 19 | 20 | export default function tRPCPagesPluginFunction( 21 | args: PluginArgs 22 | ): PagesFunction; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "page-plugin-trpc-monorepo", 3 | "private": true, 4 | "description": "This plugin allows developers to create tRPC server on Cloudflare Page Function rapidly.", 5 | "license": "MIT", 6 | "bugs": { 7 | "url": "https://github.com/toyamarinyon/cloudflare-pages-plugin-trpc/issues" 8 | }, 9 | "homepage": "https://github.com/toyamarinyon/cloudflare-pages-plugin-trpc", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/toyamarinyon/cloudflare-pages-plugin-trpc.git" 13 | }, 14 | "dependencies": { 15 | "@changesets/changelog-github": "^0.4.8", 16 | "@changesets/cli": "^2.26.0" 17 | }, 18 | "scripts": { 19 | "build": "pnpm -r build", 20 | "test": "pnpm -r test", 21 | "version": "changeset version", 22 | "release": "pnpm build && changeset publish" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/pages-plugin-trpc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudflare-pages-plugin-trpc", 3 | "version": "0.3.2", 4 | "description": "Cloudflare page plugin for tRPC", 5 | "main": "dist/index.js", 6 | "types": "index.d.ts", 7 | "files": [ 8 | "dist/index.js", 9 | "index.d.ts", 10 | "tsconfig.json" 11 | ], 12 | "scripts": { 13 | "test": "vitest", 14 | "build": "npx wrangler pages functions build --plugin --outfile=dist/index.js", 15 | "prepare": "npm run build" 16 | }, 17 | "author": "@toyamarinyon", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "@cloudflare/workers-types": "^3.19.0", 21 | "@trpc/server": "10.16.0", 22 | "esbuild": "^0.14.54", 23 | "vitest": "^0.15.2", 24 | "wrangler": "^2.12.2", 25 | "zod": "^3.21.4" 26 | }, 27 | "peerDependencies": { 28 | "@trpc/server": "10.16.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS tasks; 2 | DROP TABLE IF EXISTS sessions; 3 | DROP TABLE IF EXISTS users; 4 | 5 | CREATE TABLE tasks ( 6 | id INTEGER PRIMARY KEY AUTOINCREMENT, 7 | user_id INTEGER NOT NULL, 8 | title TEXT, 9 | description TEXT, 10 | completion_datetime DATETIME, 11 | FOREIGN KEY (user_id) REFERENCES users(id) 12 | ); 13 | DROP INDEX IF EXISTS idx_tasks_user_id; 14 | CREATE INDEX idx_tasks_user_id ON tasks(user_id); 15 | 16 | 17 | CREATE TABLE users ( 18 | id INTEGER PRIMARY KEY AUTOINCREMENT, 19 | github_user_id INTEGER NOT NULL, 20 | github_oauth_token TEXT NOT NULL 21 | ); 22 | DROP INDEX IF EXISTS idx_users_github_user_id; 23 | CREATE UNIQUE INDEX idx_users_github_user_id ON users(github_user_id); 24 | 25 | CREATE TABLE sessions ( 26 | id CHAR(64) PRIMARY KEY, 27 | user_id INTEGER NOT NULL, 28 | FOREIGN KEY (user_id) REFERENCES users(id) 29 | ); 30 | DROP INDEX IF EXISTS idx_sessions_user_id; 31 | CREATE INDEX idx_sessions_user_id ON sessions(user_id); 32 | -------------------------------------------------------------------------------- /packages/pages-plugin-trpc/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # cloudflare-pages-plugin-trpc 2 | 3 | ## 0.3.2 4 | 5 | ### Patch Changes 6 | 7 | - [#18](https://github.com/toyamarinyon/cloudflare-pages-plugin-trpc/pull/18) [`c8c6fe1`](https://github.com/toyamarinyon/cloudflare-pages-plugin-trpc/commit/c8c6fe1c871ffa622e583e1bb7705d6743561579) Thanks [@toyamarinyon](https://github.com/toyamarinyon)! - support trpc 10.16.0 8 | 9 | ## 0.3.1 10 | 11 | ### Patch Changes 12 | 13 | - [#16](https://github.com/toyamarinyon/cloudflare-pages-plugin-trpc/pull/16) [`228e606`](https://github.com/toyamarinyon/cloudflare-pages-plugin-trpc/commit/228e606abc995d12da898d4f18546deb9470439b) Thanks [@toyamarinyon](https://github.com/toyamarinyon)! - supports trpc v10.15.0 14 | 15 | ## 0.3.0 16 | 17 | ### Minor Changes 18 | 19 | - [#14](https://github.com/toyamarinyon/cloudflare-pages-plugin-trpc/pull/14) [`2b6cedb`](https://github.com/toyamarinyon/cloudflare-pages-plugin-trpc/commit/2b6cedbfa83f448ddd531246109775d30f65d882) Thanks [@toyamarinyon](https://github.com/toyamarinyon)! - Monorepo 20 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/pages/Auth.tsx: -------------------------------------------------------------------------------- 1 | import { useQueryClient } from "@tanstack/react-query"; 2 | import { useEffect } from "react"; 3 | import { z } from "zod"; 4 | import { useRouter, useSearchParam } from "../router"; 5 | import { trpc } from "../trpcUtil"; 6 | 7 | export const Auth = (): JSX.Element => { 8 | const { router } = useRouter(); 9 | 10 | const searchParam = useSearchParam(z.object({ code: z.string() })); 11 | const { mutate } = trpc.auth.login.useMutation(); 12 | const queryClient = useQueryClient(); 13 | 14 | useEffect(() => { 15 | if (!searchParam.success) { 16 | return; 17 | } 18 | mutate( 19 | { 20 | oauthToken: searchParam.data.code, 21 | }, 22 | { 23 | onSuccess: () => { 24 | queryClient 25 | .invalidateQueries(trpc.auth.currentUser.getQueryKey()) 26 | .then(() => { 27 | router.push("/"); 28 | }); 29 | }, 30 | } 31 | ); 32 | }, [mutate, router, searchParam, queryClient]); 33 | 34 | return <>; 35 | }; 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | - push 3 | - pull_request 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | 13 | - name: Install Node.js 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: 18 17 | 18 | - uses: pnpm/action-setup@v2 19 | name: Install pnpm 20 | id: pnpm-install 21 | with: 22 | version: 7 23 | run_install: false 24 | 25 | - name: Get pnpm store directory 26 | id: pnpm-cache 27 | shell: bash 28 | run: | 29 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 30 | 31 | - uses: actions/cache@v3 32 | name: Setup pnpm cache 33 | with: 34 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 35 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 36 | restore-keys: | 37 | ${{ runner.os }}-pnpm-store- 38 | 39 | - name: Install dependencies 40 | run: pnpm install 41 | - run: pnpm test 42 | - run: pnpm build 43 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/components/Page/Page.tsx: -------------------------------------------------------------------------------- 1 | import { HeartIcon } from "@heroicons/react/solid"; 2 | import { ReactNode } from "react"; 3 | 4 | interface Props { 5 | title: string; 6 | children: ReactNode; 7 | } 8 | export const Page = ({ title, children }: Props): JSX.Element => { 9 | return ( 10 |
11 |
12 |
13 | This app is powered by Cloudflare D1 14 | 15 | 16 | 20 | cloudflare-pages-plugin-trpc 21 | 22 | . 23 | 24 |
25 |
26 |
27 |

{title}

28 | {children} 29 |
30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import { trpc } from "../trpcUtil"; 2 | import { PlusIcon } from "@heroicons/react/solid"; 3 | import { Link } from "../router"; 4 | import { Page } from "../components/Page"; 5 | import { Task } from "../components/Task"; 6 | 7 | export const Home = (): JSX.Element => { 8 | const postQuery = trpc.tasks.list.useQuery(); 9 | if (postQuery.isInitialLoading) { 10 | return
Loading...
; 11 | } 12 | return ( 13 | 14 | {postQuery.isFetching ? ( 15 |
loading...
16 | ) : postQuery.data?.tasks == null || postQuery.data.tasks.length === 0 ? ( 17 | <> 18 | ) : ( 19 |
    20 | {postQuery.data.tasks.map((task) => ( 21 |
  • 22 | 23 |
  • 24 | ))} 25 |
26 | )} 27 | 31 | 32 | Add task 33 | 34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/components/Task/Task.tsx: -------------------------------------------------------------------------------- 1 | import { CheckIcon } from "@heroicons/react/outline"; 2 | import { useQueryClient } from "@tanstack/react-query"; 3 | import { useCallback } from "react"; 4 | import { Task as TaskModel } from "../../model/task"; 5 | import { trpc } from "../../trpcUtil"; 6 | 7 | type Props = TaskModel; 8 | export const Task = ({ title, description, id }: Props): JSX.Element => { 9 | const { mutateAsync } = trpc.tasks.complete.useMutation(); 10 | const queryClient = useQueryClient(); 11 | const completeTask = useCallback(async () => { 12 | await mutateAsync( 13 | { id }, 14 | { 15 | onSuccess: () => { 16 | queryClient.invalidateQueries(trpc.tasks.list.getQueryKey()); 17 | }, 18 | } 19 | ); 20 | }, [id, mutateAsync, queryClient]); 21 | return ( 22 |
23 | 29 |
30 |

{title}

31 |

{description}

32 |
33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/hooks/useForm.ts: -------------------------------------------------------------------------------- 1 | import { ChangeEventHandler, useCallback, useState } from "react"; 2 | import { z, AnyZodObject } from "zod"; 3 | 4 | export const useForm = (scheme: Z) => { 5 | type Scheme = z.infer; 6 | const [values, setValues] = useState({}); 7 | const [errors, setErrors] = useState(); 8 | const register = useCallback( 9 | (key: keyof Scheme) => { 10 | const onChange: ChangeEventHandler< 11 | HTMLInputElement | HTMLTextAreaElement 12 | > = (e) => { 13 | setValues((prev) => ({ ...prev, [key]: e.target.value })); 14 | }; 15 | const value = values?.[key] ?? ""; 16 | const error = errors?.issues.find((issue) => 17 | issue.path.find((path) => path === key) 18 | ); 19 | return { onChange, value, name: key, error }; 20 | }, 21 | [values, errors] 22 | ); 23 | const handleSubmit = useCallback( 24 | (submitHandler: (value: Scheme) => Promise) => 25 | async (e: React.FormEvent) => { 26 | e.preventDefault(); 27 | const result = scheme.safeParse(values); 28 | if (result.success) { 29 | await submitHandler(result.data); 30 | } else { 31 | setErrors(result.error); 32 | } 33 | }, 34 | [values, scheme] 35 | ); 36 | return { register, handleSubmit, errors }; 37 | }; 38 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/pages/AddTask.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { Button } from "../components/Button"; 3 | import { InputTextField } from "../components/FormField"; 4 | import { Page } from "../components/Page"; 5 | import { useForm } from "../hooks/useForm"; 6 | import { trpc } from "../trpcUtil"; 7 | import { CreateTaskPayload, createTaskScheme } from "../model/task"; 8 | import { useRouter } from "../router"; 9 | 10 | export const AddTask = (): JSX.Element => { 11 | const { router } = useRouter(); 12 | const { mutateAsync, isLoading } = trpc.tasks.create.useMutation(); 13 | const { register, handleSubmit } = useForm(createTaskScheme); 14 | const submit = useCallback( 15 | async (data: CreateTaskPayload) => { 16 | await mutateAsync(data); 17 | router.push("/") 18 | 19 | }, 20 | [mutateAsync, router] 21 | ); 22 | 23 | return ( 24 | 25 |
26 |
27 | 28 | 33 |
34 | 37 |
38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-with-vite-on-cloudflare", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "npx wrangler pages dev --local --persist --d1=DB --compatibility-date=2022-12-26 -- npm run dev:vite", 8 | "dev:vite": "vite dev", 9 | "build": "tsc && vite build" 10 | }, 11 | "dependencies": { 12 | "@heroicons/react": "^1.0.6", 13 | "@tanstack/react-query": "^4.20.4", 14 | "@trpc/client": "10.7.0", 15 | "@trpc/react-query": "10.7.0", 16 | "@trpc/server": "10.7.0", 17 | "classnames": "^2.3.2", 18 | "history": "^5.3.0", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "react-ux-form": "^1.1.5", 22 | "webcrypt-session": "^0.5.1", 23 | "zod": "^3.20.2" 24 | }, 25 | "devDependencies": { 26 | "@cloudflare/workers-types": "^4.20221111.1", 27 | "@tailwindcss/forms": "^0.5.3", 28 | "@types/react": "^18.0.26", 29 | "@types/react-dom": "^18.0.9", 30 | "@typescript-eslint/eslint-plugin": "^5.47.1", 31 | "@typescript-eslint/parser": "^5.47.1", 32 | "@vitejs/plugin-react": "^3.0.0", 33 | "autoprefixer": "^10.4.13", 34 | "cloudflare-pages-plugin-trpc": "0.2.1", 35 | "eslint": "^8.30.0", 36 | "eslint-plugin-react-hooks": "^4.6.0", 37 | "postcss": "^8.4.20", 38 | "tailwindcss": "^3.2.4", 39 | "typescript": "^4.9.4", 40 | "vite": "^4.0.0", 41 | "wrangler": "^2.6.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/model/github.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | /** 4 | * @see https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user 5 | */ 6 | export const userScheme = z.object({ 7 | login: z.string(), 8 | id: z.number(), 9 | avatar_url: z.string(), 10 | }); 11 | 12 | export const getUser = async (accessToken: string) => { 13 | const response = await fetch("https://api.github.com/user", { 14 | headers: { 15 | "content-type": "application/json", 16 | "user-agent": "cloudflare-worker", 17 | accept: "application/json", 18 | Authorization: `Bearer ${accessToken}`, 19 | }, 20 | }); 21 | return userScheme.parse(await response.json()); 22 | }; 23 | 24 | export const accessTokenScheme = z.object({ 25 | access_token: z.string(), 26 | token_type: z.string(), 27 | scope: z.string(), 28 | }); 29 | export const getAccessToken = async ({ 30 | code, 31 | clientId, 32 | clientSecret, 33 | }: { 34 | code: string; 35 | clientId: string; 36 | clientSecret: string; 37 | }) => { 38 | const response = await fetch("https://github.com/login/oauth/access_token", { 39 | method: "POST", 40 | headers: { 41 | "content-type": "application/json", 42 | "user-agent": "cloudflare-worker", 43 | accept: "application/json", 44 | }, 45 | body: JSON.stringify({ 46 | client_id: clientId, 47 | client_secret: clientSecret, 48 | code, 49 | }), 50 | }); 51 | return accessTokenScheme.parse(await response.json()).access_token; 52 | }; 53 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import cn from "classnames"; 2 | import { ButtonHTMLAttributes, ReactNode } from "react"; 3 | 4 | const Loader = (): JSX.Element => { 5 | return ( 6 | 12 | 20 | 25 | 26 | ); 27 | }; 28 | 29 | type Props = { 30 | children: ReactNode; 31 | fullWidth?: boolean; 32 | loading?: boolean; 33 | } & Pick, "type" | "disabled">; 34 | export const Button = (props: Props): JSX.Element => { 35 | return ( 36 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | if: ${{ (github.ref_name == 'main' || github.head_ref == 'next') && github.repository_owner == 'toyamarinyon' }} 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout Repo 17 | uses: actions/checkout@v3 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | - name: Setup PNPM 22 | uses: pnpm/action-setup@v2.2.1 23 | with: 24 | version: 7 25 | run_install: false 26 | 27 | - name: Setup Node 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: 18 31 | cache: 'pnpm' 32 | 33 | - name: Install Dependencies 34 | run: pnpm install 35 | 36 | - name: Create Release Pull Request or Publish to npm 37 | id: changesets 38 | uses: changesets/action@v1 39 | with: 40 | # Note: pnpm install after versioning is necessary to refresh lockfile 41 | version: pnpm run version 42 | publish: pnpm run release 43 | commit: '[ci] release' 44 | title: '[ci] release' 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 48 | 49 | # - name: Send a Slack notification if a publish happens 50 | # if: steps.changesets.outputs.published == 'true' 51 | # # You can do something when a publish happens. 52 | # run: my-slack-bot send-notification --message "A new version of ${GITHUB_REPOSITORY} was published!" 53 | -------------------------------------------------------------------------------- /examples/react-with-vite-on-cloudflare/src/components/FormField/InputTextField.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEventHandler, useCallback } from "react"; 2 | import { ZodIssue } from "zod"; 3 | import { FormField, FormFieldProps } from "./FormField"; 4 | 5 | type Props = Omit & { 6 | multiline?: boolean; 7 | value: string; 8 | placeholder?: string; 9 | onChange?: ChangeEventHandler; 10 | error?: ZodIssue 11 | }; 12 | 13 | export const InputTextField = ({ multiline, ...props }: Props): JSX.Element => { 14 | const { name, label, onChange, error } = props; 15 | const handleChange = useCallback( 16 | (e: React.ChangeEvent) => { 17 | onChange?.(e); 18 | }, 19 | [onChange] 20 | ); 21 | return ( 22 | 23 | {/* 24 | http:// 25 | */} 26 | {multiline ? ( 27 |