Lesen Sie unbedingt den Abschnitt zu Fallstricke unten!
Notiz:
libs
und „ packages
liegen.
pnpm dlx create-turbo@latest
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": "//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.
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 Stammverzeichnisses { "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" } }
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:
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.
link-workspace-packages= true prefer-workspace-packages=true
tsconfig.json
in Libs/Config
Instanziieren Sie innerhalb libs/config
eine tsconfig.lib.json
:
touch "libs/config/tsconfig.base.lib.json"
{ "$schema": "//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, });
// 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();
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"
{ "extends": "@repo/configs/tsconfig.base.lib.json", "include": ["./src"], }
@repo/configs
als Pfad zum Verweisen auf unsere tsconfig.base.lib.json verwendet wird. pnpm add tsx -D --filter=@repo/db
libs/db
-Verzeichnis hinzu touch "libs/db/.env"
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" NODE_ENV="development" MODE="node"
libs/db
Repository zu unserem Remix-Projekt hinzu 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.
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
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 //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
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.
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} />; }
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
docker-compose up -d
pnpm turbo dev
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 .
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.
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 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.
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.
link-workspace-packages= true prefer-workspace-packages=true
Dadurch wird pnpm angewiesen, zuerst die Arbeitsbereichspakete zu verwenden.
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!
Das ist es, Leute!!!