get-tbd 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +47 -28
  2. package/dist/bin.mjs +410 -170
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/cli.mjs +202 -94
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/docs/README.md +47 -28
  7. package/dist/docs/SKILL.md +61 -18
  8. package/dist/docs/guidelines/bun-monorepo-patterns.md +2096 -0
  9. package/dist/docs/guidelines/cli-agent-skill-patterns.md +79 -5
  10. package/dist/docs/guidelines/error-handling-rules.md +66 -0
  11. package/dist/docs/guidelines/pnpm-monorepo-patterns.md +2868 -0
  12. package/dist/docs/guidelines/release-notes-guidelines.md +140 -0
  13. package/dist/docs/guidelines/{sync-troubleshooting.md → tbd-sync-troubleshooting.md} +1 -1
  14. package/dist/docs/guidelines/typescript-sorting-patterns.md +234 -0
  15. package/dist/docs/guidelines/typescript-yaml-handling-rules.md +195 -0
  16. package/dist/docs/install/claude-header.md +13 -6
  17. package/dist/docs/shortcuts/standard/agent-handoff.md +1 -0
  18. package/dist/docs/shortcuts/standard/checkout-third-party-repo.md +50 -0
  19. package/dist/docs/shortcuts/standard/{cleanup-all.md → code-cleanup-all.md} +3 -2
  20. package/dist/docs/shortcuts/standard/{cleanup-update-docstrings.md → code-cleanup-docstrings.md} +1 -0
  21. package/dist/docs/shortcuts/standard/{cleanup-remove-trivial-tests.md → code-cleanup-tests.md} +1 -0
  22. package/dist/docs/shortcuts/standard/{commit-code.md → code-review-and-commit.md} +1 -0
  23. package/dist/docs/shortcuts/standard/coding-spike.md +54 -0
  24. package/dist/docs/shortcuts/standard/create-or-update-pr-simple.md +1 -0
  25. package/dist/docs/shortcuts/standard/create-or-update-pr-with-validation-plan.md +1 -0
  26. package/dist/docs/shortcuts/standard/implement-beads.md +1 -0
  27. package/dist/docs/shortcuts/standard/merge-upstream.md +1 -0
  28. package/dist/docs/shortcuts/standard/new-architecture-doc.md +1 -0
  29. package/dist/docs/shortcuts/standard/new-guideline.md +8 -0
  30. package/dist/docs/shortcuts/standard/new-plan-spec.md +1 -0
  31. package/dist/docs/shortcuts/standard/new-research-brief.md +1 -0
  32. package/dist/docs/shortcuts/standard/new-shortcut.md +27 -1
  33. package/dist/docs/shortcuts/standard/new-validation-plan.md +1 -0
  34. package/dist/docs/shortcuts/standard/plan-implementation-with-beads.md +1 -0
  35. package/dist/docs/shortcuts/standard/precommit-process.md +1 -0
  36. package/dist/docs/shortcuts/standard/review-code-python.md +1 -0
  37. package/dist/docs/shortcuts/standard/review-code-typescript.md +1 -0
  38. package/dist/docs/shortcuts/standard/review-code.md +1 -0
  39. package/dist/docs/shortcuts/standard/review-github-pr.md +89 -17
  40. package/dist/docs/shortcuts/standard/revise-all-architecture-docs.md +1 -0
  41. package/dist/docs/shortcuts/standard/revise-architecture-doc.md +1 -0
  42. package/dist/docs/shortcuts/standard/setup-github-cli.md +1 -0
  43. package/dist/docs/shortcuts/standard/sync-failure-recovery.md +6 -53
  44. package/dist/docs/shortcuts/standard/update-specs-status.md +1 -0
  45. package/dist/docs/shortcuts/standard/welcome-user.md +2 -1
  46. package/dist/docs/shortcuts/system/skill-brief.md +1 -1
  47. package/dist/docs/shortcuts/system/skill.md +48 -12
  48. package/dist/docs/skill-brief.md +1 -1
  49. package/dist/docs/tbd-design.md +13 -1
  50. package/dist/index.d.mts +20 -6
  51. package/dist/index.mjs +2 -2
  52. package/dist/{src-BfhjLZXE.mjs → src-Ct16P2Ox.mjs} +154 -22
  53. package/dist/src-Ct16P2Ox.mjs.map +1 -0
  54. package/dist/tbd +410 -170
  55. package/package.json +1 -1
  56. package/dist/docs/guidelines/typescript-monorepo-patterns.md +0 -72
  57. package/dist/src-BfhjLZXE.mjs.map +0 -1
@@ -0,0 +1,2868 @@
1
+ ---
2
+ title: pnpm Monorepo Patterns
3
+ description: Modern patterns for pnpm-based TypeScript monorepo architecture
4
+ author: Joshua Levy (github.com/jlevy) with LLM assistance
5
+ ---
6
+ # pnpm Monorepo Patterns
7
+
8
+ **Last Updated**: 2026-02-02
9
+
10
+ **Related**:
11
+
12
+ - [pnpm Workspaces Documentation](https://pnpm.io/workspaces)
13
+ - [Changesets Documentation](https://github.com/changesets/changesets)
14
+ - [tsdown Documentation](https://tsdown.dev/)
15
+ - [publint Documentation](https://publint.dev/docs/)
16
+ - [Companion: Bun Monorepo Patterns](./bun-monorepo-patterns.md)
17
+
18
+ * * *
19
+
20
+ ## Updating This Document
21
+
22
+ ### Last Researched Versions
23
+
24
+ | Tool / Package | Version | Check For Updates |
25
+ | --- | --- | --- |
26
+ | **Node.js** | 24 (LTS "Krypton") | [nodejs.org/releases](https://nodejs.org/en/about/previous-releases) — Active LTS until Oct 2026 |
27
+ | **pnpm** | 10.28.0 | [github.com/pnpm/pnpm/releases](https://github.com/pnpm/pnpm/releases) — V8 binary storage for faster cache reads |
28
+ | **TypeScript** | ^5.9.3 | [github.com/microsoft/TypeScript/releases](https://github.com/microsoft/TypeScript/releases) — 5.9.3 stable. TS 6.0 is "bridge" release; TS 7.0 (Go rewrite) in VS 2026 Insiders preview. |
29
+ | **tsdown** | ^0.20.0 | [github.com/rolldown/tsdown/releases](https://github.com/rolldown/tsdown/releases) — 0.20.0-beta.3 latest. Requires Node.js 20.19+. |
30
+ | **publint** | ^0.3.0 | [npmjs.com/package/publint](https://www.npmjs.com/package/publint) |
31
+ | **@changesets/cli** | ^2.29.0 | [github.com/changesets/changesets/releases](https://github.com/changesets/changesets/releases) |
32
+ | **@types/node** | ^24.0.0 | Should match Node.js major version (^25.0.0 also available) |
33
+ | **actions/checkout** | v6 | [github.com/actions/checkout/releases](https://github.com/actions/checkout/releases) — Latest (v6.0.2), requires Runner v2.329.0+ |
34
+ | **actions/setup-node** | v6 | [github.com/actions/setup-node/releases](https://github.com/actions/setup-node/releases) |
35
+ | **pnpm/action-setup** | v4 | [github.com/pnpm/action-setup/releases](https://github.com/pnpm/action-setup/releases) |
36
+ | **changesets/action** | v1 | [github.com/changesets/action](https://github.com/changesets/action) |
37
+ | **lefthook** | ^2.0.15 | [github.com/evilmartians/lefthook/releases](https://github.com/evilmartians/lefthook/releases) — 2.0.15 latest |
38
+ | **npm-check-updates** | ^19.0.0 | [npmjs.com/package/npm-check-updates](https://www.npmjs.com/package/npm-check-updates) |
39
+ | **tsx** | ^4.21.0 | [github.com/privatenumber/tsx/releases](https://github.com/privatenumber/tsx/releases) |
40
+ | **prettier** | ^3.0.0 | [github.com/prettier/prettier/releases](https://github.com/prettier/prettier/releases) |
41
+ | **eslint-config-prettier** | ^10.0.0 | [github.com/prettier/eslint-config-prettier/releases](https://github.com/prettier/eslint-config-prettier/releases) |
42
+ | **ESLint** | ^9.39.0 | [github.com/eslint/eslint/releases](https://github.com/eslint/eslint/releases) — 9.39.2 stable. v10.0.0 in RC phase (Jan 2026). |
43
+ | **Vitest** | ^4.0.0 | [github.com/vitest-dev/vitest/releases](https://github.com/vitest-dev/vitest/releases) — 4.0.17 latest. Browser Mode stable, visual regression testing added. |
44
+
45
+ ### Reminders When Updating
46
+
47
+ 1. **Check each version** in the table above using the linked release pages
48
+
49
+ 2. **Update the table** with new versions and any relevant notes
50
+
51
+ 3. **Search and update code examples** — version numbers appear in:
52
+
53
+ - GitHub Actions workflows (CI and Release sections)
54
+
55
+ - `tsdown.config.ts` examples (`target: "node24"`)
56
+
57
+ - `tsconfig.base.json` examples (`target`/`lib` should match Node.js ES version)
58
+
59
+ - `package.json` examples (`engines`, `packageManager`, `devDependencies`)
60
+
61
+ - Appendices A, B, and D (complete examples)
62
+
63
+ 4. **Verify compatibility** — check that tools still work together (e.g., new
64
+ pnpm/action-setup versions may change caching behavior)
65
+
66
+ 5. **Update the “Last Updated” date** at the top of the document
67
+
68
+ 6. **Review “Open Research Questions”** section for any resolved items
69
+
70
+ * * *
71
+
72
+ ## Executive Summary
73
+
74
+ This research brief provides a comprehensive guide for setting up a modern TypeScript
75
+ package that can start as a single package and grow into a multi-package monorepo.
76
+ The architecture prioritizes fast iteration during early development while maintaining a
77
+ clear path to split packages later without breaking changes.
78
+
79
+ The recommended stack uses **pnpm workspaces** for dependency management, **tsdown** for
80
+ building ESM/CJS dual outputs with TypeScript declarations, **Changesets** for
81
+ versioning and release automation, **publint** for validating package publishability,
82
+ and **lefthook** for fast local git hooks.
83
+ This approach supports private development via GitHub Packages or direct GitHub
84
+ installs, with a seamless transition to public npm publishing when ready.
85
+
86
+ **Research Questions**:
87
+
88
+ 1. What is the optimal monorepo structure for a TypeScript package that may grow from
89
+ one to many packages?
90
+
91
+ 2. How should modern TypeScript packages handle dual ESM/CJS output with proper type
92
+ declarations?
93
+
94
+ 3. What tooling provides the best developer experience for versioning, publishing, and
95
+ CI/CD automation?
96
+
97
+ 4. How can packages support optional peer dependencies (like AI SDKs or protocol
98
+ integrations) without forcing them on users?
99
+
100
+ * * *
101
+
102
+ ## Research Methodology
103
+
104
+ ### Approach
105
+
106
+ Research was conducted through documentation review, web searches for current best
107
+ practices (2025), analysis of popular open-source monorepos, and evaluation of tooling
108
+ recommendations from the TypeScript and JavaScript ecosystem maintainers.
109
+
110
+ ### Sources
111
+
112
+ - Official documentation (pnpm, TypeScript, Node.js, GitHub)
113
+
114
+ - Tool-specific documentation (tsdown, publint, Changesets)
115
+
116
+ - Developer blog posts and migration guides
117
+
118
+ - GitHub discussions and issue threads
119
+
120
+ - Real-world monorepo implementations (Effect-TS, TresJS)
121
+
122
+ * * *
123
+
124
+ ## Research Findings
125
+
126
+ ### 1. Package Manager & Workspace Structure
127
+
128
+ #### pnpm Workspaces
129
+
130
+ **Status**: Recommended
131
+
132
+ **Details**:
133
+
134
+ - pnpm offers disk space efficiency through content-addressable storage with symlinks
135
+
136
+ - Built-in workspace support without additional tools
137
+
138
+ - Strict `node_modules` prevents phantom dependencies (packages not explicitly declared)
139
+
140
+ - `workspace:` protocol ensures local packages are always used during development
141
+
142
+ - `pnpm deploy` command enables isolated production deployments for Docker
143
+
144
+ **Assessment**: pnpm is the consensus choice for TypeScript monorepos in 2025, offering
145
+ superior disk efficiency and stricter dependency management than npm or yarn.
146
+
147
+ **Key Configuration** (`pnpm-workspace.yaml`):
148
+
149
+ ```yaml
150
+ packages:
151
+ - 'packages/*'
152
+ - 'apps/*'
153
+ ```
154
+
155
+ **Root `.npmrc`**:
156
+
157
+ ```ini
158
+ save-workspace-protocol=true
159
+ prefer-workspace-packages=true
160
+ ```
161
+
162
+ **References**:
163
+
164
+ - [pnpm Workspaces](https://pnpm.io/workspaces)
165
+
166
+ - [Complete Monorepo Guide 2025](https://jsdev.space/complete-monorepo-guide/)
167
+
168
+ * * *
169
+
170
+ #### Monorepo Structure Strategy
171
+
172
+ **Status**: Recommended
173
+
174
+ **Details**:
175
+
176
+ The “start mono, stay sane” approach places packages in `packages/` from day one, even
177
+ if there’s only one package initially.
178
+ This prevents restructuring when adding new packages later.
179
+
180
+ **Recommended Directory Structure**:
181
+
182
+ ```
183
+ project-root/
184
+ .changeset/
185
+ config.json
186
+ README.md
187
+ .github/
188
+ workflows/
189
+ ci.yml
190
+ release.yml
191
+ packages/
192
+ package-name/
193
+ src/
194
+ core/ # Future: package-name-core
195
+ cli/ # Future: package-name-cli
196
+ adapters/ # Future: package-name-adapters
197
+ bin.ts
198
+ index.ts
199
+ package.json
200
+ tsconfig.json
201
+ tsdown.config.ts
202
+ .gitignore
203
+ .npmrc
204
+ eslint.config.js
205
+ package.json
206
+ pnpm-workspace.yaml
207
+ tsconfig.base.json
208
+ ```
209
+
210
+ **Assessment**: Starting with a monorepo structure from day one has minimal overhead and
211
+ prevents painful restructuring later.
212
+ Internal code organization (`core/`, `cli/`, `adapters/`) creates natural split points.
213
+
214
+ **References**:
215
+
216
+ - [Setting up a monorepo with pnpm and TypeScript](https://brockherion.dev/blog/posts/setting-up-a-monorepo-with-pnpm-and-typescript/)
217
+
218
+ - [Wisp CMS: How to Bootstrap a Monorepo with PNPM](https://www.wisp.blog/blog/how-to-bootstrap-a-monorepo-with-pnpm-a-complete-guide)
219
+
220
+ * * *
221
+
222
+ ### 2. TypeScript Configuration
223
+
224
+ #### Base Configuration
225
+
226
+ **Status**: Recommended
227
+
228
+ **Details**:
229
+
230
+ Modern TypeScript monorepos use a shared base configuration extended by each package.
231
+
232
+ **`tsconfig.base.json`**:
233
+
234
+ ```json
235
+ {
236
+ "compilerOptions": {
237
+ "target": "ES2024",
238
+ "lib": ["ES2024"],
239
+ "module": "ESNext",
240
+ "moduleResolution": "Bundler",
241
+ "resolveJsonModule": true,
242
+ "strict": true,
243
+ "skipLibCheck": true,
244
+ "noUncheckedIndexedAccess": true,
245
+ "forceConsistentCasingInFileNames": true,
246
+ "verbatimModuleSyntax": true
247
+ }
248
+ }
249
+ ```
250
+
251
+ **Package-level `tsconfig.json`**:
252
+
253
+ ```json
254
+ {
255
+ "extends": "../../tsconfig.base.json",
256
+ "compilerOptions": {
257
+ "types": ["node"],
258
+ "noEmit": true
259
+ },
260
+ "include": ["src"]
261
+ }
262
+ ```
263
+
264
+ **Assessment**: Using `moduleResolution: "Bundler"` is appropriate when a bundler
265
+ (tsdown) handles the final output.
266
+ For maximum Node.js compatibility without a bundler, `NodeNext` would be preferred.
267
+ Since tsdown generates proper ESM and CJS with correct extensions, `Bundler` mode works
268
+ well.
269
+
270
+ **References**:
271
+
272
+ - [TypeScript: Choosing Compiler Options](https://www.typescriptlang.org/docs/handbook/modules/guides/choosing-compiler-options.html)
273
+
274
+ - [Is nodenext right for libraries?](https://blog.andrewbran.ch/is-nodenext-right-for-libraries-that-dont-target-node-js/)
275
+
276
+ * * *
277
+
278
+ #### moduleResolution: Bundler vs NodeNext
279
+
280
+ **Status**: Situational
281
+
282
+ **Details**:
283
+
284
+ | Aspect | `Bundler` | `NodeNext` |
285
+ | --- | --- | --- |
286
+ | File extensions | Not required in imports | Required (.js extension) |
287
+ | Use case | When bundler handles output | Direct Node.js execution |
288
+ | Library compatibility | Requires bundler-aware consumers | Works everywhere |
289
+ | Type generation | Must ensure .d.ts aligns with output | Naturally aligned |
290
+
291
+ **Key insight**: `NodeNext` is “infectious” in a good way—code that works in Node.js
292
+ typically works in bundlers too.
293
+ However, `Bundler` is acceptable when using tsdown since it handles file extensions
294
+ correctly.
295
+
296
+ **Assessment**: Use `Bundler` for simplicity during development when tsdown generates
297
+ the final output. The bundler handles the complexity of module resolution.
298
+
299
+ **References**:
300
+
301
+ - [TypeScript moduleResolution documentation](https://www.typescriptlang.org/tsconfig/moduleResolution.html)
302
+
303
+ - [Live types in a TypeScript monorepo](https://colinhacks.com/essays/live-types-typescript-monorepo)
304
+
305
+ * * *
306
+
307
+ ### 3. Build Tooling
308
+
309
+ #### tsdown
310
+
311
+ **Status**: Strongly Recommended
312
+
313
+ **Details**:
314
+
315
+ tsdown is the modern successor to tsup, built on Rolldown (the Rust-based bundler from
316
+ the Vite ecosystem).
317
+ Key advantages:
318
+
319
+ - **ESM-first**: Properly handles file extensions in ESM output (a pain point with tsup)
320
+
321
+ - **Dual format output**: Generates both ESM (`.js`) and CJS (`.cjs`) from the same
322
+ source
323
+
324
+ - **TypeScript declarations**: Built-in `.d.ts` and `.d.cts` generation
325
+
326
+ - **Multi-entry support**: Build multiple entry points (library, CLI, adapters) in one
327
+ config
328
+
329
+ - **Plugin ecosystem**: Compatible with Rollup, Rolldown, and most Vite plugins
330
+
331
+ - **Fast**: Powered by Rust-based Oxc and Rolldown
332
+
333
+ - **Isolated declarations**: Supports TypeScript 5.5+ `--isolatedDeclarations` for
334
+ faster type generation
335
+
336
+ **Migration from tsup**: tsdown provides a `migrate` command and is compatible with most
337
+ tsup configurations.
338
+
339
+ **Configuration (`tsdown.config.ts`)**:
340
+
341
+ ```typescript
342
+ import { defineConfig } from 'tsdown';
343
+
344
+ export default defineConfig({
345
+ entry: {
346
+ index: 'src/index.ts',
347
+ cli: 'src/cli/index.ts',
348
+ adapter: 'src/adapters/index.ts',
349
+ bin: 'src/bin.ts',
350
+ },
351
+ format: ['esm', 'cjs'],
352
+ platform: 'node',
353
+ target: 'node24',
354
+ sourcemap: true,
355
+ dts: true,
356
+ clean: true,
357
+ banner: ({ fileName }) => (fileName.startsWith('bin.') ? '#!/usr/bin/env node\n' : ''),
358
+ });
359
+ ```
360
+
361
+ **Assessment**: tsdown is the recommended choice for new TypeScript library projects.
362
+ It has official backing from the Vite/Rolldown team and will become the foundation for
363
+ Rolldown Vite’s Library Mode.
364
+
365
+ **Note on tsup**: tsup is no longer actively maintained.
366
+ The project recommends migrating to tsdown.
367
+
368
+ **References**:
369
+
370
+ - [tsdown Introduction](https://tsdown.dev/guide/)
371
+
372
+ - [Switching from tsup to tsdown](https://alan.norbauer.com/articles/tsdown-bundler/)
373
+
374
+ - [Migrate from tsup](https://tsdown.dev/guide/migrate-from-tsup)
375
+
376
+ - [TresJS tsdown Migration](https://tresjs.org/blog/tresjs-tsdown-migration)
377
+
378
+ - [Dual publish ESM and CJS with tsdown](https://dev.to/hacksore/dual-publish-esm-and-cjs-with-tsdown-2l75)
379
+
380
+ * * *
381
+
382
+ ### 4. Package Exports & Dual Module Support
383
+
384
+ #### Subpath Exports
385
+
386
+ **Status**: Essential
387
+
388
+ **Details**:
389
+
390
+ The `exports` field in `package.json` enables:
391
+
392
+ - Multiple entry points (`./cli`, `./adapter`)
393
+
394
+ - Conditional exports (ESM vs CJS, types vs runtime)
395
+
396
+ - Package encapsulation (only expose intended APIs)
397
+
398
+ **Critical rule**: The `"types"` condition must come first in each export block.
399
+
400
+ **Example `package.json` exports**:
401
+
402
+ ```json
403
+ {
404
+ "name": "@scope/package-name",
405
+ "type": "module",
406
+ "main": "./dist/index.cjs",
407
+ "module": "./dist/index.js",
408
+ "types": "./dist/index.d.ts",
409
+ "exports": {
410
+ ".": {
411
+ "import": {
412
+ "types": "./dist/index.d.ts",
413
+ "default": "./dist/index.js"
414
+ },
415
+ "require": {
416
+ "types": "./dist/index.d.cts",
417
+ "default": "./dist/index.cjs"
418
+ }
419
+ },
420
+ "./cli": {
421
+ "import": {
422
+ "types": "./dist/cli.d.ts",
423
+ "default": "./dist/cli.js"
424
+ },
425
+ "require": {
426
+ "types": "./dist/cli.d.cts",
427
+ "default": "./dist/cli.cjs"
428
+ }
429
+ },
430
+ "./package.json": "./package.json"
431
+ },
432
+ "bin": {
433
+ "package-name": "./dist/bin.js"
434
+ },
435
+ "files": ["dist"]
436
+ }
437
+ ```
438
+
439
+ **Assessment**: Subpath exports are essential for future-proofing.
440
+ They allow splitting packages later without breaking the API surface—`@scope/pkg/cli`
441
+ can remain stable even if internals move to `@scope/pkg-cli`.
442
+
443
+ **References**:
444
+
445
+ - [Guide to package.json exports field](https://hirok.io/posts/package-json-exports)
446
+
447
+ - [Node.js Packages documentation](https://nodejs.org/api/packages.html)
448
+
449
+ - [Ship ESM & CJS in one Package](https://antfu.me/posts/publish-esm-and-cjs)
450
+
451
+ - [Building npm package compatible with ESM and CJS in 2024](https://snyk.io/blog/building-npm-package-compatible-with-esm-and-cjs-2024/)
452
+
453
+ * * *
454
+
455
+ #### Separate Declaration Files for ESM/CJS
456
+
457
+ **Status**: Required
458
+
459
+ **Details**:
460
+
461
+ Each entry point needs separate declaration files for ESM (`.d.ts`) and CJS (`.d.cts`).
462
+ TypeScript interprets declaration files as ESM or CJS based on file extension and the
463
+ package’s `type` field.
464
+
465
+ Using a single `.d.ts` for both formats will cause TypeScript errors for consumers using
466
+ one of the module systems.
467
+
468
+ **Assessment**: tsdown handles this automatically when `dts: true` is configured.
469
+
470
+ **References**:
471
+
472
+ - [TypeScript Modules Reference](https://www.typescriptlang.org/docs/handbook/modules/reference.html)
473
+
474
+ - [Publishing dual ESM+CJS packages](https://mayank.co/blog/dual-packages/)
475
+
476
+ * * *
477
+
478
+ ### 5. Optional Peer Dependencies
479
+
480
+ #### Strategy for Optional Integrations
481
+
482
+ **Status**: Recommended
483
+
484
+ **Details**:
485
+
486
+ For packages that optionally integrate with external SDKs (AI SDKs, MCP servers, etc.),
487
+ use:
488
+
489
+ 1. **Optional peer dependencies**: Don’t force installation
490
+
491
+ 2. **Subpath exports**: Isolate optional code in separate entry points
492
+
493
+ 3. **Dynamic imports**: Only load the SDK when the subpath is actually imported
494
+
495
+ **`package.json` configuration**:
496
+
497
+ ```json
498
+ {
499
+ "peerDependencies": {
500
+ "@modelcontextprotocol/sdk": "^1.0.0",
501
+ "ai": "^5.0.0"
502
+ },
503
+ "peerDependenciesMeta": {
504
+ "@modelcontextprotocol/sdk": { "optional": true },
505
+ "ai": { "optional": true }
506
+ }
507
+ }
508
+ ```
509
+
510
+ **Implementation pattern** (`src/adapters/mcp/index.ts`):
511
+
512
+ ```typescript
513
+ export async function createMcpServer(options: McpServerOptions) {
514
+ // Dynamic import only when this code path is executed
515
+ const { Server } = await import('@modelcontextprotocol/sdk/server');
516
+ return new Server(options);
517
+ }
518
+ ```
519
+
520
+ **Assessment**: This pattern ensures the main package remains lightweight while
521
+ providing rich integrations for users who need them.
522
+
523
+ **References**:
524
+
525
+ - [tsdown Dependencies handling](https://tsdown.dev/options/dependencies)
526
+
527
+ - [npm peer dependencies documentation](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#peerdependenciesmeta)
528
+
529
+ * * *
530
+
531
+ ### 6. Package Validation
532
+
533
+ #### publint
534
+
535
+ **Status**: Essential
536
+
537
+ **Details**:
538
+
539
+ publint validates that packages will work correctly across different environments (Vite,
540
+ Webpack, Rollup, Node.js).
541
+ It checks:
542
+
543
+ - Export field validity
544
+
545
+ - File existence for declared exports
546
+
547
+ - ESM/CJS format correctness
548
+
549
+ - Type declaration alignment
550
+
551
+ - Common configuration mistakes
552
+
553
+ **Integration**:
554
+
555
+ ```json
556
+ {
557
+ "scripts": {
558
+ "publint": "publint",
559
+ "prepack": "pnpm build"
560
+ },
561
+ "devDependencies": {
562
+ "publint": "^0.3.0"
563
+ }
564
+ }
565
+ ```
566
+
567
+ **CI Integration**: Run `pnpm publint` after build in CI to catch publishing issues
568
+ before release.
569
+
570
+ **Assessment**: publint catches issues that would only surface after users install the
571
+ package. Essential for any published package.
572
+
573
+ **References**:
574
+
575
+ - [publint documentation](https://publint.dev/docs/)
576
+
577
+ - [publint rules](https://publint.dev/rules)
578
+
579
+ * * *
580
+
581
+ ### 7. Versioning & Release Automation
582
+
583
+ #### Changesets
584
+
585
+ **Status**: Strongly Recommended
586
+
587
+ **Details**:
588
+
589
+ Changesets provides:
590
+
591
+ - **Intent-based versioning**: Developers declare the impact (patch/minor/major) when
592
+ making changes
593
+
594
+ - **Automated changelogs**: Generated from changeset descriptions
595
+
596
+ - **Monorepo-aware**: Handles inter-package dependencies automatically
597
+
598
+ - **CI integration**: GitHub Action opens release PRs and publishes automatically
599
+
600
+ **Setup**:
601
+
602
+ 1. Initialize: `pnpm add -Dw @changesets/cli && pnpm changeset init`
603
+
604
+ 2. Configure `.changeset/config.json`:
605
+
606
+ ```json
607
+ {
608
+ "$schema": "https://unpkg.com/@changesets/config/schema.json",
609
+ "changelog": "@changesets/changelog-github",
610
+ "commit": false,
611
+ "fixed": [],
612
+ "linked": [],
613
+ "access": "public",
614
+ "baseBranch": "main",
615
+ "updateInternalDependencies": "patch"
616
+ }
617
+ ```
618
+
619
+ 3. Root scripts:
620
+
621
+ ```json
622
+ {
623
+ "scripts": {
624
+ "changeset": "changeset",
625
+ "version-packages": "changeset version",
626
+ "release": "pnpm build && pnpm publint && changeset publish"
627
+ }
628
+ }
629
+ ```
630
+
631
+ **Workflow**:
632
+
633
+ 1. Developer runs `pnpm changeset` and describes changes
634
+
635
+ 2. PR includes the changeset file
636
+
637
+ 3. On merge to main, GitHub Action either:
638
+
639
+ - Opens a “Version Packages” PR (accumulating changesets)
640
+
641
+ - Publishes to npm when that PR is merged
642
+
643
+ **Assessment**: Changesets is the de facto standard for monorepo versioning.
644
+ It integrates seamlessly with pnpm and GitHub Actions.
645
+
646
+ **References**:
647
+
648
+ - [Using Changesets with pnpm](https://pnpm.io/using-changesets)
649
+
650
+ - [Changesets GitHub repository](https://github.com/changesets/changesets)
651
+
652
+ - [Frontend Handbook: Changesets](https://infinum.com/handbook/frontend/changesets)
653
+
654
+ * * *
655
+
656
+ #### Dynamic Git-Based Versioning
657
+
658
+ **Status**: Recommended for dev builds
659
+
660
+ **Details**:
661
+
662
+ While Changesets handles release versioning, development builds benefit from dynamic
663
+ git-based version strings.
664
+ This provides traceability during development without manual version bumps.
665
+
666
+ **Format**: `X.Y.Z-dev.N.hash`
667
+
668
+ | State | Format | Example |
669
+ | --- | --- | --- |
670
+ | On tag | `X.Y.Z` | `1.2.3` |
671
+ | After tag | `X.Y.Z-dev.N.hash` | `1.2.4-dev.12.a1b2c3d` |
672
+ | Dirty working dir | `X.Y.Z-dev.N.hash-dirty` | `1.2.4-dev.12.a1b2c3d-dirty` |
673
+ | No tags | `X.Y.Z-dev.N.hash` | `0.1.0-dev.42.a1b2c3d` (uses package.json version + total commits) |
674
+
675
+ **Key design decisions**:
676
+
677
+ 1. **Bump patch for dev versions**: Ensures correct semver sorting—dev versions sort
678
+ *before* the next release, not after the current one
679
+
680
+ 2. **Hash in pre-release, not metadata**: npm strips build metadata (`+hash`), so embed
681
+ the hash in the pre-release identifier (`-dev.N.hash`)
682
+
683
+ 3. **Dirty marker**: Identifies uncommitted changes during development
684
+
685
+ 4. **No git dependency in runtime**: The published package should not depend on git
686
+ being present. Git version detection happens only at build time or in dev scripts.
687
+
688
+ 5. **Single source of truth**: Extract version logic to a shared script that both the
689
+ build config and dev scripts can use.
690
+
691
+ **Why roll your own?**
692
+
693
+ No npm package provides build-time git version injection with env var support for dev
694
+ mode:
695
+
696
+ | Package | Issue |
697
+ | --- | --- |
698
+ | [git-describe](https://github.com/tvdstaaij/node-git-describe) | Last updated 2019, abandoned |
699
+ | [version-from-git](https://github.com/compulim/version-from-git) | Modifies package.json, not build-time injection |
700
+ | [esbuild-plugin-version-injector](https://github.com/favware/esbuild-plugin-version-injector) | Only injects package.json version, no git info |
701
+ | [rollup-plugin-git-version](https://www.npmjs.com/package/rollup-plugin-git-version) | Rollup-only, abandoned (2018) |
702
+
703
+ The ~60 lines of custom code is dependency-free, bundler-agnostic, and handles all edge
704
+ cases (no tags, dirty state, dev mode).
705
+ Python’s [setuptools-scm](https://github.com/pypa/setuptools-scm) is the gold standard;
706
+ this pattern is “setuptools-scm lite” for Node.js.
707
+
708
+ **Architecture Overview**:
709
+
710
+ The versioning system works in three contexts:
711
+
712
+ | Context | Version Source | Example |
713
+ | --- | --- | --- |
714
+ | Production build | Build-time injection via `__TBD_VERSION__` | `1.2.4-dev.12.a1b2c3d` |
715
+ | Dev mode (tsx) | Environment variable `TBD_DEV_VERSION` | `1.2.4-dev.12.a1b2c3d` |
716
+ | Fallback | package.json version | `0.1.0` |
717
+
718
+ **File Structure**:
719
+
720
+ ```
721
+ packages/my-cli/
722
+ ├── scripts/
723
+ │ ├── git-version.mjs # Shared git version logic (not distributed)
724
+ │ └── git-version.d.mts # TypeScript declarations
725
+ ├── src/
726
+ │ ├── index.ts # Library entry with VERSION constant
727
+ │ └── cli/
728
+ │ └── lib/
729
+ │ └── version.ts # CLI version resolution (no git dependency)
730
+ └── tsdown.config.ts # Imports from scripts/git-version.mjs
731
+ ```
732
+
733
+ **Step 1: Shared Git Version Script** (`scripts/git-version.mjs`):
734
+
735
+ ```js
736
+ /* global process, console */
737
+ /**
738
+ * Git-based version detection for build and dev scripts.
739
+ * Format: X.Y.Z-dev.N.hash
740
+ */
741
+ import { execSync } from 'node:child_process';
742
+ import { readFileSync } from 'node:fs';
743
+ import { fileURLToPath } from 'node:url';
744
+ import { dirname, join } from 'node:path';
745
+
746
+ const __dirname = dirname(fileURLToPath(import.meta.url));
747
+ const pkgPath = join(__dirname, '../package.json');
748
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
749
+
750
+ function git(args) {
751
+ return execSync(`git ${args}`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
752
+ }
753
+
754
+ function isDirty() {
755
+ try {
756
+ git('diff --quiet');
757
+ git('diff --cached --quiet');
758
+ return false;
759
+ } catch {
760
+ return true;
761
+ }
762
+ }
763
+
764
+ export function getGitVersion() {
765
+ // Try tag-based version first
766
+ try {
767
+ const tag = git('describe --tags --abbrev=0');
768
+ const tagVersion = tag.replace(/^v/, '');
769
+ const [major, minor, patch] = tagVersion.split('.').map(Number);
770
+ const commitsSinceTag = parseInt(git(`rev-list ${tag}..HEAD --count`), 10);
771
+ const hash = git('rev-parse --short=7 HEAD');
772
+ const dirty = isDirty();
773
+
774
+ if (commitsSinceTag === 0 && !dirty) {
775
+ return tagVersion;
776
+ }
777
+
778
+ const bumpedPatch = (patch ?? 0) + 1;
779
+ const suffix = dirty ? `${hash}-dirty` : hash;
780
+ return `${major}.${minor}.${bumpedPatch}-dev.${commitsSinceTag}.${suffix}`;
781
+ } catch {
782
+ // No tags - use package.json version with total commit count
783
+ try {
784
+ const totalCommits = parseInt(git('rev-list --count HEAD'), 10);
785
+ const hash = git('rev-parse --short=7 HEAD');
786
+ const dirty = isDirty();
787
+ const suffix = dirty ? `${hash}-dirty` : hash;
788
+ return `${pkg.version}-dev.${totalCommits}.${suffix}`;
789
+ } catch {
790
+ // Not a git repo
791
+ return pkg.version;
792
+ }
793
+ }
794
+ }
795
+
796
+ // When run directly, print version to stdout
797
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
798
+ console.log(getGitVersion());
799
+ }
800
+ ```
801
+
802
+ **Step 2: TypeScript Declarations** (`scripts/git-version.d.mts`):
803
+
804
+ ```ts
805
+ /**
806
+ * Get git-based version string.
807
+ * Format: X.Y.Z-dev.N.hash
808
+ */
809
+ export function getGitVersion(): string;
810
+ ```
811
+
812
+ **Step 3: Build Config** (`tsdown.config.ts`):
813
+
814
+ ```ts
815
+ import { defineConfig } from 'tsdown';
816
+ import { getGitVersion } from './scripts/git-version.mjs';
817
+
818
+ const version = getGitVersion();
819
+
820
+ export default defineConfig({
821
+ // ...
822
+ define: {
823
+ __TBD_VERSION__: JSON.stringify(version),
824
+ },
825
+ });
826
+ ```
827
+
828
+ **Step 4: Library Entry** (`src/index.ts`):
829
+
830
+ ```ts
831
+ declare const __TBD_VERSION__: string;
832
+
833
+ export const VERSION: string =
834
+ typeof __TBD_VERSION__ !== 'undefined' ? __TBD_VERSION__ : 'development';
835
+ ```
836
+
837
+ **Step 5: CLI Version Module** (`src/cli/lib/version.ts`):
838
+
839
+ ```ts
840
+ /**
841
+ * CLI version detection - no git dependency at runtime
842
+ *
843
+ * Priority:
844
+ * 1. Build-time injected __TBD_VERSION__ (production builds)
845
+ * 2. TBD_DEV_VERSION env var (dev mode, set by pnpm tbd script)
846
+ * 3. package.json version (fallback)
847
+ */
848
+ import { createRequire } from 'node:module';
849
+ import { VERSION as BUILD_VERSION } from '../../index.js';
850
+
851
+ function getVersion(): string {
852
+ // 1. Build-time injected version (production)
853
+ if (BUILD_VERSION !== 'development') {
854
+ return BUILD_VERSION;
855
+ }
856
+
857
+ // 2. Dev mode env var (set by pnpm tbd script)
858
+ if (process.env.TBD_DEV_VERSION) {
859
+ return process.env.TBD_DEV_VERSION;
860
+ }
861
+
862
+ // 3. Fallback to package.json version
863
+ const require = createRequire(import.meta.url);
864
+ const pkg = require('../../../package.json') as { version: string };
865
+ return pkg.version;
866
+ }
867
+
868
+ export const VERSION = getVersion();
869
+ ```
870
+
871
+ **Step 6: Dev Script** (`package.json`):
872
+
873
+ ```json
874
+ {
875
+ "scripts": {
876
+ "dev": "TBD_DEV_VERSION=$(node scripts/git-version.mjs) tsx src/cli/bin.ts"
877
+ }
878
+ }
879
+ ```
880
+
881
+ **Why This Pattern**:
882
+
883
+ | Concern | Solution |
884
+ | --- | --- |
885
+ | No git in runtime | Git logic only in scripts/ (not distributed) |
886
+ | Dev mode works | Env var passes version from script to tsx |
887
+ | Production works | Build-time injection via define |
888
+ | Single source of truth | One implementation in git-version.mjs |
889
+ | TypeScript support | Declaration file for type checking |
890
+ | Fallback safety | Graceful degradation to package.json |
891
+
892
+ **Comparison with Python (uv-dynamic-versioning)**:
893
+
894
+ | Aspect | npm (this approach) | Python (PEP 440) |
895
+ | --- | --- | --- |
896
+ | Format | `1.2.4-dev.12.a1b2c3d` | `1.2.4.dev12+a1b2c3d` |
897
+ | Metadata handling | In pre-release (preserved) | Local version `+` (may be stripped) |
898
+ | Sorting | Standard semver | PEP 440 compliant |
899
+ | Configuration | Shared script + bundler config | In `pyproject.toml` |
900
+
901
+ **Assessment**: This pattern provides the best balance of flexibility, maintainability,
902
+ and runtime independence.
903
+ Dynamic versioning complements Changesets—use Changesets for releases and git-based
904
+ versioning for development builds, with zero git dependency in the published package.
905
+
906
+ * * *
907
+
908
+ ### 8. Testing
909
+
910
+ #### Vitest
911
+
912
+ **Status**: Recommended
913
+
914
+ **Details**:
915
+
916
+ Vitest is the recommended test runner for pnpm/Node.js monorepos.
917
+ It provides Jest-compatible APIs with native TypeScript and ESM support, powered by
918
+ Vite’s transformation pipeline.
919
+
920
+ **Key features**:
921
+
922
+ - Jest-compatible API (`describe`, `it`, `expect`, `vi.mock`, etc.)
923
+
924
+ - Native TypeScript and ESM support (no separate ts-jest)
925
+
926
+ - Watch mode with HMR (re-runs only affected tests)
927
+
928
+ - Snapshot testing
929
+
930
+ - Code coverage via v8 provider (built-in)
931
+
932
+ - Test isolation by default
933
+
934
+ - Browser Mode (stable in Vitest 4.0) for real browser testing
935
+
936
+ - Visual regression testing (added in Vitest 4.0)
937
+
938
+ **Installation**:
939
+
940
+ ```bash
941
+ pnpm add -D vitest @vitest/coverage-v8
942
+ ```
943
+
944
+ **Running tests**:
945
+
946
+ ```bash
947
+ pnpm vitest # Watch mode (default)
948
+ pnpm vitest run # Run once
949
+ pnpm vitest --coverage # With coverage
950
+ pnpm vitest --ui # UI mode
951
+ ```
952
+
953
+ **Configuration** (`vitest.config.ts`):
954
+
955
+ ```typescript
956
+ import { defineConfig } from 'vitest/config';
957
+
958
+ export default defineConfig({
959
+ test: {
960
+ globals: true,
961
+ environment: 'node',
962
+ include: ['**/*.{test,spec}.{ts,tsx}'],
963
+ coverage: {
964
+ provider: 'v8',
965
+ reporter: ['text', 'json', 'html'],
966
+ exclude: ['**/node_modules/**', '**/dist/**'],
967
+ },
968
+ },
969
+ });
970
+ ```
971
+
972
+ **Example test**:
973
+
974
+ ```typescript
975
+ import { describe, it, expect, vi } from 'vitest';
976
+ import { processData } from '../src/index';
977
+
978
+ describe('processData', () => {
979
+ it('handles valid input', () => {
980
+ expect(processData({ key: 'value' })).toEqual({ key: 'VALUE' });
981
+ });
982
+
983
+ it('throws on invalid input', () => {
984
+ expect(() => processData(null)).toThrow('Invalid input');
985
+ });
986
+ });
987
+ ```
988
+
989
+ **Comparison with other test runners**:
990
+
991
+ | Criteria | Vitest | Jest | Node test runner | Bun test |
992
+ | --- | --- | --- | --- | --- |
993
+ | Speed | Fast | Moderate | Fast | Fastest |
994
+ | Setup | Minimal config | Moderate config | Zero-config | Zero-config |
995
+ | TypeScript | Native | Via ts-jest | Via flag/tsx | Native |
996
+ | API | Jest-compatible | Jest | Node-native | Jest-compatible |
997
+ | Isolation | Isolated by default | Isolated | Isolated | No isolation |
998
+ | Browser Mode | Stable (v4.0) | Limited | None | None |
999
+ | Coverage | v8 built-in | Via jest-coverage | Built-in | Built-in |
1000
+ | IDE support | Excellent | Excellent | Moderate | Basic |
1001
+
1002
+ **Assessment**: Vitest 4.0 is the mature choice for pnpm/Node.js monorepos, offering
1003
+ test isolation, excellent IDE integration, browser mode for component testing, and full
1004
+ Jest API compatibility.
1005
+ Use it for all TypeScript monorepo projects.
1006
+
1007
+ **References**:
1008
+
1009
+ - [Vitest Documentation](https://vitest.dev/)
1010
+
1011
+ - [Vitest 4.0 Announcement](https://vitest.dev/blog/vitest-4)
1012
+
1013
+ - [Vitest Browser Mode](https://vitest.dev/guide/browser/)
1014
+
1015
+ * * *
1016
+
1017
+ ### 9. CI/CD Configuration
1018
+
1019
+ #### GitHub Actions: CI Workflow
1020
+
1021
+ **Status**: Recommended
1022
+
1023
+ **`.github/workflows/ci.yml`**:
1024
+
1025
+ ```yaml
1026
+ name: CI
1027
+
1028
+ on:
1029
+ pull_request:
1030
+ push:
1031
+ branches: [main]
1032
+
1033
+ jobs:
1034
+ test:
1035
+ runs-on: ubuntu-latest
1036
+ steps:
1037
+ - uses: actions/checkout@v6
1038
+
1039
+ - uses: pnpm/action-setup@v4
1040
+ with:
1041
+ version: 10
1042
+
1043
+ - uses: actions/setup-node@v6
1044
+ with:
1045
+ node-version: 24
1046
+ cache: pnpm
1047
+
1048
+ - run: pnpm install --frozen-lockfile
1049
+ - run: pnpm format:check
1050
+ - run: pnpm lint:check
1051
+ - run: pnpm build
1052
+ - run: pnpm publint
1053
+ - run: pnpm test
1054
+ ```
1055
+
1056
+ **Key points**:
1057
+
1058
+ - Node.js 24 is the current LTS ("Krypton", active until Oct 2026, maintained until Apr
1059
+ 2028\)
1060
+
1061
+ - `actions/checkout@v6` requires Actions Runner v2.329.0+ (stores credentials under
1062
+ $RUNNER_TEMP)
1063
+
1064
+ - `pnpm/action-setup@v4` includes built-in caching
1065
+
1066
+ - `actions/setup-node@v6` with `cache: pnpm` provides additional caching
1067
+
1068
+ - `--frozen-lockfile` ensures CI uses exact versions from lockfile
1069
+
1070
+ **References**:
1071
+
1072
+ - [pnpm action-setup](https://github.com/pnpm/action-setup)
1073
+
1074
+ - [pnpm Continuous Integration](https://pnpm.io/continuous-integration)
1075
+
1076
+ * * *
1077
+
1078
+ #### GitHub Actions: Release Workflow
1079
+
1080
+ **Status**: Recommended
1081
+
1082
+ **`.github/workflows/release.yml`**:
1083
+
1084
+ ```yaml
1085
+ name: Release
1086
+
1087
+ on:
1088
+ push:
1089
+ branches: [main]
1090
+
1091
+ permissions:
1092
+ contents: write
1093
+ pull-requests: write
1094
+
1095
+ jobs:
1096
+ release:
1097
+ runs-on: ubuntu-latest
1098
+ steps:
1099
+ - uses: actions/checkout@v6
1100
+ with:
1101
+ fetch-depth: 0
1102
+
1103
+ - uses: pnpm/action-setup@v4
1104
+ with:
1105
+ version: 10
1106
+
1107
+ - uses: actions/setup-node@v6
1108
+ with:
1109
+ node-version: 24
1110
+ cache: pnpm
1111
+ registry-url: 'https://registry.npmjs.org'
1112
+
1113
+ - run: pnpm install --frozen-lockfile
1114
+
1115
+ - name: Create Release PR or Publish
1116
+ uses: changesets/action@v1
1117
+ with:
1118
+ publish: pnpm release
1119
+ env:
1120
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1121
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
1122
+ ```
1123
+
1124
+ **Repository settings required**:
1125
+
1126
+ - Settings → Actions → General → Workflow permissions → **Read and write**
1127
+
1128
+ - Add `NPM_TOKEN` secret when ready to publish to npm
1129
+
1130
+ **References**:
1131
+
1132
+ - [Changesets GitHub Action](https://github.com/changesets/action)
1133
+
1134
+ - [Using Changesets with pnpm](https://pnpm.io/using-changesets)
1135
+
1136
+ * * *
1137
+
1138
+ ### 10. Code Formatting
1139
+
1140
+ #### Prettier
1141
+
1142
+ **Status**: Essential
1143
+
1144
+ **Details**:
1145
+
1146
+ Prettier provides consistent code formatting across the project.
1147
+ Configure it once and let it handle all formatting decisions automatically.
1148
+
1149
+ **Installation**:
1150
+
1151
+ ```bash
1152
+ pnpm add -Dw prettier eslint-config-prettier
1153
+ ```
1154
+
1155
+ **`.prettierrc`**:
1156
+
1157
+ ```json
1158
+ {
1159
+ "$schema": "https://json.schemastore.org/prettierrc",
1160
+ "printWidth": 100,
1161
+ "singleQuote": true,
1162
+ "trailingComma": "all",
1163
+ "semi": true,
1164
+ "arrowParens": "always",
1165
+ "tabWidth": 2,
1166
+ "useTabs": false
1167
+ }
1168
+ ```
1169
+
1170
+ **`.prettierignore`**:
1171
+
1172
+ ```
1173
+ dist
1174
+ node_modules
1175
+ pnpm-lock.yaml
1176
+ *.min.js
1177
+ *.min.css
1178
+ coverage
1179
+ ```
1180
+
1181
+ **Key configuration choices**:
1182
+
1183
+ | Option | Recommended | Rationale |
1184
+ | --- | --- | --- |
1185
+ | `printWidth` | 100 | Wider than default 80; fits modern screens |
1186
+ | `singleQuote` | true | Common in JS ecosystem, less visual noise |
1187
+ | `trailingComma` | "all" | Cleaner diffs, easier reordering |
1188
+ | `semi` | true | Explicit; avoids ASI edge cases |
1189
+
1190
+ **Assessment**: Prettier eliminates formatting debates and ensures consistency.
1191
+ Use `eslint-config-prettier` to disable ESLint rules that conflict with Prettier.
1192
+
1193
+ **References**:
1194
+
1195
+ - [Prettier Documentation](https://prettier.io/docs/)
1196
+
1197
+ - [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier)
1198
+
1199
+ * * *
1200
+
1201
+ #### Format Scripts Pattern
1202
+
1203
+ **Status**: Recommended
1204
+
1205
+ **Details**:
1206
+
1207
+ Structure format and lint scripts to support both auto-fix and CI verification modes.
1208
+
1209
+ **Root `package.json` scripts**:
1210
+
1211
+ ```json
1212
+ {
1213
+ "scripts": {
1214
+ "format": "prettier --write --log-level warn .",
1215
+ "format:check": "prettier --check --log-level warn .",
1216
+ "lint": "eslint . --fix && pnpm typecheck && eslint . --max-warnings 0",
1217
+ "lint:check": "pnpm typecheck && eslint . --max-warnings 0",
1218
+ "typecheck": "tsc -b",
1219
+ "build": "pnpm format && pnpm lint:check && <build-command>"
1220
+ }
1221
+ }
1222
+ ```
1223
+
1224
+ **Script purposes**:
1225
+
1226
+ | Script | Purpose | When to use |
1227
+ | --- | --- | --- |
1228
+ | `format` | Auto-format changed files (quiet for unchanged) | Local development |
1229
+ | `format:check` | Verify formatting (quiet for valid files) | CI |
1230
+ | `lint` | Lint with auto-fix, then verify zero warnings | Local development |
1231
+ | `lint:check` | Lint without fix, zero warnings | CI, pre-build |
1232
+ | `build` | Format, lint, then build | Production builds |
1233
+
1234
+ **Key insight**: The `lint` script runs ESLint twice: first with `--fix` to auto-fix
1235
+ issues, then again with `--max-warnings 0` to catch any unfixable warnings.
1236
+ This ensures auto-fix doesn’t mask problems that require manual attention.
1237
+
1238
+ **Key insight**: Using `--log-level warn` with Prettier suppresses the verbose output
1239
+ that lists every unchanged file.
1240
+ This keeps output clean—only files that were actually changed (or have issues in check
1241
+ mode) are shown.
1242
+
1243
+ **Key insight**: The `build` script runs `format` before `lint:check`. This ensures
1244
+ formatting is applied before linting, catching any formatting issues that would fail the
1245
+ lint check.
1246
+
1247
+ **Assessment**: Separating `--fix` variants (for local use) from `--check` variants (for
1248
+ CI) provides the best developer experience while ensuring CI catches issues.
1249
+
1250
+ * * *
1251
+
1252
+ ### 11. Git Hooks & Local Validation
1253
+
1254
+ #### Lefthook
1255
+
1256
+ **Status**: Recommended
1257
+
1258
+ **Details**:
1259
+
1260
+ Lefthook is a fast, cross-platform Git hooks manager written in Go.
1261
+ It provides a better developer experience than Husky + lint-staged while being faster
1262
+ and having no Node.js runtime dependency for the hook runner itself.
1263
+
1264
+ **Why Lefthook over Husky + lint-staged**:
1265
+
1266
+ | Aspect | Lefthook | Husky + lint-staged |
1267
+ | --- | --- | --- |
1268
+ | Runtime | Go binary (fast) | Node.js (slower startup) |
1269
+ | Configuration | Single YAML file | Multiple config files |
1270
+ | Parallel execution | Built-in | Requires configuration |
1271
+ | Staged files | Native support | Via lint-staged |
1272
+ | Monorepo support | Excellent (`root:` option) | Requires workarounds |
1273
+
1274
+ **Installation**:
1275
+
1276
+ ```bash
1277
+ pnpm add -Dw lefthook
1278
+ npx lefthook install
1279
+ ```
1280
+
1281
+ **References**:
1282
+
1283
+ - [Lefthook Documentation](https://github.com/evilmartians/lefthook)
1284
+
1285
+ - [Lefthook vs Husky](https://evilmartians.com/chronicles/lefthook-knock-your-teams-code-back-into-shape)
1286
+
1287
+ * * *
1288
+
1289
+ #### Pre-commit Hooks Strategy
1290
+
1291
+ **Status**: Recommended
1292
+
1293
+ **Details**:
1294
+
1295
+ Pre-commit hooks should be **fast** (target: 2-5 seconds) to avoid disrupting developer
1296
+ flow.
1297
+ Run checks in parallel, operate only on staged files, and use caching aggressively.
1298
+
1299
+ **Key principles**:
1300
+
1301
+ 1. **Parallel execution**: Run independent checks simultaneously
1302
+
1303
+ 2. **Staged files only**: Don’t waste time checking unchanged code
1304
+
1305
+ 3. **Auto-fix and re-stage**: Fix formatting/linting issues automatically
1306
+
1307
+ 4. **Incremental type checking**: Use TypeScript’s `--incremental` flag
1308
+
1309
+ 5. **Cache everything**: ESLint cache, TypeScript build info, etc.
1310
+
1311
+ **Example `lefthook.yml` (pre-commit)**:
1312
+
1313
+ ```yaml
1314
+ pre-commit:
1315
+ parallel: true
1316
+
1317
+ commands:
1318
+ # Auto-format with prettier (~500ms)
1319
+ format:
1320
+ glob: '*.{js,ts,tsx,json}'
1321
+ run: npx prettier --write --log-level warn {staged_files}
1322
+ stage_fixed: true
1323
+ priority: 1
1324
+
1325
+ # Lint with auto-fix and caching (~1s first, ~200ms cached)
1326
+ lint:
1327
+ glob: '*.{js,ts,tsx}'
1328
+ run: >
1329
+ npx eslint
1330
+ --cache
1331
+ --cache-location node_modules/.cache/eslint
1332
+ --fix {staged_files}
1333
+ stage_fixed: true
1334
+ priority: 2
1335
+
1336
+ # Type check with incremental mode (~2s)
1337
+ typecheck:
1338
+ glob: '*.{ts,tsx}'
1339
+ run: npx tsc --noEmit --incremental
1340
+ priority: 3
1341
+ ```
1342
+
1343
+ **Monorepo considerations**: Use `root:` to scope commands to specific packages:
1344
+
1345
+ ```yaml
1346
+ commands:
1347
+ lint:
1348
+ root: 'packages/core/'
1349
+ glob: '*.{ts,tsx}'
1350
+ run: npx eslint --fix {staged_files}
1351
+ ```
1352
+
1353
+ **Assessment**: Fast pre-commit hooks catch issues early without slowing down commits.
1354
+ The auto-fix pattern reduces friction—developers don’t need to manually format code.
1355
+
1356
+ * * *
1357
+
1358
+ #### Pre-push Hooks Strategy
1359
+
1360
+ **Status**: Recommended
1361
+
1362
+ **Details**:
1363
+
1364
+ Pre-push hooks can be **slower** (target: 3-5s with cache, <30s without) since pushes
1365
+ are less frequent. Use these for comprehensive validation that would be too slow for
1366
+ pre-commit.
1367
+
1368
+ **Key principles**:
1369
+
1370
+ 1. **Run full test suite**: Not just changed files
1371
+
1372
+ 2. **Use commit-hash caching**: Skip tests if already passed for this commit
1373
+
1374
+ 3. **Detect uncommitted changes**: Re-run tests if working tree is dirty
1375
+
1376
+ 4. **Provide clear escape hatch**: Document `--no-verify` for emergencies
1377
+
1378
+ **Example `lefthook.yml` (pre-push)**:
1379
+
1380
+ ```yaml
1381
+ pre-push:
1382
+ commands:
1383
+ verify-tests:
1384
+ run: |
1385
+ echo "🔍 Checking test status for push..."
1386
+
1387
+ COMMIT_HASH=$(git rev-parse HEAD)
1388
+ CACHE_DIR="node_modules/.test-cache"
1389
+ CACHE_FILE="$CACHE_DIR/$COMMIT_HASH"
1390
+
1391
+ # Check for uncommitted changes
1392
+ if ! git diff --quiet || ! git diff --cached --quiet; then
1393
+ echo "⚠️ Uncommitted changes detected"
1394
+ echo "📊 Running test suite..."
1395
+ pnpm test
1396
+ exit $?
1397
+ fi
1398
+
1399
+ # Check cache
1400
+ if [ -f "$CACHE_FILE" ]; then
1401
+ SHORT_HASH=$(echo "$COMMIT_HASH" | cut -c1-8)
1402
+ echo "✓ Tests already passed for commit $SHORT_HASH"
1403
+ exit 0
1404
+ fi
1405
+
1406
+ # No cache, run tests
1407
+ echo "📊 Running test suite..."
1408
+ pnpm test
1409
+
1410
+ # Cache on success
1411
+ if [ $? -eq 0 ]; then
1412
+ mkdir -p "$CACHE_DIR"
1413
+ touch "$CACHE_FILE"
1414
+ echo "✅ Tests cached for commit $(echo $COMMIT_HASH | cut -c1-8)"
1415
+ else
1416
+ echo "❌ Tests failed - push blocked"
1417
+ echo "Bypass with: git push --no-verify"
1418
+ exit 1
1419
+ fi
1420
+ ```
1421
+
1422
+ **Assessment**: Commit-hash caching ensures tests only run once per commit, making
1423
+ repeated push attempts instant.
1424
+ This is especially valuable when rebasing or when a push fails for non-test reasons.
1425
+
1426
+ * * *
1427
+
1428
+ #### CI vs Local Hook Relationship
1429
+
1430
+ **Status**: Best Practice
1431
+
1432
+ **Details**:
1433
+
1434
+ Local hooks and CI should complement each other:
1435
+
1436
+ | Check | Pre-commit | Pre-push | CI |
1437
+ | --- | --- | --- | --- |
1438
+ | Format | ✅ Auto-fix | — | ✅ Verify |
1439
+ | Lint | ✅ Auto-fix | — | ✅ Verify |
1440
+ | Typecheck | ✅ Incremental | — | ✅ Full |
1441
+ | Unit tests | ⚠️ Changed only | ✅ Full | ✅ Full |
1442
+ | Integration tests | — | ⚠️ Optional | ✅ Full |
1443
+ | Build | — | — | ✅ Full |
1444
+ | publint | — | — | ✅ Full |
1445
+
1446
+ **Key insight**: Pre-commit hooks fix issues, CI verifies correctness.
1447
+ Never skip CI because hooks passed—hooks can be bypassed with `--no-verify`.
1448
+
1449
+ **Root `package.json` integration**:
1450
+
1451
+ ```json
1452
+ {
1453
+ "scripts": {
1454
+ "prepare": "lefthook install"
1455
+ },
1456
+ "devDependencies": {
1457
+ "lefthook": "^2.0.0"
1458
+ }
1459
+ }
1460
+ ```
1461
+
1462
+ **References**:
1463
+
1464
+ - [Lefthook Configuration](https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md)
1465
+
1466
+ - [Git Hooks Best Practices](https://pre-commit.com/#introduction)
1467
+
1468
+ * * *
1469
+
1470
+ ### 12. Dependency Upgrade Management
1471
+
1472
+ #### npm-check-updates (ncu)
1473
+
1474
+ **Status**: Recommended
1475
+
1476
+ **Details**:
1477
+
1478
+ npm-check-updates (`ncu`) provides a safe, structured approach to keeping dependencies
1479
+ current. It supports upgrade targets that let you control how aggressively to upgrade,
1480
+ making it easy to separate low-risk minor/patch updates from potentially breaking major
1481
+ updates.
1482
+
1483
+ **Installation**:
1484
+
1485
+ ```bash
1486
+ pnpm add -Dw npm-check-updates
1487
+ ```
1488
+
1489
+ **Key flags**:
1490
+
1491
+ | Flag | Description |
1492
+ | --- | --- |
1493
+ | `--target minor` | Only upgrade to latest minor/patch (safe) |
1494
+ | `--target patch` | Only upgrade to latest patch (safest) |
1495
+ | `--target latest` | Upgrade to latest version (includes major) |
1496
+ | `--format group` | Group output by update type (major/minor/patch) |
1497
+ | `--interactive` | Select which packages to upgrade |
1498
+ | `-u` | Update package.json (otherwise just reports) |
1499
+
1500
+ **Upgrade Targets Explained**:
1501
+
1502
+ - **patch**: Only upgrade `1.0.0` → `1.0.x` (bug fixes only)
1503
+
1504
+ - **minor**: Upgrade `1.0.0` → `1.x.x` (new features, backwards compatible)
1505
+
1506
+ - **latest**: Upgrade to latest published version (may include breaking changes)
1507
+
1508
+ - **newest**: Upgrade to newest version, even if not latest (e.g., prereleases)
1509
+
1510
+ - **greatest**: Upgrade to greatest version number
1511
+
1512
+ **Assessment**: Using upgrade targets separates routine maintenance (minor/patch) from
1513
+ potentially breaking changes (major), enabling a safer, more frequent upgrade cadence.
1514
+
1515
+ **References**:
1516
+
1517
+ - [npm-check-updates documentation](https://www.npmjs.com/package/npm-check-updates)
1518
+
1519
+ - [ncu GitHub repository](https://github.com/raineorshine/npm-check-updates)
1520
+
1521
+ * * *
1522
+
1523
+ #### Upgrade Scripts Pattern
1524
+
1525
+ **Status**: Recommended
1526
+
1527
+ **Details**:
1528
+
1529
+ Add structured upgrade scripts to your root `package.json` that encode your upgrade
1530
+ workflow. This makes upgrades consistent and discoverable.
1531
+
1532
+ **Root `package.json` scripts**:
1533
+
1534
+ ```json
1535
+ {
1536
+ "scripts": {
1537
+ "upgrade:check": "ncu --format group",
1538
+ "upgrade": "ncu --target minor -u && pnpm install && pnpm test",
1539
+ "upgrade:patch": "ncu --target patch -u && pnpm install && pnpm test",
1540
+ "upgrade:major": "ncu --target latest --interactive --format group"
1541
+ }
1542
+ }
1543
+ ```
1544
+
1545
+ **Script descriptions**:
1546
+
1547
+ | Script | Purpose |
1548
+ | --- | --- |
1549
+ | `upgrade:check` | Show available updates grouped by type (no changes) |
1550
+ | `upgrade` | Safe upgrade: minor+patch versions, install, and test |
1551
+ | `upgrade:patch` | Conservative upgrade: patch versions only |
1552
+ | `upgrade:major` | Interactive selection for major version changes |
1553
+
1554
+ **Workflow**:
1555
+
1556
+ 1. **Check for updates**: `pnpm upgrade:check` — see what’s available without changing
1557
+ anything
1558
+
1559
+ 2. **Safe upgrade**: `pnpm upgrade` — upgrade minor/patch versions and run tests to
1560
+ verify nothing breaks
1561
+
1562
+ 3. **Major upgrades**: `pnpm upgrade:major` — interactively review major version bumps,
1563
+ select which to apply, then test and review changelogs
1564
+
1565
+ **Key insight**: Running tests after `upgrade` catches regressions immediately.
1566
+ If tests fail, you can `git checkout package.json pnpm-lock.yaml && pnpm install` to
1567
+ rollback before investigating.
1568
+
1569
+ **Assessment**: This pattern enables frequent, low-risk dependency updates while
1570
+ maintaining control over potentially breaking changes.
1571
+
1572
+ * * *
1573
+
1574
+ #### Monorepo Considerations
1575
+
1576
+ **Status**: Best Practice
1577
+
1578
+ **Details**:
1579
+
1580
+ In a pnpm monorepo, run ncu from the workspace root to update all packages consistently:
1581
+
1582
+ ```bash
1583
+ # Check all workspace packages
1584
+ pnpm ncu --format group -ws
1585
+
1586
+ # Upgrade minor versions in all packages
1587
+ pnpm ncu --target minor -u -ws && pnpm install && pnpm test
1588
+ ```
1589
+
1590
+ For selective package updates:
1591
+
1592
+ ```bash
1593
+ # Upgrade specific packages only
1594
+ pnpm ncu --filter "@scope/*" --target minor -u
1595
+ ```
1596
+
1597
+ **Handling peer dependency conflicts**:
1598
+
1599
+ Some packages may have strict peer dependency requirements that conflict during
1600
+ upgrades. Options:
1601
+
1602
+ 1. **Use `--legacy-peer-deps`** (npm): `npm install --legacy-peer-deps`
1603
+
1604
+ 2. **Pin conflicting versions**: Lock specific versions in `pnpm.overrides`:
1605
+
1606
+ ```json
1607
+ {
1608
+ "pnpm": {
1609
+ "overrides": {
1610
+ "react": "^18.3.0"
1611
+ }
1612
+ }
1613
+ }
1614
+ ```
1615
+
1616
+ 3. **Staged upgrades**: Upgrade conflicting packages together in one commit
1617
+
1618
+ **References**:
1619
+
1620
+ - [pnpm overrides documentation](https://pnpm.io/package_json#pnpmoverrides)
1621
+
1622
+ * * *
1623
+
1624
+ ### 13. CLI Development Workflow
1625
+
1626
+ #### Running CLI from Source
1627
+
1628
+ **Status**: Strongly Recommended
1629
+
1630
+ **Details**:
1631
+
1632
+ During development, CLI commands should run directly from TypeScript source rather than
1633
+ requiring a build step.
1634
+ This ensures developers always work with the current code and eliminates the common
1635
+ frustration of debugging stale builds.
1636
+
1637
+ **The dual-script pattern**:
1638
+
1639
+ ```json
1640
+ {
1641
+ "scripts": {
1642
+ "cli-name": "tsx packages/package-name/src/cli/bin.ts",
1643
+ "cli-name:bin": "node packages/package-name/dist/bin.mjs"
1644
+ }
1645
+ }
1646
+ ```
1647
+
1648
+ | Script | Purpose | When to use |
1649
+ | --- | --- | --- |
1650
+ | `cli-name` | Runs source via tsx | Development—always current, no build needed |
1651
+ | `cli-name:bin` | Runs built binary | Pre-release verification of published output |
1652
+
1653
+ **Why this matters**:
1654
+
1655
+ 1. **No stale builds**: Developers never accidentally run old code while debugging
1656
+
1657
+ 2. **Faster iteration**: No build step between code changes and testing
1658
+
1659
+ 3. **Reduced confusion**: “Did I forget to build?”
1660
+ is never the answer
1661
+
1662
+ 4. **Still verifiable**: The `:bin` variant ensures the production build works correctly
1663
+
1664
+ * * *
1665
+
1666
+ #### tsx vs vite-node vs ts-node
1667
+
1668
+ **Status**: tsx Recommended
1669
+
1670
+ **Details**:
1671
+
1672
+ For running TypeScript CLI commands directly, **tsx** is the recommended choice:
1673
+
1674
+ | Aspect | tsx | vite-node | ts-node |
1675
+ | --- | --- | --- | --- |
1676
+ | **Speed** | 5-10x faster than ts-node | Fast (esbuild) | Slow |
1677
+ | **Startup time** | Single-digit milliseconds | Fast | Noticeable delay |
1678
+ | **Configuration** | Zero-config | Requires Vite familiarity | Often needs config |
1679
+ | **Use case** | CLI and scripts | Vite ecosystem projects | Legacy projects |
1680
+ | **Maintenance** | Active | Active | Active but slower |
1681
+
1682
+ **When to choose each**:
1683
+
1684
+ - **tsx**: Default choice for CLI development, scripts, and simple TypeScript execution
1685
+
1686
+ - **vite-node**: When you need Vite’s plugin ecosystem (e.g., CSS imports, asset
1687
+ handling)
1688
+
1689
+ - **ts-node**: Only for legacy projects already using it
1690
+
1691
+ **Example implementation**:
1692
+
1693
+ ```json
1694
+ {
1695
+ "scripts": {
1696
+ "markform": "tsx packages/markform/src/cli/bin.ts",
1697
+ "markform:bin": "node packages/markform/dist/bin.mjs"
1698
+ },
1699
+ "devDependencies": {
1700
+ "tsx": "^4.21.0"
1701
+ }
1702
+ }
1703
+ ```
1704
+
1705
+ **Assessment**: tsx provides the best developer experience for CLI development.
1706
+ It uses esbuild for near-instant compilation while maintaining compatibility with all
1707
+ modern TypeScript features.
1708
+ Reserve vite-node for projects that specifically need Vite’s transformation pipeline.
1709
+
1710
+ **References**:
1711
+
1712
+ - [tsx documentation](https://tsx.is/)
1713
+
1714
+ - [TSX vs ts-node comparison](https://betterstack.com/community/guides/scaling-nodejs/tsx-vs-ts-node/)
1715
+
1716
+ - [ts-runtime-comparison benchmarks](https://github.com/privatenumber/ts-runtime-comparison)
1717
+
1718
+ * * *
1719
+
1720
+ ### 14. Private Package Distribution
1721
+
1722
+ #### Option A: GitHub Packages (Recommended)
1723
+
1724
+ **Status**: Recommended for teams
1725
+
1726
+ **Details**:
1727
+
1728
+ GitHub Packages provides a private npm registry with standard npm semantics.
1729
+
1730
+ **Requirements**:
1731
+
1732
+ - Package must be scoped (`@org/package-name`)
1733
+
1734
+ - Repository name should match organization/scope
1735
+
1736
+ **Publisher `.npmrc`**:
1737
+
1738
+ ```ini
1739
+ @your-org:registry=https://npm.pkg.github.com
1740
+ //npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
1741
+ ```
1742
+
1743
+ **Consumer `.npmrc`**:
1744
+
1745
+ ```ini
1746
+ @your-org:registry=https://npm.pkg.github.com/
1747
+ //npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN
1748
+ ```
1749
+
1750
+ **Install command**: `pnpm add @your-org/package-name`
1751
+
1752
+ **Assessment**: Lowest-friction option for teams.
1753
+ Works exactly like npm but private.
1754
+ No build-on-install quirks.
1755
+
1756
+ **References**:
1757
+
1758
+ - [GitHub npm registry documentation](https://docs.github.com/packages/working-with-a-github-packages-registry/working-with-the-npm-registry)
1759
+
1760
+ - [Publish NPM Package to GitHub Packages Registry](https://www.neteye-blog.com/2024/09/publish-npm-package-to-github-packages-registry-with-github-actions/)
1761
+
1762
+ * * *
1763
+
1764
+ #### Option B: Direct GitHub Install (pnpm)
1765
+
1766
+ **Status**: Viable for development
1767
+
1768
+ **Details**:
1769
+
1770
+ pnpm v9+ supports installing from a monorepo subdirectory:
1771
+
1772
+ ```bash
1773
+ pnpm add github:org/repo#path:packages/package-name
1774
+ ```
1775
+
1776
+ **Caveats**:
1777
+
1778
+ - Requires the package to be pre-built (dist must exist) OR lifecycle scripts must build
1779
+ it
1780
+
1781
+ - Less reliable than registry-based installs
1782
+
1783
+ - Version pinning is less precise
1784
+
1785
+ **Assessment**: Good for rapid development and testing.
1786
+ Use GitHub Packages or npm for production.
1787
+
1788
+ **References**:
1789
+
1790
+ - [pnpm discussion: Add dependency from git monorepo](https://github.com/orgs/pnpm/discussions/8194)
1791
+
1792
+ * * *
1793
+
1794
+ #### Option C: Local Linking
1795
+
1796
+ **Status**: Best for development
1797
+
1798
+ **Details**:
1799
+
1800
+ For active development across repositories:
1801
+
1802
+ ```bash
1803
+ # In consumer project
1804
+ pnpm add ../path-to-monorepo/packages/package-name
1805
+ ```
1806
+
1807
+ Or use `pnpm link`:
1808
+
1809
+ ```bash
1810
+ # In package directory
1811
+ pnpm link --global
1812
+
1813
+ # In consumer project
1814
+ pnpm link --global @scope/package-name
1815
+ ```
1816
+
1817
+ **Assessment**: Essential for local development iteration.
1818
+ Not suitable for distribution.
1819
+
1820
+ * * *
1821
+
1822
+ #### Bun Compatibility Note
1823
+
1824
+ **Status**: Limited
1825
+
1826
+ **Details**:
1827
+
1828
+ Bun supports GitHub dependencies but has limited support for monorepo subdirectory
1829
+ installs. For Bun consumers, GitHub Packages or npm publishing provides the smoothest
1830
+ experience.
1831
+
1832
+ **References**:
1833
+
1834
+ - [Bun: Add a Git dependency](https://bun.sh/docs/guides/install/add-git)
1835
+
1836
+ - [Bun issue: Support installing Git dependency from subdirectory](https://github.com/oven-sh/bun/issues/15506)
1837
+
1838
+ * * *
1839
+
1840
+ ### 15. Library/CLI Hybrid Packages
1841
+
1842
+ #### Node-Free Core Pattern
1843
+
1844
+ **Status**: Recommended
1845
+
1846
+ **Details**:
1847
+
1848
+ When building a package that functions as both a library and a CLI tool, **isolate all
1849
+ Node.js dependencies to CLI-only code**. This allows the core library to be used in
1850
+ non-Node environments (browsers, edge runtimes, Cloudflare Workers, Convex, etc.).
1851
+
1852
+ Node.js-specific imports like `node:path`, `node:fs`, or `node:module` will cause
1853
+ bundler errors or runtime failures in non-Node environments.
1854
+ Even if only the CLI uses these imports, if they’re in shared code, the entire library
1855
+ becomes Node-dependent.
1856
+
1857
+ **Directory Structure for Isolation**:
1858
+
1859
+ Keep CLI code in a dedicated subdirectory:
1860
+
1861
+ ```
1862
+ src/
1863
+ ├── index.ts # Library entry point (NO node: imports)
1864
+ ├── settings.ts # Configuration constants (NO node: imports)
1865
+ ├── engine/ # Core library code (NO node: imports)
1866
+ ├── cli/ # CLI-only code (node: imports OK here)
1867
+ │ ├── cli.ts # CLI entry point
1868
+ │ ├── commands/ # Command implementations
1869
+ │ └── lib/ # CLI utilities (path resolution, etc.)
1870
+ └── integrations/ # Optional integrations (NO node: imports)
1871
+ ```
1872
+
1873
+ **Assessment**: This pattern is essential for libraries targeting multiple runtimes.
1874
+ The directory structure creates clear boundaries that are easy to enforce with automated
1875
+ tests.
1876
+
1877
+ * * *
1878
+
1879
+ #### Pattern: Move Node.js Utilities to CLI
1880
+
1881
+ **Status**: Recommended
1882
+
1883
+ **Details**:
1884
+
1885
+ Configuration constants belong in node-free files; functions that use Node.js APIs
1886
+ belong in CLI-specific code:
1887
+
1888
+ ```ts
1889
+ // BAD: Node.js import in shared settings
1890
+ // src/settings.ts
1891
+ import { resolve } from 'node:path';
1892
+
1893
+ export const DEFAULT_OUTPUT_DIR = './output';
1894
+
1895
+ export function getOutputDir(override?: string): string {
1896
+ return resolve(process.cwd(), override ?? DEFAULT_OUTPUT_DIR);
1897
+ }
1898
+
1899
+ // GOOD: Constant in settings, function in CLI
1900
+ // src/settings.ts (node-free)
1901
+ export const DEFAULT_OUTPUT_DIR = './output';
1902
+
1903
+ // src/cli/lib/paths.ts (node: imports OK)
1904
+ import { resolve } from 'node:path';
1905
+ import { DEFAULT_OUTPUT_DIR } from '../../settings.js';
1906
+
1907
+ export { DEFAULT_OUTPUT_DIR }; // Re-export for CLI convenience
1908
+
1909
+ export function getOutputDir(override?: string): string {
1910
+ return resolve(process.cwd(), override ?? DEFAULT_OUTPUT_DIR);
1911
+ }
1912
+ ```
1913
+
1914
+ **Assessment**: This pattern keeps the core library portable while providing full
1915
+ Node.js functionality in CLI contexts.
1916
+
1917
+ * * *
1918
+
1919
+ #### Pattern: Build-Time Constants
1920
+
1921
+ **Status**: Recommended
1922
+
1923
+ **Details**:
1924
+
1925
+ For values that need Node.js at build time (like reading `package.json`), use bundler
1926
+ `define` options to inject them as compile-time constants:
1927
+
1928
+ ```ts
1929
+ // tsdown.config.ts / esbuild / rollup config
1930
+ import pkg from './package.json' with { type: 'json' };
1931
+
1932
+ export default {
1933
+ define: {
1934
+ __VERSION__: JSON.stringify(pkg.version),
1935
+ },
1936
+ };
1937
+
1938
+ // src/index.ts (node-free)
1939
+ declare const __VERSION__: string;
1940
+
1941
+ export const VERSION: string = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'development';
1942
+ ```
1943
+
1944
+ **Assessment**: Build-time injection eliminates runtime Node.js dependencies for values
1945
+ that are constant at build time.
1946
+ This is cleaner than dynamic requires or filesystem reads.
1947
+
1948
+ * * *
1949
+
1950
+ #### Guard Tests for Node-Free Core
1951
+
1952
+ **Status**: Strongly Recommended
1953
+
1954
+ **Details**:
1955
+
1956
+ Add automated tests to prevent Node.js dependency leaks:
1957
+
1958
+ ```ts
1959
+ // tests/node-free-core.test.ts
1960
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
1961
+ import { join, relative } from 'node:path';
1962
+
1963
+ const SRC_DIR = 'src';
1964
+ const NODE_ALLOWED_DIRS = ['cli']; // Only CLI can use node:
1965
+ const NODE_IMPORT_PATTERN = /from\s+['"]node:/g;
1966
+
1967
+ function getAllTsFiles(dir: string): string[] {
1968
+ /* recursive scan */
1969
+ }
1970
+
1971
+ describe('Node-free core library', () => {
1972
+ it('source files outside cli/ should not import from node:', () => {
1973
+ const violations: string[] = [];
1974
+
1975
+ for (const file of getAllTsFiles(SRC_DIR)) {
1976
+ const rel = relative(SRC_DIR, file);
1977
+ if (NODE_ALLOWED_DIRS.some((d) => rel.startsWith(d + '/'))) continue;
1978
+
1979
+ const content = readFileSync(file, 'utf-8');
1980
+ if (NODE_IMPORT_PATTERN.test(content)) {
1981
+ violations.push(rel);
1982
+ }
1983
+ }
1984
+
1985
+ expect(violations).toHaveLength(0);
1986
+ });
1987
+
1988
+ it('dist/index.mjs should not reference node: modules', () => {
1989
+ const content = readFileSync('dist/index.mjs', 'utf-8');
1990
+ expect(content).not.toMatch(NODE_IMPORT_PATTERN);
1991
+ });
1992
+ });
1993
+ ```
1994
+
1995
+ **Assessment**: Guard tests catch accidental node: imports during development rather
1996
+ than discovering them when users try to use the library in browser/edge contexts.
1997
+
1998
+ * * *
1999
+
2000
+ #### Checklist for Library/CLI Packages
2001
+
2002
+ **Status**: Best Practice
2003
+
2004
+ **Checklist**:
2005
+
2006
+ - [ ] Core library entry point (`index.ts`) has no `node:` imports
2007
+
2008
+ - [ ] All `node:` imports are in `cli/` directory only
2009
+
2010
+ - [ ] Configuration constants are in node-free files
2011
+
2012
+ - [ ] Build-time values use bundler `define` injection
2013
+
2014
+ - [ ] Guard tests prevent future regressions
2015
+
2016
+ - [ ] Built output (`dist/*.mjs`) has no `node:` references
2017
+
2018
+ **References**:
2019
+
2020
+ - [CLI Tool Development Rules](../../agent-rules/typescript-cli-tool-rules.md) —
2021
+ CLI-specific patterns using Commander.js, picocolors, and @clack/prompts
2022
+
2023
+ * * *
2024
+
2025
+ ## Comparative Analysis
2026
+
2027
+ ### Build Tools Comparison
2028
+
2029
+ | Criteria | tsdown | tsup | unbuild | Rollup |
2030
+ | --- | --- | --- | --- | --- |
2031
+ | Active maintenance | Yes | No (abandoned) | Yes | Yes |
2032
+ | ESM-first | Yes | No (CJS-first) | Yes | Yes |
2033
+ | DTS generation | Built-in | Built-in | Built-in | Plugin required |
2034
+ | Multi-entry | Yes | Yes | Yes | Yes |
2035
+ | Config simplicity | Excellent | Good | Good | Complex |
2036
+ | Speed | Fast (Rust) | Fast (esbuild) | Moderate | Moderate |
2037
+ | Plugin ecosystem | Rolldown/Rollup/Vite | esbuild | unbuild | Rollup |
2038
+
2039
+ **Recommendation**: tsdown for new projects; migrate from tsup if currently using it.
2040
+
2041
+ * * *
2042
+
2043
+ ### Package Manager Comparison
2044
+
2045
+ | Criteria | pnpm | npm | yarn |
2046
+ | --- | --- | --- | --- |
2047
+ | Disk efficiency | Excellent | Poor | Moderate |
2048
+ | Workspace support | Built-in | Built-in (v7+) | Built-in |
2049
+ | Strict mode | Yes (default) | No | Optional |
2050
+ | Speed | Fast | Moderate | Fast |
2051
+ | Monorepo tooling | Excellent | Basic | Good |
2052
+
2053
+ **Recommendation**: pnpm for monorepos.
2054
+
2055
+ * * *
2056
+
2057
+ ## Best Practices
2058
+
2059
+ 1. **Scope your package names**: Use `@org/package-name` format for easier GitHub
2060
+ Packages integration and namespace clarity.
2061
+
2062
+ 2. **Structure for splitting**: Organize internal code (`core/`, `cli/`, `adapters/`) to
2063
+ make future package splits painless.
2064
+
2065
+ 3. **Use subpath exports from day one**: Define `./cli`, `./adapter` exports even in
2066
+ v0.1 to stabilize the API surface.
2067
+
2068
+ 4. **Types first in exports**: Always put `"types"` condition before `"default"` in
2069
+ export conditions.
2070
+
2071
+ 5. **Optional peer deps for integrations**: Don’t force SDK dependencies on users who
2072
+ don’t need them.
2073
+
2074
+ 6. **Validate before publish**: Run publint in CI and before every release.
2075
+
2076
+ 7. **Changeset per PR**: Require changesets for user-facing changes to maintain accurate
2077
+ changelogs.
2078
+
2079
+ 8. **Lock your tooling versions**: Pin exact versions in `packageManager` field and CI
2080
+ configurations.
2081
+
2082
+ 9. **Test both ESM and CJS**: Ensure both module formats work correctly, especially for
2083
+ CLI tools.
2084
+
2085
+ 10. **Keep the monorepo root private**: The root `package.json` should have
2086
+ `"private": true` and only contain workspace tooling.
2087
+
2088
+ 11. **Use type-aware ESLint**: Configure `recommendedTypeChecked` for comprehensive bug
2089
+ detection, especially promise safety rules.
2090
+ See Appendix C for detailed configuration.
2091
+
2092
+ 12. **Enforce code style consistency**: Use `curly: 'all'` and `brace-style: '1tbs'` to
2093
+ prevent subtle bugs and improve readability.
2094
+
2095
+ 13. **Use fast pre-commit hooks**: Run formatting and linting with auto-fix on staged
2096
+ files only. Target 2-5 seconds total.
2097
+ Use lefthook for better monorepo support.
2098
+
2099
+ 14. **Cache test results by commit hash**: In pre-push hooks, skip test runs if the
2100
+ current commit has already passed tests.
2101
+ This makes repeated pushes instant.
2102
+
2103
+ 15. **Use structured upgrade scripts**: Add `upgrade:check`, `upgrade`, and
2104
+ `upgrade:major` scripts to make dependency updates consistent and safe.
2105
+ Separate minor/patch from major upgrades.
2106
+
2107
+ 16. **Separate format and lint script variants**: Provide `format`/`format:check` and
2108
+ `lint`/`lint:check` scripts.
2109
+ Use `--fix` variants for local development and `--check`/zero-warnings variants for
2110
+ CI.
2111
+
2112
+ 17. **Run format before lint in builds**: The `build` script should run `format` then
2113
+ `lint:check` to ensure formatting is applied before linting.
2114
+
2115
+ 18. **Use dynamic git-based versioning**: Inject version at build time using
2116
+ `X.Y.Z-dev.N.hash` format.
2117
+ This provides traceability during development without manual version bumps.
2118
+ See “Dynamic Git-Based Versioning” section for implementation.
2119
+
2120
+ 19. **Run CLI from source during development**: Use the dual-script pattern with tsx to
2121
+ run CLI commands directly from TypeScript source.
2122
+ Provide a separate `:bin` script for verifying the built output.
2123
+ This eliminates “did I forget to build?”
2124
+ confusion.
2125
+
2126
+ * * *
2127
+
2128
+ ## Open Research Questions
2129
+
2130
+ 1. **Rolldown Vite Library Mode**: tsdown is positioned to become the foundation for
2131
+ Rolldown Vite’s Library Mode.
2132
+ Monitor for announcements that may affect best practices.
2133
+
2134
+ 2. **TypeScript 6.0/7.0 Transition**: TypeScript 7.0 (Go rewrite) is now available in
2135
+ Visual Studio 2026 Insiders preview with ~8x faster project load times.
2136
+ Install via `npm install -D @typescript/native-preview`. TypeScript 6.0 will serve as
2137
+ a “bridge” release deprecating features for 7.0 alignment.
2138
+ Monitor for migration guidance as 7.0 approaches stable.
2139
+
2140
+ 3. **Native TypeScript Execution**: TypeScript 5.8+ supports `--erasableSyntaxOnly`
2141
+ flag, enabling direct execution in Node.js 23.6+ without transpilation.
2142
+ This may reduce the need for tsx in some workflows.
2143
+ Monitor for broader adoption and tooling support.
2144
+
2145
+ 4. **ESLint v10 multi-config**: ESLint v10.0.0 is in RC phase (Jan 2026) with improved
2146
+ multi-config support, Node.js ^20.19.0 || ^22.13.0 || >=24 required, and complete
2147
+ removal of eslintrc config system.
2148
+ Monitor for final release.
2149
+
2150
+ * * *
2151
+
2152
+ ## Recommendations
2153
+
2154
+ ### Summary
2155
+
2156
+ Use a pnpm monorepo with tsdown for building, Changesets for versioning, publint for
2157
+ validation, Prettier for code formatting, lefthook for fast local git hooks, and
2158
+ npm-check-updates for structured dependency upgrades.
2159
+ Structure code internally for future splits while exposing a stable API through subpath
2160
+ exports.
2161
+ Start with GitHub Packages for private distribution, then transition to npm when
2162
+ ready for public release.
2163
+
2164
+ ### Recommended Approach
2165
+
2166
+ 1. **Initialize workspace** with pnpm and a single package in `packages/`
2167
+
2168
+ 2. **Configure tsdown** for dual ESM/CJS output with TypeScript declarations
2169
+
2170
+ 3. **Set up subpath exports** for main entry and any adapters/integrations
2171
+
2172
+ 4. **Add Changesets** for version management
2173
+
2174
+ 5. **Configure Prettier** with eslint-config-prettier for consistent formatting
2175
+
2176
+ 6. **Configure lefthook** for pre-commit (format, lint, typecheck) and pre-push (tests)
2177
+
2178
+ 7. **Add upgrade scripts** for structured dependency management
2179
+
2180
+ 8. **Configure CI** with format:check, lint:check, typecheck, build, publint, and test
2181
+
2182
+ 9. **Configure release workflow** with Changesets GitHub Action
2183
+
2184
+ 10. **Validate with publint** before every release
2185
+
2186
+ **Rationale**:
2187
+
2188
+ - Minimal overhead to start, clear path to scale
2189
+
2190
+ - Industry-standard tooling with active maintenance
2191
+
2192
+ - Supports both private and public distribution
2193
+
2194
+ - Enables fast iteration without accumulating technical debt
2195
+
2196
+ ### Alternative Approaches
2197
+
2198
+ - **Nx or Turborepo**: For larger monorepos with complex dependency graphs, consider
2199
+ adding Nx or Turborepo for caching and task orchestration.
2200
+ The pnpm + Changesets foundation integrates well with both.
2201
+
2202
+ - **unbuild**: If Rolldown/Vite ecosystem alignment isn’t important, unbuild is another
2203
+ solid choice with a different plugin ecosystem.
2204
+
2205
+ - **Single-package repo**: For truly simple packages that will never grow, a
2206
+ non-monorepo structure is fine.
2207
+ However, the monorepo structure overhead is minimal and provides flexibility.
2208
+
2209
+ * * *
2210
+
2211
+ ## References
2212
+
2213
+ ### Official Documentation
2214
+
2215
+ - [pnpm Workspaces](https://pnpm.io/workspaces)
2216
+
2217
+ - [pnpm Using Changesets](https://pnpm.io/using-changesets)
2218
+
2219
+ - [pnpm Continuous Integration](https://pnpm.io/continuous-integration)
2220
+
2221
+ - [tsdown Documentation](https://tsdown.dev/)
2222
+
2223
+ - [publint Documentation](https://publint.dev/docs/)
2224
+
2225
+ - [Prettier Documentation](https://prettier.io/docs/)
2226
+
2227
+ - [Changesets GitHub](https://github.com/changesets/changesets)
2228
+
2229
+ - [Node.js Packages (exports)](https://nodejs.org/api/packages.html)
2230
+
2231
+ - [TypeScript Module Documentation](https://www.typescriptlang.org/docs/handbook/modules/reference.html)
2232
+
2233
+ - [GitHub Packages npm registry](https://docs.github.com/packages/working-with-a-github-packages-registry/working-with-the-npm-registry)
2234
+
2235
+ - [Node.js Releases](https://nodejs.org/en/about/previous-releases)
2236
+
2237
+ ### Guides & Articles
2238
+
2239
+ - [Complete Monorepo Guide 2025](https://jsdev.space/complete-monorepo-guide/)
2240
+
2241
+ - [Guide to package.json exports field](https://hirok.io/posts/package-json-exports)
2242
+
2243
+ - [Ship ESM & CJS in one Package](https://antfu.me/posts/publish-esm-and-cjs)
2244
+
2245
+ - [Building npm package compatible with ESM and CJS in 2024](https://snyk.io/blog/building-npm-package-compatible-with-esm-and-cjs-2024/)
2246
+
2247
+ - [TypeScript in 2025: ESM and CJS publishing](https://lirantal.com/blog/typescript-in-2025-with-esm-and-cjs-npm-publishing)
2248
+
2249
+ - [Switching from tsup to tsdown](https://alan.norbauer.com/articles/tsdown-bundler/)
2250
+
2251
+ - [Live types in a TypeScript monorepo](https://colinhacks.com/essays/live-types-typescript-monorepo)
2252
+
2253
+ - [Is nodenext right for libraries?](https://blog.andrewbran.ch/is-nodenext-right-for-libraries-that-dont-target-node-js/)
2254
+
2255
+ ### GitHub Actions
2256
+
2257
+ - [pnpm/action-setup](https://github.com/pnpm/action-setup)
2258
+
2259
+ - [changesets/action](https://github.com/changesets/action)
2260
+
2261
+ * * *
2262
+
2263
+ ## Appendices
2264
+
2265
+ ### Appendix A: Complete package.json Example
2266
+
2267
+ ```json
2268
+ {
2269
+ "name": "@scope/package-name",
2270
+ "version": "0.1.0",
2271
+ "description": "Package description",
2272
+ "license": "MIT",
2273
+ "type": "module",
2274
+ "sideEffects": false,
2275
+ "main": "./dist/index.cjs",
2276
+ "module": "./dist/index.js",
2277
+ "types": "./dist/index.d.ts",
2278
+ "exports": {
2279
+ ".": {
2280
+ "import": {
2281
+ "types": "./dist/index.d.ts",
2282
+ "default": "./dist/index.js"
2283
+ },
2284
+ "require": {
2285
+ "types": "./dist/index.d.cts",
2286
+ "default": "./dist/index.cjs"
2287
+ }
2288
+ },
2289
+ "./cli": {
2290
+ "import": {
2291
+ "types": "./dist/cli.d.ts",
2292
+ "default": "./dist/cli.js"
2293
+ },
2294
+ "require": {
2295
+ "types": "./dist/cli.d.cts",
2296
+ "default": "./dist/cli.cjs"
2297
+ }
2298
+ },
2299
+ "./package.json": "./package.json"
2300
+ },
2301
+ "bin": {
2302
+ "package-name": "./dist/bin.js"
2303
+ },
2304
+ "files": ["dist"],
2305
+ "engines": {
2306
+ "node": ">=24"
2307
+ },
2308
+ "scripts": {
2309
+ "build": "tsdown",
2310
+ "dev": "tsdown --watch",
2311
+ "typecheck": "tsc -p tsconfig.json --noEmit",
2312
+ "test": "node --test",
2313
+ "publint": "publint",
2314
+ "prepack": "pnpm build"
2315
+ },
2316
+ "dependencies": {},
2317
+ "peerDependencies": {
2318
+ "optional-sdk": "^1.0.0"
2319
+ },
2320
+ "peerDependenciesMeta": {
2321
+ "optional-sdk": { "optional": true }
2322
+ },
2323
+ "devDependencies": {
2324
+ "@types/node": "^24.0.0",
2325
+ "publint": "^0.3.0",
2326
+ "tsdown": "^0.18.0",
2327
+ "typescript": "^5.9.0"
2328
+ }
2329
+ }
2330
+ ```
2331
+
2332
+ ### Appendix B: Root package.json Example
2333
+
2334
+ ```json
2335
+ {
2336
+ "name": "project-workspace",
2337
+ "private": true,
2338
+ "packageManager": "pnpm@10.27.0",
2339
+ "engines": {
2340
+ "node": ">=24"
2341
+ },
2342
+ "scripts": {
2343
+ "build": "pnpm -r build",
2344
+ "typecheck": "pnpm -r typecheck",
2345
+ "test": "pnpm -r test",
2346
+ "publint": "pnpm -r publint",
2347
+ "format": "prettier --write --log-level warn .",
2348
+ "format:check": "prettier --check --log-level warn .",
2349
+ "lint": "eslint . --fix && pnpm typecheck && eslint . --max-warnings 0",
2350
+ "lint:check": "pnpm typecheck && eslint . --max-warnings 0",
2351
+ "prepare": "lefthook install",
2352
+ "changeset": "changeset",
2353
+ "version-packages": "changeset version",
2354
+ "release": "pnpm build && pnpm publint && changeset publish",
2355
+ "upgrade:check": "ncu --format group",
2356
+ "upgrade": "ncu --target minor -u && pnpm install && pnpm test",
2357
+ "upgrade:major": "ncu --target latest --interactive --format group"
2358
+ },
2359
+ "devDependencies": {
2360
+ "@changesets/cli": "^2.29.0",
2361
+ "@changesets/changelog-github": "^0.5.0",
2362
+ "@eslint/js": "^9.0.0",
2363
+ "eslint": "^9.0.0",
2364
+ "eslint-config-prettier": "^10.0.0",
2365
+ "lefthook": "^2.0.0",
2366
+ "npm-check-updates": "^19.0.0",
2367
+ "prettier": "^3.0.0",
2368
+ "typescript": "^5.9.0",
2369
+ "typescript-eslint": "^8.0.0"
2370
+ }
2371
+ }
2372
+ ```
2373
+
2374
+ ### Appendix C: ESLint Flat Config Example
2375
+
2376
+ #### Minimal Configuration
2377
+
2378
+ For projects just getting started, a minimal configuration:
2379
+
2380
+ ```javascript
2381
+ // eslint.config.js
2382
+ import js from '@eslint/js';
2383
+ import tseslint from 'typescript-eslint';
2384
+ import prettier from 'eslint-config-prettier';
2385
+
2386
+ export default [
2387
+ js.configs.recommended,
2388
+ ...tseslint.configs.recommended,
2389
+ prettier, // Must be last to override conflicting rules
2390
+ {
2391
+ ignores: ['**/dist/**', '**/node_modules/**', '**/.pnpm-store/**'],
2392
+ },
2393
+ ];
2394
+ ```
2395
+
2396
+ #### Strict Type-Aware Configuration (Recommended)
2397
+
2398
+ For production projects, use type-aware linting with strict rules.
2399
+ This catches more bugs but requires tsconfig integration:
2400
+
2401
+ ```javascript
2402
+ // eslint.config.js
2403
+ import js from '@eslint/js';
2404
+ import tseslint from 'typescript-eslint';
2405
+ import prettier from 'eslint-config-prettier';
2406
+
2407
+ // Type-aware ESLint configuration using flat config.
2408
+ // Uses TypeScript's project service for precise, cross-project type information.
2409
+
2410
+ // Apply type-checked configs only to TypeScript files
2411
+ const typedRecommended = tseslint.configs.recommendedTypeChecked.map((cfg) => ({
2412
+ ...cfg,
2413
+ files: ['**/*.ts', '**/*.tsx'],
2414
+ languageOptions: {
2415
+ ...(cfg.languageOptions ?? {}),
2416
+ parserOptions: {
2417
+ ...(cfg.languageOptions?.parserOptions ?? {}),
2418
+ projectService: true,
2419
+ tsconfigRootDir: import.meta.dirname,
2420
+ },
2421
+ },
2422
+ }));
2423
+
2424
+ const typedStylistic = tseslint.configs.stylisticTypeChecked.map((cfg) => ({
2425
+ ...cfg,
2426
+ files: ['**/*.ts', '**/*.tsx'],
2427
+ languageOptions: {
2428
+ ...(cfg.languageOptions ?? {}),
2429
+ parserOptions: {
2430
+ ...(cfg.languageOptions?.parserOptions ?? {}),
2431
+ projectService: true,
2432
+ tsconfigRootDir: import.meta.dirname,
2433
+ },
2434
+ },
2435
+ }));
2436
+
2437
+ export default [
2438
+ // Global ignores
2439
+ {
2440
+ ignores: ['**/dist/**', '**/node_modules/**', '**/.pnpm-store/**', 'eslint.config.*'],
2441
+ },
2442
+
2443
+ // Base JS rules
2444
+ js.configs.recommended,
2445
+
2446
+ // Type-aware TypeScript rules
2447
+ ...typedRecommended,
2448
+ ...typedStylistic,
2449
+
2450
+ // Prettier config must be last to override conflicting rules
2451
+ prettier,
2452
+
2453
+ // TypeScript-specific rules
2454
+ {
2455
+ files: ['**/*.ts', '**/*.tsx'],
2456
+ rules: {
2457
+ // === Code Style ===
2458
+ // Enforce curly braces for all control statements (prevents bugs)
2459
+ curly: ['error', 'all'],
2460
+ // Consistent brace style: opening on same line, closing on new line
2461
+ 'brace-style': ['error', '1tbs', { allowSingleLine: false }],
2462
+
2463
+ // === Unused Variables ===
2464
+ // Allow underscore prefix for intentionally unused vars/args
2465
+ '@typescript-eslint/no-unused-vars': [
2466
+ 'error',
2467
+ {
2468
+ argsIgnorePattern: '^_',
2469
+ varsIgnorePattern: '^_',
2470
+ caughtErrorsIgnorePattern: '^_',
2471
+ },
2472
+ ],
2473
+
2474
+ // === Promise Safety (Critical for Node.js) ===
2475
+ // Catch unhandled promises (common source of silent failures)
2476
+ '@typescript-eslint/no-floating-promises': 'error',
2477
+ // Prevent passing promises where void is expected (e.g., event handlers)
2478
+ '@typescript-eslint/no-misused-promises': [
2479
+ 'error',
2480
+ { checksVoidReturn: { attributes: false } },
2481
+ ],
2482
+ // Catch awaiting non-promise values
2483
+ '@typescript-eslint/await-thenable': 'error',
2484
+ // Prevent confusing void expressions in unexpected places
2485
+ '@typescript-eslint/no-confusing-void-expression': 'error',
2486
+
2487
+ // === Type Import Consistency ===
2488
+ // Enforce `import type` for type-only imports (better tree-shaking)
2489
+ '@typescript-eslint/consistent-type-imports': [
2490
+ 'error',
2491
+ {
2492
+ prefer: 'type-imports',
2493
+ fixStyle: 'separate-type-imports',
2494
+ disallowTypeAnnotations: true,
2495
+ },
2496
+ ],
2497
+ // Prevent side effects in type-only imports
2498
+ '@typescript-eslint/no-import-type-side-effects': 'error',
2499
+
2500
+ // === Restricted Patterns ===
2501
+ // Forbid inline import() type expressions (prefer proper imports)
2502
+ 'no-restricted-syntax': [
2503
+ 'error',
2504
+ {
2505
+ selector: 'TSImportType',
2506
+ message:
2507
+ 'Inline import() type expressions are not allowed. Use a proper import statement at the top of the file instead.',
2508
+ },
2509
+ ],
2510
+ },
2511
+ },
2512
+
2513
+ // === File-Specific Overrides ===
2514
+ // Relax rules for test files where dynamic behavior is expected
2515
+ {
2516
+ files: ['**/*.test.ts', '**/*.spec.ts', '**/tests/**/*.ts'],
2517
+ rules: {
2518
+ '@typescript-eslint/no-explicit-any': 'off',
2519
+ '@typescript-eslint/no-unsafe-assignment': 'off',
2520
+ '@typescript-eslint/no-unsafe-member-access': 'off',
2521
+ '@typescript-eslint/no-unsafe-call': 'off',
2522
+ },
2523
+ },
2524
+
2525
+ // Relax rules for scripts/tooling
2526
+ {
2527
+ files: ['**/scripts/**/*.ts', '**/tools/**/*.ts'],
2528
+ rules: {
2529
+ '@typescript-eslint/no-explicit-any': 'off',
2530
+ 'no-console': 'off',
2531
+ },
2532
+ },
2533
+ ];
2534
+ ```
2535
+
2536
+ #### ESLint Best Practices
2537
+
2538
+ **Type-Aware vs Basic Linting**:
2539
+
2540
+ | Aspect | `recommended` | `recommendedTypeChecked` |
2541
+ | --- | --- | --- |
2542
+ | Setup complexity | Simple | Requires tsconfig |
2543
+ | Performance | Fast | Slower (type analysis) |
2544
+ | Bug detection | Basic | Comprehensive |
2545
+ | Promise safety | Limited | Full coverage |
2546
+ | Best for | Quick setup, small projects | Production code |
2547
+
2548
+ **Key Rules Explained**:
2549
+
2550
+ 1. **`no-floating-promises`**: Catches unhandled promises—a common source of silent
2551
+ failures in Node.js:
2552
+
2553
+ ```typescript
2554
+ // Bad: Promise result ignored, errors swallowed
2555
+ saveData();
2556
+ // Good: Explicitly handle or void
2557
+ await saveData();
2558
+ void saveData(); // Intentionally fire-and-forget
2559
+ ```
2560
+
2561
+ 2. **`consistent-type-imports`**: Enforces `import type` for type-only imports, enabling
2562
+ better tree-shaking and clearer intent:
2563
+
2564
+ ```typescript
2565
+ // Bad: Runtime import for type-only usage
2566
+ import { SomeType } from './types';
2567
+ // Good: Explicit type import
2568
+ import type { SomeType } from './types';
2569
+ ```
2570
+
2571
+ 3. **`curly: ['error', 'all']`**: Prevents bugs from missing braces:
2572
+
2573
+ ```typescript
2574
+ // Bad: Easy to introduce bugs when adding lines
2575
+ if (condition) doSomething();
2576
+ // Good: Always use braces
2577
+ if (condition) {
2578
+ doSomething();
2579
+ }
2580
+ ```
2581
+
2582
+ 4. **Underscore prefix for unused vars**: Convention for intentionally unused
2583
+ parameters:
2584
+ ```typescript
2585
+ // Clear intent: we don't use the error parameter
2586
+ .catch((_error) => handleDefaultCase())
2587
+ ```
2588
+
2589
+ **Common Gotcha with `noUncheckedIndexedAccess`**:
2590
+
2591
+ When using `noUncheckedIndexedAccess: true` in tsconfig (recommended for safety),
2592
+ ESLint’s `no-unnecessary-type-assertion` may incorrectly flag necessary assertions:
2593
+
2594
+ ```typescript
2595
+ // With noUncheckedIndexedAccess, array[0] returns T | undefined
2596
+ const first = array[0]!; // ESLint may wrongly flag this as unnecessary
2597
+ ```
2598
+
2599
+ If you encounter false positives, consider disabling the rule:
2600
+
2601
+ ```javascript
2602
+ rules: {
2603
+ "@typescript-eslint/no-unnecessary-type-assertion": "off",
2604
+ }
2605
+ ```
2606
+
2607
+ **Naming Convention Rules (Optional)**:
2608
+
2609
+ For teams wanting consistent naming, add naming convention rules:
2610
+
2611
+ ```javascript
2612
+ "@typescript-eslint/naming-convention": [
2613
+ "error",
2614
+ {
2615
+ selector: "parameter",
2616
+ format: ["camelCase", "PascalCase"],
2617
+ leadingUnderscore: "forbid",
2618
+ filter: { regex: "^_", match: false }, // Allow _ prefix for unused
2619
+ },
2620
+ {
2621
+ selector: "parameter",
2622
+ format: ["camelCase", "PascalCase"],
2623
+ leadingUnderscore: "allow",
2624
+ modifiers: ["unused"],
2625
+ },
2626
+ {
2627
+ selector: "variable",
2628
+ format: ["camelCase", "PascalCase", "UPPER_CASE"],
2629
+ leadingUnderscore: "forbid",
2630
+ filter: { regex: "^(__filename|__dirname)$", match: false },
2631
+ },
2632
+ ],
2633
+ ```
2634
+
2635
+ **CLI-Specific Rules**:
2636
+
2637
+ For CLI packages, consider restricting console usage to centralized output functions:
2638
+
2639
+ ```javascript
2640
+ {
2641
+ files: ["**/cli/**/*.ts"],
2642
+ rules: {
2643
+ "no-console": ["warn", { allow: ["error"] }],
2644
+ "no-restricted-imports": [
2645
+ "error",
2646
+ {
2647
+ patterns: [
2648
+ {
2649
+ group: ["chalk"],
2650
+ message: "Use picocolors for CLI output (smaller, faster).",
2651
+ },
2652
+ ],
2653
+ },
2654
+ ],
2655
+ },
2656
+ }
2657
+ ```
2658
+
2659
+ ### Appendix D: tsdown Config Example
2660
+
2661
+ ```typescript
2662
+ // tsdown.config.ts
2663
+ import { defineConfig } from 'tsdown';
2664
+
2665
+ export default defineConfig({
2666
+ entry: {
2667
+ index: 'src/index.ts',
2668
+ cli: 'src/cli/index.ts',
2669
+ bin: 'src/bin.ts',
2670
+ },
2671
+ format: ['esm', 'cjs'],
2672
+ platform: 'node',
2673
+ target: 'node24',
2674
+ sourcemap: true,
2675
+ dts: true,
2676
+ clean: true,
2677
+ banner: ({ fileName }) => (fileName.startsWith('bin.') ? '#!/usr/bin/env node\n' : ''),
2678
+ });
2679
+ ```
2680
+
2681
+ ### Appendix E: Complete lefthook.yml Example
2682
+
2683
+ ```yaml
2684
+ # lefthook.yml
2685
+ # Git hooks for code quality
2686
+ # Pre-commit: Fast checks with auto-fix (target: 2-5 seconds)
2687
+ # Pre-push: Full test validation with caching (target: 3-5s cached, <30s uncached)
2688
+
2689
+ # PHASE 1: Fast pre-commit checks
2690
+ pre-commit:
2691
+ parallel: true
2692
+
2693
+ commands:
2694
+ # Auto-format with prettier (~500ms)
2695
+ format:
2696
+ glob: '*.{js,ts,tsx,json,yaml,yml}'
2697
+ run: npx prettier --write --log-level warn {staged_files}
2698
+ stage_fixed: true
2699
+ priority: 1
2700
+
2701
+ # Lint with auto-fix and caching (~1s first, ~200ms cached)
2702
+ lint:
2703
+ glob: '*.{js,ts,tsx}'
2704
+ run: >
2705
+ npx eslint
2706
+ --cache
2707
+ --cache-location node_modules/.cache/eslint
2708
+ --fix {staged_files}
2709
+ stage_fixed: true
2710
+ priority: 2
2711
+
2712
+ # Type check with incremental mode (~2s)
2713
+ typecheck:
2714
+ glob: '*.{ts,tsx}'
2715
+ run: npx tsc --noEmit --incremental
2716
+ priority: 3
2717
+
2718
+ # Test only changed files (optional, ~1-3s)
2719
+ # test-changed:
2720
+ # glob: "*.{test,spec}.{ts,tsx}"
2721
+ # run: npx vitest --changed --run
2722
+ # priority: 4
2723
+
2724
+ # PHASE 2: Pre-push validation with test caching
2725
+ pre-push:
2726
+ commands:
2727
+ verify-tests:
2728
+ run: |
2729
+ echo "🔍 Checking test status for push..."
2730
+
2731
+ # Get current commit hash
2732
+ COMMIT_HASH=$(git rev-parse HEAD)
2733
+ CACHE_DIR="node_modules/.test-cache"
2734
+ CACHE_FILE="$CACHE_DIR/$COMMIT_HASH"
2735
+
2736
+ # Check for uncommitted changes
2737
+ if ! git diff --quiet || ! git diff --cached --quiet; then
2738
+ echo "⚠️ Uncommitted changes detected"
2739
+ echo "📊 Running test suite..."
2740
+ pnpm test
2741
+ exit $?
2742
+ fi
2743
+
2744
+ # Check cache
2745
+ if [ -f "$CACHE_FILE" ]; then
2746
+ SHORT_HASH=$(echo "$COMMIT_HASH" | cut -c1-8)
2747
+ echo "✓ Tests already passed for commit $SHORT_HASH"
2748
+ exit 0
2749
+ fi
2750
+
2751
+ # No cache, run tests
2752
+ echo "📊 Running test suite..."
2753
+ pnpm test
2754
+
2755
+ # Cache on success
2756
+ if [ $? -eq 0 ]; then
2757
+ mkdir -p "$CACHE_DIR"
2758
+ touch "$CACHE_FILE"
2759
+ SHORT_HASH=$(echo "$COMMIT_HASH" | cut -c1-8)
2760
+ echo "✅ Tests passed and cached for commit $SHORT_HASH"
2761
+ exit 0
2762
+ else
2763
+ echo "❌ Tests failed - push blocked"
2764
+ echo "Fix tests and try again, or bypass with: git push --no-verify"
2765
+ exit 1
2766
+ fi
2767
+ ```
2768
+
2769
+ **Monorepo variant** (scope commands to packages):
2770
+
2771
+ ```yaml
2772
+ pre-commit:
2773
+ parallel: true
2774
+
2775
+ commands:
2776
+ format-core:
2777
+ root: 'packages/core/'
2778
+ glob: '*.{ts,tsx}'
2779
+ run: npx prettier --write --log-level warn {staged_files}
2780
+ stage_fixed: true
2781
+
2782
+ lint-core:
2783
+ root: 'packages/core/'
2784
+ glob: '*.{ts,tsx}'
2785
+ run: npx eslint --cache --fix {staged_files}
2786
+ stage_fixed: true
2787
+
2788
+ typecheck-core:
2789
+ root: 'packages/core/'
2790
+ glob: '*.{ts,tsx}'
2791
+ run: npx tsc -p tsconfig.json --noEmit --incremental
2792
+ ```
2793
+
2794
+ ### Appendix F: Upgrade Scripts with Documentation
2795
+
2796
+ For projects with many scripts, a `scripts-info` field provides inline documentation
2797
+ that can be queried programmatically:
2798
+
2799
+ ```json
2800
+ {
2801
+ "scripts": {
2802
+ "upgrade:check": "ncu --format group",
2803
+ "upgrade": "ncu --target minor -u && pnpm install && pnpm test",
2804
+ "upgrade:patch": "ncu --target patch -u && pnpm install && pnpm test",
2805
+ "upgrade:major": "ncu --target latest --interactive --format group",
2806
+ "help": "tsx scripts/help.ts"
2807
+ },
2808
+ "scripts-info": {
2809
+ "upgrade:check": "Check for outdated packages grouped by type (no changes)",
2810
+ "upgrade": "Safe upgrade: minor+patch versions, install, and test",
2811
+ "upgrade:patch": "Conservative upgrade: patch versions only, install, and test",
2812
+ "upgrade:major": "Interactive upgrade for major version changes"
2813
+ }
2814
+ }
2815
+ ```
2816
+
2817
+ **Simple help script** (`scripts/help.ts`):
2818
+
2819
+ ```typescript
2820
+ import { readFileSync } from 'node:fs';
2821
+
2822
+ const pkg = JSON.parse(readFileSync('package.json', 'utf-8'));
2823
+ const info = pkg['scripts-info'] ?? {};
2824
+
2825
+ console.log('\nAvailable scripts:\n');
2826
+ for (const [name, desc] of Object.entries(info)) {
2827
+ console.log(` ${name.padEnd(20)} ${desc}`);
2828
+ }
2829
+ ```
2830
+
2831
+ **Additional ncu options for complex projects**:
2832
+
2833
+ ```json
2834
+ {
2835
+ "scripts": {
2836
+ "upgrade:check": "ncu --format group",
2837
+ "upgrade:check:all": "ncu --format group -ws",
2838
+ "upgrade": "ncu --target minor -u && pnpm install && pnpm test",
2839
+ "upgrade:all": "ncu --target minor -u -ws && pnpm install && pnpm test",
2840
+ "upgrade:major": "ncu --target latest --interactive --format group",
2841
+ "upgrade:filter": "ncu --filter"
2842
+ },
2843
+ "scripts-info": {
2844
+ "upgrade:check": "Check root package for updates (grouped by type)",
2845
+ "upgrade:check:all": "Check all workspace packages for updates",
2846
+ "upgrade": "Safe upgrade root: minor+patch, install, test",
2847
+ "upgrade:all": "Safe upgrade all workspaces: minor+patch, install, test",
2848
+ "upgrade:major": "Interactive major version upgrades",
2849
+ "upgrade:filter": "Filter upgrades by pattern, e.g.: pnpm upgrade:filter '@radix-ui/*'"
2850
+ }
2851
+ }
2852
+ ```
2853
+
2854
+ **Useful ncu filter patterns**:
2855
+
2856
+ ```bash
2857
+ # Upgrade only Radix UI packages
2858
+ ncu --filter "@radix-ui/*" --target minor -u
2859
+
2860
+ # Upgrade everything except React (held for compatibility)
2861
+ ncu --reject "react,react-dom" --target minor -u
2862
+
2863
+ # Check only dev dependencies
2864
+ ncu --dep dev --format group
2865
+
2866
+ # Upgrade with peer dependency handling
2867
+ ncu --target minor -u && pnpm install --force
2868
+ ```