·4 min läsning

Från eget repo till monorepo med pnpm, catalog och Vite+

I vintras byggde jag en MCP-server som exponerar film- och TV-data från TMDB och OMDB som tools åt Claude. Den har fått jobba hårt sedan dess. Det är ett av repona som agenterna betat av issues i under kvällsskiftena. Nu i somras ville jag bygga en sajt kring samma data, mest för skojs skull. Det uppenbara problemet dök upp direkt. Sajten skulle behöva exakt samma API-klienter, samma nyckelhantering och samma typer som MCP-servern redan hade.

Man hade kunnat kopiera koden rakt av, men vi låtsas att det aldrig var ett alternativ. Eller publicera ett paket på npm och versionsbumpa sig till vansinne. Men jag hade gått och velat labba med monorepos ett tag ändå. Vi kör det en del på jobbet, och jag ville testa den mer bleeding edge-varianten med pnpm workspaces och Vite+. Sajten blev ursäkten jag väntat på.

Själva flytten blev ingen git mv-övning. Jag lät en agent läsa igenom hela det gamla repot, ta fram en noggrann plan, och bygga upp monorepot från scratch vid sidan om, med gamla repot orört som facit. Samma arbetssätt som i Maintainer-posten, bara med större skopa.

Strukturen

Tre workspaces, ett delat datalager:

showtime/
├── packages/core @showtime/core : API-klienter, typer, formatters
├── apps/mcp @showtime/mcp : MCP-server
└── apps/web web : TanStack Start på Cloudflare Workers

Hela uppsättningen bor i en pnpm-workspace.yaml i roten:

packages:
- apps/*
- packages/*
catalog:
"@types/node": ^24
typescript: ^6
vitest: 4.1.9

packages-listan är själva monorepot: alla mappar som matchar globben blir workspaces. catalog: är den nyare finessen. Delade versionsnummer definieras en gång i roten, och varje workspace refererar dem med "catalog:" istället för ett eget versionsnummer:

"devDependencies": {
"@types/node": "catalog:",
"typescript": "catalog:"
}

Man slipper jaga vilken av tre package.json som drar in fel TypeScript-version. En rad i katalogen, alla följer med.

Interna paket utan npm

Att dela kod mellan workspaces visade sig kräva betydligt mindre ceremoni än jag trott. @showtime/core är ett helt vanligt paket med en exports-map:

{
"name": "@showtime/core",
"private": true,
"exports": {
".": "./src/index.ts",
"./tmdb": "./src/tmdb/index.ts",
"./omdb": "./src/omdb/index.ts"
}
}

Och apparna drar in det med workspace:* som version:

"dependencies": {
"@showtime/core": "workspace:*"
}

Det är allt. exports pekar rakt på TypeScript-källfilerna och workspace:* betyder “ta det som ligger i repot, alltid senaste”, så det behövs varken publicering, registry eller ett build-steg innan koden går att använda. Att skapa ett internt paket kostar ungefär en package.json, vilket gör att gränsdragningen kan handla om arkitektur istället för om man orkar med publiceringsmaskineriet.

Vad som bor i core

Gränsen satte sig ganska organiskt. I @showtime/core bor det som båda apparna behöver:

  • Klienterna. Enda koden som pratar med TMDB och OMDB. Nycklar, HTTP, retries.
  • Typerna. Hela vokabulären, TmdbMovieDetails och kompani.
  • Felklasserna. OmdbApiError med klassning i not_found kontra transienta fel. Definieras en gång, båda apparna fattar beslut på den.
  • Konstanterna. Genre-mappar, pagineringstak.

Det som inte delas är presentationen. MCP-servern använder cores formatters rakt av, eftersom en LLM vill ha text, medan webben formar samma råtyper till egna UI-shapes och har lager core inte vet något om: KV-cache, URL-parametrar, SEO. Rörläggningen och vokabulären delas, presentationen gör varje app själv. Det känns som rätt snitt än så länge.

Vite+ som toolchain

Ovanpå det kör repot Vite+, ett enda CLI (vp) som wrappar Vite, Rolldown, Vitest, tsdown, Oxlint och Oxfmt. Varje workspace slipper sin egen soppa av lint-, format- och testconfig. Allt går genom samma kommando:

Terminal
vp check # format + lint + typecheck, hela repot
vp run -r test # tester i varje workspace
vp run ready # allt ovan + build, innan man är klar

Jag har inte hunnit reta mig på något ännu, vilket för lint-verktyg får räknas som högsta betyg.

Var det svårt?

Uppsättningen var det lätta, och det hade jag inte väntat mig. Monorepos har mest varit team-territorium, delvis för att riggandet med workspace-konfig, TypeScript-referenser och toolchain är trist och lätt att göra fel. Den här gången gjorde agenten grovjobbet utifrån planen, och min kväll gick åt till granskning snarare än konfig-felsökning. Det gamla “nej, monorepo är overkill för ett hobbyprojekt” håller liksom inte längre, i alla fall inte av uppsättningsskäl.

Så MCP-servern, som en gång var hela produkten, är numera en app bland andra i sitt eget monorepo. Den verkar åtminstone inte ha gått sönder av flytten. Nästa gång ett sidoprojekt växer ur sitt repo vet jag i alla fall var ribban ligger. En idé ligger redan och skvalpar: att göra MCP-servern mer tillgänglig än bara lokalt, deployad som en worker med auth eller något åt det hållet. Vi får se om någon agent får tackla den.