development-monorepo-pnpm skill: initial import

Full-stack pnpm + Turborepo monorepo skill, independent of the separate
backend and frontend monorepo skills it draws on.

Repository shape is split four ways at the top level: applications-backend,
applications-frontend, packages-backend, packages-frontend — so a path
states which runtime a file ends up in.

Defaults: NestJS for backend services, React with IBM Carbon Design for
browser applications; the project's stated architecture overrides both.

Covers workspace globs, the Turborepo pipeline, filtered validation,
blast-radius checks, Module Federation, turbo prune image builds, and Helm.
Documents one sanctioned cross-boundary dependency: API contract types owned
by the backend, imported type-only by the frontend.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 23:49:02 +03:00
co-authored by Claude Opus 5
commit ff69bcca2f
14 changed files with 1250 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
# Shared Packages
Everything under `packages-backend/*` and `packages-frontend/*`: which root a package belongs
in, package tiers, public-API safety, and changesets publishing.
Read this when creating a shared package, changing one's exports or dependencies, deciding
where a package belongs, or cutting a release.
---
## 1. Choosing the root
**A package lives in the root that matches its consumers, never its author.** A utility
extracted from a service but used only by browser applications belongs in
`packages-frontend/*`.
| Consumed by | Root |
|---|---|
| backend services only | `packages-backend/*` |
| browser applications only | `packages-frontend/*` |
| both — API contract types | `packages-backend/<domain>-contracts`, types only (SKILL.md §3) |
| both — genuine runtime code | see below |
Truly shared **runtime** code is rarer than it looks. Before creating one, check that it is
free of Node built-ins (`fs`, `path`, `crypto`), free of DOM APIs, and free of anything
environment-specific. If it is not, you have two packages, not one — and forcing it into a
single package pulls one environment's assumptions into the other.
When such a package is justified, place it under the root of its **primary** consumer, keep it
dependency-free, and document the constraint at the top of its README. A dependency added
later is what breaks the other side.
## 2. Package tiers
| Tier | Purpose | Examples |
|---|---|---|
| Configuration presets | shared build/lint/type config | `eslint-config`, `typescript-config` |
| Contracts | API types shared across the boundary | `<domain>-contracts` |
| Domain libraries | business logic for one domain | `billing-core`, `user-core` |
| Utilities | domain-independent helpers | `date-utils`, `result` |
Configuration presets are shared from day one — that is their job. Every other tier earns its
existence only after a second consumer appears.
## 3. Package anatomy
```text
packages-<side>/<package-name>/
├── src/
│ ├── index.ts # barrel — the entire public API
│ └── <module>.ts
├── package.json
├── tsconfig.json
├── .eslintrc.cjs | eslint.config.js
└── README.md # what it is, who consumes it, constraints
```
```json
{
"name": "@<scope>/<package-name>",
"version": "0.1.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } },
"scripts": {
"build": "tsc",
"check-types": "tsc --noEmit",
"lint": "eslint src"
}
}
```
**`src/index.ts` is the public API.** Anything not exported from the barrel is internal and may
change freely; anything exported is a contract with every consumer. Deep imports into
`dist/` or `src/` from a consumer defeat this — treat one as a bug in the consumer.
Script names must match the tasks in `turbo.json`, or Turborepo silently skips the workspace.
## 4. Changing a public API
Adding an export is safe. Changing or removing one is not.
1. Search every consumer across all four roots before editing:
```bash
grep -Rn "from '@<scope>/<package>'" applications-backend applications-frontend \
packages-backend packages-frontend
```
2. Decide explicitly whether the change is backward-compatible.
3. If it is not: update consumers in the same change, or add the new export alongside the old
one, migrate, then remove.
4. Build every affected consumer with its own `--filter`.
A shared package's dependencies propagate to every consumer, so weigh each one twice. This
matters most in `packages-frontend/*`, where a dependency lands in a browser bundle that ships
to users.
## 5. Configuration presets
`typescript-config` and `eslint-config` exist so rules live in one place. Every workspace
extends them rather than restating `compilerOptions` or lint rules locally.
Their blast radius is the **whole repository** — a rule added to the shared ESLint preset can
fail the build in workspaces you never opened. Change one, then run an unfiltered
`pnpm lint && pnpm check-types` before committing.
Each side may need its own preset variant (a DOM `lib` for frontend, Node types for backend).
Keep the shared base shared and put only the genuine differences in the variant.
## 6. Changesets and publishing
```bash
pnpm changeset # describe the change, pick the bump
pnpm publish-packages # build:ci + lint, then version and publish
```
- One changeset per meaningful change, written for a consumer deciding whether to upgrade —
not a restatement of the commit message.
- A breaking change to an exported API is a **major** bump, even if every current consumer
lives in this repository.
- Never hand-edit versions or `CHANGELOG.md`; changesets owns both.
- Internal-only packages are marked `"private": true` and are never published.
## 7. Mistakes to avoid
- Putting a package in the root matching its author instead of its consumers.
- Creating a "shared" runtime package that imports Node built-ins or DOM APIs.
- Giving a contract package a runtime dependency.
- Exporting something from the barrel that was meant to be internal.
- Deep-importing another package's `src/` or `dist/` from a consumer.
- Changing an exported API without searching consumers first.
- Adding a dependency to a frontend package without considering bundle size.
- Restating `compilerOptions` or lint rules locally instead of changing the shared preset.
- Editing versions or changelogs by hand.
- Promoting logic into a shared package before a second consumer exists.