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.
- package/README.md +47 -28
- package/dist/bin.mjs +410 -170
- package/dist/bin.mjs.map +1 -1
- package/dist/cli.mjs +202 -94
- package/dist/cli.mjs.map +1 -1
- package/dist/docs/README.md +47 -28
- package/dist/docs/SKILL.md +61 -18
- package/dist/docs/guidelines/bun-monorepo-patterns.md +2096 -0
- package/dist/docs/guidelines/cli-agent-skill-patterns.md +79 -5
- package/dist/docs/guidelines/error-handling-rules.md +66 -0
- package/dist/docs/guidelines/pnpm-monorepo-patterns.md +2868 -0
- package/dist/docs/guidelines/release-notes-guidelines.md +140 -0
- package/dist/docs/guidelines/{sync-troubleshooting.md → tbd-sync-troubleshooting.md} +1 -1
- package/dist/docs/guidelines/typescript-sorting-patterns.md +234 -0
- package/dist/docs/guidelines/typescript-yaml-handling-rules.md +195 -0
- package/dist/docs/install/claude-header.md +13 -6
- package/dist/docs/shortcuts/standard/agent-handoff.md +1 -0
- package/dist/docs/shortcuts/standard/checkout-third-party-repo.md +50 -0
- package/dist/docs/shortcuts/standard/{cleanup-all.md → code-cleanup-all.md} +3 -2
- package/dist/docs/shortcuts/standard/{cleanup-update-docstrings.md → code-cleanup-docstrings.md} +1 -0
- package/dist/docs/shortcuts/standard/{cleanup-remove-trivial-tests.md → code-cleanup-tests.md} +1 -0
- package/dist/docs/shortcuts/standard/{commit-code.md → code-review-and-commit.md} +1 -0
- package/dist/docs/shortcuts/standard/coding-spike.md +54 -0
- package/dist/docs/shortcuts/standard/create-or-update-pr-simple.md +1 -0
- package/dist/docs/shortcuts/standard/create-or-update-pr-with-validation-plan.md +1 -0
- package/dist/docs/shortcuts/standard/implement-beads.md +1 -0
- package/dist/docs/shortcuts/standard/merge-upstream.md +1 -0
- package/dist/docs/shortcuts/standard/new-architecture-doc.md +1 -0
- package/dist/docs/shortcuts/standard/new-guideline.md +8 -0
- package/dist/docs/shortcuts/standard/new-plan-spec.md +1 -0
- package/dist/docs/shortcuts/standard/new-research-brief.md +1 -0
- package/dist/docs/shortcuts/standard/new-shortcut.md +27 -1
- package/dist/docs/shortcuts/standard/new-validation-plan.md +1 -0
- package/dist/docs/shortcuts/standard/plan-implementation-with-beads.md +1 -0
- package/dist/docs/shortcuts/standard/precommit-process.md +1 -0
- package/dist/docs/shortcuts/standard/review-code-python.md +1 -0
- package/dist/docs/shortcuts/standard/review-code-typescript.md +1 -0
- package/dist/docs/shortcuts/standard/review-code.md +1 -0
- package/dist/docs/shortcuts/standard/review-github-pr.md +89 -17
- package/dist/docs/shortcuts/standard/revise-all-architecture-docs.md +1 -0
- package/dist/docs/shortcuts/standard/revise-architecture-doc.md +1 -0
- package/dist/docs/shortcuts/standard/setup-github-cli.md +1 -0
- package/dist/docs/shortcuts/standard/sync-failure-recovery.md +6 -53
- package/dist/docs/shortcuts/standard/update-specs-status.md +1 -0
- package/dist/docs/shortcuts/standard/welcome-user.md +2 -1
- package/dist/docs/shortcuts/system/skill-brief.md +1 -1
- package/dist/docs/shortcuts/system/skill.md +48 -12
- package/dist/docs/skill-brief.md +1 -1
- package/dist/docs/tbd-design.md +13 -1
- package/dist/index.d.mts +20 -6
- package/dist/index.mjs +2 -2
- package/dist/{src-BfhjLZXE.mjs → src-Ct16P2Ox.mjs} +154 -22
- package/dist/src-Ct16P2Ox.mjs.map +1 -0
- package/dist/tbd +410 -170
- package/package.json +1 -1
- package/dist/docs/guidelines/typescript-monorepo-patterns.md +0 -72
- 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
|
+
```
|