Ich brauchte eine Möglichkeit, Remix mit Vite und Cloudflare Workers-Pages mit minimaler Konfiguration zu verwenden.
Ich habe andere Repositories gesehen, wie zum Beispiel:
Es gab jedoch einige Einschränkungen:
Ich wollte es nicht vorab erstellen, da ich die Repositories nicht mit weiteren Konfigurationsdateien vergiften wollte.
Cloudflare Workers/Pages hat ein anderes Ziel. Es wurde schwierig, es mit tsup anzusprechen, da Pakete wie Postgres die Knotenabhängigkeiten beim Import in Remix beschädigten.
Ich brauchte auch eine Möglichkeit, verschiedene Ziele zu nutzen (Remix-Cloudflare, Node/Bun).
Dennoch danke ich ihnen, da sie den Weg geebnet haben, der dies möglich gemacht hat!
Lesen Sie unbedingt den Abschnitt zu Fallstricke unten!
Ich baue eine öffentliche automatisierte Testplattform auf, um diese 1 %-Fehler in der Produktion abzufangen.
Ich teile meine Fortschritte bei:
Auf die vollständige Implementierung können Sie hier zugreifen.
Obwohl Sie hier durch ein neues Mono-Repository geführt werden, ist es durchaus zulässig, ein vorhandenes in ein solches umzuwandeln.
Dies setzt auch voraus, dass Sie über einige Kenntnisse zu Mono-Repos verfügen.
Notiz:
libs
und „ packages
liegen.Turborepo arbeitet auf den Arbeitsbereichen Ihres Paketmanagers, um die Skripte und Ausgaben Ihres Projekts zu verwalten (es kann Ihre Ausgabe sogar zwischenspeichern). Bisher ist es neben Rush (das ich nicht ausprobiert habe und nicht mag) das einzige funktionierende Mono-Repo-Tool.
NX verfügt nicht über die Vite-Unterstützung von Remix (zum Zeitpunkt des Schreibens dieses Artikels – 28. August 2024).
pnpm dlx create-turbo@latest
Wir werden die Arbeitsbereichsfunktionen von PNPM verwenden, um Abhängigkeiten zu verwalten.
Erstellen Sie in Ihrem Monorepo-Verzeichnis eine pnpm-workspace.yaml
.
Fügen Sie darin hinzu:
packages: - "apps/*" - "libs/*"
Dadurch wird pnpm mitgeteilt, dass alle Repositories in apps
und libs
liegen. Beachten Sie, dass die Verwendung von libs
oder packages
(wie Sie vielleicht schon anderswo gesehen haben) keine Rolle spielt.
pnpm init
{ "name": "@repo/main", "version": "1.0.0", "scripts": {}, "keywords": [], "author": "", "license": "ISC" }
Beachten Sie den name:@repo/main
Dies zeigt uns, dass dies der Haupteintrag der Anwendung ist. Sie müssen keiner bestimmten Konvention folgen oder das Präfix @
verwenden. Benutzer verwenden es, um es von lokalen/Remote-Paketen zu unterscheiden oder um es einfacher zu machen, es in einer Organisation zu gruppieren.
turbo.json
im Stammverzeichnis des Projekts: { "$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": {} } }
Die Datei turbo.json teilt dem Turbo-Repository mit, wie unsere Befehle zu interpretieren sind. Alles, was im Schlüssel „ tasks
steht, stimmt mit dem überein, was in der Datei „all package.json“ steht.
Beachten Sie, dass wir vier Befehle definieren. Diese entsprechen denen im Skriptabschnitt der package.json jedes Repositorys. Allerdings müssen nicht alle package.json diese Befehle implementieren.
Beispiel: Der dev
Befehl wird von turbo dev
ausgelöst und führt alle Pakete aus, deren dev
in package.json gefunden wird. Wenn Sie es nicht in turbo einschließen, wird es nicht ausgeführt.
apps
Ordner im Stammverzeichnis des Projekts mkdir apps
apps
Ordner (oder verschieben Sie eine vorhandene) npx create-remix --template edmundhung/remix-worker-template
Wenn Sie aufgefordert werden Install any dependencies with npm
sagen Sie „Nein“.
name
der Datei package.json in @repo/my-remix-cloudflare-app
(oder Ihren Namen) um. { - "name": "my-remix-cloudflare-app", + "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "keywords": [], "author": "", "license": "ISC" }
apps/<app>/package.json
in die Datei package.json
des StammverzeichnissesZ.B:
<Stamm>/Paket.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" } }
Überprüfen Sie, ob turbo in den devDependencies von package.json enthalten ist. Wenn es nicht aufgeführt ist, führen Sie den folgenden Befehl aus:
pnpm add turbo -D -w
Das Flag -w
weist pnpm an, es im Stammverzeichnis des Arbeitsbereichs zu installieren.
Fügen Sie den dev
Befehl zu scripts
hinzu
Fügen Sie den packageManager
zur Option hinzu
{ "name": "@repo/main", "version": "1.0.0", "scripts": { "dev": "turbo dev" }, "keywords": [], "author": "", "license": "ISC", "packageManager": "[email protected]", "dependencies": { // omitted for brevity }, "devDependencies": { // omitted for brevity } }
pnpm dev
ausführen pnpm dev
mkdir -p libs/config libs/db libs/utils
src/index.ts
hinzu. touch libs/config/src/index.ts libs/db/src/index.ts libs/utils/src/index.ts
libs/config/package.json
Folgendes hinzu: { "name": "@repo/config", "version": "1.0.0", "type": "module", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } }
libs/db/package.json
: { "name": "@repo/db", "version": "1.0.0", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } }
libs/utils/package.json
: { "name": "@repo/utils", "version": "1.0.0", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } }
Hinweise:
Ich fange an, ORMs zu verachten. Ich habe über 10 Jahre damit verbracht, 6 verschiedene zu lernen, und dieses Wissen lässt sich nicht übertragen.
Sie haben Probleme, wenn neue Technologien auf den Markt kommen. Prisma unterstützt Cloudflare-Worker nicht von Haus aus.
Mit LLMs ist es einfacher als je zuvor, komplexe SQL-Abfragen zu schreiben.
Das Erlernen von SQL ist eine universelle Sprache und wird sich wahrscheinlich nicht ändern.
pnpm add drizzle-orm drizle-kit --filter=@repo/db
Installieren Sie Postgres auf Arbeitsbereichsebene. Weitere Informationen finden Sie im Abschnitt „Fallstricke“ .
pnma add postgres -w
Hinweise:
--filter=@repo/db
weist pnpm an, das Paket zum DB-Repository hinzuzufügen. pnpm add dotenv -w
Hinweise
-w
weist pnpm an, es in der Stammdatei package.json zu installieren. pnpm add @repo/config -r --filter=!@repo/config
Hinweise :
-r
weist pnpm an, das Paket zu allen Repositories hinzuzufügen.--filter=!
weist pnpm an, das Konfigurations-Repository auszuschließen.!
vor dem Paketnamen Wenn pnpm die Pakete aus dem Repository abruft, können wir im Stammverzeichnis des Projekts eine .npmrc
Datei erstellen.
.npmrc
link-workspace-packages= true prefer-workspace-packages=true
tsconfig.json
in Libs/ConfigMithilfe der Leistungsfähigkeit von pnpm-Arbeitsbereichen können Sie Konfigurationsdateien erstellen, die projektübergreifend genutzt werden können.
Wir erstellen eine Basis-tsconfig.lib.json, die wir für unsere Bibliotheken verwenden.
Instanziieren Sie innerhalb libs/config
eine tsconfig.lib.json
:
touch "libs/config/tsconfig.base.lib.json"
Fügen Sie dann Folgendes hinzu:
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, } }
// 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, });
Die Schemadatei:
// 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(), });
Die Kundendatei:
// 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!; } }
Die Migrationsdatei:
// 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();
Dies sollte nach den Migrationen ausgeführt werden
Und exportieren Sie den Client und das Schema in die Datei src/index.ts
. Andere werden zu bestimmten Zeiten ausgeführt.
// libs/db/src/index.ts export * from "./drizzle/drizzle-client"; export * from "./drizzle/schema "
Fügen Sie in Ihrer package.json
das drizzle-kit generate
und den Code zum Ausführen des Migrationsbefehls hinzu:
{ "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" } }
tsconfig.json
für libs/db
und libs/utils
Erstellen Sie eine tsconfig.json für libs/db
und libs/utils
touch "libs/db/tsconfig.json" "libs/utils/tsconfig.json"
Fügen Sie dann jeweils hinzu:
{ "extends": "@repo/configs/tsconfig.base.lib.json", "include": ["./src"], }
@repo/configs
als Pfad zum Verweisen auf unsere tsconfig.base.lib.json verwendet wird.TypeScript Execute (TSX) ist eine Bibliotheksalternative zu ts-node. Wir werden dies verwenden, um die Migrationen von Drizzle auszuführen.
pnpm add tsx -D --filter=@repo/db
libs/db
-Verzeichnis hinzu touch "libs/db/.env"
Fügen Sie den folgenden Inhalt hinzu:
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" NODE_ENV="development" MODE="node"
libs/db
Repository zu unserem Remix-Projekt hinzuFühren Sie im Stammverzeichnis des Projekts Folgendes aus:
pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app
Wenn dies nicht funktioniert, gehen Sie zu package.json von apps/my-remix-cloudflare-app
und fügen Sie die Abhängigkeit manuell hinzu.
{ "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "dependencies": { "@repo/db": "workspace:*" } }
Beachten Sie den workspace:*
im Versionsfeld. Dadurch wird pnpm angewiesen, jede Version des Pakets im Arbeitsbereich zu verwenden.
Wenn Sie es über die CLI mithilfe von pnpm add,
wird wahrscheinlich etwas wie workspace:^
angezeigt. Solange Sie die lokalen Paketversionen nicht erhöhen, sollte dies keine Rolle spielen.
Wenn Sie dies manuell hinzugefügt haben, führen Sie pnpm install
vom Stammverzeichnis des Projekts aus.
Wir sollten in der Lage sein, @repo/db in unserem Projekt zu nutzen.
Fügen Sie diesen Code zur Datei libs/utils/src/index.ts
hinzu:
// libs/utils/src/index.ts export function hellowWorld() { return "Hello World!"; }
pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app
Wenn Sie keine Postgres-Instanz ausführen, können wir eine mit Docker-Compose starten. Beachten Sie, dass ich davon ausgehe, dass Sie Docker kennen.
Erstellen Sie eine Datei docker-compose.yml
im Stammverzeichnis des Projekts.
# 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: [email protected] PGADMIN_DEFAULT_PASSWORD: admin
Dann können Sie Folgendes ausführen:
docker-compose up -d
Das Flag -d
weist Docker-Compose an, getrennt ausgeführt zu werden, damit Sie wieder Zugriff auf Ihr Terminal haben.
Navigieren Sie jetzt zum libs/db-Repository und führen Sie db:generate
.
cd `./libs/db` && pnpm db:generate
db:generate
ein Alias für drizzle-kit generate
ist.Wir müssen die Migrationen ausführen, um ein Gerüst für alle Tabellen in unserer Datenbank zu erstellen.
Navigieren Sie zum libs/db-Repository (falls Sie nicht dort sind) und führen Sie db:generate
.
cd `./libs/db` && pnpm db:migrate
db:migrate
ein Alias für dotenv tsx ./drizzle/migrate
ist. // 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} />; }
apps/mein-Remix-Cloudflare-App/.dev.vars
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
Starten Sie die Postgres-Instanz (falls nicht bereit)
docker-compose up -d
Starten Sie das Projekt
pnpm turbo dev
In meinen Projekten neige ich dazu, ein CQRS-Muster zu implementieren, 2. Dies geht über den Rahmen dieses Tutorials hinaus.
Dennoch neige ich dazu, im Ladekontext einen Mediator (und eine Cookie-Flash-Nachricht) einzufügen, der meine gesamte Remix-Anwendung von meiner Geschäftslogik entkoppelt.
Das sieht ungefähr so aus:
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, }; };
Beachten Sie, dass der Dispatch-Code weggelassen wird. Weitere Informationen hierzu finden Sie in meinem Artikel „Wie Sie Ihre TypeScript-Entwicklererfahrung verzehnfachen können“ hier .
Ich kann Remix entfernen oder einen anderen Consumer verwenden, ohne meinen Code zu ändern.
Aber….
Es gibt eine zusätzliche Herausforderung, wenn Sie mit Turborepo in einer Monorepo-Struktur arbeiten.
Wenn Sie eine TypeScript-Datei aus einem Paket innerhalb des Ladekontexts importieren, sagen wir @repo/db
gibt Vite einen Fehler zurück, dass die Datei mit der Erweiterung .ts
unbekannt ist, und weiß nicht, wie es sie verarbeiten soll.
Dies liegt daran, dass sich der Ladekontext und die Arbeitsbereiche außerhalb des Hauptimportdiagramms der Site befinden und TypeScript-Dateien daher nicht verwendet werden können.
Der Trick besteht darin, tsx
zu verwenden und es zu laden, bevor Vite aufgerufen wird. Das funktioniert. Dies ist wichtig, da dadurch die folgenden Einschränkungen überwunden werden:
Cloudflare-Paketabhängigkeiten.
Cloudflare-Paketabhängigkeiten und Vorab-Building
Erstens war dies der Schritt, den ich vermeiden wollte, da er bedeutete, dass ich für jedes der Pakete einen Build-Schritt einführen musste, was mehr Konfiguration bedeutete.
Glücklicherweise funktionierte dies bei Cloudflare Pages nicht. Bestimmte Bibliotheken wie Postgres erkennen die Laufzeit und rufen das erforderliche Paket ab.
Es gibt einen Workaround: Wir können tsx verwenden, um alle TypeScript-Dateien zu laden und sie vor der Ausführung zu transpilieren.
Man kann argumentieren, dass es sich hierbei um einen Schritt vor dem Build handelt, aber da es sich immer noch auf der Repository-Ebene des Remixes befindet, sehe ich bei diesem Ansatz keine größeren Probleme.
Um dies zu lösen, fügen wir tsx als Abhängigkeit hinzu:
pnpm add tsx -D --filter=@repo/my-remix-cloudflare-app
Und dann müssen wir unser package.json
ändern und den tsx-Prozess zu jedem unserer Remix-Skripte hinzufügen:
{ "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
Datei Falls beim Hinzufügen Ihrer lokalen Pakete mit der Befehlszeile Probleme auftreten, können Sie im Stammverzeichnis des Projekts eine .npmrc
Datei erstellen.
.npmrc
link-workspace-packages= true prefer-workspace-packages=true
Dadurch wird pnpm angewiesen, zuerst die Arbeitsbereichspakete zu verwenden.
Danke an ZoWnx von Reddit, der mir beim Erstellen einer .nprmc-Datei geholfen hat
Seien Sie vorsichtig bei der Benennung von .client
und .server
in Ihren Dateien. Auch wenn es sich in einer separaten Bibliothek befindet. Remix verwendet diese, um zu bestimmen, ob es sich um eine Client- oder Serverdatei handelt. Das Projekt wird nicht pro Repository kompiliert, daher wird ein Importfehler ausgegeben!
Wenn Sie Probleme mit plattformübergreifenden Paketen wie Postgres haben, ist die Installation auf Arbeitsbereichsebene besser. Dadurch wird der korrekte Import erkannt. Die direkte Installation im @repo/db-Repository schlägt beim Import in Remix fehl.
Das ist es, Leute!!!
Auf die vollständige Implementierung können Sie hier zugreifen.
Ich baue einen öffentlichen automatisierten Testingenieur auf, um diese 1 % Fehler in der Produktion abzufangen.
Ich teile meine Fortschritte bei: