¡Asegúrate de leer la sección de peligros al final!
Nota:
libs
y packages
.
pnpm dlx create-turbo@latest
En su directorio Monorepo, cree un pnpm-workspace.yaml
.
Dentro de ella, añade:
packages: - "apps/*" - "libs/*"
Esto le indicará a pnpm que todos los repositorios se encontrarán dentro de apps
y libs
. Tenga en cuenta que no importa si se utilizan libs
o packages
(como puede haber visto en otro lugar).
pnpm init
{ "name": "@repo/main", "version": "1.0.0", "scripts": {}, "keywords": [], "author": "", "license": "ISC" }
Tenga en cuenta el name:@repo/main
Esto nos indica que esta es la entrada principal de la aplicación. No es necesario seguir una convención en particular ni utilizar el prefijo @
. La gente lo utiliza para diferenciarlo de los paquetes locales o remotos o para facilitar su agrupación en una organización.
turbo.json
en la raíz del proyecto: { "$schema": "//turbo.build/schema.json", "tasks": { "build": {}, "dev": { "cache": false, "persistent": true }, "start": { "dependsOn": ["^build"], "persistent": true }, "preview": { "cache": false, "persistent": true }, "db:migrate": {} } }
El archivo turbo.json le indica al repositorio de Turbo cómo interpretar nuestros comandos. Todo lo que se encuentre dentro de la clave tasks
coincidirá con lo que se encuentre en el archivo package.json.
Por ejemplo: el comando dev
se activará con turbo dev
y ejecutará todos los paquetes que dev
encuentren en package.json. Si no lo incluye en turbo, no se ejecutará.
apps
en la raíz del proyecto mkdir apps
apps
(o mueve una existente) npx create-remix --template edmundhung/remix-worker-template
Cuando le pida que Install any dependencies with npm
responda no.
name
del paquete.json a @repo/my-remix-cloudflare-app
(o su nombre) { - "name": "my-remix-cloudflare-app", + "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "keywords": [], "author": "", "license": "ISC" }
apps/<app>/package.json
al package.json
de la raíz { "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
El indicador -w
le indica a pnpm que lo instale en la raíz del espacio de trabajo.
Agregar el comando dev
a scripts
Agregue el packageManager
a la opción
{ "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
para cada uno de los paquetes. 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" } } }
Notas:
pnpm add drizzle-orm drizle-kit --filter=@repo/db
Instalar Postgres en el nivel del espacio de trabajo. Consulta la sección Problemas .
pnma add postgres -w
Notas:
--filter=@repo/db
le indica a pnpm que agregue el paquete al repositorio de base de datos. pnpm add dotenv -w
Notas-w
le dice a pnpm que lo instale en el paquete package.json de la raíz pnpm add @repo/config -r --filter=!@repo/config
Notas :
-r
le dice a pnpm que agregue el paquete a todos los repositorios.--filter=!
le indica a pnpm que excluya el repositorio de configuración.!
antes del nombre del paquete Si pnpm extrae los paquetes del repositorio, podemos crear un archivo .npmrc
en la raíz del proyecto.
link-workspace-packages= true prefer-workspace-packages=true
tsconfig.json
compartido dentro de Libs/Config
Dentro de libs/config
crea una instancia de 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();
Esto debe ejecutarse después de las migraciones.
Y exporta el cliente y el esquema en el archivo src/index.ts
. Otros se ejecutan en momentos específicos.
// libs/db/src/index.ts export * from "./drizzle/drizzle-client"; export * from "./drizzle/schema "
En su package.json
, agregue drizzle-kit generate
y el código para ejecutar el comando de migración:
{ "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
compartido para libs/db
y libs/utils
Cree un tsconfig.json para libs/db
y libs/utils
touch "libs/db/tsconfig.json" "libs/utils/tsconfig.json"
{ "extends": "@repo/configs/tsconfig.base.lib.json", "include": ["./src"], }
@repo/configs
se utiliza como ruta para hacer referencia a nuestro 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
a nuestro proyecto Remix pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app
Si esto no funciona, vaya al paquete json de apps/my-remix-cloudflare-app
y agregue la dependencia manualmente.
{ "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "dependencies": { "@repo/db": "workspace:*" } }
Tenga en cuenta el workspace:*
en el campo de versión. Esto le indica a pnpm que use cualquier versión del paquete en el espacio de trabajo.
Si lo instalaste a través de la CLI usando pnpm add,
probablemente verás algo como workspace:^
. No debería importar siempre y cuando no aumentes las versiones de los paquetes locales.
Si lo agregó manualmente, ejecute pnpm install
desde la raíz del proyecto.
Agregue este código al archivo 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
Cree un archivo docker-compose.yml
en la raíz del proyecto.
# 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
El indicador -d
le indica a docker-compose que se ejecute de forma separada para que pueda tener acceso a su terminal nuevamente.
Ahora, navegue al repositorio libs/db y ejecute db:generate
.
cd `./libs/db` && pnpm db:generate
db:generate
es un alias para: drizzle-kit generate
Navega al repositorio libs/db (si no estás allí) y ejecuta db:generate
.
cd `./libs/db` && pnpm db:migrate
db:migrate
es un alias para: 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, }; };
Tenga en cuenta que se omite el código de envío. Puede encontrar más información al respecto en mi artículo sobre cómo multiplicar por diez su experiencia de desarrollo con TypeScript aquí .
Si importa un archivo TypeScript desde un paquete dentro del contexto de carga, digamos @repo/db
Vite devolverá un error indicando que el archivo con extensión .ts
es desconocido y no sabrá cómo procesarlo.
El truco es usar tsx
y cargarlo antes de llamar a Vite, lo que funcionará. Esto es importante porque supera las siguientes limitaciones:
Dependencias de paquetes de Cloudflare y compilación previa
En primer lugar, ese era el paso que estaba tratando de evitar, ya que significaba que tenía que introducir un paso de compilación para cada uno de los paquetes, lo que implicaba más configuración.
pnpm add tsx -D --filter=@repo/my-remix-cloudflare-app
Y luego, necesitamos modificar nuestro package.json
y agregar el proceso tsx a cada uno de nuestros 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
En caso de que tenga problemas al agregar sus paquetes locales con la línea de comando, puede crear un archivo .npmrc
en la raíz del proyecto.
link-workspace-packages= true prefer-workspace-packages=true
Esto le indicará a pnpm que utilice primero los paquetes del espacio de trabajo.
Tenga cuidado al nombrar .client
y .server
en sus archivos, incluso si están en una biblioteca separada. Remix los usa para determinar si es un archivo de cliente o de servidor. El proyecto no se compila por repositorio, por lo que generará un error de importación.
¡¡¡Eso es todo, amigos!!!