Remix+CloudflareでWebサイトを作る 32(Vitest、imgタグエラー時画像差し替え、国際化)
【2024-08-31】Hello, Vitest!
書いてみる
ここを参考に<Header />
に「トップ」というテキストがあることを確認します。
pnpm add -D vitest @testing-library/react happy-dom @vitejs/plugin-react @vitest/coverage-v8
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}`),
},
},
});
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用になってないので修正。
以下のように修正。
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
同じエラーのIssueがあった。
このIssueのコメントにある、このように書くと回避できると書いてある。
Remixの公式ドキュメントにも書いてある。
export default defineConfig(({ mode }) => {
return {
plugins: [
remixCloudflareDevProxy({ getLoadContext }),
tsconfigPaths(),
- remix(),
+ !process.env.VITEST ? remix() : react(),
<img>
タグで画像が正しく読み込めなかったときにonError
でキャッチして適当な画像に差し替えるやーつ
【2024-09-02】<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";
}
}}
/>
resolve={Promise.all[x, y]}
を使うと useActionDataの値が取得できない
【2024-09-02】remix-typedjsonを使って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つになったらめっちゃ汚くなってしまう。
もっといい方法は無いものか...。
【2024-09-05】多言語対応のために言語を選択して永続化したい
多言語選択のDropdown作るのは簡単だけど、その情報ブラウザに保存するやーつってどうしよ。
とか思ったが、そもそもDarkMode対応で学んだんだったわ。
まんま流用して終わり
【2024-09-06】多言語対応のパッケージ
パッケージを調べる
名前 | 最新更新日 |
---|---|
remix-i18next | 1ヶ月前 |
react-i18next | 1ヶ月前 |
react-intl | 4ヶ月前 |
i18n-react | 5ヶ月前 |
react-i18n | 6年前 |
上3つのどれかを使いたい。
remix-i18next
Remixの公式ブログで紹介されている。
優しい。
react-i18next
react-i18nextをRemixで使うことは可能そう。
公式ドキュメントも結構充実してそう。
react-intl
こっちも公式ドキュメントも結構充実してそう。
古くからあるやつっぽい。
結論
remix-i18nextで良さそう。
公式ドキュメントで参照されるという安心感。
Text content did not match. Server:
【2024-09-07】エラー:
この記事と同じでremix-i18next使用時に発生したエラー。
英語(en
)だと発生しないが、日本語(ja
)にすると発生してしまう。
同じ用に i18next-resources-to-backend を使用したが治らず。。
Could not resolve "fs"
という
【2024-09-07】Cloudflareにデプロイしたら エラー内容
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.
調査
Issueはあるが治ってなさそう...。
解決策?
初期化するときにbackendの記述をなくしてi18next-fs-backend自体を使わないようにしてみたけど、そもそもloadPathを指定していて正常に動くので一旦消した。
await instance
.use(initReactI18next)
.init({
(省略)
// `backend:` を指定すると翻訳データの取得元をHTTP、DBなどいろいろと指定することができる
backend: {
loadPath: "/api/locales?lng={{lng}}&ns={{ns}}", // ここがあるので大丈夫
},
});
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;
【2024-09-07】remix-i18nextで参考にしたサイトまとめ
基本
README、README内で参照されているレポジトリ、Remixの公式ブログで引用されている記事の3つ見たらだいたいわかった。
entry.server.tsx
entry.server.tsx
は一部動かなかったのでこの記事のを参考にした。
routerのカスタマイズ
/ja にアクセスしたときに日本語に、/en にアクセスしたときに英語になるような実装をしたいときに参考にした。
ちょっと見た