v2へのアップグレード

このドキュメントはクラシックなRemixコンパイラを使ってv1からv2に移行する際のガイダンスを提供しています。Viteへの移行に関するガイダンスについては、Remix Viteのドキュメントを参照してください。

すべてのv2 APIとbehaviorはv1のFuture Flagsで利用可能です。プロジェクトの開発を中断させないよう、1つずつフラグを有効にしていくことができます。すべてのフラグを有効にした後は、v2にアップグレードすることで、非破壊的なアップグレードが可能です。

トラブルシューティングが必要な場合は、トラブルシューティングのセクションをご覧ください。

一般的なアップグレードの問題について、🎥 2分でv2にのビデオを参照してください。

remix dev

設定オプションについては、remix devのドキュメントをご覧ください。

remix-serve

Remix App Server (remix-serve)を使っている場合は、v2_devを有効にします:

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  future: {
    v2_dev: true,
  },
};

以上です!

カスタムアプリサーバー

独自のアプリサーバー (server.js) を使っている場合は、 テンプレートを確認してv2_devとの統合方法を参考にするか、以下の手順に従ってください:

  1. v2_devを有効化します:

    remix.config.js
    /** @type {import('@remix-run/dev').AppConfig} */
    module.exports = {
      future: {
        v2_dev: true,
      },
    };
  2. package.jsonscriptsを更新します:

    • remix watchremix 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'",
       }
     }
  3. アプリが起動したら、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));
    });
  4. (オプション) --manual

    requireキャッシュのパージングに依存していた場合は、--manualフラグを使うことで継続できます:

    remix dev --manual -c 'node ./server.js'

    詳細はマニュアルモードのガイドをご覧ください。

v1からv2にアップグレードした後

v1でfuture.v2_devフラグを有効化し、それが動作するようになったら、v2にアップグレードする準備ができています。 v2_devtrueに設定していた場合は、それを削除すれば動作するはずです。

v2_dev設定を使っていた場合は、dev設定フィールドに移動する必要があります:

remix.config.js
  /** @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
remix.config.js
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

親ルートがフォルダーネストの代わりにグループ化されているのがわかります(認証ルートなど)。同じパスを持つが同じネストではないルート(例えば dashboarddashboard_)もグループ化されます。

新しいコンベンションでは、ルートモジュールを定義する 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.jsfuture.v2_headersフラグを使って、この新しい動作をあらかじめ選択できます。

v1では、Remixはレンダリングされる最終ルートのheaders関数の結果のみを使用していました。親ルートのparentHeadersを適切にマージするように、すべての潜在的なリーフににheaders関数を追加する必要がありました。これはすぐにめんどうくさくなり、新しいルートを追加した際にヘッダーを共有したいにもかかわらず、headers関数を忘れがちでした。

v2では、Remixは描画されたルートの中で最も深いheaders関数を使用するようになりました。これにより、共通の祖先からheaders関数を共有しやすくなります。そして必要に応じて、より深いルートにheaders関数を追加できます。

ルートのmeta

Remix v2では、ルートのmeta関数の署名とRemixがメタタグを内部的に処理する方法が変更されました。

metaから返すオブジェクトの代わりに、記述子の配列を返すようになりました。これにより、metaAPIがlinksに近づき、メタタグのレンダリングをより柔軟に制御できるようになります。

さらに、<Meta />はもはやルート階層全体のメタタグをレンダリングしなくなりました。リーフルートのmetaから返されたデータのみがレンダリングされます。matchesを関数の引数としてアクセスすることで、親ルートのメタを含めることもできます。

この変更の背景については、v2のmetaに関する提案を参照してください。

v1のmeta規約をv2で使う

@remix-run/v1-metaパッケージを使用して、v1の規約を継続することができます。

metaV1関数を使うと、meta関数の引数と現在返されているオブジェクトを渡すことができます。この関数は、v2で使用可能な meta 記述子の配列に変換する前に、リーフルートのメタを直接の親ルートのメタとマージします。

app/routes/v1-route.tsx
export function meta() {
  return {
    title: "...",
    description: "...",
    "og:title": "...",
  };
}
app/routes/v2-route.tsx
import { metaV1 } from "@remix-run/v1-meta";
 
export function meta(args) {
  return metaV1(args, {
    title: "...",
    description: "...",
    "og:title": "...",
  });
}

デフォルトでは、この関数は直接の親ルートのメタとのみマージを行うことに注意してください。これは、一部のルートが直接オブジェクトの配列を返す場合、予期せぬ動作が発生する可能性があるためです。階層全体のメタをマージしたい場合は、すべてのルートのmetaエクスポートでmetaV1関数を使用してください。

parentsData引数

v2では、meta関数にはparentsData引数が渡されなくなりました。これは、metamatches引数を介してすべてのルートマッチのデータにアクセスできるようになったためです。

parentsDataAPIを再現するには、@remix-run/v1-metaパッケージのgetMatchesData関数を使用できます。これにより、各マッチのデータがルートのIDをキーとして返されます。

app/routes/v1-route.tsx
export function meta(args) {
  const parentData = args.parentsData["routes/parent"];
}

変更後:

app/routes/v2-route.tsx
import { getMatchesData } from "@remix-run/v1-meta";
 
export function meta(args) {
  const matchesData = getMatchesData(args);
  const parentData = matchesData["routes/parent"];
}

新しいmetaへの移行

app/routes/v1-route.tsx
export function meta() {
  return {
    title: "...",
    description: "...",
    "og:title": "...",
  };
}
app/routes/v2-route.tsx
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:

app/routes/v2-route.tsx
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のドキュメントには、ルートメタのマージについてのヒントがあります。

CatchBoundaryErrorBoundary

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  future: {
    v2_errorBoundary: true,
  },
};

v1では、投げられたResponseがもっとも近いCatchBoundaryをレンダリングしていましたが、それ以外の未処理の例外はErrorBoundaryをレンダリングしていました。v2ではCatchBoundaryがなく、レスポンスであれ例外であれ、すべての未処理の例外がErrorBoundaryでレンダリングされます。

さらに、エラーはもはやErrorBoundaryにプロップスとして渡されず、useRouteErrorフックを使ってアクセスできるようになりました。

app/routes/v1-route.tsx
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>
  );
}

変更後:

app/routes/v2-route.tsx
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

remix.config.js
/** @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オブジェクトに平坦化されました。

app/routes/v1-route.tsx
import { useTransition } from "@remix-run/react";
 
function SomeComponent() {
  const transition = useTransition();
  transition.submission.formData;
  transition.submission.formMethod;
  transition.submission.formAction;
  transition.type;
}
app/routes/v2-route.tsx
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.statenavigation.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.stateidle -> submitting -> idleのように遷移していました。これは完全に意味的に正しくありません。なぜなら、フォームを "送信"はしているものの、GETナビゲーションを実行し、ローダーのみを実行しているだけだからです。機能的には、<Link>navigate()と何も変わりませんが、ユーザーが入力フィールドを介して検索パラメータの値を指定できるだけです。

v2では、GET送信がより正確に "ローディングナビゲーション" として反映され、通常のリンクと同様にidle -> loading -> idleと遷移するようになりました。GET送信が<Form>submit()から来ている場合は、useNavigation.form*が入力されているので、必要に応じて区別できます。

useFetcher

useNavigationと同様に、useFetcherでもsubmissionが平坦化され、typeフィールドが削除されました。

app/routes/v1-route.tsx
import { useFetcher } from "@remix-run/react";
 
function SomeComponent() {
  const fetcher = useFetcher();
  fetcher.submission.formData;
  fetcher.submission.formMethod;
  fetcher.submission.formAction;
  fetcher.type;
}
app/routes/v2-route.tsx
import { useFetcher } from "@remix-run/react";
 
function SomeComponent() {
  const fetcher = useFetcher();
 
  // これらのキーが平坦化されました
  fetcher.formData;
  fetcher.formMethod;
  fetcher.formAction;
 
  // このキーは削除されました
  fetcher.type;
}

以前のfetcher.typeを導出するには、次のような例を参考にしてください。より簡単な方法もあるかもしれません。通常、fetcher.statefetcher.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.stateidle -> submitting -> idleのように遷移していました。これは完全に意味的に正しくありません。なぜなら、フォームを "送信"はしているものの、GETリクエストを実行し、ローダーのみを実行しているだけだからです。機能的には、fetcher.load()と何も変わりませんが、ユーザーが入力フィールドを介して検索パラメータの値を指定できるだけです。

v2では、GET送信がより正確に "ローディングリクエスト" として反映され、通常のfetcherロードと同様にidle -> loading -> idleと遷移するようになりました。GET送信が<fetcher.Form>fetcher.submit()から来ている場合は、fetcher.form*が入力されているので、必要に応じて区別できます。

Linksの imagesizesimagesrcset

ルートの linksプロパティは、HTML の小文字のプロパティではなく、Reactの camelCase 値を使う必要があります。これら2つの値がv1では小文字で入り込んでいました。v2では、camelCaseバージョンのみが有効です:

app/routes/v1-route.tsx
export const links: LinksFunction = () => {
  return [
    {
      rel: "preload",
      as: "image",
      imagesrcset: "...",
      imagesizes: "...",
    },
  ];
};
app/routes/v2-route.tsx
export const links: V2_LinksFunction = () => {
  return [
    {
      rel: "preload",
      as: "image",
      imageSrcSet: "...",
      imageSizes: "...",
    },
  ];
};

browserBuildDirectory

remix.config.jsで、browserBuildDirectoryassetsBuildDirectoryに名称変更してください。

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  browserBuildDirectory: "./public/build",
};
remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  assetsBuildDirectory: "./public/build",
};

devServerBroadcastDelay

レースコンディションの問題を解決したため、remix.config.jsからdevServerBroadcastDelayを削除してください。

remix.config.js
  /** @type {import('@remix-run/dev').AppConfig} */
  module.exports = {
-   devServerBroadcastDelay: 300,
  };

devServerPort

remix.config.jsで、devServerPortfuture.v2_dev.portに名称変更してください。

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  devServerPort: 8002,
};
remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  // v1.x中は future フラグを使います
  future: {
    v2_dev: {
      port: 8002,
    },
  },
};

v1からv2にアップグレードすると、これはルートレベルのdev設定に平坦化されます。

serverBuildDirectory

remix.config.jsで、serverBuildDirectoryserverBuildPathに名称変更し、ディレクトリ パスではなくモジュール パスを指定してください。

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  serverBuildDirectory: "./build",
};
remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  serverBuildPath: "./build/index.js",
};

Remix は以前、サーバー用に複数のモジュールを作成していましたが、現在は単一のファイルを作成しています。

serverBuildTarget

ビルドターゲットを指定する代わりに、remix.config.jsのオプションを使ってサーバーのビルドがターゲットに合うように生成してください。この変更により、Remixがより多くのJavaScriptランタイム、サーバー、ホストにデプロイできるようになりました。

現在のserverBuildTargetは以下の設定に置き換えられるはずです:

arc

remix.config.js
/** @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

remix.config.js
/** @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

remix.config.js
/** @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

remix.config.js
/** @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

remix.config.js
/** @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では、必要に応じて明示的にポリフィルを再導入する必要があります:

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  browserNodeBuiltinsPolyfill: {
    modules: {
      buffer: true,
      fs: "empty",
    },
    globals: {
      Buffer: true,
    },
  },
};

ブラウザバンドルでは一部のポリフィルが非常に大きいため、明示的にポリフィルを許可することをお勧めしますが、Remix v1のフル設定を即座に復元することもできます:

remix.config.js
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.jsserverNodeBuiltinsPolyfill.modulesを空のオブジェクトにすることで、すべてのサーバーポリフィルを削除できます:

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  serverNodeBuiltinsPolyfill: {
    modules: {},
  },
};

その後、必要に応じて任意のポリフィル(または空のポリフィル)を再導入できます。

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  serverNodeBuiltinsPolyfill: {
    modules: {
      buffer: true,
      fs: "empty",
    },
    globals: {
      Buffer: true,
    },
  },
};

参考までに、v1のデフォルトのポリフィル全体を手動で指定することができます:

remix.config.js
/** @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を使っている場合は何も必要ありません。独自のアプリサーバーを使っている場合は、自分でグローバルをインストールする必要があります。

server.ts
import { installGlobals } from "@remix-run/node";
 
installGlobals();

エクスポートされたポリフィルの削除

Remix v2では、これらのポリフィルされた実装を@remix-run/nodeからエクスポートしなくなりました。代わりにグローバル名前空間の実装を使う必要があります。これが影響する可能性のある場所は、app/entry.server.tsxファイルで、ここでPassThroughReadableStreamに変換する必要があります:

app/entry.server.tsx
  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
server.ts
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.jsserverserverBuildPathオプションを削除してください。

このアダプターの削除に伴い、Vercelテンプレート公式のVercelテンプレートに変更しました。

組み込みのPostCSS/Tailwind サポート

v2では、プロジェクトにPostCSSや Tailwindの設定ファイルがある場合、これらのツールがRemixコンパイラ内で自動的に使用されます。

Remixとは別にカスタムのPostCSSやTailwindのセットアップを維持したい場合は、remix.config.jsでこれらの機能を無効にできます。

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のセクションを参照してください。