Closed12

Remix+CloudflareでWebサイトを作る47(Amazonアソシエイト、テストは`app/routes`にまとめ、ImagesとR2の料金比較、R2にアップ時にAVIFへ変換できなかった)

saneatsusaneatsu

【2024-12-31】Amazonアソシエイトに登録

https://affiliate.amazon.co.jp/

基本ここから入力していけばOKだった。
アソシエイトIDを任意のものにしたら入力した値-22になったがこの22ってなんだ?変更したいな。

https://affiliate.amazon.co.jp/help/node/topic/GM6CHU93RDXZV7D8

Amazon.co.jp が発行したアソシエイトIDを変更することはできません。アソシエイトIDは、当サイトに提出された加入申込書に記されたご希望の登録IDに基づいて自動的に作成されます。なお、アソシエイトIDが生成される際、末尾に自動的に-22が追加されます。
IDを変えたい場合は、既存のアソシエイトIDのもとにトラッキングIDを作成し、そのIDをご利用願います。(トラッキングID作成はこちら)

22は国の識別番号的なやつなんだろうか?
口座の情報も入力。


終わるとこの画像のようなページに遷移する。
右下の「税務情報(日本)の回答状況」が「未入力」になっているはずなのでここを入力して終わり。

saneatsusaneatsu

【2024-12-31】Playwrightのテストファイルはtests/ではなくapp/routesに書きたい

背景

現在、*.spec.tsファイルがすべてtests/以下にあるが、基本的にページごとにテストを書いてある。
するとapp/routes以下と同じディレクトリ構造がtests/にもまた作られてしまうが、これは冗長なので避けたい。

書く

ということで playwright.config.tsを書き換えて、テストファイルを移動させる。

playwright.config.ts
export default defineConfig({
- testDir: "./tests",
+ testMatch: "app/routes/**/*.spec.ts",

// testMatch: "./app/routes/**/*.spec.ts", // この書き方はしない!

testMatch: "./app/routes/**/*.spec.ts", と書くとCIで実行時に以下のようなエラーが発生する。

Error: No tests found
 ELIFECYCLE  Test failed. See above for more details.

https://zenn.dev/link/comments/08b06b5b3c0384

ここで作った tests/.auth/user.json だけ適切な移動場所が無いのでここは一旦そのままで。

saneatsusaneatsu

【2024-12-31】GitHub Actionsの残り使用可能時間の確認導線

背景

Freeプランだと1ヶ月に2000時間しかGitHub Actionsを使えないという制限がある。
テストコードが増えてきたのでどれくらい使用しているか確認したい。

導線

https://github.com/settings/billing/summary

公式サイトを見るとこのURLからわかるとのこと。

月末だけど半分も使ってなかった。

saneatsusaneatsu

【2024-01-01】Vitestでloader/actionのユニットテストをする

https://zenn.dev/kyrice2525/articles/article_tech_007

背景

Playwrightを使ってE2Eテストはできるようになった。
しかし、それではどうにもならない範囲も存在するのでVitestでテストをしたい。
具体的には「なにかしらのボタンをクリックしたときにDBの値がインクリメントされるだけで、UI上に反映されるわけではない」みたいなパターンに対応したい。

コード書く

1. パッケージインストール

pnpm add -D vitest jsdom

package.jsonにテスト実行コマンドを追加。
--mode development をつけることで .env.development を参照してくれるようになる。
デフォルトは--mode test

package.json
{
  "scripts": {
+   "vitest:development": "vitest --mode development",

2. vitest.config.ts

こんな感じ。

vitest.config.ts
import * as path from "node:path";
import * as VitestConfig from "vitest/config";

export default VitestConfig.defineConfig({
  test: {
    globals: true,
    environment: "jsdom",
    include: ["**/*.test.?(c|m)[jt]s?(x)"],
  },
  define: {
    "import.meta.vitest": false,
  },
  resolve: {
    alias: {
      "~": path.resolve(__dirname, "app"),
      db: path.resolve(__dirname, "db"),
    },
  },
});

include: ["**/*.test.?(c|m)[jt]s?(x)"],

公式サイトを見るとデフォルトでは ['**/*.{test,spec}.?(c|m)[jt]s?(x)'] になっているが *.spec.ts ファイルはPlaywrightで使っているため変更した。
ちょっと適当すぎる気もする。

db: path.resolve(__dirname, "db"),

以下のエラーに対応した。

Error: Failed to resolve import "db" from "app/routes/_index/hoge.server.ts". Does the file exist?
  Plugin: vite:import-analysis
  File: /appName/app/routes/_index/hoge.server.ts:1:19
  1  |  import { db } from "db";
     |                      ^

3. tsconfig.ts

import.meta.env を使うための変更。
--mode development にしているので.env.developmentにあるVITE_はじまりのものを参照できるようになる。

tsconfig.ts
  "compilerOptions": {
    "types": [
      // vite.config.ts の `globals: true, ` に加えてここを設定することで各ファイルでimportする必要がなくなる
+     "vitest/globals",
      // import.meta.env を使うために入れたがなくても動く
+     "vitest/importMeta",
    ],

その他

実装と同一ファイルにテストコードを書く、みたいな方法もあるらしい。

実装と同一のファイルにテストコードを記述するメリットとして以下のような点があります。

  • private にしたい目的で export したくない関数をテストできる
  • 実装とテストの距離が近いのでテストが書きやすい(私はテストコードを書くときだけいつもエディタの画面を分割して表示してます)
  • さっとプロトタイプのコードを書くたいときに素早く書ける

loader/action はどうせexportするし、route.tsxと同じディレクトリにテストファイル書いているから同一ファイルに書きたいモチベーション特に無いけど必要になったらできることだけ頭の中に入れておく。

https://zenn.dev/azukiazusa/articles/vitest-same-test-file

さいごに

PlaywrightとVitestを併用する形になったがdテストのカバレッジをあげながらどこはどっちで対応するかなど考えていきたい。

saneatsusaneatsu

CI作って気づいたけどloader/actionに関することをテストするならサーバーを立ち上げる必要がある。Playwrightと違ってサーバーの立ち上げとテストの実行がうまく噛み合わない。

そもそもそういう使い方じゃなかった。

saneatsusaneatsu

【2025-01-02】Cloudflare ImagesとR2の料金比較

料金体系

Cloudflare Images

R2

  • 格納
    • $0.015 / GB・月(10GBまでは無料)
  • 配信
    • $4.50 / 100万リクエスト(100万リクエストまでは無料)

計算

以下の前提で計算する。

  • 画像1枚が30KB
  • 画像は100万枚保存する
  • 1ヶ月に1000万配信

Cloudflare Images

  • 格納
    • $5/10万枚 * 10 = $50
  • 配信
    • $1/10万枚 * 10 = $10
  • 合計
    • $60

R2

  • 格納
    • 1枚30KBの場合
      • 30KB * 100万枚 = 28.6GB
      • 10GBまで無料なので = 18.6GB
      • 18.6GB * $0.015 = $0.279
    • 1枚100KBの場合
      • 100KB * 100万枚 = 95.4GB
      • 10GBまで無料なので = 85.4GB
      • 85.4GB * $0.015 = $1.281
    • つまり画像の大きさは誤差レベル
  • 配信
    • 1000万回 * $4.5 / 100万回 = $45
    • ただし、100万回までは無料なので$4.5引いて $40.5
  • 合計
    • $0.279 + $40.5 = $40.779

結論

R2めっちゃ安い。
配信数が多くなればなるほど差がでかくなりそう。

saneatsusaneatsu

【2025-01-04】R2にアップロードする画像をAVIFに変換したい(できなかった)

https://qiita.com/taqumo/items/60de0af9699415150035

背景

Cloudflare ImagesはVariantsを使ってリサイズをしてAVIF形式に変換できる。
R2に保存する画像に対しても同じことをしたい。

AVIFへの変換方法

pnpm add sharp
ho
+ import sharp from "sharp";

export async function action({ request, context }: ActionFunctionArgs) {
  const formData = await request.formData();
  const submission = parseWithZod(formData, { schema });
  const lastResult = submission.reply();

  const key = `contents/${crypto.randomUUID()}`;
  const file = submission.value.file;

  // ファイルをBufferとして読み取る
+ const arrayBuffer = await file.arrayBuffer();
+ const buffer = Buffer.from(arrayBuffer);

  // AVIF形式に変換
+ const avifBuffer = await sharp(buffer).toFormat("avif").toBuffer();

+ const response = await context.cloudflare.env.BUCKET.put(key, avifBuffer);
  const url = `${context.cloudflare.env.R2_DOMAIN}/${response?.key}`;
  return successJson<R2UploadResponse>({ lastResult, data: { url } });
}

ビルド時にエラー発生

開発環境でremix vite:build && wrangler dev --remoteremix vite:build時に以下のエラーが発生した。

--remote をつけるとリモートのR2に接続できるようになる
https://zenn.dev/kshiva1126/articles/1478c01df8d279

[WARNING] The package "node:util" wasn't found on the file system but is built into node.
▲ [WARNING] The package "node:events" wasn't found on the file system but is built into node.
▲ [WARNING] The package "node:path" wasn't found on the file system but is built into node.
▲ [WARNING] The package "node:stream" wasn't found on the file system but is built into node.
▲ [WARNING] The package "node:child_process" wasn't found on the file system but is built into node.
▲ [WARNING] The package "node:crypto" wasn't found on the file system but is built into node.
▲ [WARNING] The package "node:os" wasn't found on the file system but is built into node.
  Your Worker may throw errors at runtime unless you enable the "nodejs_compat" compatibility flag.
  Refer to https://developers.cloudflare.com/workers/runtime-apis/nodejs/ for more details. Imported
  from:
   - node_modules/.pnpm/sharp@0.33.5/node_modules/sharp/lib/utility.js

✘ [ERROR] Build failed with 2 errors:
  ✘ [ERROR] Could not resolve "child_process"  
      node_modules/.pnpm/detect-libc@2.0.3/node_modules/detect-libc/lib/detect-libc.js:6:29:
        6 │ const childProcess = require('child_process');
          ╵                              ~~~~~~~~~~~~~~~
  
    The package "child_process" wasn't found on the file system but is built into node.
    - Add the "nodejs_compat" compatibility flag to your project.  
  
  ✘ [ERROR] Could not resolve "fs"  
      node_modules/.pnpm/detect-libc@2.0.3/node_modules/detect-libc/lib/filesystem.js:6:19:
        6 │ const fs = require('fs');
          ╵                    ~~~~
  
    The package "fs" wasn't found on the file system but is built into node.
    - Add the "nodejs_compat" compatibility flag to your project.

これはsharpをimportすると発生するエラーだけどどうにもならなさそう。

saneatsusaneatsu

大量のWarningは以下を追加したらなくなった。

wrangler.toml
+ compatibility_flags = ["nodejs_compat"]
saneatsusaneatsu

【2025-01-05】wasm-vipsを使う

https://www.npmjs.com/package/wasm-vips

導入

pnpm add wasm-vips

コード

import { parseWithZod } from "@conform-to/zod";
import Vips from "wasm-vips";
import { z } from "zod";

const schema = z.object({
  file: z
    .custom<File>()
    .transform((file) => file)
    .refine((file) => file, {
      message: "ファイルを選択してください",
    })
    .refine((file) => file.size < 500 * 1000, {
      message: "ファイルサイズは最大5MBです",
    })
    .refine(
      (file) => ["image/jpeg", "image/jpg", "image/png"].includes(file.type),
      {
        message: ".jpeg .jpgもしくは.pngのみ可能です",
      },
    ),
});

export type R2UploadResponse = {
  url: string;
};

export async function action({ request, context }: ActionFunctionArgs) {  
  const formData = await request.formData();
  const submission = await parseWithZod(formData, { schema });  
  const lastResult = await submission.reply();
      
  if (submission.status !== "success") {
    return errorJson({ error: "入力した情報が正しくありません", lastResult });
  }
    
  try {    
    const now = new Date();    
    const year = now.getFullYear();
    const month = String(now.getMonth() + 1).padStart(2, "0");
    const day = String(now.getDate()).padStart(2, "0");
    const hour = String(now.getHours()).padStart(2, "0");

    const key = `contents/${year}${month}${day}/${crypto.randomUUID()}`;
    const file = submission.value.file;

    // AVIF形式に変換    
    const vips = await Vips();    
    const arrayBuffer = await file.arrayBuffer();    
    const image = vips.Image.newFromBuffer(await file.arrayBuffer());
    const avifBuffer = image.writeToBuffer(".avif");

    // R2にアップロード
    const response = await context.cloudflare.env.BUCKET.put(key, avifBuffer);    
    const url = `${context.cloudflare.env.R2_DOMAIN}/${response?.key}`;

    return successJson<R2UploadResponse>({ lastResult, data: { url } });
  } catch (error: unknown) {    
    return errorJson({ error, lastResult });
  }
}

エラー発生

remix vite:devでは発生しないが、wrangler devで立ち上げ時に const vips = await Vips(); の部分で発生した。

TypeError: Cannot read properties of undefined (reading 'startsWith')
    at null.<anonymous> (file:///AppName/node_modules/.pnpm/wasm-vips@0.0.11/node_modules/wasm-vips/lib/vips-es6.js:9:186)
    at Cm (file:///AppName/build/server/index.js:247:14078)
    at async Object.callRouteAction (file:///AppName/node_modules/.pnpm/@remix-run+server-runtime@2.15.0_typescript@5.7.2/node_modules/@remix-run/server-runtime/dist/data.js:36:16)
    at null.<anonymous> (async file:///AppName/.wrangler/tmp/dev-AIKJbD/server.js:4392:23)
    at async callLoaderOrAction (file:///AppName/node_modules/.pnpm/@remix-run+router@1.21.0/node_modules/@remix-run/router/router.ts:4963:16)
    at async Promise.all (index 2)

クライアントサイドで変換しては?

してみたら同様にconst vips = await Vips();で以下エラーが発生。

Uncaught (in promise) ReferenceError: SharedArrayBuffer is not defined

https://scrapbox.io/heguro/wasm-vipsを使いたい→無理そう

諦めてる記事を発見。

https://github.com/ffmpegwasm/ffmpeg.wasm/issues/263

Issueもあるが自分の助けになる解決策はなかった。

このスクラップは2025/01/04にクローズされました