Introdução
Eu precisava de uma maneira de usar o Remix com o Vite e o Cloudflare Workers-Pages com configuração mínima.
Vi outros repositórios, como:
- A pilha do evangelho ,
- Garota21 ,
- Himorishige .
Mas eles tinham algumas limitações:
Eu não queria pré-construí-lo, pois não queria envenenar os repositórios com mais arquivos de configuração.
Cloudflare Workers/Pages tem um alvo diferente. Tornou-se complicado direcioná-lo com tsup, pois pacotes como Postgres puxariam as dependências do nó quebrando quando importados para o Remix.
Eu também precisava de uma maneira de consumir diferentes alvos (Remix-Cloudflare, Node/Bun)
Mesmo assim, agradeço a eles por terem aberto o caminho para tornar isso possível!
Não deixe de ler a seção sobre armadilhas no final!
Siga-me nas redes sociais!
Estou criando uma plataforma de testes automatizados em público para detectar esses erros de 1% na produção.
Compartilho meu progresso em:
Repositório GitHub
Você pode acessar a implementação completa aqui .
Passo a passo
Requisitos
- NodeJS
- PNPM
- Docker (Opcional - Para exemplo de banco de dados local)
Embora isso o oriente por um novo mono-repositório, é perfeitamente válido transformar um existente em um.
Isso também pressupõe que você tenha algum conhecimento sobre repositórios mono.
Observação:
- “at root” refere-se ao caminho inicial do seu monorepositório. Para este projeto, ele estará fora dos diretórios
libs
epackages
.
Instalar Turborepo
O Turborepo funciona em cima dos espaços de trabalho do seu gerenciador de pacotes para gerenciar os scripts e saídas do seu projeto (ele pode até mesmo armazenar em cache sua saída). Até agora, é a única ferramenta mono-repo além do Rush (que eu não testei e não gosto) que é capaz de funcionar.
O NX não tem suporte ao Vite do Remix (até o momento em que este artigo foi escrito - 28 de agosto de 2024).
pnpm dlx create-turbo@latest
1. Configurar espaços de trabalho do PNPM
Usaremos os recursos do espaço de trabalho do PNPM para gerenciar dependências.
No seu diretório Monorepo, crie um pnpm-workspace.yaml
.
Dentro dele, adicione:
packages: - "apps/*" - "libs/*"
Isso dirá ao pnpm que todos os repositórios ficarão dentro de apps
e libs
. Note que usar libs
ou packages
(como você pode ter visto em outro lugar) não importa.
2. Crie um package.json vazio na raiz do projeto:
pnpm init
{ "name": "@repo/main", "version": "1.0.0", "scripts": {}, "keywords": [], "author": "", "license": "ISC" }
Observe o name:@repo/main
Isso nos diz que esta é a entrada principal do aplicativo. Você não precisa seguir uma convenção específica ou usar o prefixo @
. As pessoas o usam para diferenciá-lo de pacotes locais/remotos ou para facilitar o agrupamento em uma organização.
3. Crie o arquivo turbo.json
na raiz do projeto:
{ "$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": {} } }
O arquivo turbo.json diz ao repositório turbo como interpretar nossos comandos. Tudo o que estiver dentro da chave tasks
corresponderá àqueles encontrados no pacote all.json.
Note que definimos quatro comandos. Eles correspondem aos da seção script do package.json de cada repositório. No entanto, nem todos os package.json devem implementar esses comandos.
Ex.: O comando dev
será acionado pelo turbo dev
, e executará todos os pacotes que dev
for encontrado dentro do package.json. Se você não incluí-lo no turbo, ele não será executado.
4. Crie uma pasta apps
na raiz do projeto
mkdir apps
5. Crie um aplicativo Remix na pasta apps
(ou mova um existente)
npx create-remix --template edmundhung/remix-worker-template
Quando for solicitado que você Install any dependencies with npm
diga não.
6. Renomeie o name
do package.json para @repo/my-remix-cloudflare-app
(ou seu nome)
{ - "name": "my-remix-cloudflare-app", + "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "keywords": [], "author": "", "license": "ISC" }
7. Copie as dependências e devDependencies de apps/<app>/package.json
para o package.json
do Root
Por exemplo:
<root>/pacote.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" } }
8. Adicione o Turbo como uma devDependency no nível raiz (isso instalará todos os pacotes do Remix).
Verifique se o turbo está dentro do devDependencies do package.json. Se não estiver listado, execute o seguinte comando:
pnpm add turbo -D -w
O sinalizador -w
informa ao pnpm para instalá-lo na raiz do espaço de trabalho.
9. Adicione as seguintes entradas ao pacote Root.json
Adicione o comando
dev
aosscripts
Adicione o
packageManager
à opção
{ "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 } }
10. Verifique se tudo está funcionando executando pnpm dev
pnpm dev
11. Crie uma pasta Libs na raiz do projeto. Adicione config, db e utils:
mkdir -p libs/config libs/db libs/utils
12. Adicione um src/index.ts
para cada um dos pacotes.
touch libs/config/src/index.ts libs/db/src/index.ts libs/utils/src/index.ts
- O arquivo index.ts será usado para exportar todos os pacotes.
- Usaremos a pasta como ponto de entrada para deixar tudo compacto.
- Esta é uma convenção e você pode optar por não segui-la.
13. Crie um package.json vazio e adicione o seguinte ao arquivo libs/config/package.json
:
{ "name": "@repo/config", "version": "1.0.0", "type": "module", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } }
14. Faça o mesmo para libs/db/package.json
:
{ "name": "@repo/db", "version": "1.0.0", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } }
15. E libs/utils/package.json
:
{ "name": "@repo/utils", "version": "1.0.0", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } }
- Especificamos o campo “exports”. Isso informa aos outros repositórios de onde importar o pacote.
- Especificamos o campo “name”. Ele é usado para instalar o pacote e referenciá-lo a outros repositórios.
16. (DB) - Adicione Drizzle e Postgres
Notas:
Estou começando a desprezar ORMs. Passei mais de 10 anos aprendendo 6 diferentes, e é um conhecimento que você não pode transferir.
Você tem problemas quando novas tecnologias surgem. O Prisma não suporta os trabalhadores do Cloudflare de fábrica.
Com LLMs, é mais fácil do que nunca escrever consultas SQL complexas.
Aprender SQL é uma linguagem universal e provavelmente não mudará.
pnpm add drizzle-orm drizle-kit --filter=@repo/db
Instale o Postgres no nível do espaço de trabalho. Veja a seção Pitfall .
pnma add postgres -w
Notas:
- O sinalizador
--filter=@repo/db
informa ao pnpm para adicionar o pacote ao repositório db.
17. Adicione o dotenv ao Repositório do Espaço de Trabalho
pnpm add dotenv -w
Notas
- O sinalizador
-w
informa ao pnpm para instalá-lo no pacote root.json
18. Adicione o Projeto de Configuração a Todos os Projetos.
pnpm add @repo/config -r --filter=!@repo/config
Notas :
- O sinalizador
-r
informa ao pnpm para adicionar o pacote a todos os repositórios. - O sinalizador
--filter=!
informa ao pnpm para excluir o repositório de configuração. - Observe o
!
antes do nome do pacote
19. (Opcional) Os comandos acima não funcionam? Use .npmrc
Se o pnpm estiver puxando os pacotes do repositório, podemos criar um arquivo .npmrc
na raiz do projeto.
.npmrc
link-workspace-packages= true prefer-workspace-packages=true
- Isso dirá ao pnpm para usar os pacotes do espaço de trabalho primeiro.
- Obrigado ao ZoWnx do Reddit, que me ajudou a criar um arquivo .nprmc
20. Configure o tsconfig.json
compartilhado dentro de Libs/Config
Usando o poder dos espaços de trabalho do pnpm, você pode criar arquivos de configuração que podem ser compartilhados entre projetos.
Criaremos um tsconfig.lib.json base que usaremos para nossas bibliotecas.
Dentro de libs/config
instancie um tsconfig.lib.json
:
touch "libs/config/tsconfig.base.lib.json"
Em seguida, adicione o seguinte:
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, } }
21. Adicione a conexão db ao repositório db.
// 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, });
O arquivo de esquema:
// 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(), });
O arquivo do cliente:
// 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!; } }
O arquivo de migração:
// 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();
Isso deve ser executado após as migrações
E exporte o cliente e o esquema no arquivo src/index.ts
. Outros são executados em momentos específicos.
// libs/db/src/index.ts export * from "./drizzle/drizzle-client"; export * from "./drizzle/schema "
No seu package.json
, adicione o drizzle-kit generate
e o código para executar o comando de migração:
{ "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" } }
22. Use o tsconfig.json
compartilhado para libs/db
e libs/utils
Crie um tsconfig.json para libs/db
e libs/utils
touch "libs/db/tsconfig.json" "libs/utils/tsconfig.json"
Em seguida, adicione a cada um:
{ "extends": "@repo/configs/tsconfig.base.lib.json", "include": ["./src"], }
- Veja que
@repo/configs
é usado como o caminho para fazer referência ao nosso tsconfig.base.lib.json. - Ela torna nosso caminho limpo.
23. Instale o TSX
TypeScript Execute (TSX) é uma alternativa de biblioteca para ts-node. Usaremos isso para executar as migrações do drizzle.
pnpm add tsx -D --filter=@repo/db
24. Adicione um .env vazio no diretório libs/db
touch "libs/db/.env"
Adicione o seguinte conteúdo:
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" NODE_ENV="development" MODE="node"
25. Adicione o repositório libs/db
ao nosso projeto Remix
Na raiz do projeto, execute:
pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app
Se isso não funcionar, vá para o pacote apps/my-remix-cloudflare-app
e adicione a dependência manualmente.
{ "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "dependencies": { "@repo/db": "workspace:*" } }
Observe o workspace:*
no campo version. Isso diz ao pnpm para usar qualquer versão do pacote no workspace.
Se você instalou via CLI usando pnpm add,
provavelmente verá algo como workspace:^
. Não deve importar, desde que você não aumente as versões locais do pacote.
Se você adicionou isso manualmente, execute pnpm install
a partir da raiz do projeto.
Deveríamos conseguir consumir o @repo/db em nosso projeto.
26. Adicione algum código compartilhado aos nossos utilitários:
Adicione este código ao arquivo libs/utils/src/index.ts
:
// libs/utils/src/index.ts export function hellowWorld() { return "Hello World!"; }
27. Instale os Libs/Utils em nosso aplicativo Remix:
pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app
28. (Opcional) Inicie um Postgres a partir de um contêiner Docker
Se você não tiver uma instância do Postgres em execução, podemos iniciar uma usando docker-compose. Observe que estou assumindo que você conhece o Docker.
Crie um arquivo docker-compose.yml
na raiz do projeto.
# 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
Então você pode executar:
docker-compose up -d
O sinalizador -d
informa ao docker-compose para ser executado desanexado para que você possa ter acesso ao seu terminal novamente.
29. Gerar o esquema do BD
Agora, navegue até o repositório libs/db e execute db:generate
.
cd `./libs/db` && pnpm db:generate
- Observe que
db:generate
é um alias para:drizzle-kit generate
- Verifique se você tem o .env apropriado.
- Além disso, isso pressupõe que você tenha uma instância do Postgres em execução.
30. Execute as migrações.
Precisamos executar as migrações para estruturar todas as tabelas em nosso banco de dados.
Navegue até o repositório libs/db (se você não estiver lá) e execute db:generate
.
cd `./libs/db` && pnpm db:migrate
- Observe que
db:migrate
é um alias para:dotenv tsx ./drizzle/migrate
- Verifique se você tem o .env apropriado.
- Além disso, isso pressupõe que você tenha uma instância do Postgres em execução.
31. Insira uma chamada DB dentro do seu aplicativo Remix.
// 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} />; }
- Observe que não estamos seguindo as melhores práticas aqui.
- Eu aconselho você a não fazer nenhuma chamada de banco de dados diretamente no seu carregador, mas criar uma abstração que as chame.
- O Cloudflare é desafiador quando se trata de definir as variáveis de ambiente. Elas são passadas por solicitação
32. Adicione em seu .dev.vars o seguinte:
aplicativos/meu-remix-cloudflare-app/.dev.vars
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
33. Execute o Projeto Remix!
Inicie a instância do postgres (se não estiver pronta)
docker-compose up -d
Lançar o projeto
pnpm turbo dev
Caso de uso avançado - CQRS em GetLoadContext em Cloudflare Workers.
Em meus projetos, costumo implementar um padrão CQRS , 2. Isso está fora do escopo deste tutorial.
No entanto, dentro do contexto de carga, costumo injetar um mediador (e uma mensagem flash de cookie) que desacoplará todo o meu aplicativo Remix da minha lógica de negócios.
Isso se parece com algo assim:
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, }; };
Note que o código de despacho é omitido. Você pode encontrar mais sobre isso no meu artigo sobre como 10x sua experiência de desenvolvimento TypeScript aqui .
Posso remover o Remix ou usar outro consumidor sem alterar meu código.
Mas….
Há um desafio adicional quando você trabalha em uma estrutura monorepo usando turborepo.
Se você importar um arquivo TypeScript de um pacote dentro do load-context, digamos que @repo/db
Vite retornará um erro informando que o arquivo com extensão .ts
é desconhecido e não saberá como processá-lo.
Isso acontece porque load-context + workspaces estão fora do gráfico de importação principal do site, deixando os arquivos TypeScript fora do jogo.
O truque é usar tsx
e carregá-lo antes de chamar Vite, o que funcionará. Isso é importante porque supera as seguintes limitações:
Dependências do pacote Cloudflare.
Dependências do pacote Cloudflare e pré-construção
Primeiro, essa era a etapa que eu estava tentando evitar, pois significava que eu teria que introduzir uma etapa de compilação para cada um dos pacotes, o que significava mais configuração.
Felizmente, isso não funcionou para o Cloudflare Pages. Bibliotecas específicas, como Postgres, detectarão o tempo de execução e puxarão o pacote necessário.
Há uma solução alternativa: podemos usar o tsx para carregar todos os arquivos TypeScript e transpilá-los antes de executar.
Você pode argumentar que esta é uma etapa de pré-construção, mas como ainda está no nível do repositório do remix, não vejo problemas significativos com essa abordagem.
Para resolver isso, adicionamos tsx como uma dependência:
pnpm add tsx -D --filter=@repo/my-remix-cloudflare-app
E então, precisamos modificar nosso package.json
e adicionar o processo tsx a cada um dos nossos 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" } }
Extras
Criando um arquivo .npmrc
Caso esteja tendo problemas ao adicionar seus pacotes locais com a linha de comando, você pode criar um arquivo .npmrc
na raiz do projeto.
.npmrc
link-workspace-packages= true prefer-workspace-packages=true
Isso dirá ao pnpm para usar os pacotes do espaço de trabalho primeiro.
Obrigado ao ZoWnx do Reddit que me ajudou a criar um arquivo .nprmc
Armadilhas -
Cuidado ao nomear
.client
e.server
em seus arquivos. Mesmo se estiver em uma biblioteca separada. O Remix usa esses para determinar se é um arquivo cliente ou servidor. O projeto não é compilado por repositório, então ele lançará um erro de importação!
Se você estiver tendo problemas com pacotes multiplataforma como o Postgres, instalá-lo no nível do espaço de trabalho é melhor. Ele detectará a importação adequada. Instalá-lo diretamente no repositório @repo/db quebrará ao importá-lo para o Remix.
É isso aí, pessoal!!!
Repositório GitHub
Você pode acessar a implementação completa aqui .
Siga-me nas redes sociais!
Estou criando um engenheiro de testes automatizados em público para capturar esses erros de 1% na produção.
Compartilho meu progresso em: