Closed8

Remix+CloudflareでWebサイトを作る 32(Vitest、imgタグエラー時画像差し替え、国際化)

saneatsusaneatsu

【2024-08-31】Hello, Vitest!

書いてみる

https://n-laboratory.jp/articles/next-13-vitest-unittest

ここを参考に<Header />に「トップ」というテキストがあることを確認します。

pnpm add -D vitest @testing-library/react happy-dom @vitejs/plugin-react @vitest/coverage-v8
vitest.config.ts
import path from "path";

import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";

const dirName = "app";

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: "happy-dom",
    coverage: {
      provider: "v8",
      include: [`${dirName}/**/*.{tsx,js,ts}`],
      all: true,
      reporter: ["html", "clover", "text"],      
    },
    root: ".",
    reporters: ["verbose"],
  },
  resolve: {
    alias: {
      "@": path.resolve(__dirname, `./${dirName}`),
      "~": path.resolve(__dirname, `./${dirName}`),
    },
  },
});
top.spec.tsx
import { render } from "@testing-library/react";
import { describe, it } from "vitest";

import Header from "~/components/Header";

describe("トップページ", () => {
  it("タブが3つ表示されている", () => {    
    const { getByText } = render(<Header />);            
    // expect(getByText("トップ")).toBeDefined();
  });
});

エラー: Error: Failed to resolve import

Error: Failed to resolve import "~/components/Header" from "tests/public/top.spec.tsx". Does the file exist?

パスのエラーだが確かにファイル事態は存在する。

解決方法

vite.config.ts と見比べると plugins: の箇所がRemix用になってないので修正。
以下のように修正。

vite.config.ts
import path from "path";

import {
  vitePlugin as remix,
  cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
} from "@remix-run/dev";
import tsconfigPaths from "vite-tsconfig-paths";
import { defineConfig } from "vitest/config";

import { getLoadContext } from "./load-context";

const dirName = "app";

export default defineConfig({
  plugins: [
    remixCloudflareDevProxy({ getLoadContext }),
    remix(),
    tsconfigPaths(), // これが必要
  ],
  test: {
    globals: true,
    environment: "happy-dom",
    coverage: {
      provider: "v8",
      include: [`${dirName}/**/*.{tsx,js,ts}`],
      all: true,
      reporter: ["html", "clover", "text"],
    },
    root: ".",
    reporters: ["verbose"],
  },
  resolve: {
    alias: {
      "@": path.resolve(__dirname, `./${dirName}`),
      "~": path.resolve(__dirname, `./${dirName}`),
    },
  },
});

エラー: Error: Remix Vite plugin can't detect preamble. Something is wrong.

次はshadcn/uiのInputコンポーネント内でエラーが発生。

 FAIL  tests/public/top.spec.tsx [ tests/public/top.spec.tsx ]
Error: Remix Vite plugin can't detect preamble. Something is wrong.
 ❯ app/components/ui/input.tsx:11:7
      9|   ({ className, type, ...props }, ref) => {
     10|     return (
     11|       <input
       |       ^
     12|         type={type}
     13|         className={cn(
 ❯ app/components/Header.tsx:6:31

https://github.com/remix-run/remix/issues/7863#issuecomment-2254495166

同じエラーのIssueがあった。
このIssueのコメントにある、このように書くと回避できると書いてある。

Remixの公式ドキュメントにも書いてある。

vite.config.ts
export default defineConfig(({ mode }) => {
  return {
    plugins: [
      remixCloudflareDevProxy({ getLoadContext }),      
      tsconfigPaths(),
-     remix(),
+     !process.env.VITEST ? remix() : react(),
saneatsusaneatsu

【2024-09-02】<img>タグで画像が正しく読み込めなかったときにonErrorでキャッチして適当な画像に差し替えるやーつ

<img
  src="https://invalid-url.com/image.png"
  alt="this is sample"
  onError={(e) => {
    const target = e.target;
    if (target instanceof HTMLImageElement) {
      target.onerror = null; // 差し替える画像が見つからない場合の無限ループを防ぐために、onErrorをクリア
      target.src = "/static/not-found.png";
    }
  }}
/>
saneatsusaneatsu

【2024-09-02】remix-typedjsonを使ってresolve={Promise.all[x, y]}を使うと useActionDataの値が取得できない

remix-typedjson を使っていてちょっとハマった。
また、取り急ぎ対応したがきれいな方法ではない

背景

以下のようなコードがあり、<Outlet /> の中でactionを呼び出している。
本来であればactionを実行後 console.log('actionが実行されました') が発火する想定だがそうならない。

import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson";

export default function PersonsIdPage() {
  const { person, tag } = useTypedLoaderData<typeof loader>();

  const actionData = useActionData<typeof action>();
  useEffect(() => {    
    if (actionData && actionData.success) {
      console.log('actionが実行されました') // これが発火する想定
    }
  }, [actionData]);

  return (
    <Suspense fallback={<p>ローディング中...</p>}>
      <TypedAwait
        resolve={Promise.all([person, occupations])}
        errorElement={
          <ErrorNotify
            title="エラー"
            message="原因不明のエラーが発生しました"
          />
        }
      >
        {([person, tag]) => (
          <Layout person={person} tag={tag} >
            <Outlet /> // この中でactionを呼び出している
          </Layout>
        )}
      </TypedAwait>
    </Suspense>
  );
}

調査

console.log の部分が発火するページもあるので違いを探ってみる。

どうやらresolve={person} のように1つだけしかない場合は正常に動くっぽい。

remix-typedjsonにはuseTypedActionDataというものがあるがそれを使ったからといってうまくいかなかった。

解決方法

import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson";

export default function PersonsIdPage() {
  const { person, tag } = useTypedLoaderData<typeof loader>();

  const actionData = useActionData<typeof action>();
  useEffect(() => {    
    if (actionData && actionData.success) {
      console.log('actionが実行されました')
    }
  }, [actionData]);

  return (
    <Suspense fallback={<p>ローディング中...</p>}>
      <TypedAwait
        resolve={person} // Promise.allを使って複数を入れない
        errorElement={
          <ErrorNotify
            title="エラー"
            message="原因不明のエラーが発生しました"
          />
        }
      >
        {(person) => (
          <TypedAwait // 無理やり入れ子にしている
            resolve={tag}
            errorElement={
              <ErrorNotify
                title="エラー"
                message="原因不明のエラーが発生しました"
              />
            }
          >
            {(tag) => (
              <Layout person={person} tag={tag}>
                <Outlet />
              </Layout>
            )}
          </TypedAwait>                                        
        )}
      </TypedAwait>
    </Suspense>
  );
}

3つとか4つになったらめっちゃ汚くなってしまう。
もっといい方法は無いものか...。

saneatsusaneatsu

【2024-09-05】多言語対応のために言語を選択して永続化したい

多言語選択のDropdown作るのは簡単だけど、その情報ブラウザに保存するやーつってどうしよ。

とか思ったが、そもそもDarkMode対応で学んだんだったわ。
まんま流用して終わり

https://zenn.dev/link/comments/a2222239b799db

saneatsusaneatsu

【2024-09-06】多言語対応のパッケージ

パッケージを調べる

名前 最新更新日
remix-i18next 1ヶ月前
react-i18next 1ヶ月前
react-intl 4ヶ月前
i18n-react 5ヶ月前
react-i18n 6年前

上3つのどれかを使いたい。

remix-i18next

https://remix.run/blog/remix-i18n

Remixの公式ブログで紹介されている。

https://www.npmjs.com/package/remix-i18next?activeTab=readme#why-remix-i18next
npmのページには動かなかったら以下のexample repositoryを参考にしてくれ、と。
優しい。

https://github.com/sergiodxa/remix-vite-i18next/tree/main

react-i18next

https://note.com/lizefield/n/naa3fb014a8c9
react-i18nextをRemixで使うことは可能そう。

https://react.i18next.com/

公式ドキュメントも結構充実してそう。

react-intl

https://formatjs.io/docs/getting-started/installation/

こっちも公式ドキュメントも結構充実してそう。
古くからあるやつっぽい。

結論

remix-i18nextで良さそう。
公式ドキュメントで参照されるという安心感。

saneatsusaneatsu

【2024-09-07】エラー: Text content did not match. Server:

https://zenn.dev/mktu/articles/7f924edd8125a4

この記事と同じでremix-i18next使用時に発生したエラー。
英語(en)だと発生しないが、日本語(ja)にすると発生してしまう。

同じ用に i18next-resources-to-backend を使用したが治らず。。

saneatsusaneatsu

【2024-09-07】Cloudflareにデプロイしたら Could not resolve "fs" という

エラー内容

https://www.npmjs.com/package/remix-i18next?activeTab=readme#why-remix-i18next

READMEに沿って以下を実行してCloudflareにデプロイする。

npm install i18next-http-backend i18next-fs-backend

すると、以下のエラーが発生した。

  ✘ [ERROR] Could not resolve "fs"
  
      ../node_modules/i18next-fs-backend/esm/fs.cjs:2:18:
        2 │   var f = require('fs')
          │                   ~~~~
          ╵                   "./fs"
  
    The package "fs" wasn't found on the file system but is built into node. Are you trying to bundle for node? You can use "platform: 'node'" to do that, which will remove this error.
  
  
  
  ✘ [ERROR] Could not resolve "fs"
  
      ../node_modules/i18next-fs-backend/esm/fs.cjs:2:18:
        2 │   var f = require('fs')
          │                   ~~~~
          ╵                   "./fs"
  
    The package "fs" wasn't found on the file system but is built into node.
    Add the "nodejs_compat" compatibility flag to your Pages project and make sure to prefix the module name with "node:" to enable Node.js compatibility.  

調査

https://github.com/i18next/next-i18next/issues/1401

Issueはあるが治ってなさそう...。

解決策?

初期化するときにbackendの記述をなくしてi18next-fs-backend自体を使わないようにしてみたけど、そもそもloadPathを指定していて正常に動くので一旦消した。

await instance
    .use(initReactI18next)
    .init({
      (省略)
      // `backend:` を指定すると翻訳データの取得元をHTTP、DBなどいろいろと指定することができる
      backend: {
        loadPath: "/api/locales?lng={{lng}}&ns={{ns}}", // ここがあるので大丈夫
      },
    });
i18n.server.ts
import { createCookie } from "@remix-run/cloudflare";
- import Backend from "i18next-fs-backend";
import { RemixI18Next } from "remix-i18next/server";

import * as i18n from "~/config/i18n";

export const localeCookie = createCookie("lng", {
  path: "/",
  sameSite: "lax",
  secure: process.env.NODE_ENV === "production",
  httpOnly: true,
});

const i18nServer = new RemixI18Next({
  detection: {
    supportedLanguages: i18n.supportedLanguages,
    fallbackLanguage: i18n.fallbackLanguage,
    cookie: localeCookie,
  },  
  i18next: {
    ...i18n,
  },
- backend: Backend,
});

export default i18nServer;
saneatsusaneatsu

【2024-09-07】remix-i18nextで参考にしたサイトまとめ

基本

https://www.npmjs.com/package/remix-i18next?activeTab=readme#why-remix-i18next
https://github.com/sergiodxa/remix-vite-i18next
https://www.smashingmagazine.com/2023/02/internationalization-i18n-right-remix-headless-cms-storyblok/

README、README内で参照されているレポジトリ、Remixの公式ブログで引用されている記事の3つ見たらだいたいわかった。

entry.server.tsx

https://zenn.dev/mktu/articles/7f924edd8125a4
entry.server.tsx は一部動かなかったのでこの記事のを参考にした。

routerのカスタマイズ

https://www.youtube.com/watch?v=EtLyfx0S7Lo

/ja にアクセスしたときに日本語に、/en にアクセスしたときに英語になるような実装をしたいときに参考にした。

ちょっと見た

https://sizu.me/tyshgc/posts/sc8tik5ncdrt

このスクラップは2ヶ月前にクローズされました