Remix v2へアップデートした時のメモ
Remixを使って作ったアプリケーションをRemix v2へアップデートした時のメモです。
- Remix v2 | Remix
- マイグレーションガイド: Upgrading to v2 | Remix
アプリケーションは Remix + Cloudflare Pages で動かしてるアプリ。
serverBuildTargetの変更
結構前にテンプレートから生成したアプリだったので、それぞれのビルドターゲットの設定方法が変わっている。 V2では、この初期値が結構変わるので、次のページを参考にしてビルドターゲットに合わせたものを設定する。
Cloudflare Pages向けのビルドをするので、remix.config.js
に次のように設定する。
この設定はV1でも動くので、まずはこれで動かす。(future.v2_dev
が有効である必要はあったのかも。忘れている)
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
- serverBuildTarget: "cloudflare-pages",
server: "./server.js",
ignoredRouteFiles: [".*", "**/*.test.*"],
+ publicPath: "/build/", // default value, can be removed
+ serverBuildPath: "functions/[[path]].js",
+ serverConditions: ["worker"],
+ serverDependenciesToBundle: "all",
+ serverMainFields: ["browser", "module", "main"],
+ serverMinify: true,
+ serverModuleFormat: "esm", // default value in 2.x, can be removed once upgraded
+ serverPlatform: "neutral",
};
future.v2_XXX
を使ってv1のままアップデートする
Remix では V1にフラグ付きでV2の機能が使えるようになっている。 そのため、V2に上げる前に、V1のままV2の挙動にきりかえることができる。
V1の最新まであげて、remix watch
をして起動するDeprecation Warningを見ながら、v2_XXX
フラグを有効化していく。
基本的には警告を見て、一つずつv2_headers
などのフラグを有効化して動くかを確認していく。
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
...
+ future: {
+ v2_headers: true
+ }
}
ルーターの変更以外は、そんなやることはなかったのでフラグを有効化して動いてるかを確認するぐらいだった。 (Metaはちょっと書き方が変わってたけどあんまり使ってなかった)
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
+ future: {
+ v2_headers: true
+ v2_meta: true,
+ v2_headers: true,
+ v2_errorBoundary: true,
+ v2_dev: true,
+ v2_normalizeFormMethod: true
+ }
}
ルーティングの対応
Remix v2ではFlat Routesというのがデフォルトになっている。
この変更を対応するのは難しすぎるので、V1と同じルーティングの動作をする @remix-run/v1-route-convention
パッケージを使うことにした。
次のように、フラグと@remix-run/v1-route-convention
を使うとV1と同じルーティングの動作をする。
+ const { createRoutesFromFolders } = require("@remix-run/v1-route-convention");
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
...
future: {
v2_headers: true
v2_meta: true,
v2_headers: true,
v2_errorBoundary: true,
v2_dev: true,
v2_normalizeFormMethod: true
+ v2_routeConvention: true,
},
+ routes(defineRoutes) {
+ // uses the v1 convention, works in v1.15+ and v2
+ return createRoutesFromFolders(defineRoutes);
+ },
}
この時になぜかサーバが立ち上がらなくって、Cloudflare側のローカルサーバであるworkerdがエラーを吐いていた。
workerd/util/symbolizer.c++:99: warning: Not symbolizing stack traces because $LLVM_SYMBOLIZER is not set. To symbolize stack traces, set $LLVM_SYMBOLIZER to the location of the llvm-symbolizer binary. When running tests under bazel, use `--test_env=LLVM_SYMBOLIZER=<path>`.
*** Fatal uncaught kj::Exception: kj/async-io-unix.c++:945: failed: ::bind(sockfd, &addr.generic, addrlen): Address already in use; toString() = 0.0.0.0:8788
stack: /workspaces/app/node_modules/@cloudflare/workerd-linux-64/bin/workerd@31cdfba /workspaces/app/node_modules/@cloudflare/workerd-linux-64/bin/workerd@31cdd5f /workspaces/app/node_modules/@cloudflare/workerd-linux-64/bin/workerd@31cbfc5 /workspaces/app/node_modules/@cloudflare/workerd-linux-64/bin/workerd@168a085 /workspaces/app/node_modules/@cloudflare/workerd-linux-64/bin/workerd@168aa00 /workspaces/app/node_modules/@cloudflare/workerd-linux-64/bin/workerd@164afdd /workspaces/app/node_modules/@cloudflare/workerd-linux-64/bin/workerd@164fd47 /workspaces/app/node_modules/@cloudflare/workerd-linux-64/bin/workerd@164fac4 /workspaces/app/node_modules/@cloudflare/workerd-linux-64/bin/workerd@164faac /workspaces/app/node_modules/@cloudflare/workerd-linux-64/bin/workerd@32061de /workspaces/app/node_modules/@cloudflare/workerd-linux-64/bin/workerd@3205ddd /workspaces/app/node_modules/@cloudflare/workerd-linux-64/bin/workerd@32040c8 /workspaces/app/node_modules/@cloudflare/workerd-linux-64/bin/workerd@3203e8a /workspaces/app/node_modules/@cloudflare/workerd-linux-64/bin/workerd@1640293 /lib/x86_64-linux-gnu/libc.so.6@24082 /workspaces/app/node_modules/@cloudflare/workerd-linux-64/bin/workerd@164002d
✘ [ERROR] MiniflareCoreError [ERR_RUNTIME_FAILURE]: The Workers runtime failed to start. There is likely additional logging output above.
エラーに紛れてわかりにくいけど、failed: ::bind(sockfd, &addr.generic, addrlen): Address already in use; toString() = 0.0.0.0:8788
となってるようになぜか8788ポートがすでに使われているというエラーになっていた。
実際にプロセスを見てみると、すでに止まってるはずの workerd
や node
が残ってることがあるので、これを止めると解決した。
ここまでで、feature flagを使ってV2の機能がV1で動くようになった。
node:assert
がないエラー
ルーティングを切り替えると、node:assert
モジュールがないというエラーが起きることがあった。
これは、app/
以下にapp/routes/app.test.ts
のようなテストファイルがあると起きていた。
ignoredRouteFiles: [".*", "**/*.test.*"],
で無視されるはず(少なくてもルーティングを切り替える前は問題なかった)なのに、無視されなくなっていた。
よくわからなかったので、app/routes
以下にテストファイルを置くのをやめて解決した
Remix V2にバージョンを上げる。
まだ、remixのパッケージはv1.x.x
のままなので、v2.x.x
に上げる。
"@remix-run/cloudflare": "^2.0.1",
"@remix-run/cloudflare-pages": "^2.0.1",
"@remix-run/dev": "^2.0.1",
"@remix-run/eslint-config": "~2.0.1",
"@remix-run/react": "^2.0.1",
"@remix-run/serve": "~2.0.1",
"@remix-run/v1-route-convention": "^0.1.4",
あげみると context.env
が参照できなくなって、Cloudflare KVが参照できなくなった。
unstorageを使っていたので、context.env.KV_NAME
がundefinedになって、bindingが参照できなくて次のようなエラーが起きていた。
Invalid binding STORAGE: undefined``
Remixはserver.js
というファイルでデプロイ先ごとのサーバの設定があるので、これが壊れる雰囲気だった。
マイグレーションガイドを読むと、Custom app serverを使ってる場合(Cloudflare Pagesとか大体全部カスタムという扱いだと思う)は、テンプレートを見て直してねとあった。
If you are using your own app server (server.js), then check out our templates for examples of how to integrate with v2_dev or follow these steps:
Cloudflare Pagesを使ってるので、テンプレートからserver.ts
の内容をそのままコピーした。
import { logDevReady } from "@remix-run/cloudflare";
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
import * as build from "@remix-run/dev/server-build";
if (process.env.NODE_ENV === "development") {
logDevReady(build);
}
export const onRequest = createPagesFunctionHandler({
build,
// ここ古いテンプレと返すものが違うので注意。この返り値がcontextになる
getLoadContext: (context) => ({ env: context.env }),
mode: build.mode,
});
コマンドの変更
npm run dev
のようなコマンドも仕組みが変わっているので、これもテンプレートを参考にコピーして直した
KV Storageも使ってるような次のような形になった。
- "build": "cross-env NODE_ENV=production remix build",
- "dev": "run-p dev:*",
- "dev:remix": "cross-env NODE_ENV=development remix watch",
- "dev:wrangler": "cross-env NODE_ENV=development wrangler pages dev ./public --kv KV_NAME --compatibility-date=2023-03-14",
- "start": "cross-env NODE_ENV=production npm run dev:wrangler",
+ "build": "remix build",
+ "dev": "remix dev --manual -c \"npm run start\"",
+ "start": "wrangler pages dev --kv KV_NAME --compatibility-date=2023-06-21 ./public",
これで、npm run dev
でRemix v2で起動するようになった。
context.env
のコンパイルエラー
アプリ自体は動作しているが、TypeScriptの型がコンパイルエラーを起こしていた。
RemixのAppLoadContext
の型定義が変わってunknown
になったので、context.env.KV_NAME
がコンパイルエラーとなった。
AppLoadContext
の定義を見てみると、拡張したい場合declaration mergingを使ってねとあった。
/**
* An object of unknown type for route loaders and actions provided by the
* server's `getLoadContext()` function. This is defined as an empty interface
* specifically so apps can leverage declaration merging to augment this type
* globally: https://www.typescriptlang.org/docs/handbook/declaration-merging.html
*/
export interface AppLoadContext {
[key: string]: unknown;
}
けどremix.env.d.ts
で次のように拡張すると、loader
関数の返り値がPromise<any>
になったりおかしなことになった。
/// <reference types="@remix-run/dev" />
/// <reference types="@remix-run/cloudflare-pages" />
/// <reference types="@cloudflare/workers-types" />
declare module "@remix-run/server-runtime" {
export interface AppLoadContext {
env: {
KV_NAME: unknown;
};
}
}
これは、@remix-run/server-runtime
を読み込む前に、AppLoadContext
を拡張しようとして壊れる、読み込み順の問題だと思う。
Workaroundとしては、@remix-run/server-runtime
を読み込んでから拡張すれば、読み込み順の問題は解決する。
/// <reference types="@remix-run/dev" />
/// <reference types="@remix-run/cloudflare-pages" />
/// <reference types="@cloudflare/workers-types" />
import "@remix-run/server-runtime";
declare module "@remix-run/server-runtime" {
export interface AppLoadContext {
env: {
KV_NAME: string;
};
}
}
ただ、declare mergingの挙動に頼りたくなかったので、次のようなヘルパーを定義してenv
を参照するような方法に変更した。
import type { AppLoadContext } from "@remix-run/cloudflare";
export type MyAppLoadContext = {
env: {
KV_NAME: unknown;
};
};
/**
* Get env value
* avoid to declare merging
* @param context
* @param key
*/
export const getEnv = (context: AppLoadContext, key: keyof MyAppLoadContext["env"]) => {
return (context as MyAppLoadContext).env[key];
};
あとは、型エラーを直していくだけだった。
📝 loader
の型定義は2.0.1ではまだ壊れてるので2.1.0とかで修正されるらしい。
おわりに
RemixのFuture FlagでV1のままV2の機能を個別にアップデートしていけるのは良いと思った。
ただ、npm run dev
とかコマンドの変更やテンプレートレベルの変更はこういうアップデートに混ぜると迷子になるのでやめた方がいいと思った。
テンプレートから目視で確認してコピーしてくるみたいな、何が正解かよくわからない状況になった。
今回のアプリは設定はほとんどいじってないけど、カスタマイズしてるアプリケーションは、アップデートがかなり難しくなりそう。
結構手作業感が強いアップデートだったので、codemodとかで半分ぐらいは自動化できると良いなと思った。 けど、Remixはcodemodは諦めたようでした。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。