--- name: typescript-library-engineering description: "Engineering skill: TypeScript npm library — how to build and ship a library with None — plain TypeScript, ESM-first, TypeScript, Vitest. Use when writing, reviewing, testing or releasing code in this stack, and when turning a Celestial design or experience skill into code." type: engineering stack: library core: "1.0" checked: 2026-10-26 --- # TypeScript npm library A publishable TypeScript package with an explicit exports map, correct types for every consumer, a tracked public API and semver releases from changesets. For SDKs, utilities and shared packages consumed by other teams or the public: API clients, validation helpers, framework-agnostic logic. The package ships ESM and type declarations, is checked for packaging mistakes before every release, and publishes to the npm registry from CI with provenance. ## Stack - **Language:** TypeScript (strict, isolatedDeclarations) ^7 (or ^5.9 where tooling has not moved yet) — Explicit return types on exports make declarations fast to emit and the API easy to review. - **Framework:** None — plain TypeScript, ESM-first — No framework dependency means no version conflicts in consumers' apps. - **Build:** tsup ^8 — Bundles ESM (and CJS when required) with declaration files from one config. - **Hosting:** npm registry, published from CI with provenance — Consumers can verify which commit and workflow built the package. - **Package manager:** pnpm ^10 — Strict installs catch undeclared dependencies that would break consumers. - **Testing:** Vitest (including type tests with expectTypeOf) ^3 — Runtime and type-level behaviour tested in one runner. - **Testing:** publint ^0.3 — Catches broken exports, missing files and wrong module formats before publishing. - **Testing:** Are the Types Wrong (@arethetypeswrong/cli) ^0.18 — Verifies types resolve for each module-resolution mode consumers use. ## Commands - dev: `pnpm dev` - lint: `pnpm lint && pnpm typecheck && pnpm publint && pnpm attw --pack .` - test: `pnpm test` - build: `pnpm build` - deploy: `pnpm changeset publish` - install: `pnpm install --frozen-lockfile` ## Structure - `src/index.ts` — The public entry point; re-exports only what is part of the API - `src//` — Implementation, one folder per concept, with tests beside the code - `src/internal/` — Helpers that are never exported from the package - `etc/.api.md` — API report generated by API Extractor; changes show up in review - `.changeset/` — One changeset per user-facing change, with its semver bump - `examples/` — Small consumer projects (ESM and CJS) used as smoke tests - `package.json` — exports map, files allow-list, sideEffects:false, engines and peerDependencies ## Naming - Files: kebab-case.ts - Tests: *.test.ts beside the file; *.test-d.ts for type-only tests - Branches: type/short-description, e.g. fix/retry-backoff-jitter - Components: Exported functions camelCase verbs; classes and types PascalCase; error classes end in Error ## From design and experience skills This stack has no UI; design skills do not apply. When a Celestial experience skill is in use, give each journey one end-to-end test (happy path plus its error and empty states) and name analytics events `object_action` from its measures. ## Conventions ### Code - Prefer small, pure functions and explicit types at module boundaries. _(Easier to test, review and change.)_ - Expose the API only through the exports map; every exported function has explicit parameter and return types and a doc comment with an example. _(Consumers cannot depend on internals, and the API report stays readable.)_ - Any removal, rename, narrowed input or widened output in the API report is a major version; additions are minor; fixes are patch. _(Consumers rely on semver ranges to upgrade without reading every release.)_ ### Components - Components read design tokens; no raw hex values, pixel sizes or font names in component code. _(One token change restyles the product.)_ ### Data - Analytics events are object_action in snake_case and come from the experience skill’s measures. _(Names stay consistent across teams and tools.)_ - Accept I/O dependencies (fetch, timers, storage) as options with sensible defaults instead of reaching for globals. _(Works across runtimes and is easy to test.)_ ### Errors - Every async action has loading, empty, error and success states, matching the experience skill. _(Unhandled states are where users get stuck.)_ - Throw subclasses of Error with a stable code property and the original error as cause; never throw strings. _(Callers can branch on codes that survive message rewording.)_ ### Security - Secrets never live in the repository; read them from the environment or a secret store. _(A leaked key outlives the commit that removed it.)_ - Validate every input on the server, even when the client already did. _(Clients can be bypassed.)_ - Web pages ship a Content-Security-Policy, HSTS, X-Frame-Options and nosniff. _(Cheap protection against the most common attacks.)_ - Publish only from CI with provenance and a granular, automation-only token; the files allow-list excludes tests, fixtures and source maps of private code. _(A hijacked laptop or a stray .env must not end up in a published tarball.)_ ### Performance - Keep sideEffects:false and no top-level work; prefer zero runtime dependencies, and frameworks only as peerDependencies. _(Bundlers can tree-shake unused exports and consumers avoid duplicate copies.)_ ### Accessibility - Use native elements first; add ARIA only when no native element fits. _(Native controls bring keyboard and screen-reader support for free.)_ ## Do - Run the examples/ consumers against the packed tarball in CI - Deprecate with a JSDoc @deprecated tag and a runtime warning one major before removal - Write the changeset in the consumer's words: what changed and what to do ## Don't - Export TypeScript enums or namespaces from the public API - Add a runtime dependency for something under 30 lines - Publish from a local machine ## Quality gates | Gate | Tool | Threshold | When | |---|---|---|---| | Lint and types | The stack’s linter and type checker | 0 errors | Every pull request | | Unit and component tests | The stack’s test runner | All pass; every design-skill component state covered | Every pull request | | Dependency and secret scan | Dependency audit + secret scanner | No high or critical findings; no secrets | Every pull request and nightly | | Package correctness | publint + @arethetypeswrong/cli on the packed tarball | 0 errors; types resolve for node16, bundler and ESM consumers | Every pull request | | API report | API Extractor | Report committed and up to date; any change has a matching changeset with the right semver bump | Every pull request | | Size budget | size-limit | Main entry ≤ 10KB minified and gzipped; no growth over 5% without approval | Every pull request | ## Workflow - Branching: Trunk-based: short-lived branches off main; maintenance branches only for supported previous majors. **Review checklist** - Correctness: does it do what the issue asks, including edge cases? - Readability: could a new teammate change it safely? - Architecture: is it in the right place, with the right boundaries? - Security: inputs, secrets, permissions, dependencies - Performance: budgets, queries, bundle size - Tests fail when the code is broken (flip a condition and re-run) - One approving review; two for any change to the API report - Reviewer reads the changeset as release notes **Definition of done** - Acceptance criteria met and demoed on the preview link - Tests added for new behaviour and every state - Accessibility and performance gates green - Analytics events named from the experience skill - Docs or changelog updated when behaviour changes - Changeset added for every user-facing change - Doc comments and README examples updated and type-checked **Release** - Small pull requests (aim for under 400 changed lines) - CI green before review - Squash-merge to main; main is always deployable - Every release has a written rollback path - A release pull request collects changesets; merging it versions, tags and publishes from CI - CHANGELOG.md generated from changesets in Keep a Changelog sections **Incidents** - Publish a fixed patch and deprecate the bad version with a message pointing to it - Write a blameless note within 48 hours: what happened, impact, fix, prevention ## Overrides of the engineering core - Journey tests: Replaced by consumer smoke tests in examples/ run against the packed tarball — A library has no user journeys; its users are other programs. - Accessibility: Not applicable — the package has no UI — There is nothing rendered to audit. - Performance budget: Replaced by the size budget gate — Bundle cost in consumers' apps is the performance a library controls. - Preview deploy: Replaced by a packed tarball attached to each CI run — Reviewers install the exact artefact that would be published. ## Examples ### Exports map and packaging fields (`package.json`) ```json { "name": "@example/retry", "version": "2.1.0", "type": "module", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "./package.json": "./package.json" }, "files": ["dist"], "sideEffects": false, "engines": { "node": ">=20" }, "publishConfig": { "access": "public", "provenance": true }, "scripts": { "build": "tsup src/index.ts --format esm --dts --clean", "test": "vitest run --typecheck" } } ``` ### Typed error with a stable code (`src/errors.ts`) ```ts export type RetryErrorCode = 'ATTEMPTS_EXHAUSTED' | 'ABORTED'; /** Thrown when an operation cannot complete within its retry policy. */ export class RetryError extends Error { readonly code: RetryErrorCode; readonly attempts: number; constructor(code: RetryErrorCode, attempts: number, options?: { cause?: unknown }) { super(code === 'ABORTED' ? 'Retry aborted by signal.' : `Gave up after ${attempts} attempts.`, options); this.name = 'RetryError'; this.code = code; this.attempts = attempts; } } ``` ## Further reading - [TypeScript documentation](https://www.typescriptlang.org/docs/) — Module resolution, declaration files and strict compiler options. - [Semantic Versioning](https://semver.org/) — The rules for major, minor and patch that the API report enforces. - [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) — How to structure the changelog consumers read before upgrading. - [SLSA](https://slsa.dev/) — Why publishing with provenance from CI matters for package consumers.