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:
@@ -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.
|
||||
Reference in New Issue
Block a user