Merge pull request #55 from hieunc229/pr-52

Pr 52
This commit is contained in:
Hieu Nguyen (Jack) authored and GitHub committed 2026-09-28 16:23:52 +07:00
commit 73d8ebfe16
24 files changed
+5593 -5014

No files matched your search

+2 -1
View File
@@ -1,6 +1,7 @@
node_modules
.next
.open-next
.next-node
.vinext
.wrangler
dist
data
+4 -4
View File
@@ -15,6 +15,7 @@
# next.js
/.next/
/.next-node/
/out/
# production
@@ -44,14 +45,12 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
# OpenNext
/.open-next
# vinext
/.vinext
/src/lib/migrations/bundle.json
# wrangler files
.wrangler
.wrangler.dev.json
.wrangler.dev.json.*.tmp
.dev.vars*
@@ -62,3 +61,4 @@ mailflare-landing
.agents
skills-lock.json
/dist/
+13 -11
View File
@@ -5,18 +5,18 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Commands
```bash
npm run dev # next dev (Cloudflare bindings via initOpenNextCloudflareForDev)
npm run dev # Vite + vinext in local workerd
npm run lint # eslint (next/core-web-vitals + next/typescript)
npm run build # next build only — does NOT produce the deployable Worker
npm run build # vinext build, including the complete Worker
npm run db:generate # drizzle-kit generate from src/db/schema/index.ts
npm run db:migrate:local # wrangler d1 migrations apply DB --local
npm run db:migrate:remote # --remote (needs a concrete database_id in wrangler.jsonc)
npm run db:seed # POST /api/seed against localhost:3000
npm run deploy # opennextjs-cloudflare build + wrangler deploy
npm run deploy # vinext build + wrangler deploy
npm run deploy # build and deploy; migrate later from Admin settings
npm run preview # local OpenNext preview
npm run preview # build and preview the vinext Worker locally
npm run cf-typegen # regenerate cloudflare-env.d.ts from wrangler.jsonc
```
@@ -24,21 +24,21 @@ There is no test script in `package.json`; the checks under `tests/` are `node:t
`next.config.ts` sets `typescript.ignoreBuildErrors: true` and `tsconfig.json` sets `noImplicitAny: false`, so the build will not catch type errors. Run `npx tsc --noEmit` if you want real type checking.
Do not deploy with `opennextjs-cloudflare deploy`. The deploy script deliberately builds with OpenNext and uploads with Wrangler because `worker.ts` — not the generated Next worker — is the entrypoint.
`npm run deploy` builds with vinext and uploads with Wrangler. The Cloudflare Vite plugin generates `dist/server/wrangler.json` and redirects Wrangler to it, preserving the custom `worker.ts` entrypoint.
## Architecture
Next.js 16 App Router running on Cloudflare Workers via OpenNext. Drizzle ORM over D1, R2 for raw MIME, attachments, and record backups, Queues for async mail processing, a Durable Object for realtime, and a cron trigger for scheduled backups.
Next.js App Router APIs running on Cloudflare Workers via vinext. Drizzle ORM over D1, R2 for raw MIME, attachments, and record backups, Queues for async mail processing, a Durable Object for realtime, and a cron trigger for scheduled backups.
### worker.ts is the entrypoint
`worker.ts` wraps the generated `.open-next/worker.js` and adds handlers Next.js cannot express:
`worker.ts` wraps `vinext/server/fetch-handler` and adds handlers Next.js cannot express:
- **`fetch`** — intercepts `/api/realtime` for the WebSocket upgrade (authenticates the session cookie, then routes to `env.REALTIME.getByName(user.id)`), delegating everything else to Next.
- **`fetch`** — intercepts `/api/realtime` for the WebSocket upgrade (authenticates the session cookie, then routes to `env.REALTIME.getByName(user.id)`), delegating everything else to vinext.
- **`email`** — the Cloudflare Email Routing handler. Resolves domain routing rules first (`resolveIncomingMail` in `src/lib/email/incoming.ts`) because `message.setReject()` and `message.forward()` only exist here, then applies optional account-level forwarding (loop-guarded by the `MAILFLARE_FORWARDED_HEADER`), writes raw MIME to R2, and enqueues to `INBOUND_QUEUE`. It never parses mail inline.
- **`queue`** — a single consumer for both queues; `isInboundQueueMessage` and `isWebhookRetryMessage` in `worker-utils.ts` discriminate inbound mail, webhook retries, and outbound payloads. Failures `retry({ delaySeconds: 10 })`.
It also re-exports `RealtimeHub`, which is why that class must live outside the Next build.
It also re-exports `RealtimeHub`, which must remain exported from the Worker entrypoint.
### Mail pipeline
@@ -81,7 +81,9 @@ The setup path only ever initializes an empty database — it refuses to touch o
### Two runtimes, one code path
The app reaches every platform service through `getEnv()` (`src/lib/cloudflare.ts`). On Workers that is OpenNext's context. In the self-hosted runtime, `server/index.ts` builds an object with the same shape (`server/runtime/env.ts`: a D1-compatible wrapper over better-sqlite3, an R2-compatible file bucket, `EMAIL` over nodemailer or the Cloudflare Sending REST API, in-process queues, a WebSocket hub standing in for the Durable Object, a fixed-window rate limiter) and publishes it as `globalThis.__mailflareNodeEnv` before Next starts; `getNodeEnv()` in `src/lib/runtime.ts` returns it. Application code must not care which one it got. The few places that must differ check `isNodeRuntime(env)`: setup requirement checks, the self-update button, and domain provisioning, which without Cloudflare credentials records the zone as `"manual"` (`src/lib/domains/provision.ts`) so every Cloudflare call is a no-op and the DNS page lists records to set by hand. Inbound mail off Workers goes through `intakeIncomingMail` (`src/lib/email/intake.ts`) from either the SMTP listener (`server/runtime/smtp.ts`) or the signed `/api/inbound` webhook the relay Worker in `deploy/cloudflare-email-relay` calls. `npm run build:node` builds Next in Node mode and bundles the server with esbuild to `dist/server.mjs`; the Dockerfile runs that. Migrations are applied from `drizzle/migrations` at start (`server/runtime/migrate.ts`), so the bootstrap schema in `src/lib/setup/migration.ts` is not used there.
The Node build aliases `cloudflare:workers` to `server/runtime/cloudflare-workers.ts`, allowing the shared helper to use its existing `getNodeEnv()` fallback. That alias only applies when `MAILFLARE_RUNTIME=node`; vinext uses the native Workers module. Next outputs to `.next-node` so its generated types do not collide with vinext's `.next/types`.
The app reaches every platform service through `getEnv()` (`src/lib/cloudflare.ts`). On Workers that is the native `cloudflare:workers` env. In the self-hosted runtime, `server/index.ts` builds an object with the same shape (`server/runtime/env.ts`: a D1-compatible wrapper over better-sqlite3, an R2-compatible file bucket, `EMAIL` over nodemailer or the Cloudflare Sending REST API, in-process queues, a WebSocket hub standing in for the Durable Object, a fixed-window rate limiter) and publishes it as `globalThis.__mailflareNodeEnv` before Next starts; `getNodeEnv()` in `src/lib/runtime.ts` returns it. Application code must not care which one it got. The few places that must differ check `isNodeRuntime(env)`: setup requirement checks, the self-update button, and domain provisioning, which without Cloudflare credentials records the zone as `"manual"` (`src/lib/domains/provision.ts`) so every Cloudflare call is a no-op and the DNS page lists records to set by hand. Inbound mail off Workers goes through `intakeIncomingMail` (`src/lib/email/intake.ts`) from either the SMTP listener (`server/runtime/smtp.ts`) or the signed `/api/inbound` webhook the relay Worker in `deploy/cloudflare-email-relay` calls. `npm run build:node` builds Next in Node mode and bundles the server with esbuild to `dist/server.mjs`; the Dockerfile runs that. Migrations are applied from `drizzle/migrations` at start (`server/runtime/migrate.ts`), so the bootstrap schema in `src/lib/setup/migration.ts` is not used there.
### JMAP lives in `src/lib/jmap/`
@@ -124,7 +126,7 @@ The admin overview dispatches `deploy-update.yml` (constant in `src/app/api/admi
- Tabs for indentation. `@/*` maps to `src/*`.
- Types and pure helpers are split out of components and modules into sibling `*-types.d.ts` and `*-utils.ts` files (41 and 27 of them respectively). Follow this when adding anything non-trivial.
- Server code reaches bindings through `getEnv()` / `getEnvAsync()` in `src/lib/cloudflare.ts`, then `getDb(env)` from `src/db`. Never import `getCloudflareContext` directly.
- Server code reaches bindings through `getEnv()` / `getEnvAsync()` in `src/lib/cloudflare.ts`, then `getDb(env)` from `src/db`. Keep binding access centralized here.
- API routes return `NextResponse.json({ error: "..." }, { status })` for failures; there is no shared error envelope helper.
- UI is Tailwind v4 + shadcn/Radix primitives in `src/components/ui/`. `DialogContent` sets no max height, so a tall dialog overflows the viewport with an unreachable submit button — add `max-h-[calc(100vh-4rem)] overflow-y-auto` on any dialog with more than a few fields.
- `cloudflare-env.d.ts` is generated (500KB) — regenerate with `cf-typegen`, never hand-edit.
+2 -2
View File
@@ -15,7 +15,7 @@ FROM deps AS build
COPY . .
ARG NEXT_PUBLIC_TURNSTILE_SITE_KEY
ENV NEXT_PUBLIC_TURNSTILE_SITE_KEY=$NEXT_PUBLIC_TURNSTILE_SITE_KEY
RUN npm run build:node && rm -rf .next/cache
RUN npm run build:node && rm -rf .next-node/cache
# Production dependencies only. The Workers toolchain arrives as transitive
# dependencies of the Cloudflare adapter and is never loaded here, so it goes.
@@ -26,7 +26,7 @@ RUN npm prune --omit=dev --ignore-scripts \
FROM base AS runtime
ENV NODE_ENV=production DATA_DIR=/data PORT=3000 SMTP_INBOUND_PORT=25
COPY --chown=node:node --from=prod-deps /app/node_modules ./node_modules
COPY --chown=node:node --from=build /app/.next ./.next
COPY --chown=node:node --from=build /app/.next-node ./.next-node
COPY --chown=node:node --from=build /app/dist ./dist
COPY --chown=node:node --from=build /app/public ./public
COPY --chown=node:node --from=build /app/drizzle ./drizzle
+4
View File
@@ -80,6 +80,10 @@ npm run dev
Add your Cloudflare credentials to `.dev.vars`, then open [http://localhost:3000](http://localhost:3000). For sample local data, run `npm run db:seed` while the development server is running.
The Cloudflare app uses vinext and the Cloudflare Vite plugin, including local D1, R2, Queues, and Durable Objects. Remote bindings are disabled by default. To use Workers AI locally, authenticate with Wrangler, select your account with `CLOUDFLARE_ACCOUNT_ID`, and run `CLOUDFLARE_REMOTE_BINDINGS=true npm run dev`.
`npm run build` builds the complete Worker; `npm run start` previews that build locally. `npm run deploy` builds and deploys it. The separate Node/Docker runtime still uses Next.js and the existing `build:node`, `start:node`, and `dev:node` commands.
## Documentation
- [Deployment and configuration](docs/deployment.md)
+2861 -427
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -61,7 +61,7 @@ npm install
npm run deploy:local
```
The local deploy command builds and uploads the complete Worker with Wrangler. It does not modify D1. The complete Worker is required because `worker.ts` also handles inbound email, queues, scheduled backups, and the real-time Durable Object.
The local deploy command builds with vinext and uploads the complete Worker with Wrangler. The Cloudflare Vite plugin generates `dist/server/wrangler.json` and redirects Wrangler to that build. It does not modify D1. The complete Worker is required because `worker.ts` also handles inbound email, queues, scheduled backups, and the real-time Durable Object.
For manual recovery, pending migrations can still be applied with:
+36
View File
@@ -0,0 +1,36 @@
# OpenNext → vinext migration check
Checked 28 September 2026 against PR #52 (`26923a4`). The OpenNext comparison checkout was local `main` (`92134bb`); the PR and `main` share base `20a941b`. No production deployment was made.
## Conclusion
No breaking application behavior was reproduced in the local paths exercised. The vinext Worker builds, previews, and passes a Wrangler deployment dry run; its HTTP, email, queue, and scheduled entrypoints work locally. **One new tooling regression should be fixed before merging:** the locked dependency set makes `tsc --noEmit` stop while parsing a dependency declaration, before it can check Mailflare source.
## Finding: TypeScript checking stops in a new dependency (P2)
`npx tsc --noEmit --incremental false` exits with three parse errors at `node_modules/@vitejs/plugin-react/dist/index.d.ts:62` (`TS1003`, `TS1005`, `TS1128`). The declaration exports `viteReactForCjs as "module.exports"`, which the locked TypeScript 5.9.3 parser rejects. PR #52 adds `@vitejs/plugin-react: ^6.0.0`; its lockfile installs 6.1.1, while `typescript: ^5.7.4` resolves to 5.9.3. `skipLibCheck` does not suppress declaration *syntax* errors. The OpenNext `main` checkout reaches application diagnostics instead of failing at this declaration, although that branch also has existing type errors.
Impact: the project cannot use its normal type-check command to find migration or application type errors. Align and pin a compatible TypeScript/plugin version pair, regenerate the lockfile, then rerun `tsc --noEmit`. See [package.json](../package.json) and [tsconfig.json](../tsconfig.json).
## Checks performed
| Check | Result |
| --- | --- |
| `npm run build` (vinext) | Passed; generated `dist/server/wrangler.json` and client assets. Geist font files were emitted locally under `dist/client/_next/static/_vinext_fonts`. |
| `vite preview` of that build | `/`, `/login`, `/api/setup/status`, `/api/branding/icon`, and a local font returned 200. Unauthenticated `/api/auth/me` and `/api/messages` returned 401. JMAP discovery redirected to `/jmap/session`, which returned 401 without credentials. `/mcp` returned 401. The realtime endpoint returned 426 without an upgrade and logged 401 for an unauthenticated upgrade. Security headers appeared on page and API responses. |
| `wrangler deploy --dry-run` | Passed using the generated Wrangler config. Output included the custom Worker, 219 static assets, and the expected D1, R2, Queues, Durable Object, AI, Email, Assets, and rate-limit bindings. No upload occurred. |
| `wrangler dev --local` with isolated persistence | Passed. All 44 D1 migrations applied. A test Email Routing event returned `outcome: ok`, stored a 265-byte raw MIME object in R2, and dispatched one inbound queue message (`1/1`). The message had no configured recipient, so this did **not** prove inbox delivery. A `*/5 * * * *` scheduled invocation returned `outcome: ok`. `/` and `/api/setup/status` returned 200; `/api/realtime` returned 426 without an upgrade. |
| `npm run build:node` and isolated Node runtime | Passed. The Node server applied 44 migrations in a temporary data directory. `/`, `/api/setup/status`, and `/api/branding/icon` returned 200; `/api/auth/me` returned 401 without credentials. |
| `node --test tests/*.test.mjs` | PR: 27 passed, 3 failed. OpenNext `main`, using Node 24: the same 27 passed and the same 3 failed (agent email-tools assertion and two bootstrap-schema assertions). These are not migration regressions. |
| ESLint on changed code files | 0 errors, 1 unused `ctx` warning in `worker.ts`. Repository-wide `npm run lint` failed amid unrelated local `.agents` and `mailflare-landing`/generated files, so it is not a useful migration comparison in this checkout. |
The separate OpenNext `main` production build was attempted in an isolated worktree but did not finish: its fresh npm installation could not load the optional native `@tailwindcss/oxide-darwin-arm64` binding from Turbopack, even after a local dependency repair attempt. That environment failure does not establish an OpenNext/vinext behavior difference.
## Coverage still needed before a production switch
- Test an authenticated WebSocket connection and notification through `RealtimeHub`; the local checks covered only the unauthenticated paths.
- Test Email Routing into an actual mailbox and outbound Email Sending on a disposable Cloudflare deployment. The local email check proved handler → R2 → queue dispatch, not delivery.
- Exercise authenticated inbox, message actions, JMAP/MCP clients, and Workers AI on that deployment. These paths were not covered by the smoke checks here. The PR description reports broader local browser testing, but this report did not independently repeat it.
- Inspect Cloudflare build/deploy settings for installations that still invoke OpenNext. The PR changes `npm run build` and `npm run deploy` to vinext; an account-specific command can bypass them.
The generated-config deployment path used here matches [Cloudflare's Vite plugin deployment guide](https://developers.cloudflare.com/workers/vite-plugin/tutorial/). All runtime writes made for the Worker event checks used `/tmp/mailflare-pr52-cf-audit`; the Node check used `/tmp/mailflare-pr52-node-audit`. Existing local app data and the user's `.gitignore` and `.wrangler.dev.json` were left untouched.
+3 -1
View File
@@ -5,7 +5,9 @@ const eslintConfig = [
{
ignores: [
".next/**",
".open-next/**",
".next-node/**",
".vinext/**",
".wrangler/**",
"node_modules/**",
"drizzle/**",
"dist/**",
+4 -7
View File
@@ -1,10 +1,13 @@
import type { NextConfig } from "next";
import { getSecurityHeaders } from "./src/lib/security/headers";
import { createDevWranglerConfig } from "./scripts/dev-wrangler-config";
const nextConfig: NextConfig = {
distDir: process.env.MAILFLARE_RUNTIME === "node" ? ".next-node" : undefined,
turbopack: {
root: import.meta.dirname,
resolveAlias: process.env.MAILFLARE_RUNTIME === "node" ? {
"cloudflare:workers": "./server/runtime/cloudflare-workers.ts",
} : {},
},
allowedDevOrigins: ['mail.dev'],
typescript: {
@@ -26,9 +29,3 @@ const nextConfig: NextConfig = {
};
export default nextConfig;
// Enable calling `getCloudflareContext()` in `next dev`. The self-hosted
// runtime provides its own env, so it skips this.
// See https://opennext.js.org/cloudflare/bindings#local-access-to-bindings.
import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";
if (process.env.MAILFLARE_RUNTIME !== "node") initOpenNextCloudflareForDev({ configPath: createDevWranglerConfig() });
-9
View File
@@ -1,9 +0,0 @@
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
export default defineCloudflareConfig({
// Uncomment to enable R2 cache,
// It should be imported as:
// `import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";`
// See https://opennext.js.org/cloudflare/caching for more details
// incrementalCache: r2IncrementalCache,
});
+2610 -4503
View File
File diff suppressed because it is too large. Load diff
+21 -14
View File
@@ -1,18 +1,18 @@
{
"name": "mailflare",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"scripts": {
"dev": "npm run db:bundle && next dev",
"build": "npm run db:bundle && next build",
"build:next": "npm run db:bundle && next build",
"start": "next start",
"dev": "npm run db:bundle && vite dev --port 3000",
"build": "npm run db:bundle && vite build",
"build:next": "npm run db:bundle && MAILFLARE_RUNTIME=node next build",
"start": "vite preview --port 3000",
"lint": "eslint .",
"deploy": "npm run db:bundle && opennextjs-cloudflare build && npm run deploy:worker",
"deploy:worker": "OPEN_NEXT_DEPLOY=true wrangler deploy",
"deploy": "npm run build && npm run deploy:worker",
"deploy:worker": "wrangler deploy",
"deploy:local": "npm run deploy",
"upload": "npm run db:bundle && opennextjs-cloudflare build && OPEN_NEXT_DEPLOY=true wrangler versions upload",
"preview": "npm run db:bundle && opennextjs-cloudflare build && OPEN_NEXT_DEPLOY=true wrangler dev",
"upload": "npm run build && wrangler versions upload",
"preview": "npm run build && vite preview --port 3000",
"cf-typegen": "wrangler types --env-interface CloudflareEnv ./cloudflare-env.d.ts",
"db:bundle": "node scripts/generate-migration-bundle.mjs",
"db:generate": "drizzle-kit generate",
@@ -22,7 +22,7 @@
"db:seed": "curl -X POST http://localhost:3000/api/seed",
"build:node": "npm run db:bundle && MAILFLARE_RUNTIME=node next build && node scripts/build-server.mjs",
"start:node": "MAILFLARE_RUNTIME=node NODE_ENV=production node dist/server.mjs",
"dev:node": "npm run db:bundle && MAILFLARE_RUNTIME=node node --import tsx server/index.ts"
"dev:node": "npm run db:bundle && node scripts/build-server.mjs && MAILFLARE_RUNTIME=node node dist/server.mjs"
},
"cloudflare": {
"bindings": {
@@ -60,7 +60,6 @@
"@ai-sdk/openai-compatible": "^3.0.55",
"@ai-sdk/react": "^4.0.116",
"@modelcontextprotocol/server": "^2.1.0",
"@opennextjs/cloudflare": "^1.19.9",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
@@ -70,6 +69,7 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15",
"@tanstack/react-query": "^5.100.11",
"@vinext/cloudflare": "1.0.0-beta.11",
"ai": "^7.0.113",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^13.0.3",
@@ -84,17 +84,20 @@
"nodemailer": "^10.0.9",
"postal-mime": "^2.7.4",
"qrcode": "^1.5.4",
"react": "^19.1.7",
"react-dom": "^19.1.7",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-markdown": "^10.1.0",
"react-server-dom-webpack": "^19.2.6",
"remark-gfm": "^4.0.1",
"sass": "^1.105.0",
"smtp-server": "^3.19.12",
"tailwind-merge": "^3.6.0",
"vinext": "1.0.0-beta.13",
"workers-ai-provider": "^4.0.0",
"zod": "^4.4.3"
},
"devDependencies": {
"@cloudflare/vite-plugin": "^1.61.0",
"@cloudflare/workers-types": "^5.20260828.1",
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
@@ -106,6 +109,8 @@
"@types/react": "^19.1.7",
"@types/react-dom": "^19.1.7",
"@types/smtp-server": "^3.5.13",
"@vitejs/plugin-react": "^6.0.0",
"@vitejs/plugin-rsc": "^0.5.34",
"drizzle-kit": "^0.31.10",
"esbuild": "^0.28.2",
"eslint": "^9",
@@ -113,11 +118,13 @@
"tailwindcss": "^4",
"tsx": "^4.23.13",
"typescript": "^5.7.4",
"vite": "^8.0.0",
"wrangler": "^4.127.1"
},
"overrides": {
"@esbuild-kit/core-utils": {
"esbuild": "^0.25.4"
}
}
},
"type": "module"
}
+2 -1
View File
@@ -12,7 +12,8 @@ await build({
target: "node22",
sourcemap: true,
tsconfig: "tsconfig.json",
external: ["next", "better-sqlite3", "nodemailer", "smtp-server", "ws", "react", "react-dom", "@opennextjs/cloudflare", "cloudflare:workers"],
external: ["next", "better-sqlite3", "nodemailer", "smtp-server", "ws", "react", "react-dom"],
alias: { "cloudflare:workers": "./server/runtime/cloudflare-workers.ts" },
banner: { js: "import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);" },
define: { "process.env.MAILFLARE_RUNTIME": '"node"' },
logLevel: "info",
-22
View File
@@ -1,22 +0,0 @@
import { readFileSync, renameSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import * as ts from "typescript";
/** OpenNext's Next dev proxy has no Worker exports, so it cannot host our Durable Object. */
export function createDevWranglerConfig(): string {
const sourcePath = fileURLToPath(new URL("../wrangler.jsonc", import.meta.url));
const parsed = ts.parseConfigFileTextToJson(sourcePath, readFileSync(sourcePath, "utf8"));
if (parsed.error || !parsed.config || typeof parsed.config !== "object") {
throw new Error("Could not read wrangler.jsonc for local development");
}
const devConfig = { ...parsed.config };
delete devConfig.durable_objects;
delete devConfig.migrations;
const outputPath = join(dirname(sourcePath), ".wrangler.dev.json");
const temporaryPath = `${outputPath}.${process.pid}.tmp`;
writeFileSync(temporaryPath, JSON.stringify(devConfig));
renameSync(temporaryPath, outputPath);
return outputPath;
}
+2
View File
@@ -0,0 +1,2 @@
// The self-hosted build gets its bindings from getNodeEnv(), not workerd.
export const env = undefined;
+3 -3
View File
@@ -1,10 +1,10 @@
import { getCloudflareContext } from "@opennextjs/cloudflare";
import { env } from "cloudflare:workers";
import { getNodeEnv } from "@/lib/runtime";
export function getEnv(): CloudflareEnv {
return getNodeEnv() ?? (getCloudflareContext().env as CloudflareEnv);
return getNodeEnv() ?? (env as CloudflareEnv);
}
export async function getEnvAsync(): Promise<CloudflareEnv> {
return getNodeEnv() ?? ((await getCloudflareContext({ async: true })).env as CloudflareEnv);
return getEnv();
}
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* Where the app is running. On Cloudflare Workers the bindings come from
* OpenNext; the self-hosted Node server builds an equivalent env object and
* cloudflare:workers; the self-hosted Node server builds an equivalent env object and
* publishes it on globalThis before Next starts, so route handlers reach it
* the same way.
*/
+4 -1
View File
@@ -30,7 +30,10 @@ await build({
target: "node24",
tsconfig: join(root, "tsconfig.json"),
packages: "external",
alias: { "next/headers": "next/headers.js" },
alias: {
"next/headers": "next/headers.js",
"cloudflare:workers": "./server/runtime/cloudflare-workers.ts",
},
logLevel: "silent",
});
const { SqliteDatabase, applyMigrations, createAgentChatStream, runEmailTool, requestAgentSend, createSession, postAgentChat } = await import(pathToFileURL(join(bundleDirectory, "entry.mjs")).href);
+1 -1
View File
@@ -29,6 +29,6 @@
"@cloudflare/workers-types"
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", ".next-node/types/**/*.ts", ".next-node/dev/types/**/*.ts"],
"exclude": ["node_modules"]
}
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from "vite";
import vinext from "vinext";
import { cloudflare } from "@cloudflare/vite-plugin";
import { imagesOptimizer } from "@vinext/cloudflare/images/images-optimizer";
export default defineConfig({
server: { allowedHosts: ["mailflare.local", "mail.dev"] },
plugins: [
vinext({ images: { optimizer: imagesOptimizer() } }),
cloudflare({
remoteBindings: process.env.CLOUDFLARE_REMOTE_BINDINGS === "true",
viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] },
}),
],
});
+2 -3
View File
@@ -1,5 +1,4 @@
// @ts-ignore — generated at build time
import { default as nextHandler } from "./.open-next/worker.js";
import vinextHandler from "vinext/server/fetch-handler";
import {
processInboundMessage,
storeRawToR2,
@@ -44,7 +43,7 @@ export default {
return hub.fetch(hubRequest);
}
return nextHandler.fetch(request, env, ctx);
return vinextHandler.fetch(request, env, ctx);
},
async email(message: ForwardableEmailMessage, env: CloudflareEnv, ctx: ExecutionContext) {
+1 -1
View File
@@ -10,7 +10,7 @@
],
"assets": {
"binding": "ASSETS",
"directory": ".open-next/assets",
"not_found_handling": "none",
},
"images": {
"binding": "IMAGES",
+1 -1
View File
@@ -9,7 +9,7 @@
],
"assets": {
"binding": "ASSETS",
"directory": ".open-next/assets",
"not_found_handling": "none",
},
"images": {
"binding": "IMAGES",