Мне нужен был способ использовать Remix с Vite и Cloudflare Workers-Pages с минимальной настройкой.
Я видел и другие репозитории, такие как:
Но у них были некоторые ограничения:
Я не хотел выполнять предварительную сборку, так как не хотел отравлять репозитории дополнительными файлами конфигурации.
Cloudflare Workers/Pages имеет другую цель. Стало сложно нацеливаться на нее с помощью tsup, поскольку такие пакеты, как Postgres, вытягивали зависимости узлов, ломающиеся при импорте в Remix.
Мне также нужен был способ использования различных целей (Remix-Cloudflare, Node/Bun)
Тем не менее, я благодарен им, поскольку они проложили путь к тому, чтобы это стало возможным!
Обязательно прочитайте раздел «Подводные камни» внизу!
Я создаю автоматизированную платформу тестирования для публичного использования, чтобы выявлять эти 1% ошибок в производстве.
Я делюсь своим прогрессом по следующим темам:
Полную версию реализации можно посмотреть здесь .
Хотя это проведет вас через новый монорепозиторий, вполне допустимо преобразовать в него уже существующий.
Также предполагается, что у вас есть некоторые знания о Mono Repo.
Примечание:
libs
и packages
.Turborepo работает поверх рабочих пространств вашего менеджера пакетов для управления скриптами и выходами вашего проекта (он даже может кэшировать ваш выход). Пока что это единственный инструмент для монорепозитория, помимо Rush (который я не пробовал и который мне не нравится), который способен работать.
NX не поддерживает Vite от Remix (на момент написания статьи — 28 августа 2024 г.).
pnpm dlx create-turbo@latest
Мы будем использовать возможности рабочего пространства PNPM для управления зависимостями.
В каталоге 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": "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 сообщает репозиторию turbo, как интерпретировать наши команды. Все, что находится внутри ключа tasks
будет соответствовать тому, что находится в файле all package.json.
Обратите внимание, что мы определяем четыре команды. Они соответствуют командам в разделе скриптов package.json каждого репозитория. Однако не все package.json должны реализовывать эти команды.
Например: команда dev
будет вызвана turbo dev
, и она выполнит все пакеты, которые dev
найдены в package.json. Если вы не включите его в turbo, он не выполнится.
apps
в корне проекта. mkdir apps
apps
(или переместите существующее) npx create-remix --template edmundhung/remix-worker-template
Когда вас попросят Install any dependencies with npm
ответьте «нет».
name
package.json в @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
в package.json
корневого каталога.Например:
<корень>/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" } }
Проверьте, что turbo находится внутри package.json's devDependencies. Если его нет в списке, выполните следующую команду:
pnpm add turbo -D -w
Флаг -w
сообщает pnpm о необходимости установки в корне рабочей области.
Добавьте команду dev
в scripts
Добавьте 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" } } }
Примечания:
Я начинаю презирать ORM. Я потратил более 10 лет на изучение 6 разных, и эти знания невозможно передать.
У вас проблемы, когда выходят новые технологии. Prisma не поддерживает Cloudflare worker'ов из коробки.
Благодаря LLM писать сложные SQL-запросы стало проще, чем когда-либо.
Изучение SQL — универсального языка, и вряд ли что-то изменится.
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 о необходимости исключить репозиторий конфигурации.!
перед именем пакета. Если pnpm извлекает пакеты из репозитория, мы можем создать файл .npmrc
в корне проекта.
.npmrc
link-workspace-packages= true prefer-workspace-packages=true
tsconfig.json
внутри Libs/ConfigИспользуя возможности рабочих пространств pnpm, вы можете создавать файлы конфигурации, которые можно совместно использовать в разных проектах.
Мы создадим базовый файл tsconfig.lib.json, который будем использовать для наших библиотек.
Внутри libs/config
создайте экземпляр tsconfig.lib.json
:
touch "libs/config/tsconfig.base.lib.json"
Затем добавьте следующее:
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, });
Файл схемы:
// 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" } }
tsconfig.json
для libs/db
и libs/utils
Создайте tsconfig.json для libs/db
и libs/utils
touch "libs/db/tsconfig.json" "libs/utils/tsconfig.json"
Затем добавьте к каждому:
{ "extends": "@repo/configs/tsconfig.base.lib.json", "include": ["./src"], }
@repo/configs
используется в качестве пути для ссылки на наш tsconfig.base.lib.json.TypeScript Execute (TSX) — это библиотека, альтернативная ts-node. Мы будем использовать ее для выполнения миграций drizzle.
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
в наш проект ремиксаИз корня проекта запустите:
pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app
Если это не сработает, перейдите в package.json apps/my-remix-cloudflare-app
и добавьте зависимость вручную.
{ "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "dependencies": { "@repo/db": "workspace:*" } }
Обратите внимание workspace:*
в поле версии. Это говорит pnpm использовать любую версию пакета в workspace.
Если вы установили его через CLI с помощью pnpm add,
вы, вероятно, увидите что-то вроде workspace:^
. Это не должно иметь значения, если вы не увеличите версии локальных пакетов.
Если вы добавили это вручную, то запустите pnpm install
из корня проекта.
Мы должны иметь возможность использовать @repo/db в нашем проекте.
Добавьте этот код в файл 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
Если у вас нет запущенного экземпляра Postgres, мы можем запустить его с помощью docker-compose. Обратите внимание, я предполагаю, что вы знаете Docker.
Создайте файл 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 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
Затем вы можете запустить:
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} />; }
apps/my-remix-cloudflare-app/.dev.vars
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
Запустить экземпляр postgres (если не готов)
docker-compose up -d
Запустить проект
pnpm turbo dev
В своих проектах я, как правило, реализую шаблон CQRS , 2. Это выходит за рамки данного руководства.
Тем не менее, в контексте загрузки я стараюсь внедрять посредника (и флэш-сообщение cookie), который отделит все мое приложение Remix от моей бизнес-логики.
Это выглядит примерно так:
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, }; };
Обратите внимание, что код отправки опущен. Вы можете узнать больше об этом в моей статье о том, как в 10 раз улучшить свой опыт разработки TypeScript здесь .
Я могу удалить Remix или использовать другой потребитель, не изменяя свой код.
Но….
При работе в структуре монорепозитория с использованием турборепозитория возникают дополнительные трудности.
Если вы импортируете файл TypeScript из пакета в контексте загрузки, скажем @repo/db
Vite вернет ошибку, что файл с расширением .ts
неизвестен, и не будет знать, как его обработать.
Это происходит из-за того, что load-context + workspaces находятся за пределами основного графика импорта сайта, в результате чего файлы TypeScript остаются вне воспроизведения.
Хитрость заключается в том, чтобы использовать tsx
и загрузить его перед вызовом Vite, что сработает. Это важно, поскольку это преодолевает следующие ограничения:
Зависимости пакетов Cloudflare.
Зависимости пакетов Cloudflare и предварительная сборка
Прежде всего, это был тот шаг, которого я пытался избежать, поскольку это означало, что мне пришлось бы ввести шаг сборки для каждого из пакетов, что означало бы дополнительную настройку.
К счастью, это не сработало для Cloudflare Pages. Определенные библиотеки, такие как Postgres, обнаружат среду выполнения и вытащат требуемый пакет.
Есть обходной путь: мы можем использовать tsx для загрузки всех файлов TypeScript и транспилировать их перед выполнением.
Вы можете утверждать, что это этап предварительной сборки, но поскольку он все еще находится на уровне репозитория ремикса, я не вижу существенных проблем с этим подходом.
Чтобы решить эту проблему, мы добавляем tsx как зависимость:
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
в корне проекта.
.npmrc
link-workspace-packages= true prefer-workspace-packages=true
Это сообщит pnpm о необходимости сначала использовать пакеты рабочего пространства.
Спасибо ZoWnx из Reddit, который помог мне создать файл .nprmc
Будьте осторожны с именами .client
и .server
в ваших файлах. Даже если это отдельная библиотека. Remix использует их, чтобы определить, является ли это файлом клиента или сервера. Проект не компилируется для каждого репозитория, поэтому он выдаст ошибку импорта!
Если у вас возникли проблемы с многоплатформенными пакетами, такими как Postgres, лучше установить его на уровне рабочей области. Он обнаружит правильный импорт. Установка его напрямую в репозиторий @repo/db сломает его при импорте в Remix.
Вот и всё, ребята!!!
Полную версию реализации можно посмотреть здесь .
Я создаю автоматизированного инженера по тестированию для общественности, чтобы выявлять эти 1% ошибок в производстве.
Я делюсь своим прогрессом по следующим темам: