# 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/-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 | `-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-// ├── src/ │ ├── index.ts # barrel — the entire public API │ └── .ts ├── package.json ├── tsconfig.json ├── .eslintrc.cjs | eslint.config.js └── README.md # what it is, who consumes it, constraints ``` ```json { "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 '@/'" 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.