diff --git a/cloudflare-d1/README.md b/cloudflare-d1/README.md index 6406c02..e7e300c 100644 --- a/cloudflare-d1/README.md +++ b/cloudflare-d1/README.md @@ -50,10 +50,24 @@ npm run build Deployment is done using the Wrangler CLI. -To deploy directly to production: +First, you need to create a d1 database in Cloudflare. ```sh -npx wrangler deploy +npx wrangler d1 create +``` + +Be sure to update the `wrangler.toml` file with the correct database name and id. + +You will also need to [update the `drizzle.config.ts` file](https://orm.drizzle.team/docs/guides/d1-http-with-drizzle-kit), and then run the production migration: + +```sh +npm run db:migrate-production +``` + +To build and deploy directly to production: + +```sh +npm run deploy ``` To deploy a preview URL: diff --git a/cloudflare-d1/app/routes/home.tsx b/cloudflare-d1/app/routes/home.tsx index 7ca780f..8253b9d 100644 --- a/cloudflare-d1/app/routes/home.tsx +++ b/cloudflare-d1/app/routes/home.tsx @@ -1,4 +1,3 @@ -import { database } from "~/database/context"; import * as schema from "~/database/schema"; import type { Route } from "./+types/home"; @@ -11,7 +10,7 @@ export function meta({}: Route.MetaArgs) { ]; } -export async function action({ request }: Route.ActionArgs) { +export async function action({ request, context }: Route.ActionArgs) { const formData = await request.formData(); let name = formData.get("name"); let email = formData.get("email"); @@ -25,18 +24,15 @@ export async function action({ request }: Route.ActionArgs) { return { guestBookError: "Name and email are required" }; } - const db = database(); try { - await db.insert(schema.guestBook).values({ name, email }); + await context.db.insert(schema.guestBook).values({ name, email }); } catch (error) { return { guestBookError: "Error adding to guest book" }; } } export async function loader({ context }: Route.LoaderArgs) { - const db = database(); - - const guestBook = await db.query.guestBook.findMany({ + const guestBook = await context.db.query.guestBook.findMany({ columns: { id: true, name: true, @@ -45,7 +41,7 @@ export async function loader({ context }: Route.LoaderArgs) { return { guestBook, - message: context.VALUE_FROM_CLOUDFLARE, + message: context.cloudflare.env.VALUE_FROM_CLOUDFLARE, }; } diff --git a/cloudflare-d1/database/context.ts b/cloudflare-d1/database/context.ts deleted file mode 100644 index c5cce22..0000000 --- a/cloudflare-d1/database/context.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { AsyncLocalStorage } from "node:async_hooks"; - -import type { DrizzleD1Database } from "drizzle-orm/d1"; - -import * as schema from "./schema"; - -export const DatabaseContext = new AsyncLocalStorage< - DrizzleD1Database ->(); - -export function database() { - const db = DatabaseContext.getStore(); - if (!db) { - throw new Error("DatabaseContext not set"); - } - return db; -} diff --git a/cloudflare-d1/load-context.ts b/cloudflare-d1/load-context.ts new file mode 100644 index 0000000..a9bca3b --- /dev/null +++ b/cloudflare-d1/load-context.ts @@ -0,0 +1,32 @@ +import { drizzle, type DrizzleD1Database } from "drizzle-orm/d1"; +import * as schema from "./database/schema"; +import type { ExecutionContext } from "@cloudflare/workers-types"; +import type { AppLoadContext } from "react-router"; + +declare global { + interface CloudflareEnvironment extends Env {} +} + +declare module "react-router" { + export interface AppLoadContext { + cloudflare: { + env: CloudflareEnvironment; + ctx: Omit; + }; + db: DrizzleD1Database; + } +} + +type GetLoadContextArgs = { + request: Request; + context: Pick; +}; + +export function getLoadContext({ context }: GetLoadContextArgs) { + const db = drizzle(context.cloudflare.env.DB, { schema }); + + return { + cloudflare: context.cloudflare, + db, + }; +} diff --git a/cloudflare-d1/package.json b/cloudflare-d1/package.json index 894d7bb..24781f3 100644 --- a/cloudflare-d1/package.json +++ b/cloudflare-d1/package.json @@ -8,6 +8,7 @@ "db:migrate-production": "dotenv -- drizzle-kit migrate", "dev": "react-router dev", "start": "wrangler dev", + "deploy": "npm run build && wrangler deploy", "typecheck": "react-router typegen && tsc -b" }, "dependencies": { @@ -21,7 +22,6 @@ }, "devDependencies": { "@cloudflare/workers-types": "^4.20241112.0", - "@hiogawa/vite-node-miniflare": "0.1.1", "@react-router/dev": "*", "@types/node": "^20", "@types/react": "^19.0.1", diff --git a/cloudflare-d1/tsconfig.cloudflare.json b/cloudflare-d1/tsconfig.cloudflare.json index fea23d7..f7acf06 100644 --- a/cloudflare-d1/tsconfig.cloudflare.json +++ b/cloudflare-d1/tsconfig.cloudflare.json @@ -6,7 +6,9 @@ "app/**/.server/**/*", "app/**/.client/**/*", "database/**/*", - "workers/**/*" + "workers/**/*", + "load-context.ts", + "worker-configuration.d.ts" ], "compilerOptions": { "composite": true, diff --git a/cloudflare-d1/tsconfig.node.json b/cloudflare-d1/tsconfig.node.json index 8e3f1d3..d04147d 100644 --- a/cloudflare-d1/tsconfig.node.json +++ b/cloudflare-d1/tsconfig.node.json @@ -1,6 +1,12 @@ { "extends": "./tsconfig.json", - "include": ["tailwind.config.ts", "vite.config.ts"], + "include": [ + "tailwind.config.ts", + "vite.config.ts", + "database/**/*", + "load-context.ts", + "worker-configuration.d.ts" + ], "compilerOptions": { "composite": true, "strict": true, diff --git a/cloudflare-d1/vite.config.ts b/cloudflare-d1/vite.config.ts index f08a45a..19b2e0e 100644 --- a/cloudflare-d1/vite.config.ts +++ b/cloudflare-d1/vite.config.ts @@ -1,9 +1,10 @@ -import { vitePluginViteNodeMiniflare } from "@hiogawa/vite-node-miniflare"; import { reactRouter } from "@react-router/dev/vite"; +import { cloudflareDevProxy } from "@react-router/dev/vite/cloudflare"; import autoprefixer from "autoprefixer"; import tailwindcss from "tailwindcss"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; +import { getLoadContext } from "./load-context"; export default defineConfig(({ isSsrBuild }) => ({ build: { @@ -21,7 +22,6 @@ export default defineConfig(({ isSsrBuild }) => ({ ssr: { target: "webworker", noExternal: true, - external: ["node:async_hooks"], resolve: { conditions: ["workerd", "browser"], }, @@ -37,15 +37,8 @@ export default defineConfig(({ isSsrBuild }) => ({ }, }, plugins: [ - vitePluginViteNodeMiniflare({ - entry: "./workers/app.ts", - miniflareOptions: (options) => { - options.compatibilityDate = "2024-11-18"; - options.compatibilityFlags = ["nodejs_compat"]; - options.d1Databases = { DB: "your-database-id" }; - // match where wrangler applies migrations to - options.d1Persist = ".wrangler/state/v3/d1"; - }, + cloudflareDevProxy({ + getLoadContext, }), reactRouter(), tsconfigPaths(), diff --git a/cloudflare-d1/worker-configuration.d.ts b/cloudflare-d1/worker-configuration.d.ts new file mode 100644 index 0000000..9376344 --- /dev/null +++ b/cloudflare-d1/worker-configuration.d.ts @@ -0,0 +1,6 @@ +// Generated by Wrangler by running `wrangler types` + +interface Env { + VALUE_FROM_CLOUDFLARE: "Hello from Cloudflare"; + DB: D1Database; +} diff --git a/cloudflare-d1/workers/app.ts b/cloudflare-d1/workers/app.ts index 4025aac..2d41703 100644 --- a/cloudflare-d1/workers/app.ts +++ b/cloudflare-d1/workers/app.ts @@ -1,19 +1,6 @@ -import { drizzle } from "drizzle-orm/d1"; +import { getLoadContext } from "load-context"; import { createRequestHandler } from "react-router"; -import { DatabaseContext } from "~/database/context"; -import * as schema from "~/database/schema"; - -interface CloudflareEnvironment { - DB: D1Database; -} - -declare module "react-router" { - export interface AppLoadContext { - VALUE_FROM_CLOUDFLARE: string; - } -} - const requestHandler = createRequestHandler( // @ts-expect-error - virtual module provided by React Router at build time () => import("virtual:react-router/server-build"), @@ -21,12 +8,11 @@ const requestHandler = createRequestHandler( ); export default { - fetch(request, env) { - const db = drizzle(env.DB, { schema }); - return DatabaseContext.run(db, () => - requestHandler(request, { - VALUE_FROM_CLOUDFLARE: "Hello from Cloudflare", - }) - ); + fetch(request, env, ctx) { + const loadContext = getLoadContext({ + request, + context: { cloudflare: { env, ctx } }, + }); + return requestHandler(request, loadContext); }, } satisfies ExportedHandler; diff --git a/cloudflare-d1/wrangler.toml b/cloudflare-d1/wrangler.toml index 6f2c640..65df886 100644 --- a/cloudflare-d1/wrangler.toml +++ b/cloudflare-d1/wrangler.toml @@ -5,6 +5,9 @@ compatibility_flags = ["nodejs_compat"] main = "./build/server/index.js" assets = { directory = "./build/client/" } +[vars] +VALUE_FROM_CLOUDFLARE = "Hello from Cloudflare" + [[d1_databases]] binding = "DB" database_name = "your-database-name" diff --git a/cloudflare/README.md b/cloudflare/README.md index 2c0ba39..6a31081 100644 --- a/cloudflare/README.md +++ b/cloudflare/README.md @@ -44,10 +44,10 @@ npm run build Deployment is done using the Wrangler CLI. -To deploy directly to production: +To build and deploy directly to production: ```sh -npx wrangler deploy +npm run deploy ``` To deploy a preview URL: diff --git a/cloudflare/package.json b/cloudflare/package.json index b029a82..773b471 100644 --- a/cloudflare/package.json +++ b/cloudflare/package.json @@ -5,6 +5,7 @@ "build": "react-router build", "dev": "react-router dev", "start": "wrangler dev", + "deploy": "npm run build && wrangler deploy", "typecheck": "react-router typegen && tsc -b" }, "dependencies": { diff --git a/cloudflare/workers/app.ts b/cloudflare/workers/app.ts index bdc0f1b..a0a7946 100644 --- a/cloudflare/workers/app.ts +++ b/cloudflare/workers/app.ts @@ -21,7 +21,6 @@ const requestHandler = createRequestHandler( export default { fetch(request, env, ctx) { - console.log("yo"); return requestHandler(request, { cloudflare: { env, ctx }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7aa6718..9fb0f5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -115,9 +115,6 @@ importers: '@cloudflare/workers-types': specifier: ^4.20241112.0 version: 4.20241112.0 - '@hiogawa/vite-node-miniflare': - specifier: 0.1.1 - version: 0.1.1(vite@5.4.11(@types/node@20.17.6)) '@react-router/dev': specifier: '*' version: 7.1.1(@react-router/serve@7.1.1(react-router@7.1.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.2))(@types/node@20.17.6)(react-router@7.1.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.2)(vite@5.4.11(@types/node@20.17.6))(wrangler@3.88.0(@cloudflare/workers-types@4.20241112.0)) @@ -1792,30 +1789,6 @@ packages: '@fastify/static@7.0.4': resolution: {integrity: sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==} - '@hattip/adapter-node@0.0.44': - resolution: {integrity: sha512-Qf0kcrE4yHFrmKgfntrxHGNkk0/9a45aKmAO3ex6OiRKat5DZCFDdFMHV85Z594IohzT7Q+WcWiHQl/b0Jvo7Q==} - - '@hattip/compose@0.0.44': - resolution: {integrity: sha512-0K8fhk3dOI8p6FmCxiGPz5Cr1Svu65Q19/hX2r5OJVdj0U6D/yPzrXyMDQ0Mr8h7PWtAIDCJpa/HxQw+z84HZQ==} - - '@hattip/core@0.0.44': - resolution: {integrity: sha512-hW0scygJk2FjC8lL+NAaV8eY5XtseXvX+EyORFgtURJQ33myy/epSFm7uLZcVNVU9aA0C4WyJQSza+U0rxt2Sg==} - - '@hattip/headers@0.0.44': - resolution: {integrity: sha512-bwNQ+o8RRTox9FWkmprTP8/rNki9BXkrthCDdnT5zgfkjZk6VFAloV5jmYqnqrNkh3Aw5DFGUcQJRoQBb4aQCw==} - - '@hattip/polyfills@0.0.44': - resolution: {integrity: sha512-ldNES22u/135K8Yg6nti1djcwZeIw6e1SY/iqW0qh+AfyYNHuGTix7YL/9OKAD9wbOUj2NHqXYOGCT69NsfIqA==} - - '@hattip/walk@0.0.44': - resolution: {integrity: sha512-tjYO71byyQpdsOVKJ4cySAwWSYqU9IWbROiU1D9SqlQyEYN+TOEaB6n8YkX1gyRbLVkMe+/DJcuagm9AQdPO5A==} - hasBin: true - - '@hiogawa/vite-node-miniflare@0.1.1': - resolution: {integrity: sha512-JG3C6rh7AXLP/YWIr6kncoJ3aXzMTrZNwQPh6bIn0N1VMhVKFCC/agvBKWVyKnpEkcgbOQEhsz9YeZbmunukRQ==} - peerDependencies: - vite: '*' - '@humanwhocodes/momoa@2.0.4': resolution: {integrity: sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==} engines: {node: '>=10.10.0'} @@ -1855,9 +1828,6 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@kamilkisiela/fast-url-parser@1.1.4': - resolution: {integrity: sha512-gbkePEBupNydxCelHCESvFSFM8XPh1Zs/OAVRW/rKpEqPAl5PbOM90Si8mv9bvnR53uPD2s/FiRxdvSejpRJew==} - '@lukeed/ms@2.0.2': resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} engines: {node: '>=8'} @@ -2556,14 +2526,6 @@ packages: '@vercel/static-config@3.0.0': resolution: {integrity: sha512-2qtvcBJ1bGY0dYGYh3iM7yGKkk971FujLEDXzuW5wcZsPr1GSEjO/w2iSr3qve6nDDtBImsGoDEnus5FI4+fIw==} - '@whatwg-node/fetch@0.9.23': - resolution: {integrity: sha512-7xlqWel9JsmxahJnYVUj/LLxWcnA93DR4c9xlw3U814jWTiYalryiH1qToik1hOxweKKRLi4haXHM5ycRksPBA==} - engines: {node: '>=18.0.0'} - - '@whatwg-node/node-fetch@0.6.0': - resolution: {integrity: sha512-tcZAhrpx6oVlkEsRngeTEEE7I5/QdLjeEz4IlekabGaESP7+Dkm/6a9KcF1KdCBB7mO9PXtBkwCuTCt8+UPg8Q==} - engines: {node: '>=18.0.0'} - '@xhmikosr/archive-type@6.0.1': resolution: {integrity: sha512-PB3NeJL8xARZt52yDBupK0dNPn8uIVQDe15qNehUpoeeLWCZyAOam4vGXnoZGz2N9D1VXtjievJuCsXam2TmbQ==} engines: {node: ^14.14.0 || >=16.0.0} @@ -2936,10 +2898,6 @@ packages: builtins@5.1.0: resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==} - busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - byline@5.0.0: resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} engines: {node: '>=0.10.0'} @@ -6192,10 +6150,6 @@ packages: stream-slice@0.1.2: resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==} - streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - streamx@2.21.0: resolution: {integrity: sha512-Qz6MsDZXJ6ur9u+b+4xCG18TluU7PGlRfXVAAjNiGsFrBUt/ioyLkxbFaKJygoPs+/kW4VyBj0bSj89Qu0IGyg==} @@ -6656,9 +6610,6 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - urlpattern-polyfill@10.0.0: - resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} - urlpattern-polyfill@8.0.2: resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==} @@ -7751,45 +7702,6 @@ snapshots: fastq: 1.17.1 glob: 10.4.5 - '@hattip/adapter-node@0.0.44': - dependencies: - '@hattip/core': 0.0.44 - '@hattip/polyfills': 0.0.44 - '@hattip/walk': 0.0.44 - - '@hattip/compose@0.0.44': - dependencies: - '@hattip/core': 0.0.44 - - '@hattip/core@0.0.44': {} - - '@hattip/headers@0.0.44': - dependencies: - '@hattip/core': 0.0.44 - - '@hattip/polyfills@0.0.44': - dependencies: - '@hattip/core': 0.0.44 - '@whatwg-node/fetch': 0.9.23 - node-fetch-native: 1.6.4 - - '@hattip/walk@0.0.44': - dependencies: - '@hattip/headers': 0.0.44 - cac: 6.7.14 - mime-types: 2.1.35 - - '@hiogawa/vite-node-miniflare@0.1.1(vite@5.4.11(@types/node@20.17.6))': - dependencies: - '@hattip/adapter-node': 0.0.44 - '@hattip/compose': 0.0.44 - miniflare: 3.20241106.0 - vite: 5.4.11(@types/node@20.17.6) - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - '@humanwhocodes/momoa@2.0.4': {} '@iarna/toml@2.2.5': {} @@ -7835,8 +7747,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@kamilkisiela/fast-url-parser@1.1.4': {} - '@lukeed/ms@2.0.2': {} '@mapbox/node-pre-gyp@1.0.11(supports-color@9.4.0)': @@ -8784,18 +8694,6 @@ snapshots: json-schema-to-ts: 1.6.4 ts-morph: 12.0.0 - '@whatwg-node/fetch@0.9.23': - dependencies: - '@whatwg-node/node-fetch': 0.6.0 - urlpattern-polyfill: 10.0.0 - - '@whatwg-node/node-fetch@0.6.0': - dependencies: - '@kamilkisiela/fast-url-parser': 1.1.4 - busboy: 1.6.0 - fast-querystring: 1.1.2 - tslib: 2.8.1 - '@xhmikosr/archive-type@6.0.1': dependencies: file-type: 18.7.0 @@ -9224,10 +9122,6 @@ snapshots: dependencies: semver: 7.6.3 - busboy@1.6.0: - dependencies: - streamsearch: 1.1.0 - byline@5.0.0: {} bytes@3.1.2: {} @@ -12680,8 +12574,6 @@ snapshots: stream-slice@0.1.2: {} - streamsearch@1.1.0: {} - streamx@2.21.0: dependencies: fast-fifo: 1.3.2 @@ -13218,8 +13110,6 @@ snapshots: dependencies: punycode: 2.3.1 - urlpattern-polyfill@10.0.0: {} - urlpattern-polyfill@8.0.2: {} util-deprecate@1.0.2: {}