v2へのアップグレード
すべてのv2 APIとbehaviorはv1のFuture Flagsで利用可能です。プロジェクトの開発を中断させないよう、1つずつフラグを有効にしていくことができます。すべてのフラグを有効にした後は、v2にアップグレードすることで、非破壊的なアップグレードが可能です。
トラブルシューティングが必要な場合は、トラブルシューティングのセクションをご覧ください。
一般的なアップグレードの問題について、🎥 2分でv2にのビデオを参照してください。
remix dev
設定オプションについては、remix dev
のドキュメントをご覧ください。
remix-serve
Remix App Server (remix-serve
)を使っている場合は、v2_dev
を有効にします:
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
future: {
v2_dev: true,
},
};
以上です!
カスタムアプリサーバー
独自のアプリサーバー (server.js
) を使っている場合は、
テンプレートを確認してv2_dev
との統合方法を参考にするか、以下の手順に従ってください:
-
v2_dev
を有効化します:remix.config.js /** @type {import('@remix-run/dev').AppConfig} */ module.exports = { future: { v2_dev: true, }, };
-
package.json
のscripts
を更新します:remix watch
をremix dev
に置き換えます- 冗長な
NODE_ENV=development
を削除します -c
/--command
を使ってアプリサーバーを実行します
例:
package.json { "scripts": { - "dev:remix": "cross-env NODE_ENV=development remix watch", - "dev:server": "cross-env NODE_ENV=development node ./server.js" + "dev": "remix dev -c 'node ./server.js'", } }
-
アプリが起動したら、Remixコンパイラに "ready" メッセージを送信します
server.js import { broadcastDevReady } from "@remix-run/node"; // import { logDevReady } from "@remix-run/cloudflare" // CloudFlareを使う場合は `logDevReady` を使う const BUILD_DIR = path.join(process.cwd(), "build"); // ... サーバーの設定コードここに ... const port = 3000; app.listen(port, async () => { console.log(`👉 http://localhost:${port}`); broadcastDevReady(await import(BUILD_DIR)); });
-
(オプション)
--manual
require
キャッシュのパージングに依存していた場合は、--manual
フラグを使うことで継続できます:remix dev --manual -c 'node ./server.js'
詳細はマニュアルモードのガイドをご覧ください。
v1からv2にアップグレードした後
v1でfuture.v2_dev
フラグを有効化し、それが動作するようになったら、v2にアップグレードする準備ができています。
v2_dev
をtrue
に設定していた場合は、それを削除すれば動作するはずです。
v2_dev
設定を使っていた場合は、dev
設定フィールドに移動する必要があります:
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
- future: {
- v2_dev: {
- port: 4004
- }
- }
+ dev: {
+ port: 4004
+ }
}
ファイルシステムルートコンベンション
ファイルを変更せずにアップグレードする
今のところ変更したくない (あるいは永遠に変更したくない) 場合は、@remix-run/v1-route-convention
を使ってv1のコンベンションを継続できます。
npm i -D @remix-run/v1-route-convention
const {
createRoutesFromFolders,
} = require("@remix-run/v1-route-convention");
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
future: {
// v1.15+でwarningを消します
v2_routeConvention: true,
},
routes(defineRoutes) {
// v1のコンベンションを使います。v1.15+とv2で機能します
return createRoutesFromFolders(defineRoutes);
},
};
新しいコンベンションへのアップグレード
- ルートのネストは、フォルダーのネストではなく、ファイル名のドット(
.
)で作成されます - セグメントの
suffixed_
アンダースコアはネストをオプトアウトし、親ルートとのマッチングを避けます(ドット(.
)の代わりに) - セグメントの
_prefixed
アンダースコアは、__double
アンダースコアのプレフィックスの代わりにパスのないレイアウトルートを作成します _index.tsx
ファイルはインデックスルートを作成し、index.tsx
ではありません
v1では次のようなルートフォルダーが:
app/
├── routes/
│ ├── __auth/
│ │ ├── login.tsx
│ │ ├── logout.tsx
│ │ └── signup.tsx
│ ├── __public/
│ │ ├── about-us.tsx
│ │ ├── contact.tsx
│ │ └── index.tsx
│ ├── dashboard/
│ │ ├── calendar/
│ │ │ ├── $day.tsx
│ │ │ └── index.tsx
│ │ ├── projects/
│ │ │ ├── $projectId/
│ │ │ │ ├── collaborators.tsx
│ │ │ │ ├── edit.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ ├── settings.tsx
│ │ │ │ └── tasks.$taskId.tsx
│ │ │ ├── $projectId.tsx
│ │ │ └── new.tsx
│ │ ├── calendar.tsx
│ │ ├── index.tsx
│ │ └── projects.tsx
│ ├── __auth.tsx
│ ├── __public.tsx
│ └── dashboard.projects.$projectId.print.tsx
└── root.tsx
v2_routeConvention
を使うと以下のようになります:
app/
├── routes/
│ ├── _auth.login.tsx
│ ├── _auth.logout.tsx
│ ├── _auth.signup.tsx
│ ├── _auth.tsx
│ ├── _public._index.tsx
│ ├── _public.about-us.tsx
│ ├── _public.contact.tsx
│ ├── _public.tsx
│ ├── dashboard._index.tsx
│ ├── dashboard.calendar._index.tsx
│ ├── dashboard.calendar.$day.tsx
│ ├── dashboard.calendar.tsx
│ ├── dashboard.projects.$projectId._index.tsx
│ ├── dashboard.projects.$projectId.collaborators.tsx
│ ├── dashboard.projects.$projectId.edit.tsx
│ ├── dashboard.projects.$projectId.settings.tsx
│ ├── dashboard.projects.$projectId.tasks.$taskId.tsx
│ ├── dashboard.projects.$projectId.tsx
│ ├── dashboard.projects.new.tsx
│ ├── dashboard.projects.tsx
│ └── dashboard_.projects.$projectId.print.tsx
└── root.tsx
親ルートがフォルダーネストの代わりにグループ化されているのがわかります(認証ルートなど)。同じパスを持つが同じネストではないルート(例えば dashboard
とdashboard_
)もグループ化されます。
新しいコンベンションでは、ルートモジュールを定義する route.tsx
ファイルを持つディレクトリとしてルートを表すことができます。これにより、ルートで使用されるモジュールをコロケーションできます:
例えば、_public.tsx
を_public/route.tsx
に移動し、ルートで使用されるモジュールをコロケーションできます:
app/
├── routes/
│ ├── _auth.tsx
│ ├── _public/
│ │ ├── footer.tsx
│ │ ├── header.tsx
│ │ └── route.tsx
│ ├── _public._index.tsx
│ ├── _public.about-us.tsx
│ └── etc.
└── root.tsx
この変更の背景については、元の "フラットルート" の提案を参照してください。
headers
ルートのRemix v2では、ルートの headers
関数の動作がわずかに変更されました。remix.config.js
のfuture.v2_headers
フラグを使って、この新しい動作をあらかじめ選択できます。
v1では、Remixはレンダリングされる最終ルートのheaders
関数の結果のみを使用していました。親ルートのparentHeaders
を適切にマージするように、すべての潜在的なリーフににheaders
関数を追加する必要がありました。これはすぐにめんどうくさくなり、新しいルートを追加した際にヘッダーを共有したいにもかかわらず、headers
関数を忘れがちでした。
v2では、Remixは描画されたルートの中で最も深いheaders
関数を使用するようになりました。これにより、共通の祖先からheaders
関数を共有しやすくなります。そして必要に応じて、より深いルートにheaders
関数を追加できます。
meta
ルートのRemix v2では、ルートのmeta
関数の署名とRemixがメタタグを内部的に処理する方法が変更されました。
meta
から返すオブジェクトの代わりに、記述子の配列を返すようになりました。これにより、meta
APIがlinks
に近づき、メタタグのレンダリングをより柔軟に制御できるようになります。
さらに、<Meta />
はもはやルート階層全体のメタタグをレンダリングしなくなりました。リーフルートのmeta
から返されたデータのみがレンダリングされます。matches
を関数の引数としてアクセスすることで、親ルートのメタを含めることもできます。
この変更の背景については、v2のmeta
に関する提案を参照してください。
meta
規約をv2で使う
v1の@remix-run/v1-meta
パッケージを使用して、v1の規約を継続することができます。
metaV1
関数を使うと、meta
関数の引数と現在返されているオブジェクトを渡すことができます。この関数は、v2で使用可能な meta 記述子の配列に変換する前に、リーフルートのメタを直接の親ルートのメタとマージします。
export function meta() {
return {
title: "...",
description: "...",
"og:title": "...",
};
}
import { metaV1 } from "@remix-run/v1-meta";
export function meta(args) {
return metaV1(args, {
title: "...",
description: "...",
"og:title": "...",
});
}
デフォルトでは、この関数は直接の親ルートのメタとのみマージを行うことに注意してください。これは、一部のルートが直接オブジェクトの配列を返す場合、予期せぬ動作が発生する可能性があるためです。階層全体のメタをマージしたい場合は、すべてのルートのmeta
エクスポートでmetaV1
関数を使用してください。
parentsData
引数
v2では、meta
関数にはparentsData
引数が渡されなくなりました。これは、meta
がmatches
引数を介してすべてのルートマッチのデータにアクセスできるようになったためです。
parentsData
APIを再現するには、@remix-run/v1-meta
パッケージのgetMatchesData
関数を使用できます。これにより、各マッチのデータがルートのIDをキーとして返されます。
export function meta(args) {
const parentData = args.parentsData["routes/parent"];
}
変更後:
import { getMatchesData } from "@remix-run/v1-meta";
export function meta(args) {
const matchesData = getMatchesData(args);
const parentData = matchesData["routes/parent"];
}
meta
への移行
新しいexport function meta() {
return {
title: "...",
description: "...",
"og:title": "...",
};
}
export function meta() {
return [
{ title: "..." },
{ name: "description", content: "..." },
{ property: "og:title", content: "..." },
// SEO関連の<links>も追加できます
{ tagName: "link", rel: "canonical", href: "..." },
// <script type=ld+json>も追加できます
{
"script:ld+json": {
some: "value",
},
},
];
}
matches
引数
v1では、ネストされたルートからの返り値がすべてマージされていましたが、now you'll need to manage the merge yourself with matches
:
export function meta({ matches }) {
const rootMeta = matches[0].meta;
const title = rootMeta.find((m) => m.title);
return [
title,
{ name: "description", content: "..." },
{ property: "og:title", content: "..." },
// SEO関連の<links>も追加できます
{ tagName: "link", rel: "canonical", href: "..." },
// <script type=ld+json>も追加できます
{
"script:ld+json": {
"@context": "https://schema.org",
"@type": "Organization",
name: "Remix",
},
},
];
}
metaのドキュメントには、ルートメタのマージについてのヒントがあります。
CatchBoundary
とErrorBoundary
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
future: {
v2_errorBoundary: true,
},
};
v1では、投げられたResponse
がもっとも近いCatchBoundary
をレンダリングしていましたが、それ以外の未処理の例外はErrorBoundary
をレンダリングしていました。v2ではCatchBoundary
がなく、レスポンスであれ例外であれ、すべての未処理の例外がErrorBoundary
でレンダリングされます。
さらに、エラーはもはやErrorBoundary
にプロップスとして渡されず、useRouteError
フックを使ってアクセスできるようになりました。
import { useCatch } from "@remix-run/react";
export function CatchBoundary() {
const caught = useCatch();
return (
<div>
<h1>Oops</h1>
<p>Status: {caught.status}</p>
<p>{caught.data.message}</p>
</div>
);
}
export function ErrorBoundary({ error }) {
console.error(error);
return (
<div>
<h1>Uh oh ...</h1>
<p>Something went wrong</p>
<pre>{error.message || "Unknown error"}</pre>
</div>
);
}
変更後:
import {
useRouteError,
isRouteErrorResponse,
} from "@remix-run/react";
export function ErrorBoundary() {
const error = useRouteError();
// trueの場合、これまで`CatchBoundary`に渡されていたもの
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>Oops</h1>
<p>Status: {error.status}</p>
<p>{error.data.message}</p>
</div>
);
}
// 自身のロジックでタイプチェックを忘れずに。
// 投げられる値はエラーだけではありません!
let errorMessage = "Unknown error";
if (isDefinitelyAnError(error)) {
errorMessage = error.message;
}
return (
<div>
<h1>Uh oh ...</h1>
<p>Something went wrong.</p>
<pre>{errorMessage}</pre>
</div>
);
}
formMethod
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
future: {
v2_normalizeFormMethod: true,
},
};
複数のAPIがformMethod
を返しますが、v1では小文字のメソッド名が返されていました。v2ではHTTPとfetch仕様に合わせて、大文字のメソッド名が返されるようになりました。
function Something() {
const navigation = useNavigation();
// v1
navigation.formMethod === "post";
// v2
navigation.formMethod === "POST";
}
export function shouldRevalidate({ formMethod }) {
// v1
formMethod === "post";
// v2
formMethod === "POST";
}
useTransition
このフックは最近Reactで同名のフックが登場したため、useNavigation
に名称が変更されました。また、type
フィールドがなくなり、submission
オブジェクトがnavigation
オブジェクトに平坦化されました。
import { useTransition } from "@remix-run/react";
function SomeComponent() {
const transition = useTransition();
transition.submission.formData;
transition.submission.formMethod;
transition.submission.formAction;
transition.type;
}
import { useNavigation } from "@remix-run/react";
function SomeComponent() {
const navigation = useNavigation();
// transition.submissionのキーが`navigation[key]`に平坦化されます
navigation.formData;
navigation.formMethod;
navigation.formAction;
// このキーは削除されました
navigation.type;
}
以前のtransition.type
を導出するには、次のような例を参考にしてください。より簡単な方法もあるかもしれません。通常、navigation.state
、navigation.formData
、またはアクションから返されるデータをuseActionData
で取得することで、求めるUXを得られます。Discordで質問してください。サポさせていただきます :D
function Component() {
const navigation = useNavigation();
// transition.type === "actionSubmission"
const isActionSubmission =
navigation.state === "submitting";
// transition.type === "actionReload"
const isActionReload =
navigation.state === "loading" &&
navigation.formMethod != null &&
navigation.formMethod != "GET" &&
// 送信ナビゲーションがあり、送信されたロケーションをロードしている
navigation.formAction === navigation.location.pathname;
// transition.type === "actionRedirect"
const isActionRedirect =
navigation.state === "loading" &&
navigation.formMethod != null &&
navigation.formMethod != "GET" &&
// 送信ナビゲーションがあり、別のロケーションに移動している
navigation.formAction !== navigation.location.pathname;
// transition.type === "loaderSubmission"
const isLoaderSubmission =
navigation.state === "loading" &&
navigation.state.formMethod === "GET" &&
// ローダー送信があり、送信されたロケーションに移動している
navigation.formAction === navigation.location.pathname;
// transition.type === "loaderSubmissionRedirect"
const isLoaderSubmissionRedirect =
navigation.state === "loading" &&
navigation.state.formMethod === "GET" &&
// ローダー送信があり、新しいロケーションに移動している
navigation.formAction !== navigation.location.pathname;
}
GETの送信について
Remix v1では、<Form method="get">
やsubmit({}, { method: 'get' })
のようなGET送信は、transition.state
で idle -> submitting -> idle
のように遷移していました。これは完全に意味的に正しくありません。なぜなら、フォームを "送信"はしているものの、GETナビゲーションを実行し、ローダーのみを実行しているだけだからです。機能的には、<Link>
やnavigate()
と何も変わりませんが、ユーザーが入力フィールドを介して検索パラメータの値を指定できるだけです。
v2では、GET送信がより正確に "ローディングナビゲーション" として反映され、通常のリンクと同様にidle -> loading -> idle
と遷移するようになりました。GET送信が<Form>
やsubmit()
から来ている場合は、useNavigation.form*
が入力されているので、必要に応じて区別できます。
useFetcher
useNavigation
と同様に、useFetcher
でもsubmission
が平坦化され、type
フィールドが削除されました。
import { useFetcher } from "@remix-run/react";
function SomeComponent() {
const fetcher = useFetcher();
fetcher.submission.formData;
fetcher.submission.formMethod;
fetcher.submission.formAction;
fetcher.type;
}
import { useFetcher } from "@remix-run/react";
function SomeComponent() {
const fetcher = useFetcher();
// これらのキーが平坦化されました
fetcher.formData;
fetcher.formMethod;
fetcher.formAction;
// このキーは削除されました
fetcher.type;
}
以前のfetcher.type
を導出するには、次のような例を参考にしてください。より簡単な方法もあるかもしれません。通常、fetcher.state
、fetcher.formData
、またはアクションから返されるデータをfetcher.data
で取得することで、求めるUXを得られます。Discordで質問してください。サポさせていただきます :D
function Component() {
const fetcher = useFetcher();
// fetcher.type === "init"
const isInit =
fetcher.state === "idle" && fetcher.data == null;
// fetcher.type === "done"
const isDone =
fetcher.state === "idle" && fetcher.data != null;
// fetcher.type === "actionSubmission"
const isActionSubmission = fetcher.state === "submitting";
// fetcher.type === "actionReload"
const isActionReload =
fetcher.state === "loading" &&
fetcher.formMethod != null &&
fetcher.formMethod != "GET" &&
// データが返された場合、リロードしているはず
fetcher.data != null;
// fetcher.type === "actionRedirect"
const isActionRedirect =
fetcher.state === "loading" &&
fetcher.formMethod != null &&
fetcher.formMethod != "GET" &&
// データがない場合、リダイレクトしたはず
fetcher.data == null;
// fetcher.type === "loaderSubmission"
const isLoaderSubmission =
fetcher.state === "loading" &&
fetcher.formMethod === "GET";
// fetcher.type === "normalLoad"
const isNormalLoad =
fetcher.state === "loading" &&
fetcher.formMethod == null;
}
GETの送信について
Remix v1では、<fetcher.Form method="get">
やfetcher.submit({}, { method: 'get' })
のようなGET送信は、fetcher.state
で idle -> submitting -> idle
のように遷移していました。これは完全に意味的に正しくありません。なぜなら、フォームを "送信"はしているものの、GETリクエストを実行し、ローダーのみを実行しているだけだからです。機能的には、fetcher.load()
と何も変わりませんが、ユーザーが入力フィールドを介して検索パラメータの値を指定できるだけです。
v2では、GET送信がより正確に "ローディングリクエスト" として反映され、通常のfetcherロードと同様にidle -> loading -> idle
と遷移するようになりました。GET送信が<fetcher.Form>
やfetcher.submit()
から来ている場合は、fetcher.form*
が入力されているので、必要に応じて区別できます。
imagesizes
と imagesrcset
Linksの ルートの links
プロパティは、HTML の小文字のプロパティではなく、Reactの camelCase 値を使う必要があります。これら2つの値がv1では小文字で入り込んでいました。v2では、camelCaseバージョンのみが有効です:
export const links: LinksFunction = () => {
return [
{
rel: "preload",
as: "image",
imagesrcset: "...",
imagesizes: "...",
},
];
};
export const links: V2_LinksFunction = () => {
return [
{
rel: "preload",
as: "image",
imageSrcSet: "...",
imageSizes: "...",
},
];
};
browserBuildDirectory
remix.config.js
で、browserBuildDirectory
をassetsBuildDirectory
に名称変更してください。
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
browserBuildDirectory: "./public/build",
};
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
assetsBuildDirectory: "./public/build",
};
devServerBroadcastDelay
レースコンディションの問題を解決したため、remix.config.js
からdevServerBroadcastDelay
を削除してください。
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
- devServerBroadcastDelay: 300,
};
devServerPort
remix.config.js
で、devServerPort
をfuture.v2_dev.port
に名称変更してください。
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
devServerPort: 8002,
};
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
// v1.x中は future フラグを使います
future: {
v2_dev: {
port: 8002,
},
},
};
v1からv2にアップグレードすると、これはルートレベルのdev
設定に平坦化されます。
serverBuildDirectory
remix.config.js
で、serverBuildDirectory
をserverBuildPath
に名称変更し、ディレクトリ パスではなくモジュール パスを指定してください。
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
serverBuildDirectory: "./build",
};
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
serverBuildPath: "./build/index.js",
};
Remix は以前、サーバー用に複数のモジュールを作成していましたが、現在は単一のファイルを作成しています。
serverBuildTarget
ビルドターゲットを指定する代わりに、remix.config.js
のオプションを使ってサーバーのビルドがターゲットに合うように生成してください。この変更により、Remixがより多くのJavaScriptランタイム、サーバー、ホストにデプロイできるようになりました。
現在のserverBuildTarget
は以下の設定に置き換えられるはずです:
arc
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
publicPath: "/_static/build/",
serverBuildPath: "server/index.js",
serverMainFields: ["main", "module"], // デフォルト値、削除可能
serverMinify: false, // デフォルト値、削除可能
serverModuleFormat: "cjs", // 1.xのデフォルト値、アップグレード前に追加
serverPlatform: "node", // デフォルト値、削除可能
};
cloudflare-pages
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
publicPath: "/build/", // デフォルト値、削除可能
serverBuildPath: "functions/[[path]].js",
serverConditions: ["worker"],
serverDependenciesToBundle: "all",
serverMainFields: ["browser", "module", "main"],
serverMinify: true,
serverModuleFormat: "esm", // 2.xのデフォルト値、アップグレード後は削除可能
serverPlatform: "neutral",
};
cloudflare-workers
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
publicPath: "/build/", // デフォルト値、削除可能
serverBuildPath: "build/index.js", // デフォルト値、削除可能
serverConditions: ["worker"],
serverDependenciesToBundle: "all",
serverMainFields: ["browser", "module", "main"],
serverMinify: true,
serverModuleFormat: "esm", // 2.xのデフォルト値、アップグレード後は削除可能
serverPlatform: "neutral",
};
deno
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
publicPath: "/build/", // デフォルト値、削除可能
serverBuildPath: "build/index.js", // デフォルト値、削除可能
serverConditions: ["deno", "worker"],
serverDependenciesToBundle: "all",
serverMainFields: ["module", "main"],
serverMinify: false, // デフォルト値、削除可能
serverModuleFormat: "esm", // 2.xのデフォルト値、アップグレード後は削除可能
serverPlatform: "neutral",
};
node-cjs
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
publicPath: "/build/", // デフォルト値、削除可能
serverBuildPath: "build/index.js", // デフォルト値、削除可能
serverMainFields: ["main", "module"], // デフォルト値、削除可能
serverMinify: false, // デフォルト値、削除可能
serverModuleFormat: "cjs", // 1.xのデフォルト値、アップグレード前に追加
serverPlatform: "node", // デフォルト値、削除可能
};
serverModuleFormat
サーバーモジュールの出力フォーマットのデフォルトがcjs
からesm
に変更されました。v2でも引き続きCJSを使うことができますが、アプリの依存関係の多くがESMに対応していない可能性があります。
remix.config.js
で、serverModuleFormat: "cjs"
を指定して既存の動作を維持するか、serverModuleFormat: "esm"
を指定して新しい動作に切り替えることができます。
browserNodeBuiltinsPolyfill
Node.js組み込みモジュールのポリフィルは、デフォルトでブラウザには提供されなくなりました。Remix v2では、必要に応じて明示的にポリフィルを再導入する必要があります:
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
browserNodeBuiltinsPolyfill: {
modules: {
buffer: true,
fs: "empty",
},
globals: {
Buffer: true,
},
},
};
ブラウザバンドルでは一部のポリフィルが非常に大きいため、明示的にポリフィルを許可することをお勧めしますが、Remix v1のフル設定を即座に復元することもできます:
const { builtinModules } = require("node:module");
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
browserNodeBuiltinsPolyfill: {
modules: builtinModules,
},
};
serverNodeBuiltinsPolyfill
Node.js組み込みモジュールのポリフィルは、Node.js以外のサーバープラットフォームではデフォルトで提供されなくなりました。
Node.js以外のサーバープラットフォームをターゲットにしており、v1のデフォルト動作に切り替えたい場合、remix.config.js
でserverNodeBuiltinsPolyfill.modules
を空のオブジェクトにすることで、すべてのサーバーポリフィルを削除できます:
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
serverNodeBuiltinsPolyfill: {
modules: {},
},
};
その後、必要に応じて任意のポリフィル(または空のポリフィル)を再導入できます。
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
serverNodeBuiltinsPolyfill: {
modules: {
buffer: true,
fs: "empty",
},
globals: {
Buffer: true,
},
},
};
参考までに、v1のデフォルトのポリフィル全体を手動で指定することができます:
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
serverNodeBuiltinsPolyfill: {
modules: {
_stream_duplex: true,
_stream_passthrough: true,
_stream_readable: true,
_stream_transform: true,
_stream_writable: true,
assert: true,
"assert/strict": true,
buffer: true,
console: true,
constants: true,
crypto: "empty",
diagnostics_channel: true,
domain: true,
events: true,
fs: "empty",
"fs/promises": "empty",
http: true,
https: true,
module: true,
os: true,
path: true,
"path/posix": true,
"path/win32": true,
perf_hooks: true,
process: true,
punycode: true,
querystring: true,
stream: true,
"stream/promises": true,
"stream/web": true,
string_decoder: true,
sys: true,
timers: true,
"timers/promises": true,
tty: true,
url: true,
util: true,
"util/types": true,
vm: true,
wasi: true,
worker_threads: true,
zlib: true,
},
},
};
installGlobals
Node内蔵のfetchの実装を使用する準備として、fetchグローバルのインストールがアプリサーバーの責任になりました。remix-serve
を使っている場合は何も必要ありません。独自のアプリサーバーを使っている場合は、自分でグローバルをインストールする必要があります。
import { installGlobals } from "@remix-run/node";
installGlobals();
エクスポートされたポリフィルの削除
Remix v2では、これらのポリフィルされた実装を@remix-run/node
からエクスポートしなくなりました。代わりにグローバル名前空間の実装を使う必要があります。これが影響する可能性のある場所は、app/entry.server.tsx
ファイルで、ここでPassThrough
をReadableStream
に変換する必要があります:
import { PassThrough } from "node:stream";
import type { AppLoadContext, EntryContext } from "@remix-run/node"; // or cloudflare/deno
- import { Response } from "@remix-run/node"; // or cloudflare/deno
+ import { createReadableStreamFromReadable } from "@remix-run/node"; // or cloudflare/deno
import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server";
const ABORT_DELAY = 5_000;
export default function handleRequest({ /* ... */ }) { ... }
function handleBotRequest(...) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer ... />,
{
onAllReady() {
shellRendered = true;
const body = new PassThrough();
responseHeaders.set("Content-Type", "text/html");
resolve(
- new Response(body, {
+ new Response(createReadableStreamFromReadable(body), {
headers: responseHeaders,
status: responseStatusCode,
})
);
pipe(body);
},
...
onShellError(error: unknown) { ... }
onError(error: unknown) { ... }
}
);
setTimeout(abort, ABORT_DELAY);
});
}
function handleBrowserRequest(...) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer ... />,
{
onShellReady() {
shellRendered = true;
const body = new PassThrough();
responseHeaders.set("Content-Type", "text/html");
resolve(
- new Response(body, {
+ new Response(createReadableStreamFromReadable(body), {
headers: responseHeaders,
status: responseStatusCode,
})
);
pipe(body);
},
onShellError(error: unknown) { ... },
onError(error: unknown) { ... },
}
);
setTimeout(abort, ABORT_DELAY);
});
}
source-map-support
ソースマップサポートはアプリサーバーの責任になりました。remix-serve
を使っている場合は何も必要ありません。独自のアプリサーバーを使っている場合は、自分で source-map-support
をインストールする必要があります。
npm i source-map-support
import sourceMapSupport from "source-map-support";
sourceMapSupport.install();
Netlifyアダプター
@remix-run/netlify
ランタイムアダプターは、@netlify/remix-adapter
と @netlify/remix-edge-adapter
に非推奨となり、Remix v2で削除されました。すべての@remix-run/netlify
のインポートを@netlify/remix-adapter
に変更してください。
@netlify/remix-adapter
は@netlify/functions@^1.0.0
を要求するため、@remix-run/netlify
で現在サポートされている@netlify/functions
バージョンよりも破壊的な変更になることに注意してください。
このアダプターの削除に伴い、Netlifyテンプレートも公式のNetlifyテンプレートに変更しました。
Vercelアダプター
@remix-run/vercel
ランタイムアダプターは、Vercelの標準機能に非推奨となり、Remix v2で削除されました。package.json
から@remix-run/vercel
と@vercel/node
を削除し、server.js
/server.ts
ファイルを削除し、remix.config.js
のserver
とserverBuildPath
オプションを削除してください。
このアダプターの削除に伴い、Vercelテンプレートも公式のVercelテンプレートに変更しました。
組み込みのPostCSS/Tailwind サポート
v2では、プロジェクトにPostCSSや Tailwindの設定ファイルがある場合、これらのツールがRemixコンパイラ内で自動的に使用されます。
Remixとは別にカスタムのPostCSSやTailwindのセットアップを維持したい場合は、remix.config.js
でこれらの機能を無効にできます。
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
postcss: false,
tailwind: false,
};
トラブルシューティング
ESM / CommonJSのエラー
"SyntaxError: Named export '<something>' not found. The requested module '<something>' is a CommonJS module, which may not support all module.exports as named exports."
serverModuleFormat
のセクションを参照してください。