chore: restructure into pnpm workspaces — apps/api + placeholders (#181) #195
No reviewers
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol!195
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "181-workspace-restructure"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #181. Implements the workspace shape declared in ADR-0027 / epic #176.
What moved
apps/api/— the Next.js API service, moved as a unit viagit mvso history is preserved. Workspace name@carol/api.apps/client/— empty placeholder. Filled in by #183 (Expo scaffolding).packages/api-client/— empty placeholder. Filled in by #182 (generated typed client).packages/i18n/— the next-intl catalogs moved out ofapps/api/messages/so the Expo client (#183) can consume the same source-of-truth files.@carol/i18n.scripts/ci/; the OpenAPI generation scripts move underapps/api/scripts/.Dockerfile,compose.*.yaml,openapi.json,lefthook.yml, the workflows, and the docs stay at the repo root.Mechanics
packageManager+.tool-versions(matches CI'scorepack enable).lavamoat.allowScriptstopnpm.onlyBuiltDependenciesin the rootpackage.json. Same gate:pnpm install --ignore-scriptsthenpnpm rebuildruns scripts only for the listed packages.pnpm.overridespins a singlezodversion so the strict pnpm layout doesn't produce duplicate copies in the build graph.outputFileTracingRootpinned at the repo root inapps/api/next.config.mjsso Next's standalone bundle picks up workspace packages.package-lock.jsonremoved;pnpm-lock.yamlcommitted.CI (
.forgejo/workflows/pr.yml)Every job now:
corepack enable→pnpm install --frozen-lockfile --ignore-scripts→pnpm rebuild→pnpm -F @carol/api <task>.pnpm audit --prodreplacesnpm audit --omit=dev(same JSON shape); OSV-Scanner readspnpm-lock.yaml; coverage reporter runsgit -C apps/api diffso reportable paths (app/,lib/,db/) match the workspace layout. The package-age policy degrades to a soft no-op until a follow-up teaches the helper to walkpnpm-lock.yaml— soft signal, never blocks merge.Dockerfile
Multi-stage rebuild for workspaces. The
runtimestage copiesapps/api/.next/standaloneinto/app/; that tree mirrors the workspace layout because ofoutputFileTracingRoot, so the entrypoint isnode apps/api/server.js. Build succeeds and the container serves/api/openapi.jsonend-to-end (verified withpodman build+podman run).Reviewer-attention notes
/api/openapi.jsonroute now serves the committedopenapi.jsoninstead of regenerating from the zod registry per request. The route imports the file as a JSON module so webpack bundles it. The reason: under pnpm's strict layout, webpack's parallel async-module ordering means DTOs imported bylib/api/openapi-routesmay be constructed beforeextendZodWithOpenApi(z)has run inlib/api/openapi, leaving their prototypes without the.openapi(...)chain. The committed file IS the contract (CI'sopenapi:checkkeeps it fresh), so serving it directly is the strongest consistency guarantee. Build-time generation viapnpm -F @carol/api openapi:generatestill produces source-of-truth bytes — that path doesn't go through webpack. Comment with the full rationale is in the route file.import enMessages from "@carol/i18n/messages/en.json") instead of the previous dynamic template. Webpack can't statically resolveimport(\@carol/i18n/messages/${locale}.json`)through a workspace package'sexportsmap, so the file maintains an explicit registry. Adding a locale means: ship the JSON, append the static import + registry entry inapps/api/lib/i18n/request.ts, add the code toSUPPORTED_LOCALES`. Documented inline.sharpis now an explicitapps/apidep. It was previously a transitive ofnext(npm's flat hoist made it visible tolib/profile/picture.ts); pnpm's strict layout doesn't, so the build broke on theimport sharp from "sharp"line. Declaring it explicitly is the correct hygiene either way.tests/scripts/package-ages.test.tscan't use the@/scripts/lib/package-ages.mjsalias anymore (@/now points inside the api workspace, not the repo root). It uses a relative import that walks four levels up to the repo-rootscripts/lib/instead.pnpm-lock.yamlis large (~7k lines). Reviewing the manifests + workflow + Dockerfile is the substantive read; the lockfile is mechanical.Local verification (run from the worktree)
pnpm installclean.pnpm -F @carol/api typecheckgreen.pnpm -F @carol/api lintgreen.pnpm -F @carol/api testgreen — 499 tests pass, 107 skipped (sqlite engine locally; Postgres leg runs in CI).pnpm -F @carol/api openapi:checkandopenapi:coveragegreen;openapi.jsonbyte-identical after regeneration.pnpm -F @carol/api buildgreen.podman build -t carol-test .succeeds;GET /api/openapi.jsonreturns 200 with a valid OpenAPI 3.1 doc.Reviewer's local checkout
After pulling:
rm -rf node_modules(npm flat hoist won't help anymore),corepack enable(ormise install), thenpnpm install. The first install is ~3 minutes; subsequent ones are seconds.Out-of-scope follow-ups
scripts/lib/package-ages.mjsto walkpnpm-lock.yamlso the package-age soft check resumes catching new packages on PRs.lib/api/openapi.ts+lib/api/openapi-routes.tsto fix the webpack ordering race so the/api/openapi.jsonroute can regenerate per request if we ever want it to. Not urgent — the committed-file approach is sound.🤖 Generated with Claude Code
OSV-Scanner
Threshold:
high· Total findings: 6 · At/above threshold: 24.1.08.0.16📊 Test coverage
Patch coverage: no testable lines changed.
Overall (
app/,lib/,db/, excluding UI per ADR-0019):Soft thresholds per ADR-0019. Coverage is informational and does not block merge.
Trivy (container image)
Threshold:
high· Total findings: 121 · At/above threshold: 16.27.0, 7.28.0, 8.5.0f1d15994846f626f57a4