Remix'i Vite ve Cloudflare Workers-Pages ile minimum yapılandırmayla kullanmanın bir yoluna ihtiyacım vardı.
Diğer depoları da gördüm, örneğin:
Ancak bazı sınırlamaları vardı:
Önceden derlemek istemedim, çünkü daha fazla yapılandırma dosyasıyla depoları zehirlemek istemedim.
Cloudflare Workers/Pages'ın farklı bir hedefi var. Postgres gibi paketler Remix'e aktarıldığında düğüm bağımlılıklarını bozarak çekeceğinden, tsup ile hedeflemek zorlaştı.
Ayrıca farklı hedefleri (Remix-Cloudflare, Node/Bun) tüketmenin bir yoluna ihtiyacım vardı
Yine de onlara teşekkür ediyorum, çünkü bu başarının önünü açtılar!
Alt taraftaki tuzaklar bölümünü mutlaka okuyun!
Üretimdeki %1'lik hataları yakalamak için kamuya açık bir otomatik test platformu kuruyorum.
İlerlememi şu şekilde paylaşıyorum:
Uygulamanın tamamına buradan ulaşabilirsiniz.
Bu sizi yeni bir mono-depoda gezdirse de, var olan bir mono-depoyu tek bir mono-depoya dönüştürmek de tamamen geçerlidir.
Bu aynı zamanda mono repo konusunda da bilgi sahibi olduğunuzu varsayar.
Not:
libs
ve packages
dizinlerinin dışında olacaktır.Turborepo, projenizin betiklerini ve çıktılarını yönetmek için paket yöneticinizin çalışma alanlarının üstünde çalışır (hatta çıktınızı önbelleğe alabilir). Şimdiye kadar, Rush'ın (denemediğim ve sevmediğim) dışında çalışabilen tek mono-repo aracıdır.
NX'te Remix'in Vite desteği yok (bu yazının yazıldığı tarih itibarıyla - 28 Ağustos 2024).
pnpm dlx create-turbo@latest
Bağımlılıkları yönetmek için PNPM'nin çalışma alanı yeteneklerini kullanacağız.
Monorepo dizininizde pnpm-workspace.yaml
oluşturun.
İçine şunu ekleyin:
packages: - "apps/*" - "libs/*"
Bu pnpm'e tüm depoların apps
ve libs
içinde yer alacağını söyleyecektir. libs
veya packages
kullanmanın (başka bir yerde görmüş olabileceğiniz gibi) bir önemi olmadığını unutmayın.
pnpm init
{ "name": "@repo/main", "version": "1.0.0", "scripts": {}, "keywords": [], "author": "", "license": "ISC" }
name:@repo/main
Bu bize bunun uygulamanın ana girişi olduğunu söyler. Belirli bir kuralı takip etmeniz veya @
önekini kullanmanız gerekmez. İnsanlar bunu yerel/uzak paketlerden ayırt etmek veya bir organizasyonda gruplandırmayı kolaylaştırmak için kullanırlar.
turbo.json
Dosyasını Oluşturun: { "$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 dosyası turbo deposuna komutlarımızı nasıl yorumlayacağını söyler. tasks
anahtarının içinde bulunan her şey all package.json'da bulunanlarla eşleşecektir.
Dört komut tanımladığımızı unutmayın. Bunlar her deponun package.json dosyasının script bölümündekilerle eşleşir. Ancak, tüm package.json dosyaları bu komutları uygulamak zorunda değildir.
Örn: dev
komutu turbo dev
tarafından tetiklenecek ve package.json içinde dev
bulunan tüm paketleri çalıştıracaktır. Eğer bunu turbo'ya dahil etmezseniz, çalıştırılmayacaktır.
apps
klasörü oluşturun mkdir apps
apps
klasöründe bir Remix Uygulaması oluşturun (veya mevcut olanı taşıyın) npx create-remix --template edmundhung/remix-worker-template
Install any dependencies with npm
istediğinde hayır deyin.
name
@repo/my-remix-cloudflare-app
(veya kendi adınız) olarak değiştirin. { - "name": "my-remix-cloudflare-app", + "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "keywords": [], "author": "", "license": "ISC" }
apps/<app>/package.json
Root'un package.json
kopyalayınÖrn:
<kök>/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" } }
Turbo'nun package.json'ın devDependencies'inde olduğunu doğrulayın. Listelenmemişse, aşağıdaki komutu çalıştırın:
pnpm add turbo -D -w
-w
bayrağı pnpm'e bunu çalışma alanı köküne kurmasını söyler.
dev
komutunu scripts
ekleyin
packageManager
seçeneğe ekleyin
{ "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
komutunu çalıştırarak her şeyin çalıştığını doğrulayın pnpm dev
mkdir -p libs/config libs/db libs/utils
src/index.ts
ekleyin. touch libs/config/src/index.ts libs/db/src/index.ts libs/utils/src/index.ts
libs/config/package.json
dosyasına aşağıdakileri ekleyin: { "name": "@repo/config", "version": "1.0.0", "type": "module", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } }
libs/db/package.json
için de yapın: { "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" } } }
Notlar:
ORM'lerden nefret etmeye başlıyorum. 6 farklı ORM öğrenmek için 10 yıldan fazla zaman harcadım ve bunlar aktarılamayan bilgiler.
Yeni teknolojiler çıktığında sorunlar yaşıyorsunuz. Prisma, Cloudflare çalışanlarını anında desteklemiyor.
LLM sayesinde karmaşık SQL sorguları yazmak her zamankinden daha kolay.
SQL öğrenmek evrensel bir dildir ve büyük olasılıkla değişmeyecektir.
pnpm add drizzle-orm drizle-kit --filter=@repo/db
Postgres'i çalışma alanı düzeyinde yükleyin. Pitfall bölümüne bakın .
pnma add postgres -w
Notlar:
--filter=@repo/db
bayrağı pnpm'e paketi db deposuna eklemesini söyler. pnpm add dotenv -w
Notlar
-w
bayrağı pnpm'e bunu root'un package.json dosyasına kurmasını söyler pnpm add @repo/config -r --filter=!@repo/config
Notlar :
-r
bayrağı pnpm'e paketi tüm depolarına eklemesini söyler.--filter=!
bayrağı pnpm'e yapılandırma deposunu hariç tutmasını söyler.!
işaretine dikkat edin Eğer pnpm paketleri depodan çekiyorsa, projenin kökünde bir .npmrc
dosyası oluşturabiliriz.
.npmrc
link-workspace-packages= true prefer-workspace-packages=true
tsconfig.json
YapılandırınPnpm çalışma alanlarının gücünü kullanarak, projeler arasında paylaşılabilen yapılandırma dosyaları oluşturabilirsiniz.
Kütüphanelerimiz için kullanacağımız temel tsconfig.lib.json'ı oluşturacağız.
libs/config
içerisinde tsconfig.lib.json
dosyasını oluşturun:
touch "libs/config/tsconfig.base.lib.json"
Daha sonra şunu ekleyin:
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, });
Şema dosyası:
// 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(), });
Müşteri dosyası:
// 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!; } }
Göç dosyası:
// 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();
Bu, göçlerden sonra yürütülmelidir
Ve istemciyi ve şemayı src/index.ts
dosyasına aktarın. Diğerleri belirli zamanlarda çalıştırılır.
// libs/db/src/index.ts export * from "./drizzle/drizzle-client"; export * from "./drizzle/schema "
package.json
dosyanıza drizzle-kit generate
komutunu ve geçiş komutunu çalıştıracak kodu ekleyin:
{ "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" } }
libs/db
ve libs/utils
için Paylaşılan tsconfig.json
kullanın libs/db
ve libs/utils
için bir tsconfig.json oluşturun
touch "libs/db/tsconfig.json" "libs/utils/tsconfig.json"
Sonra her birine şunu ekleyin:
{ "extends": "@repo/configs/tsconfig.base.lib.json", "include": ["./src"], }
@repo/configs
tsconfig.base.lib.json dosyamıza referans vermek için kullanılan yol olduğunu görün.TypeScript Execute (TSX), ts-node'a bir kütüphane alternatifidir. Bunu, drizzle'ın geçişlerini yürütmek için kullanacağız.
pnpm add tsx -D --filter=@repo/db
libs/db
dizinine boş bir .env ekleyin touch "libs/db/.env"
Aşağıdaki içerikleri ekleyin:
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" NODE_ENV="development" MODE="node"
libs/db
Deposunu Remix Projemize EkleyinProjenin kökünden şunu çalıştırın:
pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app
Eğer bu işe yaramazsa, apps/my-remix-cloudflare-app
dosyasının package.json dosyasına gidin ve bağımlılığı manuel olarak ekleyin.
{ "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "dependencies": { "@repo/db": "workspace:*" } }
Sürüm alanında workspace:*
i not edin. Bu, pnpm'e çalışma alanındaki paketin herhangi bir sürümünü kullanmasını söyler.
Eğer pnpm add,
muhtemelen workspace:^
gibi bir şey göreceksiniz. Yerel paket sürümlerini artırmadığınız sürece sorun olmamalı.
Eğer bunu manuel olarak eklediyseniz, projenin kökünden pnpm install
çalıştırın.
@repo/db'yi projemizde tüketebilmeliyiz.
Bu kodu libs/utils/src/index.ts
dosyasına ekleyin:
// libs/utils/src/index.ts export function hellowWorld() { return "Hello World!"; }
pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app
Çalışan bir Postgres örneğiniz yoksa, docker-compose kullanarak bir tane başlatabiliriz. Docker'ı bildiğinizi varsayıyorum.
Projenin kökünde docker-compose.yml
dosyasını oluşturun.
# 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
Daha sonra şunu çalıştırabilirsiniz:
docker-compose up -d
-d
bayrağı docker-compose'a bağımsız çalışmasını söyler, böylece terminalinize tekrar erişebilirsiniz.
Şimdi libs/db deposuna gidin ve db:generate
çalıştırın.
cd `./libs/db` && pnpm db:generate
db:generate
drizzle-kit generate
için bir takma ad olduğunu unutmayınVeritabanımızdaki tüm tabloları desteklemek için göçleri çalıştırmamız gerekiyor.
libs/db deposuna gidin (orada değilseniz) ve db:generate
çalıştırın.
cd `./libs/db` && pnpm db:migrate
db:migrate
dotenv tsx ./drizzle/migrate
için bir takma ad olduğunu unutmayın // 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} />; }
uygulamalar/benim-remix-cloudflare-uygulamam/.dev.vars
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
Postgres örneğini başlatın (hazır değilse)
docker-compose up -d
Projeyi başlatın
pnpm turbo dev
Projelerimde CQRS desenini uygulamaya çalışıyorum, 2. Bu, bu eğitimin kapsamı dışındadır.
Bununla birlikte, yükleme bağlamı içerisinde, tüm Remix Uygulamamı iş mantığımdan ayıracak bir aracı (ve bir çerez flaş mesajı) enjekte etme eğilimindeyim.
Bu şuna benzer bir şey:
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, }; };
Dağıtım kodunun atlandığını unutmayın. TypeScript geliştirme deneyiminizi 10 katına çıkarma hakkında daha fazla bilgiyi buradaki makalemde bulabilirsiniz.
Kodumu değiştirmeden Remix'i kaldırabilir veya başka bir tüketici kullanabilirim.
Ancak….
Turborepo kullanarak monorepo yapısında çalıştığınızda ek bir zorluk daha ortaya çıkar.
Örneğin, load-context içindeki bir paketten bir TypeScript dosyası içe aktarırsanız, @repo/db
Vite'ın .ts
uzantılı dosyanın bilinmediğine dair bir hata döndüreceğini ve onu nasıl işleyeceğini bilemeyeceğini varsayalım.
Bunun nedeni, yükleme bağlamı + çalışma alanlarının sitenin ana içe aktarma grafiğinin dışında olması ve TypeScript dosyalarının oyunun dışında kalmasıdır.
İşin püf noktası tsx
kullanmak ve Vite'ı çağırmadan önce yüklemektir, bu işe yarayacaktır. Bu önemlidir çünkü aşağıdaki sınırlamaları aşar:
Cloudflare Paket Bağımlılıkları.
Cloudflare Paket Bağımlılıkları ve Ön Yapı
Öncelikle kaçınmaya çalıştığım adım buydu, çünkü bu, paketlerin her biri için bir derleme adımı eklemem gerektiği anlamına geliyordu ve bu da daha fazla yapılandırma anlamına geliyordu.
Neyse ki bu Cloudflare Pages için işe yaramadı. Postgres gibi belirli kütüphaneler çalışma zamanını algılayacak ve gerekli paketi çekecektir.
Bir çözüm yolu var: tsx'i kullanarak tüm TypeScript dosyalarını yükleyebilir ve çalıştırmadan önce derleyebiliriz.
Bunun önceden oluşturulmuş bir adım olduğunu iddia edebilirsiniz, ancak hala remiksin depo düzeyinde olduğu için bu yaklaşımda önemli bir sorun görmüyorum.
Bunu çözmek için tsx'i bağımlılık olarak ekliyoruz:
pnpm add tsx -D --filter=@repo/my-remix-cloudflare-app
Daha sonra package.json
dosyamızı düzenleyip, remix scriptlerimizin her birine tsx sürecini eklememiz gerekiyor:
{ "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
Dosyası Oluşturma Yerel paketlerinizi komut satırıyla eklerken sorun yaşıyorsanız, projenin kök dizininde .npmrc
dosyası oluşturabilirsiniz.
.npmrc
link-workspace-packages= true prefer-workspace-packages=true
Bu, pnpm'e öncelikle çalışma alanı paketlerini kullanmasını söyleyecektir.
.nprmc dosyasını oluşturmama yardımcı olan Reddit'ten ZoWnx'e teşekkürler
Dosyalarınızda .client
ve .server
isimlendirmesine dikkat edin. Ayrı bir kütüphanede olsa bile. Remix, bir istemci veya sunucu dosyası olup olmadığını belirlemek için bunları kullanır. Proje depo başına derlenmediğinden içe aktarma hatası verecektir!
Postgres gibi çoklu platform paketleriyle ilgili sorunlar yaşıyorsanız, bunu çalışma alanı düzeyinde yüklemek daha iyidir. Uygun içe aktarımı algılayacaktır. @repo/db deposuna doğrudan yüklemek, Remix'e içe aktarırken bozulacaktır.
İşte bu kadar arkadaşlar!!!
Uygulamanın tamamına buradan ulaşabilirsiniz.
Üretimdeki %1'lik hataları yakalamak için kamuya açık bir otomatik test mühendisi kuruyorum.
İlerlememi şu şekilde paylaşıyorum: