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/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 490c505..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 @@ -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..a74265f 100644 --- a/docs/reference/npm-scripts.md +++ b/docs/reference/npm-scripts.md @@ -13,9 +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 deploy` | build + `opennextjs-cloudflare deploy` | **ต้องได้รับอนุมัติก่อน** ดู [deploy-cloudflare.md](../operations/deploy-cloudflare.md) | +| `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` | `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) | @@ -50,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-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..9ab6256 100644 --- a/package.json +++ b/package.json @@ -15,9 +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", - "deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy", + "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": "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", @@ -92,7 +93,9 @@ "vitest": "^4.1.11", "wrangler": "^4.149.0" }, - "engines": {}, + "engines": { + "node": ">=22" + }, "overrides": { "@node-minify/core": { "glob": "10.5.0" 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"); +}