Files
development-monorepo-pnpm-3…/SKILL.md
T

313 lines
16 KiB
Markdown

---
name: development-monorepo-pnpm--3darch
description: >
Create, review, refactor, debug, and maintain full-stack monorepos built on pnpm workspaces
and Turborepo, split four ways — applications-backend, applications-frontend,
packages-backend, packages-frontend. Backend services default to NestJS; browser
applications default to React with IBM Carbon Design, unless the project architecture states
otherwise. Use when adding or changing a service, controller, module, React page, component
or hook; assigning ports and API base paths; sharing API contract types across the
front/back boundary; wiring Module Federation; editing shared eslint or typescript presets;
cutting changesets; editing Dockerfiles, Helm charts, or Turborepo tasks; running filtered
validation or blast-radius checks; or fixing workspace, dependency, lockfile, or
build-pipeline problems — even when the user only names their repository instead of saying
"monorepo".
license: Proprietary
metadata:
author: workspace-skills-code-agent
version: "1.0"
spec: agentskills.io/specification
compatibility: Requires pnpm (via corepack) and Node. Designed for Cline, Claude Code, GitHub Copilot, OpenAI Codex, and other compatible agent environments
---
# Full-Stack pnpm + Turborepo Monorepo
## 3D Architecture Wizzard Project Adoption
This is the primary project-adopted skill for **3D Architecture Wizzard** (`3darch`).
- Central source: https://gitea.lego-cloud.eu/home-v1-skills-code-agent/development-monorepo-pnpm
- Source branch: `test`
- Source commit: `ff69bcca2fb3b673297230710a74c70410fd1996`
- Adopted repository: https://gitea.lego-cloud.eu/corp-v1-3darch-skills-code-agent/development-monorepo-pnpm--3darch
- Application: https://gitea.lego-cloud.eu/corp-v1-3darch/corp-v1-3darch
- Documentation: https://gitea.lego-cloud.eu/corp-v1-3darch/corp-v1-3darch-documentation
- Environment namespace: `CORP_V1_3DARCH_*`
- Global diagram skill: https://gitea.lego-cloud.eu/home-v1-skills-code-agent/drawio-main
- Global glossary skill: https://gitea.lego-cloud.eu/home-v1-skills-code-agent/corp-v1--glossary
- Project automation: none; no scheduler job is authorized.
Project engineering peers:
- `development-branching-strategy--3darch` — https://gitea.lego-cloud.eu/corp-v1-3darch-skills-code-agent/development-branching-strategy--3darch
- `development-gitops-argo-cd--3darch` — https://gitea.lego-cloud.eu/corp-v1-3darch-skills-code-agent/development-gitops-argo-cd--3darch
- `development-monorepo-pnpm--3darch` — https://gitea.lego-cloud.eu/corp-v1-3darch-skills-code-agent/development-monorepo-pnpm--3darch
- `development-scripts--3darch` — https://gitea.lego-cloud.eu/corp-v1-3darch-skills-code-agent/development-scripts--3darch
- `devsecops-ci-cd-gitea--3darch` — https://gitea.lego-cloud.eu/corp-v1-3darch-skills-code-agent/devsecops-ci-cd-gitea--3darch
- `documentation-docusaurus--3darch` — https://gitea.lego-cloud.eu/corp-v1-3darch-skills-code-agent/documentation-docusaurus--3darch
- `template-engine-copier--3darch` — https://gitea.lego-cloud.eu/corp-v1-3darch-skills-code-agent/template-engine-copier--3darch
Work rules for monorepos holding **both** backend services and browser applications, using
**pnpm workspaces** for dependency resolution and **Turborepo** for task orchestration.
The defining choice in this layout is that the front/back split is expressed in the
**directory names themselves**, not in a nested convention. Four top-level workspace roots,
each with one job.
## Scope routing
Pick the reference that matches the change; do not load all of them.
| The change touches | Read |
|---|---|
| A service under `applications-backend/*` — modules, controllers, providers, config | `references/applications-backend.md` |
| An app under `applications-frontend/*` — components, routing, Carbon, Module Federation | `references/applications-frontend.md` |
| Anything under `packages-backend/*` or `packages-frontend/*` — exports, presets, changesets | `references/packages.md` |
| `pnpm-workspace.yaml`, `turbo.json`, root scripts, formatting, dependency overrides | `references/workspace-and-tooling.md` |
| Dockerfiles, images, Helm charts, values, deploy automation | `references/containers-and-helm.md` |
## 1. Canonical repository shape
```text
<repo>/
├── applications-backend/ # deployable services — NestJS by default
│ └── <service-name>/
├── applications-frontend/ # deployable browser apps — React + Carbon by default
│ └── <app-name>/
├── packages-backend/ # libraries consumed by services
│ └── <package-name>/
├── packages-frontend/ # libraries consumed by browser apps
│ └── <package-name>/
├── helm-charts/<chart-name>/ # deployment assets
├── .scripts/ # shell modules: docker, images, helm, pipelines
├── package.json # root scripts + packageManager + engines + overrides
├── pnpm-workspace.yaml # the four globs below
├── turbo.json # task pipeline
├── pnpm-lock.yaml
├── .prettierrc # single formatting authority
└── Taskfile.yml # thin delegator — includes .scripts/ modules only
```
```yaml
# pnpm-workspace.yaml
packages:
- "applications-backend/*"
- "applications-frontend/*"
- "packages-backend/*"
- "packages-frontend/*"
```
Applications sit **directly** under their root — `applications-backend/<name>`, not
`applications-backend/<name>/core`. The four-way split already gives each side its own
namespace, so no extra nesting layer is needed.
**A workspace exists only if a glob matches it.** Creating a directory is not enough: confirm
the glob, then run `pnpm install`. A directory outside these four roots is not part of the
build graph, and Turborepo will silently ignore it.
### Why the split is in the directory name
One glance at a path tells you which runtime a file ends up in. That matters because the two
sides have genuinely different constraints — a browser bundle ships to untrusted clients and
pays for every kilobyte, a service does not; a service holds credentials, a bundle must never.
When both live under one `packages/` root, the only thing standing between a secret-handling
utility and a browser bundle is someone reading the import path carefully.
## 2. Default stacks
| Layer | Default | Applies unless |
|---|---|---|
| `applications-backend/*` | **NestJS** — modules, controllers, providers, DI | the project architecture specifies another framework |
| `applications-frontend/*` | **React** with **IBM Carbon Design** (`@carbon/react`) | the project architecture specifies another framework or design system |
These are defaults, not mandates. **The project's stated architecture always wins** — read the
paired code-agent repository's `memory-bank/` (§9) and any architecture decision record
*before* scaffolding. If the architecture is silent, use the defaults above and record the
choice; if it disagrees, follow it and do not "correct" it toward these defaults.
Do not mix design systems inside one frontend application. Carbon components and a second
component library in the same app produce two type scales, two grid systems, and a token set
that fights itself.
## 3. The one sanctioned cross-boundary dependency
Frontend workspaces must not import from `packages-backend/*`, and backend workspaces must not
import from `packages-frontend/*` — with exactly one exception:
**API contract types.** The service that defines an endpoint owns its contract, so contract
packages live under `packages-backend/<domain>-contracts` and expose **types only** — no
runtime code, no dependencies, no side effects. Frontend consumers import them type-only:
```ts
import type { UserDto } from "@<scope>/user-contracts";
```
`import type` is erased at compile time, so nothing from the backend package reaches the
browser bundle. A plain `import` from the same package does reach it — that is the failure
this rule exists to prevent, and it will not show up as a type error.
A contract package that acquires a runtime dependency has stopped being a contract package.
Split it before it ships anything into a bundle.
## 4. Tooling baseline
Root `package.json` pins the contract for every contributor and CI runner: `packageManager`
(exact pnpm version, read by corepack), `engines.node`, `pnpm.overrides` for security pins,
and root scripts that always delegate to Turborepo — never to a single workspace.
```bash
pnpm build # turbo run build
pnpm dev # turbo run dev
pnpm lint # turbo run lint
pnpm check-types # turbo run check-types
pnpm format # prettier --write
pnpm publish-packages # build:ci + lint, then changeset version && changeset publish
pnpm prepush # turbo build --filter=...[HEAD^] — only what the branch touched
```
`build` depends on `^build`, `check-types`, and `lint`, so a type error or lint error is a
build failure for every downstream workspace, not a warning. Full pipeline, cache outputs, and
override rules → `references/workspace-and-tooling.md`.
## 5. Before making changes
1. Identify the exact workspace path, and therefore which of the four roots you are in.
2. Read that workspace's `package.json` — scripts, dependencies, whether it is publishable.
3. Inspect its `tsconfig.json`, lint config, build config, and source layout before adding.
4. Map what it consumes from shared packages, and what consumes it.
5. For Module Federation changes, map the host↔remote contract before editing either side.
6. Read the paired code-agent repository's `rules/` and `memory-bank/` when one exists (§9) —
it carries architecture decisions the code does not.
```bash
find applications-backend applications-frontend -maxdepth 2 -name package.json -print
find packages-backend packages-frontend -maxdepth 2 -name package.json -print
pnpm list --depth 0
task --list
```
## 6. Implementation rules
### Workspace boundaries
- Application logic stays inside its own workspace.
- Promote into a shared package only when **more than one** application needs it *and* the
abstraction has stopped changing. Config presets are the exception — shared from day one.
Premature promotion is the most expensive mistake here: every consumer inherits the churn.
- A shared package goes in the root matching its **consumers**, never its author.
- Never cross a workspace boundary with a relative import (`../../other-app/src`). Depend on
the workspace package, or the Module Federation contract, or not at all.
### TypeScript
- Every workspace extends a shared config from the `typescript-config` preset. Do not restate
`compilerOptions` locally beyond `outDir`, `rootDir`, `include`, `exclude`.
- `check-types` is `tsc --noEmit`, named identically in every workspace so Turborepo can run
it across the graph.
- Keep types explicit at public boundaries; avoid broad `any`.
### Dependencies
- Add a dependency to the **smallest** workspace that needs it.
- Root dependencies are monorepo tooling only — turbo, prettier, husky, changesets,
typescript. Nothing an application imports at runtime.
- Inter-workspace dependencies use `workspace:*`.
- Run `pnpm install` after every `package.json` edit and commit the lockfile change with the
edit that caused it.
- Never remove a `pnpm.overrides` entry without a security review — removing one silently
reintroduces a vulnerable transitive version and nothing in the build complains.
### Formatting
Root `.prettierrc` is the single authority. Do not add per-package Prettier config; a second
config produces churn diffs every time someone formats a file from the other side.
## 7. Validation
Validate the narrowest scope covering the change, then widen if it touched shared code.
```bash
# Single workspace
pnpm check-types --filter="@<scope>/<name>"
pnpm lint --filter="@<scope>/<name>"
pnpm build --filter="@<scope>/<name>"
# Shared package or cross-workspace change — unfiltered, plus each consumer
pnpm check-types && pnpm lint && pnpm build
pnpm build --filter="@<scope>/<consumer>"
# Only what the branch changed
pnpm prepush
```
Module Federation changes additionally require **host and remote running together** — a green
build on each side separately proves nothing about a runtime contract:
```bash
pnpm dev --filter="@<scope>/<host>"
pnpm dev --filter="@<scope>/<remote>"
```
Record the commands run and their outcome in the task record (§9). "It builds", without the
filter you used, is not a validation result.
## 8. Blast-radius checks
Before changing anything another workspace can observe — an exported symbol, a REST base path,
a port, an environment variable name, an MFE `exposes` key:
1. Enumerate the surface.
2. Search every consumer across all four roots.
3. Decide explicitly whether the change is backward-compatible.
4. If it is not: update consumers in the same change, or ship a compatible wrapper first.
5. Validate each affected consumer with its own `--filter` build.
```bash
grep -Rn "from '@<scope>/<package>'" applications-backend applications-frontend \
packages-backend packages-frontend
grep -Rn "api/<name>" applications-frontend # base-path callers
```
A port or base path change is never local: it also lands in the Helm chart values, the gateway
routing, and every frontend caller. See `references/containers-and-helm.md`.
## 9. Durable records
Monorepos in this family pair with a `<repo>-code-agent/` repository holding `rules/`,
`memory-bank/`, and `tasks/`. Update `memory-bank/` when durable facts change — application
inventory, ports, base paths, MFE topology, chosen frameworks and design system, chart
structure. Create a task record under `tasks/` for substantial work.
Never place task records inside the target repository unless explicitly asked.
## 10. Common mistakes to avoid
- Importing a `packages-backend/*` package from a frontend workspace with a value import
instead of `import type` — it silently pulls server code into the browser bundle.
- Putting a shared package in the root that matches its author rather than its consumers.
- Adding a workspace outside the four roots, or without updating `pnpm-workspace.yaml`.
- Re-introducing a `core/` nesting layer under an application directory.
- Changing a service port or base path without updating Helm values, gateway routing, and
every caller.
- Changing an MFE `exposes` key, remote name, or remote URL on one side only.
- Removing a `pnpm.overrides` entry without a security review.
- Adding a root-level dependency for a need local to one workspace.
- Skipping `--filter` validation for the exact workspace you edited.
- Mixing a second component library into a Carbon application.
- Overriding the project's stated architecture because it differs from the defaults in §2.
- Adding tasks directly to root `Taskfile.yml` instead of a `.scripts/` module.
- Editing generated artifacts (`dist/`, rendered manifests, lockfiles) by hand.
- Committing `.env`, `key.pem`, `cert.pem`, registry tokens, or `.npmrc` secrets — including
into memory-bank or task documentation.
## Reference files
| File | Contents | Read when |
|---|---|---|
| `references/applications-backend.md` | NestJS service layout, modules/controllers/providers, config and env, ports and base paths, health checks, service Dockerfile | Touching `applications-backend/*` |
| `references/applications-frontend.md` | React app layout, IBM Carbon setup and theming, routing, state, Module Federation wiring and failure modes, nginx image | Touching `applications-frontend/*` |
| `references/packages.md` | Package tiers per root, contract packages, public-API safety, export and dependency rules, changesets and publishing | Touching `packages-backend/*` or `packages-frontend/*` |
| `references/workspace-and-tooling.md` | Workspace globs, `turbo.json` pipeline and caching, root scripts, Prettier, `pnpm.overrides`, lockfile discipline | Touching root config or the task pipeline |
| `references/containers-and-helm.md` | `turbo prune` image builds for both sides, chart layout, values and secrets, port/base-path propagation | Touching Dockerfiles, images, or charts |
| `assets/example-shared-package/` | Minimal shared TypeScript package — ESM, barrel export, shared tsconfig and eslint | Scaffolding a package under either packages root |