Assurez-vous de lire la section des pièges en bas !
Note:
libs
et packages
.
pnpm dlx create-turbo@latest
Dans votre répertoire Monorepo, créez un pnpm-workspace.yaml
.
À l'intérieur, ajoutez :
packages: - "apps/*" - "libs/*"
Cela indiquera à pnpm que tous les dépôts se trouveront dans apps
et libs
. Notez que l'utilisation libs
ou packages
(comme vous l'avez peut-être vu ailleurs) n'a pas d'importance.
pnpm init
{ "name": "@repo/main", "version": "1.0.0", "scripts": {}, "keywords": [], "author": "", "license": "ISC" }
Notez le name:@repo/main
Cela nous indique qu'il s'agit de l'entrée principale de l'application. Vous n'avez pas besoin de suivre une convention particulière ou d'utiliser le préfixe @
. Les gens l'utilisent pour le différencier des packages locaux/distants ou pour faciliter son regroupement dans une organisation.
turbo.json
à la racine du projet : { "$schema": "//turbo.build/schema.json", "tasks": { "build": {}, "dev": { "cache": false, "persistent": true }, "start": { "dependsOn": ["^build"], "persistent": true }, "preview": { "cache": false, "persistent": true }, "db:migrate": {} } }
Le fichier turbo.json indique au référentiel turbo comment interpréter nos commandes. Tout ce qui se trouve à l'intérieur de la clé tasks
correspondra à ceux trouvés dans le package all.json.
Par exemple : la commande dev
sera déclenchée par turbo dev
et exécutera tous les packages dont dev
se trouve dans package.json. Si vous ne l'incluez pas dans turbo, elle ne s'exécutera pas.
apps
à la racine du projet mkdir apps
apps
(ou déplacez-en une existante) npx create-remix --template edmundhung/remix-worker-template
Lorsqu'il vous demande d' Install any dependencies with npm
dites non.
name
du package.json en @repo/my-remix-cloudflare-app
(ou votre nom) { - "name": "my-remix-cloudflare-app", + "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "keywords": [], "author": "", "license": "ISC" }
apps/<app>/package.json
vers le package.json
de la racine { "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
L'indicateur -w
indique à pnpm de l'installer à la racine de l'espace de travail.
Ajoutez la commande dev
aux scripts
Ajoutez le packageManager
à l'option
{ "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
pnpm dev
mkdir -p libs/config libs/db libs/utils
src/index.ts
pour chacun des packages. touch libs/config/src/index.ts libs/db/src/index.ts libs/utils/src/index.ts
libs/config/package.json
: { "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" } } }
Remarques :
pnpm add drizzle-orm drizle-kit --filter=@repo/db
Installez Postgres au niveau de l'espace de travail. Voir la section Piège .
pnma add postgres -w
Remarques :
--filter=@repo/db
indique à pnpm d'ajouter le package au référentiel db. pnpm add dotenv -w
Remarques-w
indique à pnpm de l'installer dans le package.json de la racine pnpm add @repo/config -r --filter=!@repo/config
Remarques :
-r
indique à pnpm d'ajouter le package à tous les référentiels.--filter=!
indique à pnpm d'exclure le référentiel de configuration.!
avant le nom du package Si pnpm extrait les packages du référentiel, nous pouvons créer un fichier .npmrc
à la racine du projet.
link-workspace-packages= true prefer-workspace-packages=true
tsconfig.json
partagé dans Libs/Config
Dans libs/config
instanciez un 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();
Ceci devrait être exécuté après les migrations
Et exportez le client et le schéma dans le fichier src/index.ts
. D'autres sont exécutés à des moments précis.
// libs/db/src/index.ts export * from "./drizzle/drizzle-client"; export * from "./drizzle/schema "
Dans votre package.json
, ajoutez le drizzle-kit generate
et le code pour exécuter la commande de migration :
{ "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
partagé pour libs/db
et libs/utils
Créez un tsconfig.json pour libs/db
et libs/utils
touch "libs/db/tsconfig.json" "libs/utils/tsconfig.json"
{ "extends": "@repo/configs/tsconfig.base.lib.json", "include": ["./src"], }
@repo/configs
est utilisé comme chemin pour faire référence à notre tsconfig.base.lib.json. pnpm add tsx -D --filter=@repo/db
libs/db
touch "libs/db/.env"
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" NODE_ENV="development" MODE="node"
libs/db
à notre projet de remix pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app
Si cela ne fonctionne pas, accédez au package.json de apps/my-remix-cloudflare-app
et ajoutez la dépendance manuellement.
{ "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "dependencies": { "@repo/db": "workspace:*" } }
Notez l' workspace:*
dans le champ version. Cela indique à pnpm d'utiliser n'importe quelle version du package dans l'espace de travail.
Si vous l'avez installé via la CLI en utilisant pnpm add,
vous verrez probablement quelque chose comme workspace:^
. Cela ne devrait pas avoir d'importance tant que vous n'augmentez pas les versions des packages locaux.
Si vous l'avez ajouté manuellement, exécutez pnpm install
depuis la racine du projet.
Ajoutez ce code au fichier libs/utils/src/index.ts
:
// libs/utils/src/index.ts export function hellowWorld() { return "Hello World!"; }
pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app
Créez un fichier docker-compose.yml
à la racine du projet.
# 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
L'indicateur -d
indique à docker-compose de s'exécuter séparément afin que vous puissiez à nouveau accéder à votre terminal.
Accédez maintenant au référentiel libs/db et exécutez db:generate
.
cd `./libs/db` && pnpm db:generate
db:generate
est un alias pour : drizzle-kit generate
Accédez au référentiel libs/db (si vous n'y êtes pas) et exécutez db:generate
.
cd `./libs/db` && pnpm db:migrate
db:migrate
est un alias pour : dotenv tsx ./drizzle/migrate
// 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, }; };
Notez que le code de répartition est omis. Vous pouvez en savoir plus à ce sujet dans mon article sur la façon de décupler votre expérience de développement TypeScript ici .
Si vous importez un fichier TypeScript à partir d'un package dans le contexte de chargement, disons que @repo/db
Vite renverra une erreur indiquant que le fichier avec l'extension .ts
est inconnu et ne saura pas comment le traiter.
L'astuce consiste à utiliser tsx
et à le charger avant d'appeler Vite, ce qui fonctionnera. Ceci est important car cela permet de surmonter les limitations suivantes :
Dépendances et pré-construction des packages Cloudflare
Tout d’abord, c’était l’étape que j’essayais d’éviter, car cela signifiait que je devais introduire une étape de construction pour chacun des packages, ce qui impliquait plus de configuration.
pnpm add tsx -D --filter=@repo/my-remix-cloudflare-app
Et puis, nous devons modifier notre package.json
et ajouter le processus tsx à chacun de nos scripts de remix :
{ "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
Si vous rencontrez des problèmes lors de l'ajout de vos packages locaux avec la ligne de commande, vous pouvez créer un fichier .npmrc
à la racine du projet.
link-workspace-packages= true prefer-workspace-packages=true
Cela indiquera à pnpm d’utiliser d’abord les packages de l’espace de travail.
Soyez prudent avec les noms de .client
et .server
dans vos fichiers. Même s'ils se trouvent dans une bibliothèque distincte. Remix les utilise pour déterminer s'il s'agit d'un fichier client ou serveur. Le projet n'est pas compilé par référentiel, il génère donc une erreur d'importation !
C'est tout, les amis !!!