Open9

fork: qwik-city + cloudflare-pages で cloudflare binding を触りながら高速に再ビルドしたい

babiebabie

プロジェクト名はqwik-on-cloudlflareとし、全てデフォルトで作成:

$ pnpm create qwik@latest

試行:

$ pnpm start

動く。SSRのTodoサンプルアプリも動く。

babiebabie

Wranglerインストール:

$ pnpm add --save-dev wrangler

Qwik CityのCloudflare Pages Adapterをインストール:

$ pnpm run qwik add cloudflare-pages

試行:

$ pnpm wrangler pages dev -- pnpm vite --mode ssr

動く。HMRもちゃんとしてる。

babiebabie

wranglerコマンドでローカルKVをセットアップ:

$ pnpm wrangler kv:namespace create "QOC_KV_DEV" --preview
...
🌀 Creating namespace with title "worker-QOC_KV_DEV_preview"
✨ Success!
Add the following to your configuration file in your kv_namespaces array:
{ binding = "QOC_KV_DEV", preview_id = "<PREVIEW_ID>" }

なんか認証走る。ローカルだけの時は要らんやろ。<PREVIEW_ID>は長めのハッシュ値です。

出力されたpreview_id等をwrangler設定ファイルに書く:

wrangler.toml
kv_namespaces = [
   { binding = "QOC_KV_DEV", preview_id = "<PREVIEW_ID>" }
]

起動:

$ pnpm wrangler pages dev -- pnpm vite --mode ssr
...
 [ERROR] Processing wrangler.toml configuration:

    - "kv_namespaces[0]" bindings should have a string "id" field but got
  {"binding":"QOC_KV_DEV","preview_id":"<PREVIEW_ID>"}.


If you think this is a bug then please create an issue at https://github.com/cloudflare/workers-sdk/issues/new/choose

なんでや。idなんて出力されてへんやろ。

イシュー探索:
https://github.com/cloudflare/workers-sdk/issues/3621#issuecomment-1663924541
idpreview_idは同じにするととりあえず動くらしい。

再編集:

wrangler.toml
kv_namespaces = [
   { binding = "QOC_KV_DEV", id = "<PREVIEW_ID>", preview_id = "<PREVIEW_ID>" }
]

再起動:

$ pnpm wrangler pages dev -- pnpm vite --mode ssr
...
Your worker has access to the following bindings:
- KV Namespaces:
  - QOC_KV_DEV: <PREVIEW_ID>
...

立ち上がったっぽい。

型入れとく

$ pnpm add --save-dev @cloudflare/workers-types

デモアプリのtodolistをKVからget/putするように改変する:

src/routes/demo/todolist/index.tsx
diff --git a/src/routes/demo/todolist/index.tsx b/src/routes/demo/todolist/index.tsx
index 943235e..3ced350 100644
--- a/src/routes/demo/todolist/index.tsx
+++ b/src/routes/demo/todolist/index.tsx
@@ -1,4 +1,5 @@
 import { component$ } from "@builder.io/qwik";
+import type { RequestEventLoader } from "@builder.io/qwik-city";
 import {
   type DocumentHead,
   routeLoader$,
@@ -8,6 +9,8 @@ import {
   Form,
 } from "@builder.io/qwik-city";
 import styles from "./todolist.module.css";
+import { type PlatformCloudflarePages } from "@builder.io/qwik-city/middleware/cloudflare-pages";
+import { type KVNamespace } from "@cloudflare/workers-types";

 interface ListItem {
   text: string;
@@ -15,20 +18,27 @@ interface ListItem {

 export const list: ListItem[] = [];

-export const useListLoader = routeLoader$(() => {
-  return list;
-});
+export const useListLoader = routeLoader$(
+  async ({ platform }: RequestEventLoader<PlatformCloudflarePages>) => {
+    const kv: KVNamespace = platform.env && platform.env["QOC_KV_DEV"];
+    const todos = (await kv.get<ListItem[]>("todos", "json")) || [];
+    return todos;
+  }
+);

 export const useAddToListAction = routeAction$(
-  (item) => {
-    list.push(item);
+  async (item, { platform }) => {
+    const kv: KVNamespace = platform.env && platform.env["QOC_KV_DEV"];
+    const prevTodos = await kv.get<ListItem[]>("todos", "json");
+    const nextTodos = prevTodos ? [...prevTodos, item] : [];
+    await kv.put("todos", JSON.stringify(nextTodos));
     return {
       success: true,
     };
   },
   zod$({
     text: z.string().trim().min(1),
-  }),
+  })
 );

 export default component$(() => {

参考:
https://qwik.builder.io/docs/deployments/cloudflare-pages/#context

エラー。

まだできてないけど、長くなったので一旦投稿。

babiebabie

Cloudflare Pages Fuctions(実質Cloudflare Workers?)では、onRequest(context)context.env.MY_KVのように、Cloudflare KVを参照できるらしいのですが、Qwik City側のAdapter/Middlewareは特になんもしとらんような……

babiebabie

reqev.platform.envじゃなくてreqev.envにアクセスしてるの見たので試行:

src/routes/demo/todolist/index.tsx
diff --git a/src/routes/demo/todolist/index.tsx b/src/routes/demo/todolist/index.tsx
index 943235e..925999f 100644
--- a/src/routes/demo/todolist/index.tsx
+++ b/src/routes/demo/todolist/index.tsx
@@ -1,4 +1,5 @@
 import { component$ } from "@builder.io/qwik";
+import type { RequestEventLoader } from "@builder.io/qwik-city";
 import {
   type DocumentHead,
   routeLoader$,
@@ -8,6 +9,12 @@ import {
   Form,
 } from "@builder.io/qwik-city";
 import styles from "./todolist.module.css";
+import { type PlatformCloudflarePages } from "@builder.io/qwik-city/middleware/cloudflare-pages";
+import { type KVNamespace } from "@cloudflare/workers-types";
+
+export interface Env {
+  QOC_KV_DEV: KVNamespace;
+}

 interface ListItem {
   text: string;
@@ -15,20 +22,27 @@ interface ListItem {

 export const list: ListItem[] = [];

-export const useListLoader = routeLoader$(() => {
-  return list;
-});
+export const useListLoader = routeLoader$(
+  async (reqev: RequestEventLoader<PlatformCloudflarePages>) => {
+    const kv = reqev.env.get("QOC_KV_DEV") as unknown as KVNamespace;
+    const todos = (await kv.get<ListItem[]>("todos", "json")) || [];
+    return todos;
+  }
+);

 export const useAddToListAction = routeAction$(
-  (item) => {
-    list.push(item);
+  async (item, reqev) => {
+    const kv = reqev.env.get("QOC_KV_DEV") as unknown as KVNamespace;
+    const prevTodos = await kv.get<ListItem[]>("todos", "json");
+    const nextTodos = prevTodos ? [...prevTodos, item] : [];
+    await kv.put("todos", JSON.stringify(nextTodos));
     return {
       success: true,
     };
   },
   zod$({
     text: z.string().trim().min(1),
-  }),
+  })
 );

 export default component$(() => {

アカンね。

babiebabie

これ使ってみる:
https://github.com/james-elicx/cf-bindings-proxy

インストール:

$ pnpm add cf-bindings-proxy

コード更新:

git diff -u src/routes/demo/todolist/index.tsx
diff --git a/src/routes/demo/todolist/index.tsx b/src/routes/demo/todolist/index.tsx
index 925999f..4551126 100644
--- a/src/routes/demo/todolist/index.tsx
+++ b/src/routes/demo/todolist/index.tsx
@@ -10,6 +10,7 @@ import {
 } from "@builder.io/qwik-city";
 import styles from "./todolist.module.css";
 import { type PlatformCloudflarePages } from "@builder.io/qwik-city/middleware/cloudflare-pages";
+import { binding } from "cf-bindings-proxy";
 import { type KVNamespace } from "@cloudflare/workers-types";

 export interface Env {
@@ -23,16 +24,20 @@ interface ListItem {
 export const list: ListItem[] = [];

 export const useListLoader = routeLoader$(
-  async (reqev: RequestEventLoader<PlatformCloudflarePages>) => {
-    const kv = reqev.env.get("QOC_KV_DEV") as unknown as KVNamespace;
+  async ({ platform }: RequestEventLoader<PlatformCloudflarePages>) => {
+    const kv = binding<KVNamespace>("QOC_KV_DEV", {
+      fallback: platform.env as Record<string, unknown>,
+    });
     const todos = (await kv.get<ListItem[]>("todos", "json")) || [];
     return todos;
   }
 );

 export const useAddToListAction = routeAction$(
-  async (item, reqev) => {
-    const kv = reqev.env.get("QOC_KV_DEV") as unknown as KVNamespace;
+  async (item, { platform }) => {
+    const kv = binding<KVNamespace>("QOC_KV_DEV", {
+      fallback: platform.env as Record<string, unknown>,
+    });
     const prevTodos = await kv.get<ListItem[]>("todos", "json");
     const nextTodos = prevTodos ? [...prevTodos, item] : [];
     await kv.put("todos", JSON.stringify(nextTodos));

実行:

term-1
$ ENABLE_BINDINGS_PROXY=true pnpm exec cf-bindings-proxy --kv=QOC_KV_DEV
term-2
$ pnpm vite --mode ssr

動いた!

確認:

$ pnpm wrangler kv:key list --namespace-id=QOC_KV_DEV --local
[
  {
    "name": "todos"
  }
]
$ pnpm wrangler kv:key get todos --namespace-id=QOC_KV_DEV --local
[{"text":"b"},{"text":"c"},{"text":"a"},{"text":"d"},{"text":"e"},{"text":"f"}]

ある。

bindingとnamespace-idがゴッチャになってるのは多分俺のkvの作り方が悪かったんだろうな:

tree .wrangler/
.wrangler/
└── state
    └── v3
        └── kv
            ├── 6ae710e2775143a0adaf97d3a9278a3d
            │   └── db.sqlite
            └── QOC_KV_DEV
                ├── blobs
                │   └── 2e5fae1c0215852d61a8638b30e7150ca72feb475ad7a1cf18cfd2366becbd540005d3381d0ad147
                ├── db.sqlite
                ├── db.sqlite-shm
                └── db.sqlite-wal

7 directories, 5 files

ターミナル2つ使うのが怠いので要改良だが、とりあえず動いた!!

babiebabie

あとは、binding名を直したり、wrangler.tomlを環境毎の設定を入れるとか整理したいわね。