From 350617b1c9c08afa0fa1de9cbd7f1b2992e24a56 Mon Sep 17 00:00:00 2001 From: thanabartbb <326990420+thanabartbb@users.noreply.github.com> Date: Sat, 17 Oct 2026 14:01:51 +0700 Subject: [PATCH 1/2] build: pin Node 22 and add preview:local (populateCache before wrangler dev) Running wrangler dev directly after opennextjs-cloudflare build skips copying .open-next/cache into assets/cdn-cgi/_next_cache, so every App Router page renders at request time in workerd and hangs (500 on /login). preview:local runs 'opennextjs-cloudflare populateCache local' first. Adds .nvmrc (22) and engines.node >=22 (wrangler 4 refuses Node 20). Docs updated. --- .nvmrc | 1 + docs/operations/local-dev.md | 1 + docs/reference/npm-scripts.md | 1 + package-lock.json | 3 +++ package.json | 5 ++++- 5 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/docs/operations/local-dev.md b/docs/operations/local-dev.md index 490c505..2ff38d8 100644 --- a/docs/operations/local-dev.md +++ b/docs/operations/local-dev.md @@ -49,6 +49,7 @@ npm run cf:build # ต้องผ่านก่อน deploy ## ปัญหาที่พบบ่อย - **`wrangler` บอกว่า Node เก่าเกินไป** → ใช้ Node 22+ +- **หลัง `opennextjs-cloudflare build` แล้วรัน `wrangler dev` ตรงๆ ทุกหน้า (เช่น `/login`) ค้างแล้วได้ 500** (log: `Worker's code had hung`) → ยังไม่ได้คัดลอก `.open-next/cache` ไปที่ `.open-next/assets/cdn-cgi/_next_cache` ให้รัน `npm run preview:local` (หรือ `npx opennextjs-cloudflare populateCache local` ก่อน `wrangler dev`) `opennextjs-cloudflare preview` และ `deploy` ทำขั้นนี้ให้อยู่แล้ว - **หน้า `/builder` ค้างที่ "Loading builder…"** → เปิด `http://localhost:3000` Next.js 16 บล็อก HMR เมื่อต้นทางเป็น `127.0.0.1` (ตอบคำว่า `Unauthorized` แบบไม่มีสถานะ HTTP) หน้า client-only จึงไม่ hydrate - **พอร์ต 8787 ถูกใช้** → ปิด preview เก่าตามคำสั่งข้างบน - **AI ไม่ทำงานใน dev** → ตรวจว่า `OPENAI_API_KEY` อยู่ใน `.env.local` หรือ `.dev.vars` และตรงรูปแบบ `^[\w-]{20,400}$`; `GET /api/plan` จะบอก `serverKeyConfigured` diff --git a/docs/reference/npm-scripts.md b/docs/reference/npm-scripts.md index 50ca474..fbf90eb 100644 --- a/docs/reference/npm-scripts.md +++ b/docs/reference/npm-scripts.md @@ -15,6 +15,7 @@ | `npm run export -- [opts]` | `tsx scripts/export-template.ts` เขียน setup.sh/ps1/README ของเทมเพลตลงดิสก์ | ทดสอบเทมเพลต end-to-end | | `npm run cf:build` | `opennextjs-cloudflare build` → `.open-next/` | ตรวจว่า build เป็น Worker ได้ | | `npm run preview` | build + `opennextjs-cloudflare preview` ใน workerd ที่ :8787 | ตรวจพฤติกรรม/ภาพแบบ production | +| `npm run preview:local` | `opennextjs-cloudflare populateCache local` แล้ว `wrangler dev` (ใช้หลัง `npm run cf:build`) | รัน build ที่มีอยู่ใน workerd ในเครื่อง ถ้ารัน `wrangler dev` ตรงๆ โดยไม่ populate cache ทุกหน้า App Router จะค้างแล้วได้ 500 | | `npm run deploy` | build + `opennextjs-cloudflare deploy` | **ต้องได้รับอนุมัติก่อน** ดู [deploy-cloudflare.md](../operations/deploy-cloudflare.md) | | `npm run cf:typegen` | `wrangler types ...` → `cloudflare-env.d.ts` | หลังแก้ binding ใน wrangler.jsonc | | `npm run db:migrate:local` | `wrangler d1 migrations apply nvx-db --local` | ทดสอบ migration ในฐานข้อมูลจำลองบนเครื่อง | diff --git a/package-lock.json b/package-lock.json index 4e58683..166bc07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,9 @@ "unist-util-visit": "^5.1.0", "vitest": "^4.1.11", "wrangler": "^4.149.0" + }, + "engines": { + "node": ">=22" } }, "node_modules/@alloc/quick-lru": { diff --git a/package.json b/package.json index 32b6892..393ab01 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "export": "tsx scripts/export-template.ts", "cf:build": "opennextjs-cloudflare build", "preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview", + "preview:local": "opennextjs-cloudflare populateCache local && wrangler dev", "deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy", "cf:typegen": "wrangler types --env-interface CloudflareEnv --include-runtime=false cloudflare-env.d.ts", "db:migrate:local": "wrangler d1 migrations apply nvx-db --local", @@ -92,7 +93,9 @@ "vitest": "^4.1.11", "wrangler": "^4.149.0" }, - "engines": {}, + "engines": { + "node": ">=22" + }, "overrides": { "@node-minify/core": { "glob": "10.5.0" From 3f0841e4d0bb95cbd5a07a04d94107ff1d3464a0 Mon Sep 17 00:00:00 2001 From: thanabartbb <326990420+thanabartbb@users.noreply.github.com> Date: Sat, 17 Oct 2026 14:08:29 +0700 Subject: [PATCH 2/2] build: fail cf:build when the OpenNext preview-props patch is missing scripts/check-opennext-patch.mjs runs before opennextjs-cloudflare build (adapter in node_modules must contain the patched glob) and after it (.open-next handler must inline .next/server/preview-props.json), exiting 1 with a clear error otherwise. A build without the postinstall patch caused Worker version 0286ef58 to 500 on every request. preview and deploy now go through npm run cf:build. postinstall patch unchanged. Docs updated. --- docs/DEPLOY.md | 4 +-- docs/architecture/cloudflare-runtime.md | 1 + docs/operations/local-dev.md | 4 +-- docs/reference/npm-scripts.md | 5 ++-- package.json | 6 ++-- scripts/check-opennext-patch.mjs | 37 +++++++++++++++++++++++++ 6 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 scripts/check-opennext-patch.mjs diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index b7ee7db..77673ee 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -39,7 +39,7 @@ - Node.js **22+** for Wrangler 4 (`wrangler` refuses Node 20). Next.js itself runs on Node 20.9+. - A Cloudflare account and an API token (see [Token permissions](#token-permissions)). -- `npm install`. It runs `postinstall` → `scripts/patch-opennext.mjs`, which patches `@opennextjs/cloudflare` so it also loads Next 16.4's `preview-props.json`. The patch is idempotent and safe to re-run. +- `npm install`. It runs `postinstall` → `scripts/patch-opennext.mjs`, which patches `@opennextjs/cloudflare` so it also loads Next 16.4's `preview-props.json`. The patch is idempotent and safe to re-run. `npm run cf:build` (also used by `preview` and `deploy`) runs `scripts/check-opennext-patch.mjs` before and after the build and exits 1 if the patch is missing. ### 2. Local development @@ -93,7 +93,7 @@ npm run db:migrations:list npm run db:migrate:remote # 3) build + deploy this Worker and its configured custom domains -npm run deploy # = opennextjs-cloudflare build && opennextjs-cloudflare deploy +npm run deploy # = npm run cf:build (with patch checks) && opennextjs-cloudflare deploy # 4) secret (after the worker exists) printenv OPENAI_API_KEY | npx wrangler secret put OPENAI_API_KEY diff --git a/docs/architecture/cloudflare-runtime.md b/docs/architecture/cloudflare-runtime.md index e027ab5..c539e12 100644 --- a/docs/architecture/cloudflare-runtime.md +++ b/docs/architecture/cloudflare-runtime.md @@ -19,6 +19,7 @@ | [`.dev.vars.example`](../../.dev.vars.example) | ต้นแบบ `.dev.vars` สำหรับ `npm run preview` (ห้าม commit `.dev.vars`) | | [`public/_headers`](../../public/_headers) | headers ของ static assets ที่เสิร์ฟตรงจาก `.open-next/assets` | | [`scripts/patch-opennext.mjs`](../../scripts/patch-opennext.mjs) | patch ตอน `postinstall` ให้ adapter inline `preview-props.json` ของ Next 16.4 (ไม่งั้นทุกคำขอ error `Unexpected loadManifest(...preview-props.json)`) idempotent และเตือนถ้า pattern เปลี่ยน ลบได้เมื่อ adapter แก้เอง | +| [`scripts/check-opennext-patch.mjs`](../../scripts/check-opennext-patch.mjs) | รันใน `npm run cf:build` (และ `preview`/`deploy`) ให้ build ล้มด้วย exit 1 ถ้า patch ข้างบนไม่ได้ติดตั้ง หรือ `.open-next` ไม่ได้ inline `preview-props.json` (เคยทำให้ version `0286ef58` ได้ 500 ทุกคำขอ) | | [`next.config.ts`](../../next.config.ts) | headers, 404 rewrite, และ `initOpenNextCloudflareForDev()` ใน dev เพื่อให้ `next dev` เห็น binding | ## การทำงานตอนรัน diff --git a/docs/operations/local-dev.md b/docs/operations/local-dev.md index 2ff38d8..bf22a26 100644 --- a/docs/operations/local-dev.md +++ b/docs/operations/local-dev.md @@ -15,7 +15,7 @@ cp .env.example .env.local # สำหรับ next dev (ถ้าต้ cp .dev.vars.example .dev.vars # สำหรับ wrangler preview (ถ้าต้องการ AI) ``` -`postinstall` patch adapter ของ OpenNext ให้รองรับ `preview-props.json` ของ Next 16.4 ถ้าเห็น `[patch-opennext] pattern not found` แปลว่า adapter เปลี่ยนเวอร์ชันแล้ว ให้ตรวจว่ายังต้อง patch หรือไม่ (ดู [cloudflare-runtime.md](../architecture/cloudflare-runtime.md)) +`postinstall` patch adapter ของ OpenNext ให้รองรับ `preview-props.json` ของ Next 16.4 ถ้าเห็น `[patch-opennext] pattern not found` แปลว่า adapter เปลี่ยนเวอร์ชันแล้ว ให้ตรวจว่ายังต้อง patch หรือไม่ (ดู [cloudflare-runtime.md](../architecture/cloudflare-runtime.md)) `npm run cf:build` ตรวจ patch นี้ทั้งก่อนและหลัง build ด้วย `scripts/check-opennext-patch.mjs` ถ้าขึ้น `[check-opennext-patch] ERROR` แปลว่าติดตั้งแบบข้าม postinstall (เช่น `--ignore-scripts`) ให้รัน `npm ci` ใหม่ ## สองวิธีรัน และควรใช้เมื่อไร @@ -26,7 +26,7 @@ cp .dev.vars.example .dev.vars # สำหรับ wrangler preview (ถ้ ข้อสังเกตจากประสบการณ์: `next dev` บางครั้งแสดง CSS ค้าง (stale) หลังแก้ token มาก ๆ ให้ยืนยันภาพด้วย `npm run preview` ก่อนสรุปผลเสมอ -`npm run preview` = `opennextjs-cloudflare build && opennextjs-cloudflare preview` build ใช้เวลาประมาณ 1 นาที ถ้าต้องการรัน preview ซ้ำโดยไม่ build ใหม่: `npx opennextjs-cloudflare preview --port 8787` ปิด preview ที่ค้าง: `pkill -f "opennextjs-cloudflare preview"; pkill -f workerd` +`npm run preview` = `npm run cf:build && opennextjs-cloudflare preview` build ใช้เวลาประมาณ 1 นาที ถ้าต้องการรัน preview ซ้ำโดยไม่ build ใหม่: `npx opennextjs-cloudflare preview --port 8787` ปิด preview ที่ค้าง: `pkill -f "opennextjs-cloudflare preview"; pkill -f workerd` ## ชุดตรวจก่อน commit diff --git a/docs/reference/npm-scripts.md b/docs/reference/npm-scripts.md index fbf90eb..a74265f 100644 --- a/docs/reference/npm-scripts.md +++ b/docs/reference/npm-scripts.md @@ -13,10 +13,10 @@ | `npm run test:watch` | `vitest` แบบ watch | ระหว่างเขียน test | | `npm run typecheck` | `tsc --noEmit` | ก่อน commit; CI | | `npm run export -- [opts]` | `tsx scripts/export-template.ts` เขียน setup.sh/ps1/README ของเทมเพลตลงดิสก์ | ทดสอบเทมเพลต end-to-end | -| `npm run cf:build` | `opennextjs-cloudflare build` → `.open-next/` | ตรวจว่า build เป็น Worker ได้ | +| `npm run cf:build` | `scripts/check-opennext-patch.mjs` → `opennextjs-cloudflare build` → `.open-next/` → ตรวจ output อีกรอบ | ตรวจว่า build เป็น Worker ได้ ถ้า patch ของ OpenNext ไม่ได้ติดตั้ง จะหยุดด้วย exit 1 | | `npm run preview` | build + `opennextjs-cloudflare preview` ใน workerd ที่ :8787 | ตรวจพฤติกรรม/ภาพแบบ production | | `npm run preview:local` | `opennextjs-cloudflare populateCache local` แล้ว `wrangler dev` (ใช้หลัง `npm run cf:build`) | รัน build ที่มีอยู่ใน workerd ในเครื่อง ถ้ารัน `wrangler dev` ตรงๆ โดยไม่ populate cache ทุกหน้า App Router จะค้างแล้วได้ 500 | -| `npm run deploy` | build + `opennextjs-cloudflare deploy` | **ต้องได้รับอนุมัติก่อน** ดู [deploy-cloudflare.md](../operations/deploy-cloudflare.md) | +| `npm run deploy` | `npm run cf:build` + `opennextjs-cloudflare deploy` | **ต้องได้รับอนุมัติก่อน** ดู [deploy-cloudflare.md](../operations/deploy-cloudflare.md) | | `npm run cf:typegen` | `wrangler types ...` → `cloudflare-env.d.ts` | หลังแก้ binding ใน wrangler.jsonc | | `npm run db:migrate:local` | `wrangler d1 migrations apply nvx-db --local` | ทดสอบ migration ในฐานข้อมูลจำลองบนเครื่อง | | `npm run db:migrate:remote` | `wrangler d1 migrations apply nvx-db --remote` | **ต้องได้รับอนุมัติก่อน** apply บน D1 จริง ดู [d1-database.md](../operations/d1-database.md) | @@ -51,6 +51,7 @@ npm run export -- fastapi-ai --py uv --addons testing,docker --out /tmp/fastapi |---|---|---| | [`scripts/gen-brand-icons.mjs`](../../scripts/gen-brand-icons.mjs) | `node scripts/gen-brand-icons.mjs` | สร้าง `src/components/brand/brand-icons.data.ts` จาก simple-icons (รันหลังเพิ่ม id โลโก้) | | [`scripts/patch-opennext.mjs`](../../scripts/patch-opennext.mjs) | อัตโนมัติ (postinstall) | patch `preview-props.json` | +| [`scripts/check-opennext-patch.mjs`](../../scripts/check-opennext-patch.mjs) | อัตโนมัติ (`cf:build`) | exit 1 ถ้า adapter ไม่ได้ patch (ก่อน build) หรือ `.open-next` ไม่ได้ inline `preview-props.json` (หลัง build) | | [`scripts/docs-check.mjs`](../../scripts/docs-check.mjs) | `npm run docs:check` | docs freshness | | [`scripts/docs-pdf.mjs`](../../scripts/docs-pdf.mjs) | `npm run docs:pdf` | PDF คู่มือผู้ใช้ | | [`scripts/build-docs.mjs`](../../scripts/build-docs.mjs) | `npm run docs:bundle` (และ pre-hooks) | docs bundle สำหรับ `/docs` ([docs-site.md](../architecture/docs-site.md)) | diff --git a/package.json b/package.json index 393ab01..9ab6256 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "pretypecheck": "node scripts/build-docs.mjs && next typegen", "typecheck": "tsc --noEmit", "export": "tsx scripts/export-template.ts", - "cf:build": "opennextjs-cloudflare build", - "preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview", + "cf:build": "node scripts/check-opennext-patch.mjs && opennextjs-cloudflare build && node scripts/check-opennext-patch.mjs --output", + "preview": "npm run cf:build && opennextjs-cloudflare preview", "preview:local": "opennextjs-cloudflare populateCache local && wrangler dev", - "deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy", + "deploy": "npm run cf:build && opennextjs-cloudflare deploy", "cf:typegen": "wrangler types --env-interface CloudflareEnv --include-runtime=false cloudflare-env.d.ts", "db:migrate:local": "wrangler d1 migrations apply nvx-db --local", "db:migrate:remote": "wrangler d1 migrations apply nvx-db --remote", diff --git a/scripts/check-opennext-patch.mjs b/scripts/check-opennext-patch.mjs new file mode 100644 index 0000000..9c113fb --- /dev/null +++ b/scripts/check-opennext-patch.mjs @@ -0,0 +1,37 @@ +// Fails the Cloudflare build when the OpenNext compatibility patch is missing. +// scripts/patch-opennext.mjs (postinstall) adds preview-props.json to the manifests the adapter +// inlines. If postinstall was skipped (npm ci --ignore-scripts, pnpm/bun without lifecycle scripts, +// or a reinstall of the adapter), the Worker builds fine but every request fails in production with +// "Unexpected loadManifest(/.next/server/preview-props.json) call!" (Worker version 0286ef58, 2026-10-17). +// +// node scripts/check-opennext-patch.mjs check node_modules before `opennextjs-cloudflare build` +// node scripts/check-opennext-patch.mjs --output check .open-next after the build +// +// Keep the pattern in sync with scripts/patch-opennext.mjs. +import { existsSync, readFileSync } from "node:fs"; + +const ADAPTER_FILE = "node_modules/@opennextjs/cloudflare/dist/cli/build/patches/plugins/load-manifest.js"; +const PATCHED = "**/{*-manifest,required-server-files,prefetch-hints,preview-props}.json"; +const HANDLER = ".open-next/server-functions/default/handler.mjs"; +// The inlined loadManifest() returns the manifest object for preview-props.json when the patch was active. +const INLINED = /endsWith\(\s*["'`]\/server\/preview-props\.json["'`]\s*\)\s*\)\s*\{?\s*return\s*\{/; +const FIX = "Run `npm ci` (with install scripts enabled) or `node scripts/patch-opennext.mjs`, then build again."; + +function fail(msg) { + console.error(`\n[check-opennext-patch] ERROR: ${msg}\n[check-opennext-patch] ${FIX}\n`); + process.exit(1); +} + +if (process.argv.includes("--output")) { + if (!existsSync(HANDLER)) fail(`${HANDLER} not found; the OpenNext build did not produce a server handler.`); + if (!INLINED.test(readFileSync(HANDLER, "utf8"))) { + fail(`${HANDLER} does not inline .next/server/preview-props.json. Every request would fail with "Unexpected loadManifest(/.next/server/preview-props.json) call!". Do not deploy this build.`); + } + console.log("[check-opennext-patch] build output inlines preview-props.json"); +} else { + if (!existsSync(ADAPTER_FILE)) fail(`${ADAPTER_FILE} not found; @opennextjs/cloudflare is not installed or its layout changed.`); + if (!readFileSync(ADAPTER_FILE, "utf8").includes(PATCHED)) { + fail(`@opennextjs/cloudflare is not patched (scripts/patch-opennext.mjs did not run): ${ADAPTER_FILE} does not contain the preview-props.json pattern.`); + } + console.log("[check-opennext-patch] adapter patch present"); +}