꼭 마지막 부분의 함정 섹션을 읽어보세요!
메모:
libs
및 packages
디렉터리 외부에 있을 것입니다.
pnpm dlx create-turbo@latest
Monorepo 디렉토리에서 pnpm-workspace.yaml
만듭니다.
그 안에 다음을 추가하세요.
packages: - "apps/*" - "libs/*"
이렇게 하면 pnpm에 모든 저장소가 apps
와 libs
내부에 있을 것이라고 알려줍니다. libs
나 packages
사용하는 것은 중요하지 않습니다(다른 곳에서 보셨을 수도 있지만).
pnpm init
{ "name": "@repo/main", "version": "1.0.0", "scripts": {}, "keywords": [], "author": "", "license": "ISC" }
name:@repo/main
이것은 이것이 애플리케이션의 주요 항목임을 알려줍니다. 특정 규칙을 따르거나 @
접두사를 사용할 필요가 없습니다. 사람들은 로컬/원격 패키지와 구별하거나 조직으로 그룹화하기 쉽게 하기 위해 이를 사용합니다.
turbo.json
파일을 만듭니다. { "$schema": "//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 파일은 turbo repo에 명령을 해석하는 방법을 알려줍니다. tasks
키 안에 있는 모든 것은 all package.json에서 발견된 것과 일치합니다.
예: dev
명령은 turbo dev
에 의해 트리거되고 package.json에서 dev
가 발견된 모든 패키지를 실행합니다. turbo에 포함하지 않으면 실행되지 않습니다.
apps
폴더를 만듭니다. mkdir apps
apps
폴더에서 리믹스 앱 만들기(또는 기존 앱 이동) npx create-remix --template edmundhung/remix-worker-template
Install any dependencies with npm
하라고 하면 '아니요'를 선택하세요.
name
@repo/my-remix-cloudflare-app
(또는 사용자 이름)으로 변경합니다. { - "name": "my-remix-cloudflare-app", + "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "keywords": [], "author": "", "license": "ISC" }
apps/<app>/package.json
에서 Root의 package.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" } }
pnpm add turbo -D -w
-w
플래그는 pnpm에게 작업 공간 루트에 설치하라고 알려줍니다.
scripts
에 dev
명령 추가
옵션에 packageManager
추가합니다
{ "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
추가합니다. 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" } } }
참고사항:
pnpm add drizzle-orm drizle-kit --filter=@repo/db
작업공간 레벨에서 Postgres를 설치합니다. 함정 섹션을 참조하세요 .
pnma add postgres -w
참고사항:
--filter=@repo/db
플래그는 pnpm에게 패키지를 db 저장소에 추가하라고 알려줍니다. pnpm add dotenv -w
노트-w
플래그는 pnpm에게 루트의 package.json에 설치하라고 지시합니다. pnpm add @repo/config -r --filter=!@repo/config
참고사항 :
-r
플래그는 pnpm에게 모든 저장소에 패키지를 추가하라고 알려줍니다.--filter=!
플래그는 pnpm에게 config 저장소를 제외하라고 지시합니다.!
를 주의하세요. pnpm이 저장소에서 패키지를 끌어오는 경우 프로젝트 루트에 .npmrc
파일을 만들 수 있습니다.
link-workspace-packages= true prefer-workspace-packages=true
tsconfig.json
구성
libs/config
내부에서 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();
이것은 마이그레이션 후에 실행되어야 합니다.
그리고 src/index.ts
파일에 클라이언트와 스키마를 내보냅니다. 다른 것들은 특정 시간에 실행됩니다.
// libs/db/src/index.ts export * from "./drizzle/drizzle-client"; export * from "./drizzle/schema "
package.json
에 drizzle-kit generate
와 마이그레이션 명령을 실행하는 코드를 추가합니다.
{ "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
및 libs/utils
에 대해 공유 tsconfig.json
사용합니다. libs/db
및 libs/utils
에 대한 tsconfig.json을 만듭니다.
touch "libs/db/tsconfig.json" "libs/utils/tsconfig.json"
{ "extends": "@repo/configs/tsconfig.base.lib.json", "include": ["./src"], }
@repo/configs
tsconfig.base.lib.json을 참조하는 경로로 사용되는지 확인합니다. pnpm add tsx -D --filter=@repo/db
libs/db
디렉토리에 빈 .env 추가 touch "libs/db/.env"
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" NODE_ENV="development" MODE="node"
libs/db
저장소 추가 pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app
여기서도 작동하지 않는다면 apps/my-remix-cloudflare-app
의 package.json으로 가서 종속성을 수동으로 추가하세요.
{ "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "dependencies": { "@repo/db": "workspace:*" } }
버전 필드의 workspace:*
에 주의하세요. 이것은 pnpm이 작업공간에서 패키지의 모든 버전을 사용하도록 지시합니다.
pnpm add,
아마도 workspace:^
와 비슷한 것을 볼 수 있을 것입니다. 로컬 패키지 버전을 늘리지 않는 한 문제가 되지 않습니다.
수동으로 추가한 경우 프로젝트 루트에서 pnpm install
실행하세요.
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
프로젝트 루트에 docker-compose.yml
파일을 만듭니다.
# 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
-d
플래그는 docker-compose가 분리된 상태로 실행되도록 하여 터미널에 다시 액세스할 수 있도록 합니다.
이제 libs/db 저장소로 이동하여 db:generate
실행하세요.
cd `./libs/db` && pnpm db:generate
db:generate
drizzle-kit generate
의 별칭입니다.
libs/db 저장소로 이동합니다(없는 경우). 그리고 db:generate
실행합니다.
cd `./libs/db` && pnpm db:migrate
db:migrate
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, }; };
디스패치 코드는 생략된 점에 유의하세요. 자세한 내용은 TypeScript 개발 경험을 10배 높이는 방법에 대한 제 기사 에서 확인할 수 있습니다.
로드 컨텍스트 내에서 패키지에서 TypeScript 파일을 가져오는 경우, 예를 들어 @repo/db
Vite는 확장자가 .ts
인 파일을 알 수 없다는 오류를 반환하고, 해당 파일을 어떻게 처리해야 할지 알 수 없습니다.
비결은 tsx
사용하여 Vite를 호출하기 전에 로드하는 것입니다. 이렇게 하면 작동합니다. 이는 다음과 같은 제한을 극복하기 때문에 중요합니다.
Cloudflare 패키지 종속성 및 사전 빌드
우선, 이 단계는 제가 피하려고 했던 단계였습니다. 이 단계를 수행하려면 각 패키지에 대한 빌드 단계를 도입해야 하며, 이는 더 많은 구성을 필요로 하기 때문입니다.
pnpm add tsx -D --filter=@repo/my-remix-cloudflare-app
그런 다음 package.json
수정하고 각 리믹스 스크립트에 tsx 프로세스를 추가해야 합니다.
{ "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
파일 생성 명령줄로 로컬 패키지를 추가하는 동안 문제가 발생하는 경우 프로젝트 루트에 .npmrc
파일을 만들 수 있습니다.
link-workspace-packages= true prefer-workspace-packages=true
이렇게 하면 pnpm이 작업 공간 패키지를 먼저 사용하게 됩니다.
파일에서 .client
와 .server
이름을 지정할 때는 조심하세요. 별도의 라이브러리에 있더라도요. Remix는 이를 사용하여 클라이언트 파일인지 서버 파일인지 판별합니다. 프로젝트는 리포지토리별로 컴파일되지 않으므로 가져오기 오류가 발생합니다!
그게 다예요, 여러분!!!