導入 最小限の構成で、Remix を Vite および Cloudflare Workers-Pages とともに使用する方法が必要でした。 次のような他のリポジトリも確認しました: 、 福音の山 、 ギリッシュ21 。 日盛重 しかし、いくつかの制限がありました。 リポジトリにさらに多くの構成ファイルを追加したくなかったので、事前にビルドしたくありませんでした。 Cloudflare Workers/Pages はターゲットが異なります。Postgres などのパッケージは、Remix にインポートされるとノードの依存関係が壊れるため、tsup でターゲットにすることが難しくなりました。 また、異なるターゲット(Remix-Cloudflare、Node/Bun)を消費する方法も必要でした。 それでも、これを可能にする道を開いてくれた彼らに感謝したいと思います。 下部の落とし穴のセクションを必ず読んでください。 ソーシャルで私をフォローしてください! 私は、本番環境での 1% のエラーを検出するための自動テスト プラットフォームを公開で構築しています。 私は以下の進捗状況を共有しています: X/Twitter @javiasilis LinkedIn @javiasilis GitHub リポジトリ アクセスできます。 完全な実装については、ここから ステップバイステップ 要件 ノードJS PNPM Docker (オプション - ローカル データベースの例) ここでは新しいモノリポジトリについて説明しますが、既存のモノリポジトリをモノリポジトリに変換することも完全に有効です。 また、モノリポジトリに関する知識があることも前提としています。 注記: 「ルート」はモノリポジトリの開始パスを指します。このプロジェクトの場合、 および ディレクトリの外側になります。 libs packages Turborepoをインストールする Turborepo はパッケージ マネージャーのワークスペース上で動作し、プロジェクトのスクリプトと出力を管理します (出力をキャッシュすることもできます)。これまでのところ、Rush (試したことがなく、好きではありません) 以外で機能する唯一のモノ リポジトリ ツールです。 NX には Remix の Vite サポートがありません (この記事の執筆時点 - 2024 年 8 月 28 日)。 https://turbo.build/ pnpm dlx create-turbo@latest 1. PNPMワークスペースを構成する 依存関係を管理するには、PNPM のワークスペース機能を使用します。 Monorepo ディレクトリに を作成します。 pnpm-workspace.yaml その中に以下を追加します: packages: - "apps/*" - "libs/*" これにより、すべてのリポジトリが と 内に存在することが pnpm に通知されます。libs または (他の場所で見たように) 使用は重要ではないことに注意してください。 apps libs packages libs 2. プロジェクトのルートに空の package.json を作成します。 pnpm init { "name": "@repo/main", "version": "1.0.0", "scripts": {}, "keywords": [], "author": "", "license": "ISC" } これは、これがアプリケーションのメイン エントリであることを示しています。特定の規則に従ったり、 プレフィックスを使用したりする必要はありません。これは、ローカル/リモート パッケージと区別したり、組織にグループ化しやすくしたりするために使用されます。 name:@repo/main @ 3. プロジェクトのルートに ファイルを作成します。 turbo.json { "$schema": "https://turbo.build/schema.json", "tasks": { "build": {}, "dev": { "cache": false, "persistent": true }, "start": { "dependsOn": ["^build"], "persistent": true }, "preview": { "cache": false, "persistent": true }, "db:migrate": {} } } turbo.json ファイルは、turbo リポジトリにコマンドの解釈方法を指示します。tasks キー内にあるものはすべて all package.json にあるものと一致します。 tasks 4 つのコマンドを定義していることに注意してください。これらは、各リポジトリの package.json のスクリプト セクションにあるコマンドと一致します。ただし、すべての package.json でこれらのコマンドを実装する必要はありません。 例: コマンドは によってトリガーされ、package.json 内で が見つかったすべてのパッケージを実行します。turbo に含めない場合は実行されません。 dev turbo dev dev 4. プロジェクトのルートに フォルダを作成する apps mkdir apps 5. フォルダにリミックスアプリを作成する(または既存のアプリを移動する) apps npx create-remix --template edmundhung/remix-worker-template ように求められたら、いいえと答えます。 Install any dependencies with npm 6. package.json の を (またはあなたの名前) に変更します。 name @repo/my-remix-cloudflare-app { - "name": "my-remix-cloudflare-app", + "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "keywords": [], "author": "", "license": "ISC" } 7. Dependencies と devDependencies を からルートの にコピーします。 apps/<app>/package.json package.json 例えば: <ルート>/package.json { "name": "@repo/main", "version": "1.0.0", "scripts": {}, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@markdoc/markdoc": "^0.4.0", "@remix-run/cloudflare": "^2.8.1", "@remix-run/cloudflare-pages": "^2.8.1", "@remix-run/react": "^2.8.1", "isbot": "^3.6.5", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240222.0", "@octokit/types": "^12.6.0", "@playwright/test": "^1.42.1", "@remix-run/dev": "^2.8.1", "@remix-run/eslint-config": "^2.8.1", "@tailwindcss/typography": "^0.5.10", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.21", "autoprefixer": "^10.4.18", "concurrently": "^8.2.2", "cross-env": "^7.0.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "husky": "^9.0.11", "lint-staged": "^15.2.2", "msw": "^2.2.3", "postcss": "^8.4.35", "prettier": "^3.2.5", "prettier-plugin-tailwindcss": "^0.5.12", "rimraf": "^5.0.5", "tailwindcss": "^3.4.1", "typescript": "^5.4.2", "vite": "^5.1.5", "vite-tsconfig-paths": "^4.3.1", "wrangler": "^3.32.0" } } 8. ルート レベルで Turbo を devDependency として追加します (これにより、Remix のすべてのパッケージがインストールされます)。 turbo が package.json の devDependencies 内にあることを確認します。リストされていない場合は、次のコマンドを実行します。 pnpm add turbo -D -w フラグは、pnpm にワークスペースのルートにインストールするように指示します。 -w 9. ルートpackage.jsonに次のエントリを追加します。 に コマンドを追加する scripts dev オプションに を追加する packageManager { "name": "@repo/main", "version": "1.0.0", "scripts": { "dev": "turbo dev" }, "keywords": [], "author": "", "license": "ISC", "packageManager": "pnpm@9.1.0", "dependencies": { // omitted for brevity }, "devDependencies": { // omitted for brevity } } 10. を実行してすべてが機能していることを確認する pnpm dev pnpm dev 11. プロジェクトのルートに Libs フォルダを作成します。config、db、utils を追加します。 mkdir -p libs/config libs/db libs/utils 12. 各パッケージに を追加します。 src/index.ts touch libs/config/src/index.ts libs/db/src/index.ts libs/utils/src/index.ts すべてのパッケージをエクスポートするには、index.ts ファイルが使用されます。 すべてをコンパクトにするために、フォルダーをエントリ ポイントとして使用します。 これは慣例であり、これに従わないこともできます。 13. 空の package.json を作成し、 ファイルに次の内容を追加します。 libs/config/package.json { "name": "@repo/config", "version": "1.0.0", "type": "module", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } } 14. に対しても同じ操作を行います。 libs/db/package.json { "name": "@repo/db", "version": "1.0.0", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } } 15. そして : libs/utils/package.json { "name": "@repo/utils", "version": "1.0.0", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } } 「exports」フィールドを指定します。これにより、他のリポジトリにパッケージをインポートする場所が通知されます。 「name」フィールドを指定します。これは、パッケージをインストールし、他のリポジトリに参照するために使用されます。 16. (DB) - DrizzleとPostgresを追加する 注: 私は ORM を軽蔑し始めています。私は 10 年以上かけて 6 つの異なる ORM を学習しましたが、その知識は移転できません。 新しいテクノロジーが登場すると問題が発生します。Prisma は、そのままでは Cloudflare ワーカーをサポートしていません。 LLM を使用すると、複雑な SQL クエリをこれまで以上に簡単に記述できます。 SQL の学習は普遍的な言語であり、変更される可能性は低いです。 pnpm add drizzle-orm drizle-kit --filter=@repo/db ワークスペース レベルで Postgres をインストールします。 。 「落とし穴」セクションを参照してください pnma add postgres -w 注: フラグは、pnpm にパッケージを db リポジトリに追加するように指示します。 --filter=@repo/db 17. ワークスペースリポジトリにdotenvを追加する pnpm add dotenv -w 注記 フラグはpnpmにルートのpackage.jsonにインストールするように指示します。 -w 18. Config プロジェクトをすべてのプロジェクトに追加します。 pnpm add @repo/config -r --filter=!@repo/config : 注記 フラグは、pnpm にパッケージをすべてのリポジトリに追加するように指示します。 -r フラグは、pnpm に設定リポジトリを除外するように指示します。 --filter=! パッケージ名の前の に注意してください ! 19. (オプション) 上記のコマンドが機能しない場合は、.npmrc を使用してください。 pnpm がリポジトリからパッケージをプルしている場合は、プロジェクトのルートに ファイルを作成できます。 .npmrc .npmrc link-workspace-packages= true prefer-workspace-packages=true これにより、pnpm は最初にワークスペース パッケージを使用するようになります。 .nprmcファイルの作成を手伝ってくれたRedditの に感謝します ZoWnx 20. Libs/Config 内の共有 を構成する tsconfig.json pnpm ワークスペースの機能を活用して、プロジェクト間で共有できる構成ファイルを作成できます。 ライブラリに使用する基本の tsconfig.lib.json を作成します。 内で インスタンス化します。 libs/config tsconfig.lib.json touch "libs/config/tsconfig.base.lib.json" 次に、次のコードを追加します。 tsconfig.base.lib.json { "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "lib": ["ES2022"], "module": "ESNext", "moduleResolution": "Bundler", "resolveJsonModule": true, "target": "ES2022", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "allowImportingTsExtensions": true, "allowJs": true, "noUncheckedIndexedAccess": true, "noEmit": true, "incremental": true, "composite": false, "declaration": true, "declarationMap": true, "inlineSources": false, "isolatedModules": true, "noUnusedLocals": false, "noUnusedParameters": false, "preserveWatchOutput": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "sourceMap": true, } } 21. db リポジトリに db 接続を追加します。 // libs/db/drizzle.config.ts (Yes, this one is at root of the db package, outside the src folder) // We don't want to export this file as this is ran at setup. import "dotenv/config"; // make sure to install dotenv package import { defineConfig } from "drizzle-kit"; export default defineConfig({ dialect: "postgresql", out: "./src/generated", schema: "./src/drizzle/schema.ts", dbCredentials: { url: process.env.DATABASE_URL!, }, // Print all statements verbose: true, // Always ask for confirmation strict: true, }); スキーマ ファイル: // libs/db/src/drizzle/schema.ts export const User = pgTable("User", { userId: char("userId", { length: 26 }).primaryKey().notNull(), subId: char("subId", { length: 36 }).notNull(), // We are not making this unique to support merging accounts in later // iterations email: text("email"), loginProvider: loginProviderEnum("loginProvider").array().notNull(), createdAt: timestamp("createdAt", { precision: 3, mode: "date" }).notNull(), updatedAt: timestamp("updatedAt", { precision: 3, mode: "date" }).notNull(), }); クライアント ファイル: // libs/db/src/drizzle-client.ts import { drizzle, PostgresJsDatabase } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "./schema"; export type DrizzleClient = PostgresJsDatabase<typeof schema>; let drizzleClient: DrizzleClient | undefined; type GetClientInput = { databaseUrl: string; env: string; mode?: "cloudflare" | "node"; }; declare var window: typeof globalThis; declare var self: typeof globalThis; export function getDrizzleClient(input: GetClientInput) { const { mode, env } = input; if (mode === "cloudflare") { return generateClient(input); } const globalObject = typeof globalThis !== "undefined" ? globalThis : typeof global !== "undefined" ? global : typeof window !== "undefined" ? window : self; if (env === "production") { drizzleClient = generateClient(input); } else if (globalObject) { if (!(globalObject as any).__db__) { (globalObject as any).__db__ = generateClient(input); } drizzleClient = (globalObject as any).__db__; } else { drizzleClient = generateClient(input); } return drizzleClient; } type GenerateClientInput = { databaseUrl: string; env: string; }; function generateClient(input: GenerateClientInput) { const { databaseUrl, env } = input; const isLoggingEnabled = env === "development"; // prepare: false for serverless try { const client = postgres(databaseUrl, { prepare: false }); const db = drizzle(client, { schema, logger: isLoggingEnabled }); return db; } catch (e) { console.log("ERROR", e); return undefined!; } } 移行ファイル: // libs/db/src/drizzle/migrate.ts import { config } from "dotenv"; import { migrate } from "drizzle-orm/postgres-js/migrator"; import postgres from "postgres"; import { drizzle } from "drizzle-orm/postgres-js"; import "dotenv/config"; import path from "path"; config({ path: "../../../../apps/my-remix-cloudflare-app/.dev.vars" }); const ssl = process.env.ENVIRONMENT === "development" ? undefined : "require"; const databaseUrl = drizzle( postgres(`${process.env.DATABASE_URL}`, { ssl, max: 1 }) ); // Somehow the current starting path is /libs/db // Remember to have the DB running before running this script const migration = path.resolve("./src/generated"); const main = async () => { try { await migrate(databaseUrl, { migrationsFolder: migration, }); console.log("Migration complete"); } catch (error) { console.log(error); } process.exit(0); }; main(); これは移行後に実行する必要があります そして、 ファイルにクライアントとスキーマをエクスポートします。その他は特定の時間に実行されます。 src/index.ts // libs/db/src/index.ts export * from "./drizzle/drizzle-client"; export * from "./drizzle/schema " に、 と、移行コマンドを実行するコードを追加します。 package.json drizzle-kit generate { "name": "@repo/db", "version": "1.0.0", "main": "./src/index.ts", "module": "./src/index.ts", "types": "./src/index.ts", "scripts": { "db:generate": "drizzle-kit generate", "db:migrate": "dotenv tsx ./drizzle/migrate", }, "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } }, "dependencies": { "@repo/configs": "workspace:^", "drizzle-kit": "^0.24.1", "drizzle-orm": "^0.33.0", }, "devDependencies": { "@types/node": "^22.5.0" } } 22. と に共有 を使用する libs/db libs/utils tsconfig.json と の tsconfig.json を作成します。 libs/db libs/utils touch "libs/db/tsconfig.json" "libs/utils/tsconfig.json" 次に、それぞれに以下を追加します。 { "extends": "@repo/configs/tsconfig.base.lib.json", "include": ["./src"], } が tsconfig.base.lib.json を参照するパスとして使用されていることを確認します。 @repo/configs それは私たちの道をきれいにします。 23. TSXをインストールする TypeScript Execute (TSX) は、ts-node の代替となるライブラリです。これを使用して、drizzle の移行を実行します。 pnpm add tsx -D --filter=@repo/db 24. ディレクトリに空の.envを追加する libs/db touch "libs/db/.env" 次の内容を追加します。 DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" NODE_ENV="development" MODE="node" 25. Remix プロジェクトに リポジトリを追加する libs/db プロジェクトのルートから、次のコマンドを実行します。 pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app これが機能しない場合は、 の package.json に移動し、依存関係を手動で追加します。 apps/my-remix-cloudflare-app { "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "dependencies": { "@repo/db": "workspace:*" } } バージョン フィールドの に注意してください。これにより、pnpm はワークスペース内のパッケージの任意のバージョンを使用するように指示されます。 workspace:* おそらく のようなものが表示されます。ローカル パッケージのバージョンを上げない限り、問題にはなりません。 pnpm add, workspace:^ これを手動で追加した場合は、プロジェクトのルートから 実行します。 pnpm install プロジェクトで @repo/db を使用できるはずです。 26. ユーティリティに共有コードを追加する: 次のコードを ファイルに追加します。 libs/utils/src/index.ts // libs/utils/src/index.ts export function hellowWorld() { return "Hello World!"; } 27. Remix アプリにライブラリ/ユーティリティをインストールします。 pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app 28. (オプション) Docker コンテナから Postgres を起動する Postgres インスタンスを実行していない場合は、docker-compose を使用して起動できます。なお、ここでは Docker について知っていることを前提としています。 プロジェクトのルートに ファイルを作成します。 docker-compose.yml # Auto-generated docker-compose.yml file. version: '3.8' # Define services. services: postgres: image: postgres:latest restart: always environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - POSTGRES_DB=postgres ports: - "5432:5432" volumes: - ./postgres-data:/var/lib/postgresql/data pgadmin: # To connect PG Admin, navigate to http://localhost:8500 use: # host.docker.internal # postgres # (username) postgres # (password) postgres image: dpage/pgadmin4 ports: - "8500:80" environment: PGADMIN_DEFAULT_EMAIL: admin@a.com PGADMIN_DEFAULT_PASSWORD: admin 次に、次のコマンドを実行します。 docker-compose up -d フラグは、docker-compose にデタッチされた状態で実行するように指示し、ターミナルに再度アクセスできるようにします。 -d 29. DBスキーマを生成する 次に、libs/db リポジトリに移動して、 実行します。 db:generate cd `./libs/db` && pnpm db:generate の別名であることに注意してください。 db:generate drizzle-kit generate 適切な .env があることを確認します。 さらに、Postgres インスタンスが実行されていることを前提としています。 30. 移行を実行します。 データベース内のすべてのテーブルをスキャフォールディングするには、移行を実行する必要があります。 libs/db リポジトリに移動し (まだそこにない場合)、 実行します。 db:generate cd `./libs/db` && pnpm db:migrate のエイリアスであることに注意してください。 db:migrate dotenv tsx ./drizzle/migrate 適切な .env があることを確認します。 さらに、Postgres インスタンスが実行されていることを前提としています。 31. Remix アプリ内に DB 呼び出しを挿入します。 // apps/my-remix-cloudflare-app/app/routes/_index.tsx import type { LoaderFunctionArgs } from '@remix-run/cloudflare'; import { json, useLoaderData } from '@remix-run/react'; import { getDrizzleClient } from '@repo/db'; import { Markdown } from '~/components'; import { getFileContentWithCache } from '~/services/github.server'; import { parse } from '~/services/markdoc.server'; export async function loader({ context }: LoaderFunctionArgs) { const client = await getDrizzleClient({ databaseUrl: context.env.DATABASE_URL, env: 'development', mode: 'cloudflare', }); if (client) { const res = await client.query.User.findFirst(); console.log('res', res); } const content = await getFileContentWithCache(context, 'README.md'); return json( { content: parse(content), // user: firstUser, }, { headers: { 'Cache-Control': 'public, max-age=3600', }, }, ); } export default function Index() { const { content } = useLoaderData<typeof loader>(); return <Markdown content={content} />; } ここではベストプラクティスに従っていないことに注意してください。 ローダー内で直接 DB 呼び出しを行わず、それらを呼び出す抽象化を作成することをお勧めします。 Cloudflareは環境変数の設定が難しい。環境変数はリクエストによって渡される。 32. .dev.vars に以下を追加します。 アプリ/my-remix-cloudflare-app/.dev.vars DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" 33. リミックスプロジェクトを実行する! postgresインスタンスを起動します(準備ができていない場合) docker-compose up -d プロジェクトを開始する pnpm turbo dev 高度な使用例 - Cloudflare Workers の GetLoadContext における CQRS。 私のプロジェクトでは、 を実装する傾向があります。これはこのチュートリアルの範囲外です。 CQRS パターン 2 それでも、ロード コンテキスト内では、Remix アプリケーション全体をビジネス ロジックから切り離すメディエーター (および Cookie フラッシュ メッセージ) を挿入する傾向があります。 これは次のようになります: export const getLoadContext: GetLoadContext = async ({ context, request }) => { const isEnvEmpty = Object.keys(context.cloudflare.env).length === 0; const env = isEnvEmpty ? process.env : context.cloudflare.env; const sessionFlashSecret = env.SESSION_FLASH_SECRET; const flashStorage = createCookieSessionStorage({ cookie: { name: "__flash", httpOnly: true, maxAge: 60, path: "/", sameSite: "lax", secrets: [sessionFlashSecret], secure: true, }, }); return { ...context, cloudflare: { ...context.cloudflare, env, }, dispatch: (await dispatchWithContext({ env: env as unknown as Record<string, string>, request, })) as Dispatch, flashStorage, }; }; ディスパッチ コードが省略されていることに注意してください。詳細については、TypeScript 開発エクスペリエンスを 10 倍にする方法に関する私の記事 ご覧ください。 を コードを変更せずに、Remix を削除したり、別のコンシューマーを使用したりできます。 しかし…。 turborepo を使用してモノレポ構造で作業する場合は、追加の課題があります。 ロード コンテキスト内のパッケージから TypeScript ファイルをインポートすると、 Vite は拡張子 のファイルが不明であるというエラーを返し、そのファイルを処理する方法がわかりません。 @repo/db .ts これは、load-context + ワークスペースがサイトのメインのインポート グラフの外側にあり、TypeScript ファイルが再生されないために発生します。 コツは、 使用して、Vite を呼び出す ロードすることです。これは、次の制限を克服するため重要です。 tsx 前に Cloudflare パッケージの依存関係。 Cloudflare パッケージの依存関係と事前ビルド まず第一に、これは私が避けようとしていたステップでした。パッケージごとにビルド ステップを導入する必要があり、構成が増えることを意味していたからです。 幸いなことに、これは Cloudflare Pages では機能しませんでした。Postgres などの特定のライブラリがランタイムを検出し、必要なパッケージを取得します。 回避策があります。tsx を使用してすべての TypeScript ファイルを読み込み、実行前にトランスパイルすることができます。 これはビルド前のステップだと主張することもできますが、まだリミックスのリポジトリ レベルであるため、このアプローチに大きな問題はないと思います。 これを解決するには、依存関係として tsx を追加します。 pnpm add tsx -D --filter=@repo/my-remix-cloudflare-app 次に、 変更し、各リミックス スクリプトに tsx プロセスを追加する必要があります。 package.json { "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "scripts": { // Other scripts omitted "build": "NODE_OPTIONS=\"--import tsx/esm\" remix vite:build", "dev": "NODE_OPTIONS=\"--import tsx/esm\" remix vite:dev", "start": "NODE_OPTIONS=\"--import tsx/esm\" wrangler pages dev ./build/client" } } 特典 ファイルの作成 .npmrc コマンドラインを使用してローカル パッケージを追加するときに問題が発生する場合は、プロジェクトのルートに ファイルを作成できます。 .npmrc .npmrc link-workspace-packages= true prefer-workspace-packages=true これにより、pnpm は最初にワークスペース パッケージを使用するようになります。 .nprmcファイルの作成を手伝ってくれたRedditの に感謝します ZoWnx 落とし穴 - ファイル内の および 命名には注意してください。別のライブラリ内にある場合でも同様です。Remix はこれらを使用して、クライアント ファイルかサーバー ファイルかを判断します。プロジェクトはリポジトリごとにコンパイルされないため、インポート エラーが発生します。 .client .server Postgres などのマルチプラットフォーム パッケージで問題が発生する場合は、ワークスペース レベルでインストールすることをお勧めします。適切なインポートが検出されます。@repo/db リポジトリに直接インストールすると、Remix にインポートするときに問題が発生します。 以上です、皆さん!!! GitHub リポジトリ アクセスできます。 完全な実装については、ここから ソーシャルで私をフォローしてください! 私は、本番環境での 1% のエラーを検出するために、公開されている自動テスト エンジニアを構築しています。 私は以下の進捗状況を共有しています: X/Twitter @javiasilis LinkedIn @javiasilis