Vite

Vite は、JavaScript プロジェクトのための強力で高性能で拡張性のある開発環境です。Remix のバンドリング機能を改善し拡張するために、私たちは今Viteをもう一つのコンパイラとしてサポートしています。近い将来、Viteがデフォルトのコンパイラになるでしょう。

クラシックなRemixコンパイラvsRemixのVite

既存のRemixコンパイラは、remix buildおよびremix dev CLIコマンドで利用でき、remix.config.jsで設定されています。これは「クラシックなRemixコンパイラ」と呼ばれるようになりました。

Remix Viteプラグインとremix vite:buildおよびremix vite:devCLIコマンドを総称して「Remix Vite」と呼びます。

今後、特に記載がない限り、ドキュメントはRemix Viteの使用を前提とします。

はじめに

様々なVite ベースのテンプレートをご用意しています。

# 最小構成:
npx create-remix@latest
 
# Express:
npx create-remix@latest --template remix-run/remix/templates/express
 
# Cloudflare: 
npx create-remix@latest --template remix-run/remix/templates/cloudflare
 
# Cloudflare Workers:
npx create-remix@latest --template remix-run/remix/templates/cloudflare-workers

これらのテンプレートには、Remix Viteプラグインが設定されたvite.config.tsファイルが含まれています。

設定

Remix Viteプラグインは、プロジェクトのルートにあるvite.config.tsファイルで設定されます。詳細はVite設定のドキュメントをご覧ください。

Cloudflare

Cloudflareを使い始めるには、cloudflareテンプレートを使うと良いでしょう:

npx create-remix@latest --template remix-run/remix/templates/cloudflare

Viteでローカルで実行する2つの方法があります:

# Vite
remix vite:dev
 
# Wrangler
remix vite:build # アプリケーションを先にビルドしてから wrangler を実行
wrangler pages dev ./build/client

Viteはより良い開発体験を提供しますが、Wranglerはクラウドワーカーのworkerdランタイムで実行することで、Cloudflare環境をより忠実に模擬できます。

Cloudflareプロキシ

Viteでクラウドワーカー環境をシミュレーションするために、WranglerはローカルのworkerdバインディングへのNodeプロキシを提供しています。 Remix のCloudflareプロキシプラグインはこれらのプロキシを設定してくれます:

vite.config.ts
import {
  vitePlugin as remix,
  cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
} from "@remix-run/dev";
import { defineConfig } from "vite";
 
export default defineConfig({
  plugins: [remixCloudflareDevProxy(), remix()],
});

これにより、loaderaction関数内でcontext.cloudflareから使えるようになります:

export const loader = ({ context }: LoaderFunctionArgs) => {
  const { env, cf, ctx } = context.cloudflare;
  // ... more loader code here...
};

Cloudflare のgetPlatformProxyドキュメントで、これらのプロキシについてさらに詳しく確認できます。

バインディング

ローカル開発でのViteやWranglerの設定にはwrangler.tomlを、デプロイ時にはCloudflareダッシュボードのCloudflare Pages バインディングを使います。

wrangler.tomlファイルを変更したら、必ずwrangler typesを実行してバインディングを再生成する必要があります。

その後、context.cloudflare.envからバインディングにアクセスできます。 例えば、KVネームスペースMY_KVとしてバインドされている場合:

app/routes/_index.tsx
export async function loader({
  context,
}: LoaderFunctionArgs) {
  const { MY_KV } = context.cloudflare.env;
  const value = await MY_KV.get("my-key");
  return json({ value });
}

ロードコンテキストの拡張

ロードコンテキストに追加のプロパティを追加したい場合は、共有モジュールからgetLoadContext関数をエクスポートする必要があります。これにより、Vite、Wrangler、Cloudflare Pagesでロードコンテキストが一貫して拡張されます:

load-context.ts
import { type AppLoadContext } from "@remix-run/cloudflare";
import { type PlatformProxy } from "wrangler";
 
// `wrangler.toml`を使ってバインディングを設定する場合、
// `wrangler types`がそれらのバインディングの型を
// グローバルな`Env`インターフェイスに生成します。
// `wrangler.toml`が存在しない場合でもタイプチェックが通るよう、
// 空のインターフェイスを定義しています。
interface Env {}
 
type Cloudflare = Omit<PlatformProxy<Env>, "dispose">;
 
declare module "@remix-run/cloudflare" {
  interface AppLoadContext {
    cloudflare: Cloudflare;
    extra: string; // 拡張
  }
}
 
type GetLoadContext = (args: {
  request: Request;
  context: { cloudflare: Cloudflare }; // 拡張前のロードコンテキスト
}) => AppLoadContext;
 
// Vite、Wrangler、Cloudflare Pagesで互換性のある共有実装
export const getLoadContext: GetLoadContext = ({
  context,
}) => {
  return {
    ...context,
    extra: "stuff",
  };
};

Cloudflareプロキシプラグインとリクエストハンドラfunctions/[[path]].tsの両方でgetLoadContextを渡す必要があります。そうしないと、アプリの実行方法によってロードコンテキストの拡張が一貫していません。

まず、Vite設定でCloudflareプロキシプラグインにgetLoadContextを渡して、Viteの実行時にロードコンテキストを拡張します:

vite.config.ts
import {
  vitePlugin as remix,
  cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
} from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
 
import { getLoadContext } from "./load-context";
 
export default defineConfig({
  plugins: [
    remixCloudflareDevProxy({ getLoadContext }),
    remix(),
  ],
});

次に、Wranglerや Cloudflare Pagesへのデプロイ時にロードコンテキストを拡張するため、functions/[[path]].tsのリクエストハンドラにgetLoadContextを渡します:

functions/[[path]].ts
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
 
// @ts-ignore - the server build file is generated by `remix vite:build`
import * as build from "../build/server";
import { getLoadContext } from "../load-context";
 
export const onRequest = createPagesFunctionHandler({
  build,
  getLoadContext,
});

クライアントとサーバーコードの分離

Viteは、クラシックなRemixコンパイラとは異なる方法でクライアントとサーバーコードを扱います。詳細はクライアントとサーバーコードの分離のドキュメントをご覧ください。

新しいビルド出力パス

Viteがpublicディレクトリを扱う方法と、既存のRemixコンパイラとは大きな違いがあります。Viteはpublicディレクトリからファイルをクライアントビルドディレクトリにコピーしますが、Remixコンパイラはpublicディレクトリを変更せず、別のサブディレクトリ(public/build)をクライアントビルドディレクトリとして使っていました。

Remix のデフォルトプロジェクト構造をViteの動作に合わせるため、ビルド出力パスが変更されました。assetsBuildDirectoryserverBuildDirectoryのオプションが廃止され、単一のbuildDirectoryオプションに統一されました。デフォルトでは、サーバーはbuild/serverに、クライアントはbuild/clientにビルドされます。

これに伴い、以下のデフォルト設定も変更されています:

  • publicPathViteの"base"オプションに置き換えられ、デフォルトは"/"になりました (以前は"/build/"でした)。
  • serverBuildPathserverBuildFileに置き換えられ、デフォルトは"index.js"になりました。このファイルは、設定したbuildDirectory内のサーバーディレクトリに書き込まれます。

RemixがViteに移行する理由の1つは、Remixを採用する際の学習コストを下げることです。 つまり、追加の bundling 機能を使う場合は、Remix のドキュメントではなく ViteのドキュメントViteプラグインコミュニティを参照してください。

Viteには、既存のRemixコンパイラには含まれていない多くの 機能プラグインがあります。 これらの機能を使う場合は、以降Viteのみを使うことを意図している必要があります。そうでない場合、既存のRemixコンパイラではアプリをコンパイルできなくなります。

移行

Viteのセットアップ

👉 Viteを開発依存関係としてインストール

npm install -D vite

Remixはもはやスタンドアロンのコンパイラではなく、Viteプラグインに過ぎないので、Viteにフックアップする必要があります。

👉 Remix アプリのルートに vite.config.ts を作成し、remix.config.js を削除

vite.config.ts
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
 
export default defineConfig({
  plugins: [remix()],
});

サポートされているRemix設定オプションのサブセットは、プラグインに直接渡す必要があります:

vite.config.ts
export default defineConfig({
  plugins: [
    remix({
      ignoredRouteFiles: ["**/*.css"],
    }),
  ],
});

HMR & HDR

Viteは HMR やその他の開発機能のためのロバストなクライアントサイドランタイムを提供するので、<LiveReload />コンポーネントは不要になりました。Remix Viteプラグインを使って開発する際は、<Scripts />コンポーネントがViteのクライアントサイドランタイムやその他の開発用スクリプトを自動的に含むようになります。

👉 <LiveReload/>を削除し、<Scripts />を残す

  import {
-   LiveReload,
    Outlet,
    Scripts,
  }
 
  export default function App() {
    return (
      <html>
        <head>
        </head>
        <body>
          <Outlet />
-         <LiveReload />
          <Scripts />
        </body>
      </html>
    )
  }

TypeScript統合

Viteは様々なファイルタイプのインポートを扱いますが、既存のRemixコンパイラとは異なる方法で行う場合があるので、@remix-run/devの古い型ではなく、vite/clientの型を参照する必要があります。

vite/clientが提供する型は@remix-run/devに暗黙的に含まれる型と互換性がないため、TypeScript設定でskipLibCheckフラグを有効にする必要があります。将来的にViteプラグインがデフォルトのコンパイラになれば、Remixはこのフラグを必要としなくなるでしょう。

👉 tsconfig.jsonを更新

tsconfig.jsontypesフィールドを更新し、skipLibCheckmodulemoduleResolutionが適切に設定されていることを確認してください。

tsconfig.json
{
  "compilerOptions": {
    "types": ["@remix-run/node", "vite/client"],
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "Bundler"
  }
}

👉 remix.env.d.tsの更新/削除

remix.env.d.tsから以下の型宣言を削除します

rm remix.env.d.ts

Remix App Serverからの移行

開発時にremix-serveを使っていた場合 (またはremix dev-cフラグをつけていなかった場合)、新しいミニマルなdevサーバーに切り替える必要があります。 これは Remix Viteプラグインに組み込まれており、remix vite:devを実行すると自動的に起動します。

Remix ViteプラグインはグローバルなNode polyfillをインストールしないので、remix-serveに依存していた場合は自分でインストールする必要があります。これは、Vite設定の冒頭でinstallGlobalsを呼び出すのが一番簡単です。

Viteのdevサーバーのデフォルトポートはremix-serveとは異なるので、同じポートを維持したい場合は、Viteのserver.portオプションで設定する必要があります。

また、新しいビルド出力パスに合わせて更新する必要があります。サーバーはbuild/serverに、クライアントアセットはbuild/clientにビルドされます。

👉 devbuildstartスクリプトを更新

package.json
{
  "scripts": {
    "dev": "remix vite:dev",
    "build": "remix vite:build",
    "start": "remix-serve ./build/server/index.js"
  }
}

👉 グローバルなNode polyfillをVite設定に追加

vite.config.ts
import { vitePlugin as remix } from "@remix-run/dev";
+import { installGlobals } from "@remix-run/node";
import { defineConfig } from "vite";
 
+installGlobals();
 
export default defineConfig({
  plugins: [remix()],
});

👉 Viteのdevサーバーポートを設定(オプション)

vite.config.ts
export default defineConfig({
  server: {
    port: 3000,
  },
  plugins: [remix()],
});

カスタムサーバーの移行

カスタムサーバーを開発時に使っていた場合、カスタムサーバーを編集して Viteのconnectミドルウェアを使う必要があります。 これにより、開発中にアセットリクエストと初期レンダリングリクエストをViteにデリゲートできるので、Viteの優れたDXを活用できます。

その上で、開発時に"virtual:remix/server-build"という仮想モジュールをロードして、Viteベースのリクエストハンドラを作成できます。

また、サーバーコードを更新して新しいビルド出力パスを参照する必要がああります。サーバービルドはbuild/serverに、クライアントアセットはbuild/clientにビルドされます。

Expressを使っていた場合の例は以下の通りです。

👉 server.mjsファイルを更新

server.mjs
import { createRequestHandler } from "@remix-run/express";
import { installGlobals } from "@remix-run/node";
import express from "express";
 
installGlobals();
 
const viteDevServer =
  process.env.NODE_ENV === "production"
    ? undefined
    : await import("vite").then((vite) =>
        vite.createServer({
          server: { middlewareMode: true },
        })
      );
 
const app = express();
 
// アセットリクエストを処理
if (viteDevServer) {
  app.use(viteDevServer.middlewares);
} else {
  app.use(
    "/assets",
    express.static("build/client/assets", {
      immutable: true,
      maxAge: "1y",
    })
  );
}
app.use(express.static("build/client", { maxAge: "1h" }));
 
// SSRリクエストを処理
app.all(
  "*",
  createRequestHandler({
    build: viteDevServer
      ? () =>
          viteDevServer.ssrLoadModule(
            "virtual:remix/server-build"
          )
      : await import("./build/server/index.js"),
  })
);
 
const port = 3000;
app.listen(port, () =>
  console.log("http://localhost:" + port)
);

👉 builddevstartスクリプトを更新

package.json
{
  "scripts": {
    "dev": "node ./server.mjs",
    "build": "remix vite:build",
    "start": "cross-env NODE_ENV=production node ./server.mjs"
  }
}

TypeScriptでカスタムサーバーを書きたい場合は、tsxtsmなどのツールを使ってサーバーを実行できます:

tsx ./server.ts
node --loader tsm ./server.ts

ただし、サーバー起動時の初期遅延が若干目立つ可能性があります。

Cloudflareファンクションの移行

Remix Viteプラグインは、フルスタックアプリケーション向けに設計されたCloudflare Pagesのみをサポートしています。Cloudflare Workers Sitesをご利用の場合は、Cloudflare Pagesへの移行ガイドをご覧ください。

👉 remixプラグインの前にcloudflareDevProxyVitePluginを追加して、Viteのdevサーバーのミドルウェアを適切に上書きする!

vite.config.ts
import {
  vitePlugin as remix,
  cloudflareDevProxyVitePlugin,
} from "@remix-run/dev";
import { defineConfig } from "vite";
 
export default defineConfig({
  plugins: [cloudflareDevProxyVitePlugin(), remix()],
});

アプリケーションでは、Remix設定のserverフィールドを使ってCatchallのCloudflareファンクションを生成していた可能性があります。 Viteでは、このような間接的な方法は不要になりました。 代わりに、Express用やその他のカスタムサーバー用と同じように、Cloudflare用の Catchall ルートを直接記述できます。

👉 Remixの Catchall ルートを作成

functions/[[page]].ts
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
 
// @ts-ignore - the server build file is generated by `remix vite:build`
import * as build from "../build/server";
 
export const onRequest = createPagesFunctionHandler({
  build,
});

👉 バインディングと環境変数はcontext.cloudflare.envから、context.envではなく

主に開発時にはViteを使いますが、Wranglerでプレビューやデプロイすることもできます。

詳細は、このドキュメントのCloudflareセクションをご覧ください。

👉 package.jsonのスクリプトを更新

package.json
{
  "scripts": {
    "dev": "remix vite:dev",
    "build": "remix vite:build",
    "preview": "wrangler pages dev ./build/client",
    "deploy": "wrangler pages deploy ./build/client"
  }
}

ビルド出力パスの参照の更新

既存のRemixコンパイラのデフォルトオプションを使っていた場合、サーバーはbuildに、クライアントはpublic/buildにビルドされていました。Viteがpublicディレクトリを扱う方法と、既存のRemixコンパイラとは異なるため、これらの出力パスが変更されました。

👉 ビルド出力パスの参照を更新

  • サーバーは現在、デフォルトでbuild/serverにビルドされます。
  • クライアントは現在、デフォルトでbuild/clientにビルドされます。

例えば、Blues StackのDockerfileを更新する場合:

Dockerfile
-COPY --from=build /myapp/build /myapp/build
-COPY --from=build /myapp/public /myapp/public
+COPY --from=build /myapp/build/server /myapp/build/server
+COPY --from=build /myapp/build/client /myapp/build/client

パスエイリアスの設定

Remixコンパイラは、tsconfig.jsonpathsオプションを活用してパスエイリアスを解決していました。Remixコミュニティでは、appディレクトリに~というエイリアスを定義するのが一般的でした。

Viteはデフォルトではパスエイリアスを提供していません。この機能に依存していた場合は、vite-tsconfig-pathsプラグインを使ってRemixコンパイラと同様の動作を実現できます:

👉 vite-tsconfig-pathsをインストール

npm install -D vite-tsconfig-paths

👉 Vite設定にvite-tsconfig-pathsを追加

vite.config.ts
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
 
export default defineConfig({
  plugins: [remix(), tsconfigPaths()],
});

@remix-run/css-bundleの削除

Viteには、CSS Side Effect インポート、PostCSS、CSS Modules など、CSS バンドリングの機能が組み込まれています。Remix Viteプラグインは、自動的にバンドルされたCSSをリレバントなルートに適用します。

@remix-run/css-bundleパッケージは、Viteを使う場合は不要になります。なぜなら、そのcssBundleHrefエクスポートは常にundefinedになるからです。

👉 @remix-run/css-bundleをアンインストール

npm uninstall @remix-run/css-bundle

👉 cssBundleHrefの参照を削除

app/root.tsx
- import { cssBundleHref } from "@remix-run/css-bundle";
  import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno
 
  export const links: LinksFunction = () => [
-   ...(cssBundleHref
-     ? [{ rel: "stylesheet", href: cssBundleHref }]
-     : []),
    // ...
  ];

ルートのlinks関数がcssBundleHrefを設定するためだけに使われている場合は、完全に削除できます。

app/root.tsx
- import { cssBundleHref } from "@remix-run/css-bundle";
- import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno
 
- export const links: LinksFunction = () => [
-   ...(cssBundleHref
-     ? [{ rel: "stylesheet", href: cssBundleHref }]
-     : []),
- ];

linksで参照されるCSSインポートを修正

これは、CSSバンドリングの他の形式、例えばCSSモジュール、CSS Side Effect インポート、Vanilla Extract などでは必要ありません。

links関数でCSSを参照している場合、対応するCSSインポートをViteの明示的な?urlインポート構文に更新する必要があります。

👉 linksで使われているCSSインポートに?urlを追加

.css?urlインポートには、Vite v5.1以降が必要です

-import styles from "~/styles/dashboard.css";
+import styles from "~/styles/dashboard.css?url";
 
export const links = () => {
  return [
    { rel: "stylesheet", href: styles }
  ];
}

TailwindにPostCSSを使って有効化

プロジェクトが Tailwind CSS を使っている場合、Remix コンパイラでは tailwind オプションを有効にするだけで設定が不要でした。 しかし、Viteの場合は明示的にPostCSSの設定ファイルが必要です。

👉 PostCSSの設定ファイルがない場合は追加し、tailwindcssプラグインを含める

postcss.config.mjs
export default {
  plugins: {
    tailwindcss: {},
  },
};

プロジェクトにすでにPostCSSの設定ファイルがある場合は、tailwindcssプラグインが含まれていない可能性があります。 これは、Remixコンパイラの tailwind 設定オプションが有効だった場合、自動的に含まれていたためです。

👉 PostCSSの設定にTailwindプラグインが含まれていない場合は追加

postcss.config.mjs
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

👉 Tailwind CSS インポートの移行

links関数でTailwind CSSファイルを参照している場合、Tailwind CSSインポートステートメントを移行する必要があります

Vanilla Extractプラグインの追加

Vanilla Extractを使っている場合は、Viteプラグインを設定する必要があります。

👉 Vanilla Extractの公式Viteプラグインをインストール

npm install -D @vanilla-extract/vite-plugin

👉 Vite設定にVanilla Extractプラグインを追加

vite.config.ts
import { vitePlugin as remix } from "@remix-run/dev";
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
import { defineConfig } from "vite";
 
export default defineConfig({
  plugins: [remix(), vanillaExtractPlugin()],
});

MDXプラグインの追加

MDXを使っている場合、ViteのプラグインAPI はRollupのプラグインAPI の拡張なので、公式のMDX Rollupプラグインを使うべきです:

👉 MDX Rollupプラグインをインストール

npm install -D @mdx-js/rollup

RemixプラグインはJavaScriptやTypeScriptファイルを処理することを期待しているので、他の言語 (MDXなど) からの変換は先に行う必要があります。 この場合、MDXプラグインをRemixプラグインの前に配置する必要があります。

👉 Vite設定にMDX Rollupプラグインを追加

vite.config.ts
import mdx from "@mdx-js/rollup";
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
 
export default defineConfig({
  plugins: [mdx(), remix()],
});
MDXフロントマターのサポート

Remixコンパイラでは、MDXでフロントマターを定義できました。この機能を使っていた場合、Viteではremark-mdx-frontmatterを使って実現できます。

👉 必要なRemarkフロントマタープラグインをインストール

npm install -D remark-frontmatter remark-mdx-frontmatter

👉 MDX RollupプラグインにRemarkフロントマタープラグインを渡す

vite.config.ts
import mdx from "@mdx-js/rollup";
import { vitePlugin as remix } from "@remix-run/dev";
import remarkFrontmatter from "remark-frontmatter";
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
import { defineConfig } from "vite";
 
export default defineConfig({
  plugins: [
    mdx({
      remarkPlugins: [
        remarkFrontmatter,
        remarkMdxFrontmatter,
      ],
    }),
    remix(),
  ],
});

Remixコンパイラでは、フロントマターのエクスポート名はattributesでしたが、フロントマタープラグインのデフォルトのエクスポート名はfrontmatterです。エクスポート名を設定することは可能ですが、代わりにアプリコードを更新して、デフォルトのエクスポート名を使うことをお勧めします。

👉 MDXファイル内のMDX attributes エクスポートをfrontmatterに変更

app/posts/first-post.mdx
  ---
  title: Hello, World!
  ---
 
- # {attributes.title}
+ # {frontmatter.title}

👉 MDX attributesエクスポートをfrontmatterに変更(consumer側)

app/routes/posts/first-post.tsx
  import Component, {
-   attributes,
+   frontmatter,
  } from "./posts/first-post.mdx";
MDXファイルの型定義

👉 env.d.tsにMDX用の型を追加

env.d.ts
/// <reference types="@remix-run/node" />
/// <reference types="vite/client" />
 
declare module "*.mdx" {
  let MDXComponent: (props: any) => JSX.Element;
  export const frontmatter: any;
  export default MDXComponent;
}
MDXのフロントマターをルートエクスポートにマッピング

Remixコンパイラでは、フロントマターからheadersmetahandleルートエクスポートを定義できました。 この Remix 固有の機能は、remark-mdx-frontmatterプラグインではサポートされていません。 この機能を使っていた場合は、手動でフロントマターをルートエクスポートにマッピングする必要があります:

👉 MDXルートでフロントマターをルートエクスポートにマッピング

---
meta:
  - title: My First Post
  - name: description
    content: Isn't this awesome?
headers:
  Cache-Control: no-cache
---
 
export const meta = frontmatter.meta;
export const headers = frontmatter.headers;
 
# Hello World

注意点として、MDXのルートエクスポートを明示的にマッピングすることで、好きな形式のフロントマターを使うことができるようになります。

---
title: My First Post
description: Isn't this awesome?
---
 
export const meta = () => {
  return [
    { title: frontmatter.title },
    {
      name: "description",
      content: frontmatter.description,
    },
  ];
};
 
# Hello World
MDXファイル名の使用の更新

Remixコンパイラでは、すべてのMDXファイルからfilenameエクスポートが提供されていました。 これは主に、MDXルートのコレクションにリンクする際に使われていました。 この機能を使っていた場合、Viteではグローブインポートを使うと、ファイル名をモジュールにマッピングする便利なデータ構造が得られます。 これにより、個々のMDXファイルをすべて手動でインポートする必要がなくなります。

例えば、postsディレクトリ内のすべてのMDXファイルをインポートするには:

const posts = import.meta.glob("./posts/*.mdx");

これは、以下のように個々にインポートするのと同等です:

const posts = {
  "./posts/a.mdx": () => import("./posts/a.mdx"),
  "./posts/b.mdx": () => import("./posts/b.mdx"),
  "./posts/c.mdx": () => import("./posts/c.mdx"),
  // etc.
};

すぐにMDXファイルをインポートしたい場合は、以下のようにできます:

const posts = import.meta.glob("./posts/*.mdx", {
  eager: true,
});

デバッグ

NODE_OPTIONS環境変数を使ってデバッグセッションを開始できます:

NODE_OPTIONS="--inspect-brk" npm run dev

次に、ブラウザからデバッガーを接続できます。 例えば、Chromeでは chrome://inspect を開いたり、開発者ツールのNodeJSアイコンをクリックするとデバッガーに接続できます。

vite-plugin-inspect

vite-plugin-inspectは、各Viteプラグインがコードをどのように変換しているか、およびプラグインごとの所要時間を表示してくれます。

パフォーマンス

Remixには--profileフラグがあり、パフォーマンスプロファイリングが可能です。

remix vite:build --profile

--profileモードで実行すると、.cpuprofileファイルが生成されます。これをspeedscope.appにアップロードして分析できます。

開発中も、p + enterを押してプロファイリングセッションを開始/停止できます。 開発サーバーの起動時にプロファイリングを行いたい場合は、--profileフラグを使います:

remix vite:dev --profile

Viteのパフォーマンスドキュメントも参考にしてください!

バンドル分析

バンドルを視覚化、分析するには、rollup-plugin-visualizerプラグインを使えます:

vite.config.ts
import { vitePlugin as remix } from "@remix-run/dev";
import { visualizer } from "rollup-plugin-visualizer";
 
export default defineConfig({
  plugins: [
    remix(),
    // `emitFile`は Remix が複数のバンドルを構築するため必要
    visualizer({ emitFile: true }),
  ],
});

remix vite:buildを実行すると、各バンドルにstats.htmlファイルが生成されます:

build
├── client
│   ├── assets/
│   ├── favicon.ico
│   └── stats.html 👈
└── server
    ├── index.js
    └── stats.html 👈

stats.htmlをブラウザで開いて、バンドルの分析ができます。

トラブルシューティング

デバッグパフォーマンスのセクションで、一般的なトラブルシューティングのヒントを確認してください。 また、githubのRemix Viteプラグインの既知の問題で、同様の問題が報告されていないかチェックしてください。

HMR

ホットアップデートを期待しているのにフルページリロードが発生する場合は、ホットモジュール置換に関する議論を確認し、一般的な問題への対処方法を学んでください。

ESM / CJS

ViteはESMとCJSの両方のディペンデンシーをサポートしますが、時々ESM/CJSの相互運用性の問題に遭遇することがあります。 通常、これはディペンデンシーがESMをうまくサポートしていないためです。 そしてそれは非常に難しい問題なので、私たちも彼らを非難するつもりはありませんESMとCJSの両立は本当に難しい

例の不具合を修正する方法の詳細については、🎥 How to Fix CJS/ESM Bugs in Remixをご覧ください。

ディペンデンシーが正しく設定されていないかどうかを診断するには、publintAre The Types Wrongを使ってください。 さらに、Viteのssr.noExternalオプションを使って、Remixコンパイラの serverDependenciesToBundleと同様の動作をEmulateするこ ともできます。

開発中にブラウザにサーバーコードのエラーが表示される

開発中にブラウザのコンソールにサーバーコードに関するエラーが表示される場合は、明示的にサーバー専用コードを分離する必要があります。 例えば、以下のようなエラーが表示された場合:

Uncaught ReferenceError: process is not defined

processなどのサーバー専用のグローバル変数を使っているモジュールを特定し、別の.serverモジュールやvite-env-onlyを使って隔離する必要があります。 Viteはロールアップを使ってコードをツリーシェイクするので、これらのエラーは開発中にのみ発生します。

他のViteベースのツールでのプラグインの使用 (Vitest、Storybookなど)

Remix Viteプラグインは、アプリケーションの開発サーバーと本番ビルドでのみ使用することを意図しています。 一方、Vitest やStorybookなどの他のViteベースのツールもVite設定ファイルを使用しますが、Remix Viteプラグインはこれらのツールと連携するように設計されていません。 現時点では、他のViteベースのツールと併用する際は、プラグインを除外することをお勧めします。

Vitest の場合:

vite.config.ts
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig, loadEnv } from "vite";
 
export default defineConfig({
  plugins: [!process.env.VITEST && remix()],
  test: {
    environment: "happy-dom",
    // 加えて、vitest実行時に ".env.test" を読み込む
    env: loadEnv("test", process.cwd(), ""),
  },
});

Storybookの場合:

vite.config.ts
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
 
const isStorybook = process.argv[1]?.includes("storybook");
 
export default defineConfig({
  plugins: [!isStorybook && remix()],
});

または、ツールごとに別のVite設定ファイルを使うこともできます。 Remixに特化したVite設定ファイルを使う例:

remix vite:dev --config vite.config.remix.ts

Remix Viteプラグインを提供しない場合は、Vite Plugin Reactを自分で提供する必要がある

ドキュメントがリマウントされるときにスタイルが消える問題

Remix では、ドキュメント全体をReactで描画しています。この際、head要素に動的に要素が挿入されると問題が発生する可能性があります。ドキュメントが再マウントされると、既存のhead要素が削除され新しいものに置き換わるため、Viteが開発中に注入したstyle要素が削除されてしまうのです。

これは Reactの既知の問題で、canary版で修正されています。リスクを理解した上で、特定のReactバージョンにピンポイントでアプリを固定し、パッケージのオーバーライドを使ってプロジェクト全体で同じバージョンのReactを使うことができます。例:

package.json
{
  "dependencies": {
    "react": "18.3.0-canary-...",
    "react-dom": "18.3.0-canary-..."
  },
  "overrides": {
    "react": "18.3.0-canary-...",
    "react-dom": "18.3.0-canary-..."
  }
}

参考までに、Next.jsはこの方法を内部的に採用しているので、一般的によく使われる手法です。Remixではデフォルトで提供されていませんが、これを使う選択肢はあります。

この Viteによって開発中に注入されたスタイルの問題は、本番ビルドでは発生しません。なぜなら、静的なCSSファイルが生成されるからです。

Remixでは、ルートコンポーネントのデフォルトエクスポートと、そのErrorBoundaryやHydrateFallbackエクスポートの間で描画が切り替わると、新しいドキュメントレベルのコンポーネントがマウントされるため、この問題が表面化する可能性があります。

hydrationエラーも同様の問題を引き起こす可能性があります。hydrationエラーはアプリのコードが原因である場合もありますし、ブラウザ拡張機能によってドキュメントが書き換えられることが原因になることもあります。

これがViteに関連するのは、開発時にViteがCSSインポートをJSファイルに変換し、それらを副作用としてドキュメントにインジェクションしているためです。これにより、遅延読み込みやCSSファイルのHMRをサポートしています。

例えば、アプリに以下のようなCSSファイルがあるとします:

app/styles.css
* { margin: 0 }

開発時、このCSSファイルは以下のようなJavaScriptコードに変換されます:

import {createHotContext as __vite__createHotContext} from "/@vite/client";
import.meta.hot = __vite__createHotContext("/app/styles.css");
import {updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle} from "/@vite/client";
const __vite__id = "/path/to/app/styles.css";
const __vite__css = "*{margin:0}"
__vite__updateStyle(__vite__id, __vite__css);
import.meta.hot.accept();
import.meta.hot.prune(()=>__vite__removeStyle(__vite__id));

この変換は本番コードには適用されないため、この問題は開発時にのみ発生します。

開発時のWranglerエラー

Cloudflare Pagesを使っている場合、wrangler pages devから以下のようなエラーが発生することがあります:

ERROR: Your worker called response.clone(), but did not read the body of both clones.
This is wasteful, as it forces the system to buffer the entire response body
in memory, rather than streaming it through. This may cause your worker to be
unexpectedly terminated for going over the memory limit. If you only meant to
copy the response headers and metadata (e.g. in order to be able to modify
them), use `new Response(response.body, response)` instead.

これはWranglerの既知の問題です。

謝辞

Viteは素晴らしいプロジェクトで、Viteチームの皆様に感謝しています。 特に、Matias Capeletto、Arnaud Barré、Bjorn Luのようなメンバーからの指導に感謝します。

RemixコミュニティもすぐにViteサポートを探求してくれ、その貢献に感謝しています:

最後に、他のフレームワークがViteサポートをどのように実装したかにも触発されました: