From 6d262a229cd87542fb9ef741b2307723dfbc0717 Mon Sep 17 00:00:00 2001 From: musingfox Date: Thu, 1 Oct 2026 18:36:53 +0800 Subject: [PATCH 1/8] build: run the worker through cf Finish the cf migration started in 51499c9: dev, deploy and type generation use cf, which reads cloudflare.config.ts and writes types to .cloudflare/types, now gitignored in place of worker-configuration.d.ts. Co-Authored-By: Claude Opus 5.5 (1M context) --- .gitignore | 2 +- package.json | 6 +++--- tsconfig.json | 2 +- tsconfig.test.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index a77a680..ea45fc0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .obsidian.yaml node_modules/ .wrangler/ -worker-configuration.d.ts +.cloudflare/ diff --git a/package.json b/package.json index 4a08a26..74c6564 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "private": true, "type": "module", "scripts": { - "dev": "wrangler dev", - "deploy": "wrangler deploy", - "typecheck": "wrangler types && tsc -p tsconfig.json && tsc -p tsconfig.test.json", + "dev": "cf dev", + "deploy": "cf deploy", + "typecheck": "cf workers types && tsc -p tsconfig.json && tsc -p tsconfig.test.json", "check": "bun run typecheck && bun test", "smoke": "bash scripts/smoke.sh" }, diff --git a/tsconfig.json b/tsconfig.json index b423627..db06d78 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,5 @@ "verbatimModuleSyntax": true, "forceConsistentCasingInFileNames": true }, - "include": ["worker-configuration.d.ts", "src/**/*.ts"] + "include": [".cloudflare/types/index.d.ts", "src/**/*.ts"] } diff --git a/tsconfig.test.json b/tsconfig.test.json index d3ec04f..ee28e1a 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -3,5 +3,5 @@ "compilerOptions": { "types": ["bun"] }, - "include": ["worker-configuration.d.ts", "src/**/*.ts", "test/**/*.ts"] + "include": [".cloudflare/types/index.d.ts", "src/**/*.ts", "test/**/*.ts"] } From 95cb14212de49f398d0f1405dd4b28bb6a4e813e Mon Sep 17 00:00:00 2001 From: musingfox Date: Thu, 1 Oct 2026 18:39:41 +0800 Subject: [PATCH 2/8] build(smoke): start the worker with cf dev cf dev forwards only --port, --host, --mode and --local to its dev server, so the explicit --ip is dropped; the default localhost bind still answers on 127.0.0.1. Co-Authored-By: Claude Opus 5.5 (1M context) --- scripts/smoke.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/smoke.sh b/scripts/smoke.sh index f0cce36..3fa5ebb 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -6,10 +6,10 @@ cd "$(dirname "$0")/.." mkdir -p .wrangler/home export HOME="$PWD/.wrangler/home" -./node_modules/.bin/wrangler --version +./node_modules/.bin/cf --version mkdir -p .wrangler -WRANGLER_SEND_METRICS=false ./node_modules/.bin/wrangler dev --ip 127.0.0.1 --port 8799 >.wrangler/smoke.log 2>&1 & +WRANGLER_SEND_METRICS=false ./node_modules/.bin/cf dev --port 8799 >.wrangler/smoke.log 2>&1 & pid=$! tmp=$(mktemp -d) cleanup() { @@ -28,7 +28,7 @@ for _ in $(seq 1 60); do sleep 1 done if [ "$ready" -ne 1 ]; then - echo "wrangler dev did not become ready" >&2 + echo "cf dev did not become ready" >&2 tail -n 80 .wrangler/smoke.log >&2 || true exit 1 fi From f82ed3024d48ad773b6f53bae5069ff0a9eb796c Mon Sep 17 00:00:00 2001 From: musingfox Date: Thu, 1 Oct 2026 18:39:56 +0800 Subject: [PATCH 3/8] ci: install node 24 for cf Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ed9a8b..72b1d9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,10 @@ jobs: - uses: actions/checkout@v5 with: persist-credentials: false + - uses: actions/setup-node@v7 + with: + # cf needs Node 22.18 or later; 24 matches the Workers Builds image. + node-version: 24 - uses: oven-sh/setup-bun@v2 with: # bun.lock must stay at lockfileVersion 1; Bun 1.3 reads and writes that version. From 5bd6d2dcf72b0617ab3eac9db4ac65612cb0c684 Mon Sep 17 00:00:00 2001 From: musingfox Date: Thu, 1 Oct 2026 18:40:33 +0800 Subject: [PATCH 4/8] test(config): check cloudflare.config.ts instead of wrangler.jsonc Storage bindings in cf config are typed kv, r2, d1, durable-object and queue, which the old snake_case pattern would miss. The sampling check now covers both the top-level and the logs headSamplingRate. Co-Authored-By: Claude Opus 5.5 (1M context) --- test/config.test.ts | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/test/config.test.ts b/test/config.test.ts index c03969a..62858f8 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -1,27 +1,24 @@ import { expect, test } from "bun:test"; +import type { WorkerConfig } from "cf/config"; +import config from "../cloudflare.config"; -const STORAGE_BINDING = /kv_namespaces|r2_buckets|d1_databases|durable_objects|queues/; +const STORAGE_BINDING = /"type":"(kv|r2|d1|durable-object|queue)"/; -test("wrangler config names a main module and declares no storage binding", async () => { - const text = await Bun.file(new URL("../wrangler.jsonc", import.meta.url)).text(); - expect(text).toContain('"main"'); - expect(text.match(STORAGE_BINDING)).toBeNull(); +test("cf config names an entrypoint and declares no storage binding", () => { + expect(config.worker.entrypoint).toBe("src/index.ts"); + expect(JSON.stringify(config).match(STORAGE_BINDING)).toBeNull(); }); test("the storage-binding pattern matches a known-bad sample", () => { - const sample = '{"kv_namespaces":[{"binding":"CACHE","id":"x"}]}'; - expect(sample.match(STORAGE_BINDING)).not.toBeNull(); + const sample = { worker: { env: { CACHE: { type: "kv" } }, exports: { Room: { type: "durable-object" } } } }; + expect(JSON.stringify(sample).match(STORAGE_BINDING)).not.toBeNull(); }); -test("wrangler config records every request in Workers Logs", async () => { - const config = JSON.parse(await Bun.file(new URL("../wrangler.jsonc", import.meta.url)).text()); - expect(config.observability.enabled).toBe(true); - expect(config.observability.head_sampling_rate ?? 1).toBe(1); -}); - -test("wrangler config is only wrangler.jsonc", async () => { - expect(await Bun.file(new URL("../wrangler.toml", import.meta.url)).exists()).toBe(false); - expect(await Bun.file(new URL("../wrangler.json", import.meta.url)).exists()).toBe(false); +test("cf config records every request in Workers Logs", () => { + const observability: WorkerConfig["observability"] = config.worker.observability; + expect(observability?.enabled).toBe(true); + expect(observability?.headSamplingRate ?? 1).toBe(1); + expect(observability?.logs?.headSamplingRate ?? 1).toBe(1); }); test("bun.lock stays at lockfile version 1 so Workers Builds' default Bun can install it", async () => { From fadd0296676a5adce2ebed9ceb58fed29a941525 Mon Sep 17 00:00:00 2001 From: musingfox Date: Thu, 1 Oct 2026 18:40:40 +0800 Subject: [PATCH 5/8] chore: delete wrangler.jsonc now that cf reads cloudflare.config.ts cf ignores the Wrangler file once cloudflare.config.ts exists, and Cloudflare's migration guide says to delete it after scripts and CI use cf. A test keeps a second config from coming back. Co-Authored-By: Claude Opus 5.5 (1M context) --- test/config.test.ts | 6 ++++++ wrangler.jsonc | 9 --------- 2 files changed, 6 insertions(+), 9 deletions(-) delete mode 100644 wrangler.jsonc diff --git a/test/config.test.ts b/test/config.test.ts index 62858f8..5e366fd 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -21,6 +21,12 @@ test("cf config records every request in Workers Logs", () => { expect(observability?.logs?.headSamplingRate ?? 1).toBe(1); }); +test("no wrangler config file sits beside cloudflare.config.ts", async () => { + for (const name of ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]) { + expect(await Bun.file(new URL(`../${name}`, import.meta.url)).exists()).toBe(false); + } +}); + test("bun.lock stays at lockfile version 1 so Workers Builds' default Bun can install it", async () => { // Bun 1.2.15, the Workers Builds default, rejects lockfileVersion 2 under --frozen-lockfile. const text = await Bun.file(new URL("../bun.lock", import.meta.url)).text(); diff --git a/wrangler.jsonc b/wrangler.jsonc deleted file mode 100644 index 20cb3ff..0000000 --- a/wrangler.jsonc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "node_modules/wrangler/config-schema.json", - "name": "better-link", - "main": "src/index.ts", - "compatibility_date": "2026-09-25", - "observability": { - "enabled": true - } -} From 5efd9c3bd310c5362b6f4df0771bdd88f4bd6ac5 Mon Sep 17 00:00:00 2001 From: musingfox Date: Thu, 1 Oct 2026 18:40:58 +0800 Subject: [PATCH 6/8] docs(spec): point free-plan-bindings-only at cloudflare.config.ts The wrangler files are gone, and the old grep would miss cf's kv, r2, d1 and queue binding types; verify now runs the config tests that check them. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/spec/free-plan-bindings-only.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/spec/free-plan-bindings-only.md b/docs/spec/free-plan-bindings-only.md index 0fe7afc..b86a4c9 100644 --- a/docs/spec/free-plan-bindings-only.md +++ b/docs/spec/free-plan-bindings-only.md @@ -2,18 +2,16 @@ id: free-plan-bindings-only status: accepted scope: - - "wrangler.toml" - - "wrangler.json" - - "wrangler.jsonc" + - "cloudflare.config.ts" - "src/**" -verify: check:test -f wrangler.jsonc && ! grep -qE 'kv_namespaces|r2_buckets|d1_databases|durable_objects|queues' wrangler.jsonc +verify: check:bun test ./test/config.test.ts -t "storage.binding" related: [cpu-budget-10ms, signed-cdn-urls-never-outlive-signature] source: better-link-design-spec adr: null --- 只使用 Workers 免費方案內含的功能,不宣告 KV、R2、D1、Durable Objects、Queues 或任何付費 binding。 -需要暫存的資料一律用 Cache API(`caches.default` 的 `put` 和 `match`)。wrangler 設定檔裡不出現任何儲存類 binding。 +需要暫存的資料一律用 Cache API(`caches.default` 的 `put` 和 `match`)。`cloudflare.config.ts` 裡不出現任何儲存類 binding。 環境變數和 secrets 不算 binding,可以使用。Cache API 只存在寫入它的資料中心、不會同步到其他地方,這是接受的代價,不是要修的問題。 From 3a4b5f9ea9bb1402d8095fc35de1cf046f3519bd Mon Sep 17 00:00:00 2001 From: musingfox Date: Thu, 1 Oct 2026 18:41:34 +0800 Subject: [PATCH 7/8] docs: describe the cf toolchain in the agent guide and readmes Co-Authored-By: Claude Opus 5.5 (1M context) --- CLAUDE.md | 16 ++++++++-------- README.md | 7 ++++--- README.zh-TW.md | 7 ++++--- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8f2cb47..2756a9c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,24 +4,24 @@ better-link is a Cloudflare Worker that turns social media post URLs into share ## Commands -Bun is used for package management and tests; wrangler runs the Worker. +Bun is used for package management and tests; the `cf` CLI runs, builds and deploys the Worker from `cloudflare.config.ts`, delegating the build to Wrangler. `cf` runs on Node 22.18 or later. ```sh bun install -bun run dev # wrangler dev, run the Worker locally -bun run typecheck # wrangler types generates worker-configuration.d.ts, then checks src and test separately +bun run dev # cf dev, run the Worker locally +bun run typecheck # cf workers types generates .cloudflare/types/index.d.ts, then checks src and test separately bun test # all unit tests bun test ./test/clean.test.ts # a single file bun run check # typecheck + test, run this before committing -bun run deploy # wrangler deploy -bun run smoke # starts wrangler dev on 127.0.0.1:8799 and sends end-to-end requests to real Instagram / Facebook +bun run deploy # cf deploy +bun run smoke # starts cf dev on port 8799 and sends end-to-end requests to real Instagram / Facebook ``` -- `worker-configuration.d.ts` is generated by `wrangler types` and gitignored. Until typecheck has run, types such as `Env` and `ExecutionContext` are missing. -- The README's Deploy to Cloudflare button relies on `wrangler.jsonc` at the repo root, the `deploy` script and the `cloudflare` field in `package.json`. Keep it working without any bindings, variables or secrets, so a fresh copy deploys with no setup. +- `.cloudflare/` holds generated types and build output and is gitignored. Until typecheck has run, types such as `Env` and `ExecutionContext` are missing. `wrangler.config.ts` sets `types.generate: false`, so `cf dev` and `cf build` do not regenerate them. +- The README's Deploy to Cloudflare button relies on `cloudflare.config.ts`, the `deploy` script and the `cloudflare` field in `package.json`. Do not add a Wrangler config file; `test/config.test.ts` fails if one appears. Keep it working without any bindings, variables or secrets, so a fresh copy deploys with no setup. - `bun.lock` must stay at `lockfileVersion` 1. Workers Builds installs with Bun 1.2.15 by default, which rejects version 2 under `--frozen-lockfile`; Bun 1.4 reads version 1 without rewriting it but writes version 2 when it regenerates the lock. `test/config.test.ts` fails if that happens; regenerate the lock with Bun 1.3. - Pull requests are reviewed by CodeRabbit (GitHub App), configured in `.coderabbit.yaml`. Its path instructions restate the specs; update them when specs are added or removed. -- GitHub Actions (`.github/workflows/ci.yml`) runs `bun install --frozen-lockfile` and `bun run check` with Bun 1.3.11 on pull requests and pushes to `main`. Keep its Bun version on 1.3 so the lock stays at version 1. +- GitHub Actions (`.github/workflows/ci.yml`) runs `bun install --frozen-lockfile` and `bun run check` with Bun 1.3.11 and Node 24 on pull requests and pushes to `main`. Keep its Bun version on 1.3 so the lock stays at version 1. - `smoke` needs outbound network access and its result depends on what upstream returns at the time, so it is not suitable as a required CI check. ## Architecture diff --git a/README.md b/README.md index 0b5b897..04e935e 100644 --- a/README.md +++ b/README.md @@ -80,12 +80,13 @@ To deploy manually instead: ```sh bun install -bun run deploy # wrangler deploy, logs in to Cloudflare on first run +npx cf auth login # once; cf does not reuse a Wrangler login +bun run deploy # cf deploy ``` ## Development -Requires [Bun](https://bun.sh). +Requires [Bun](https://bun.sh) and Node.js 22.18 or later. ```sh bun install @@ -94,6 +95,6 @@ bun run check # typecheck + unit tests bun run smoke # end-to-end tests against real Instagram / Facebook, needs network ``` -Configuration lives in `wrangler.jsonc`. The project only uses features of the Workers free plan and needs no bindings. +Configuration lives in `cloudflare.config.ts`. The project only uses features of the Workers free plan and needs no bindings. Design constraints are written in [`docs/spec/`](docs/spec/) (in Traditional Chinese), e.g. the 10ms CPU limit per request, falling back to the original URL on failure, and never putting expiring CDN URLs directly in og tags. Read the relevant specs before changing code. Instructions for AI agents are in [`CLAUDE.md`](CLAUDE.md) (`AGENTS.md` is a symlink to it). diff --git a/README.zh-TW.md b/README.zh-TW.md index 1449bb9..8971f67 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -80,12 +80,13 @@ Cloudflare 會把這個 repo 複製到你的 GitHub 或 GitLab 帳號、建置 ```sh bun install -bun run deploy # wrangler deploy,第一次會要求登入 Cloudflare +npx cf auth login # 只需一次;cf 不沿用 Wrangler 的登入 +bun run deploy # cf deploy ``` ## 開發 -需要 [Bun](https://bun.sh)。 +需要 [Bun](https://bun.sh) 和 Node.js 22.18 以上。 ```sh bun install @@ -94,6 +95,6 @@ bun run check # 型別檢查 + 單元測試 bun run smoke # 對真實 Instagram / Facebook 的端到端測試,需要網路 ``` -設定在 `wrangler.jsonc`。專案只用 Workers 免費方案的功能,不需要任何 binding。 +設定在 `cloudflare.config.ts`。專案只用 Workers 免費方案的功能,不需要任何 binding。 設計約束寫在 [`docs/spec/`](docs/spec/),例如每請求 CPU 10ms 上限、失敗時退回原網址、og 標籤不直接放會過期的 CDN 網址。修改程式前先讀相關規格;給 AI 代理的說明在 [`CLAUDE.md`](CLAUDE.md)(`AGENTS.md` 是它的 symlink)。 From 3c309cc7d0611d9b68e884ba3b507f11fb37c4b3 Mon Sep 17 00:00:00 2001 From: musingfox Date: Thu, 1 Oct 2026 20:44:04 +0800 Subject: [PATCH 8/8] test(config): build the known-bad binding samples with cf/config helpers The hand-written sample only proved the pattern matched itself. Building the samples with bindings.kv/r2/d1/queue and exports.durableObject ties the pattern to the shape cf actually emits. Co-Authored-By: Claude Opus 5.5 (1M context) --- test/config.test.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/config.test.ts b/test/config.test.ts index 5e366fd..f8806ef 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import type { WorkerConfig } from "cf/config"; +import { bindings, exports, type WorkerConfig } from "cf/config"; import config from "../cloudflare.config"; const STORAGE_BINDING = /"type":"(kv|r2|d1|durable-object|queue)"/; @@ -9,9 +9,11 @@ test("cf config names an entrypoint and declares no storage binding", () => { expect(JSON.stringify(config).match(STORAGE_BINDING)).toBeNull(); }); -test("the storage-binding pattern matches a known-bad sample", () => { - const sample = { worker: { env: { CACHE: { type: "kv" } }, exports: { Room: { type: "durable-object" } } } }; - expect(JSON.stringify(sample).match(STORAGE_BINDING)).not.toBeNull(); +test("the storage-binding pattern matches every storage binding cf/config can build", () => { + const samples = [bindings.kv(), bindings.r2(), bindings.d1(), bindings.queue(), exports.durableObject({ storage: "sqlite" })]; + for (const sample of samples) { + expect(JSON.stringify({ worker: { env: { X: sample } } }).match(STORAGE_BINDING)).not.toBeNull(); + } }); test("cf config records every request in Workers Logs", () => {