get-tbd 0.1.21 → 0.1.23

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.
@@ -5,7 +5,7 @@ author: Joshua Levy (github.com/jlevy) with LLM assistance
5
5
  ---
6
6
  # pnpm Monorepo Patterns
7
7
 
8
- **Last Updated**: 2026-02-02
8
+ **Last Updated**: 2026-02-18
9
9
 
10
10
  **Related**:
11
11
 
@@ -23,9 +23,9 @@ author: Joshua Levy (github.com/jlevy) with LLM assistance
23
23
 
24
24
  | Tool / Package | Version | Check For Updates |
25
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. |
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.2 | [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
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
30
  | **publint** | ^0.3.0 | [npmjs.com/package/publint](https://www.npmjs.com/package/publint) |
31
31
  | **@changesets/cli** | ^2.29.0 | [github.com/changesets/changesets/releases](https://github.com/changesets/changesets/releases) |
@@ -34,13 +34,13 @@ author: Joshua Levy (github.com/jlevy) with LLM assistance
34
34
  | **actions/setup-node** | v6 | [github.com/actions/setup-node/releases](https://github.com/actions/setup-node/releases) |
35
35
  | **pnpm/action-setup** | v4 | [github.com/pnpm/action-setup/releases](https://github.com/pnpm/action-setup/releases) |
36
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 |
37
+ | **lefthook** | ^2.0.15 | [github.com/evilmartians/lefthook/releases](https://github.com/evilmartians/lefthook/releases) — 2.1.1 latest. v2 dropped regexp `exclude`, `skip_output`. |
38
38
  | **npm-check-updates** | ^19.0.0 | [npmjs.com/package/npm-check-updates](https://www.npmjs.com/package/npm-check-updates) |
39
39
  | **tsx** | ^4.21.0 | [github.com/privatenumber/tsx/releases](https://github.com/privatenumber/tsx/releases) |
40
40
  | **prettier** | ^3.0.0 | [github.com/prettier/prettier/releases](https://github.com/prettier/prettier/releases) |
41
41
  | **eslint-config-prettier** | ^10.0.0 | [github.com/prettier/eslint-config-prettier/releases](https://github.com/prettier/eslint-config-prettier/releases) |
42
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. |
43
+ | **Vitest** | ^4.0.0 | [github.com/vitest-dev/vitest/releases](https://github.com/vitest-dev/vitest/releases) — 4.0.18 latest. Browser Mode stable, visual regression testing added. `coverage.all` removed in v4. |
44
44
 
45
45
  ### Reminders When Updating
46
46
 
@@ -261,6 +261,11 @@ Modern TypeScript monorepos use a shared base configuration extended by each pac
261
261
  }
262
262
  ```
263
263
 
264
+ **Note on target/lib version**: Use `ES2024` when targeting Node.js 22+ (which supports
265
+ all ES2024 features).
266
+ Use `ES2023` if your minimum is Node.js 20. The target should match what your
267
+ `engines.node` field supports.
268
+
264
269
  **Assessment**: Using `moduleResolution: "Bundler"` is appropriate when a bundler
265
270
  (tsdown) handles the final output.
266
271
  For maximum Node.js compatibility without a bundler, `NodeNext` would be preferred.
@@ -379,6 +384,56 @@ The project recommends migrating to tsdown.
379
384
 
380
385
  * * *
381
386
 
387
+ #### ESM-Only Alternative (Node.js 22+)
388
+
389
+ **Status**: Recommended for Node.js-only packages
390
+
391
+ **When to use**: If your package targets Node.js 22+ exclusively and doesn’t need to
392
+ support CommonJS consumers (bundlers, older Node.js, or specific CJS-only tools), an
393
+ ESM-only build is simpler and sufficient.
394
+
395
+ **Simplified tsdown config**:
396
+
397
+ ```typescript
398
+ export default defineConfig({
399
+ entry: { index: 'src/index.ts' },
400
+ format: ['esm'], // ESM only
401
+ platform: 'node',
402
+ target: 'node24',
403
+ sourcemap: true,
404
+ dts: true,
405
+ clean: true,
406
+ });
407
+ ```
408
+
409
+ **Simplified package.json exports**:
410
+
411
+ ```json
412
+ {
413
+ "type": "module",
414
+ "main": "./dist/index.mjs",
415
+ "types": "./dist/index.d.mts",
416
+ "exports": {
417
+ ".": {
418
+ "types": "./dist/index.d.mts",
419
+ "default": "./dist/index.mjs"
420
+ }
421
+ }
422
+ }
423
+ ```
424
+
425
+ **Trade-offs**:
426
+
427
+ - ✅ Simpler config, smaller package, faster builds
428
+ - ✅ No dual-format complexity
429
+ - ❌ CJS consumers cannot use the package
430
+ - ❌ Some bundlers may require additional config
431
+
432
+ **Assessment**: ESM-only is the right choice for modern Node.js libraries.
433
+ Only provide dual ESM/CJS if you have confirmed CJS consumer requirements.
434
+
435
+ * * *
436
+
382
437
  ### 4. Package Exports & Dual Module Support
383
438
 
384
439
  #### Subpath Exports
@@ -606,7 +661,7 @@ Changesets provides:
606
661
  ```json
607
662
  {
608
663
  "$schema": "https://unpkg.com/@changesets/config/schema.json",
609
- "changelog": "@changesets/changelog-github",
664
+ "changelog": "@changesets/changelog-github", // or "@changesets/cli/changelog" for simpler output
610
665
  "commit": false,
611
666
  "fixed": [],
612
667
  "linked": [],
@@ -653,6 +708,100 @@ It integrates seamlessly with pnpm and GitHub Actions.
653
708
 
654
709
  * * *
655
710
 
711
+ #### Alternative: Tag-Triggered OIDC Publishing
712
+
713
+ **Status**: Recommended for single-package repos, viable for monorepos
714
+
715
+ **Details**:
716
+
717
+ For simpler release workflows without Changesets, use tag-triggered GitHub Actions with
718
+ OIDC trusted publishing.
719
+ No NPM_TOKEN needed, no “Version Packages” PR workflow.
720
+
721
+ **Workflow**:
722
+
723
+ 1. Manually bump version in package.json
724
+ 2. Update CHANGELOG.md or release-notes.md
725
+ 3. Commit, tag (e.g., `v1.2.3`), and push
726
+ 4. GitHub Action publishes automatically on tag push
727
+
728
+ **One-time setup**:
729
+
730
+ 1. Publish package manually once: `npm publish --access public`
731
+ 2. Configure OIDC on npmjs.com → package settings → Trusted Publishing:
732
+ - Publisher: GitHub Actions
733
+ - Organization: your-org
734
+ - Repository: your-repo
735
+ - Workflow: `release.yml`
736
+
737
+ **Release workflow** (`.github/workflows/release.yml`):
738
+
739
+ ```yaml
740
+ name: Release
741
+
742
+ on:
743
+ push:
744
+ tags: ['v*']
745
+
746
+ permissions:
747
+ contents: write
748
+ id-token: write # Required for OIDC
749
+
750
+ jobs:
751
+ release:
752
+ runs-on: ubuntu-latest
753
+ steps:
754
+ - uses: actions/checkout@v6
755
+ with:
756
+ fetch-depth: 0
757
+
758
+ - uses: pnpm/action-setup@v4
759
+
760
+ - uses: actions/setup-node@v6
761
+ with:
762
+ node-version: 24
763
+ cache: pnpm
764
+ registry-url: 'https://registry.npmjs.org'
765
+
766
+ - run: pnpm install --frozen-lockfile
767
+ - run: pnpm build
768
+ - run: pnpm publint
769
+
770
+ - name: Publish to npm
771
+ run: pnpm -r publish --access public --no-git-checks
772
+ env:
773
+ NPM_CONFIG_PROVENANCE: true # Automatic provenance attestation
774
+
775
+ - name: Create GitHub Release
776
+ uses: softprops/action-gh-release@v2
777
+ with:
778
+ body_path: release-notes.md
779
+ ```
780
+
781
+ **Advantages**:
782
+
783
+ - No NPM_TOKEN secret to manage or rotate
784
+ - Provenance attestation included automatically
785
+ - Simpler workflow (no Changesets PR dance)
786
+ - Works with existing git tag practices
787
+
788
+ **Disadvantages**:
789
+
790
+ - Manual version bumps (vs Changesets automation)
791
+ - No automated changelog generation
792
+ - Requires public GitHub repository
793
+
794
+ **Assessment**: Ideal for single-package repos or teams comfortable with manual
795
+ versioning. For large monorepos with many packages, Changesets provides better
796
+ automation.
797
+
798
+ **References**:
799
+
800
+ - [npm Trusted Publishing](https://docs.npmjs.com/generating-provenance-statements)
801
+ - [GitHub OIDC tokens](https://docs.github.com/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
802
+
803
+ * * *
804
+
656
805
  #### Dynamic Git-Based Versioning
657
806
 
658
807
  **Status**: Recommended for dev builds
@@ -1020,7 +1169,7 @@ Use it for all TypeScript monorepo projects.
1020
1169
 
1021
1170
  **Status**: Recommended
1022
1171
 
1023
- **`.github/workflows/ci.yml`**:
1172
+ **`.github/workflows/ci.yml`** (minimal single-job):
1024
1173
 
1025
1174
  ```yaml
1026
1175
  name: CI
@@ -1037,8 +1186,6 @@ jobs:
1037
1186
  - uses: actions/checkout@v6
1038
1187
 
1039
1188
  - uses: pnpm/action-setup@v4
1040
- with:
1041
- version: 10
1042
1189
 
1043
1190
  - uses: actions/setup-node@v6
1044
1191
  with:
@@ -1053,6 +1200,46 @@ jobs:
1053
1200
  - run: pnpm test
1054
1201
  ```
1055
1202
 
1203
+ **Multi-job CI with cross-platform testing** (recommended for CLI tools):
1204
+
1205
+ ```yaml
1206
+ jobs:
1207
+ test:
1208
+ name: Test (${{ matrix.os }})
1209
+ runs-on: ${{ matrix.os }}
1210
+ strategy:
1211
+ fail-fast: false
1212
+ matrix:
1213
+ os: [ubuntu-latest, macos-latest, windows-latest]
1214
+ steps:
1215
+ - uses: actions/checkout@v6
1216
+ - uses: pnpm/action-setup@v4
1217
+ - uses: actions/setup-node@v6
1218
+ with:
1219
+ node-version: 24
1220
+ cache: pnpm
1221
+ - run: pnpm install --frozen-lockfile
1222
+ - run: pnpm build
1223
+ - run: pnpm test
1224
+
1225
+ lint:
1226
+ name: Lint & Coverage
1227
+ runs-on: ubuntu-latest
1228
+ steps:
1229
+ - uses: actions/checkout@v6
1230
+ - uses: pnpm/action-setup@v4
1231
+ - uses: actions/setup-node@v6
1232
+ with:
1233
+ node-version: 24
1234
+ cache: pnpm
1235
+ - run: pnpm install --frozen-lockfile
1236
+ - run: pnpm format:check
1237
+ - run: pnpm lint:check
1238
+ - run: pnpm build
1239
+ - run: pnpm publint
1240
+ - run: pnpm test:coverage
1241
+ ```
1242
+
1056
1243
  **Key points**:
1057
1244
 
1058
1245
  - Node.js 24 is the current LTS ("Krypton", active until Oct 2026, maintained until Apr
@@ -1061,12 +1248,19 @@ jobs:
1061
1248
  - `actions/checkout@v6` requires Actions Runner v2.329.0+ (stores credentials under
1062
1249
  $RUNNER_TEMP)
1063
1250
 
1064
- - `pnpm/action-setup@v4` includes built-in caching
1251
+ - `pnpm/action-setup@v4` includes built-in caching (no `version:` needed if
1252
+ `packageManager` is set in `package.json`)
1065
1253
 
1066
1254
  - `actions/setup-node@v6` with `cache: pnpm` provides additional caching
1067
1255
 
1068
1256
  - `--frozen-lockfile` ensures CI uses exact versions from lockfile
1069
1257
 
1258
+ - For CLI tools, cross-platform testing catches platform-specific issues (path
1259
+ separators, file permissions, line endings)
1260
+
1261
+ - Separating lint/coverage from tests enables parallel execution and clearer failure
1262
+ diagnosis
1263
+
1070
1264
  **References**:
1071
1265
 
1072
1266
  - [pnpm action-setup](https://github.com/pnpm/action-setup)
@@ -1184,7 +1378,7 @@ coverage
1184
1378
  | --- | --- | --- |
1185
1379
  | `printWidth` | 100 | Wider than default 80; fits modern screens |
1186
1380
  | `singleQuote` | true | Common in JS ecosystem, less visual noise |
1187
- | `trailingComma` | "all" | Cleaner diffs, easier reordering |
1381
+ | `trailingComma` | all | Cleaner diffs, easier reordering |
1188
1382
  | `semi` | true | Explicit; avoids ASI edge cases |
1189
1383
 
1190
1384
  **Assessment**: Prettier eliminates formatting debates and ensures consistency.
@@ -1198,6 +1392,74 @@ Use `eslint-config-prettier` to disable ESLint rules that conflict with Prettier
1198
1392
 
1199
1393
  * * *
1200
1394
 
1395
+ #### Markdown Formatting with flowmark
1396
+
1397
+ **Status**: Optional
1398
+
1399
+ **Details**:
1400
+
1401
+ For markdown files, [flowmark](https://github.com/jlevy/flowmark) provides semantic
1402
+ line-breaking (reflowing) that creates cleaner git diffs than traditional hard-wrap
1403
+ formatters.
1404
+
1405
+ **Key differences from Prettier**:
1406
+
1407
+ - Prettier doesn’t format markdown by default (prose-wrap: preserve)
1408
+ - flowmark breaks lines at semantic boundaries (after sentences, list items)
1409
+ - Result: Git diffs show actual content changes, not just rewrapping
1410
+
1411
+ **Installation**: None required if using `uvx` (uv’s tool runner)
1412
+
1413
+ **Configuration**:
1414
+
1415
+ Add to `.prettierignore` to prevent Prettier from touching markdown:
1416
+
1417
+ ```
1418
+ *.md
1419
+ ```
1420
+
1421
+ Add a `.flowmarkignore` file at the repo root to skip tool-managed files:
1422
+
1423
+ ```
1424
+ .tbd/
1425
+ node_modules/
1426
+ dist/
1427
+ attic/
1428
+ template/
1429
+ coverage/
1430
+ .changeset/
1431
+ ```
1432
+
1433
+ **Lefthook integration**:
1434
+
1435
+ ```yaml
1436
+ pre-commit:
1437
+ commands:
1438
+ format-md:
1439
+ glob: '*.md'
1440
+ exclude:
1441
+ - CLAUDE.md
1442
+ - AGENTS.md
1443
+ - template/**
1444
+ run: uvx flowmark@latest --auto {staged_files}
1445
+ stage_fixed: true
1446
+ ```
1447
+
1448
+ **CI consideration**: flowmark is typically NOT enforced in CI (unlike Prettier for
1449
+ code). Markdown formatting rarely causes functional issues, and flowmark can be brittle
1450
+ on edge cases (tables, complex nesting).
1451
+
1452
+ **Assessment**: Useful for projects with extensive markdown documentation.
1453
+ The cleaner diffs make reviews easier.
1454
+ Optional tool; requires `uv` installed locally.
1455
+
1456
+ **References**:
1457
+
1458
+ - [flowmark on GitHub](https://github.com/jlevy/flowmark)
1459
+ - [uv installation](https://docs.astral.sh/uv/)
1460
+
1461
+ * * *
1462
+
1201
1463
  #### Format Scripts Pattern
1202
1464
 
1203
1465
  **Status**: Recommended
@@ -1317,7 +1579,7 @@ pre-commit:
1317
1579
  commands:
1318
1580
  # Auto-format with prettier (~500ms)
1319
1581
  format:
1320
- glob: '*.{js,ts,tsx,json}'
1582
+ glob: '*.{js,ts,tsx,json,yaml,yml}'
1321
1583
  run: npx prettier --write --log-level warn {staged_files}
1322
1584
  stage_fixed: true
1323
1585
  priority: 1
@@ -1717,6 +1979,202 @@ Reserve vite-node for projects that specifically need Vite’s transformation pi
1717
1979
 
1718
1980
  * * *
1719
1981
 
1982
+ #### CJS Bootstrap for Compile Cache
1983
+
1984
+ **Status**: Recommended for CLI tools
1985
+
1986
+ **Details**:
1987
+
1988
+ Node.js 22.8.0+ supports `module.enableCompileCache()`, which caches compiled bytecode
1989
+ on disk for faster subsequent runs.
1990
+ However, this must be called **before** any ESM modules are loaded—ESM static imports
1991
+ are resolved before module code runs, so calling it in an ESM file is “too late.”
1992
+
1993
+ The solution is a **CJS bootstrap**: a tiny CommonJS entry point that enables compile
1994
+ cache, then dynamically imports the real ESM CLI binary.
1995
+
1996
+ **`src/cli/bin-bootstrap.cjs`**:
1997
+
1998
+ ```js
1999
+ 'use strict';
2000
+
2001
+ const path = require('node:path');
2002
+ const { pathToFileURL } = require('node:url');
2003
+
2004
+ // Enable compile cache BEFORE loading any ESM modules.
2005
+ // Available in Node 22.8.0+, gracefully ignored in older versions.
2006
+ try {
2007
+ const mod = require('node:module');
2008
+ if (typeof mod.enableCompileCache === 'function') {
2009
+ mod.enableCompileCache();
2010
+ }
2011
+ } catch {
2012
+ // Silently ignore - caching is an optimization, not required.
2013
+ }
2014
+
2015
+ // Load the bundled CLI binary (ESM).
2016
+ const binPath = path.join(__dirname, 'bin.mjs');
2017
+ import(pathToFileURL(binPath).href);
2018
+ ```
2019
+
2020
+ **`package.json` bin field**:
2021
+
2022
+ ```json
2023
+ {
2024
+ "bin": {
2025
+ "cli-name": "./dist/bin-bootstrap.cjs"
2026
+ }
2027
+ }
2028
+ ```
2029
+
2030
+ **Why this matters**: On repeated invocations (common for CLI tools), compile cache
2031
+ reduces startup time significantly—Node.js skips re-parsing and re-compiling JavaScript
2032
+ that hasn’t changed.
2033
+
2034
+ **Assessment**: Essential optimization for any CLI tool that targets Node.js 22+. The
2035
+ CJS bootstrap adds minimal complexity (one small file) for meaningful startup
2036
+ improvement.
2037
+
2038
+ * * *
2039
+
2040
+ #### Dependency Bundling for CLI Startup
2041
+
2042
+ **Status**: Recommended for CLI tools
2043
+
2044
+ **Details**:
2045
+
2046
+ CLI tools benefit from bundling their runtime dependencies directly into the binary
2047
+ instead of resolving them from `node_modules` at runtime.
2048
+ tsdown’s `noExternal` option enables this.
2049
+
2050
+ **tsdown config for bundled CLI**:
2051
+
2052
+ ```typescript
2053
+ {
2054
+ entry: { bin: 'src/cli/bin.ts' },
2055
+ format: ['esm'],
2056
+ platform: 'node',
2057
+ target: 'node24',
2058
+ noExternal: [
2059
+ 'yaml',
2060
+ 'commander',
2061
+ 'picocolors',
2062
+ 'zod',
2063
+ // ... all runtime deps
2064
+ ],
2065
+ // Acknowledge intentional bundling (suppresses tsdown 0.20+ warning)
2066
+ inlineOnly: false,
2067
+ }
2068
+ ```
2069
+
2070
+ **Trade-offs**:
2071
+
2072
+ | Aspect | Bundled | Unbundled |
2073
+ | --- | --- | --- |
2074
+ | Startup time | Faster (no resolution) | Slower (resolves deps) |
2075
+ | Binary size | Larger (~1MB+ typical) | Smaller |
2076
+ | Deduplication | No (each package bundles its own) | Yes (shared node\_modules) |
2077
+ | Use case | CLI tools, serverless | Libraries |
2078
+
2079
+ **Assessment**: Bundling is the right choice for CLI tools where startup time matters.
2080
+ Libraries should NOT bundle dependencies (consumers may need to deduplicate).
2081
+
2082
+ * * *
2083
+
2084
+ #### Multi-Config tsdown (Array Pattern)
2085
+
2086
+ **Status**: Recommended for CLI/library hybrid packages
2087
+
2088
+ **Details**:
2089
+
2090
+ When a package serves as both a library and a CLI tool, use `defineConfig([...])` with
2091
+ separate configurations for each output type:
2092
+
2093
+ ```typescript
2094
+ import { defineConfig } from 'tsdown';
2095
+
2096
+ const commonOptions = {
2097
+ format: ['esm'] as 'esm'[],
2098
+ platform: 'node' as const,
2099
+ target: 'node24' as const,
2100
+ sourcemap: true,
2101
+ dts: true,
2102
+ define: {
2103
+ __VERSION__: JSON.stringify(version),
2104
+ },
2105
+ };
2106
+
2107
+ export default defineConfig([
2108
+ // Library entry points (ESM + DTS, no bundled deps)
2109
+ {
2110
+ ...commonOptions,
2111
+ entry: {
2112
+ index: 'src/index.ts',
2113
+ cli: 'src/cli/cli.ts',
2114
+ },
2115
+ clean: true,
2116
+ },
2117
+ // CLI binary (ESM, bundled deps for fast startup)
2118
+ {
2119
+ ...commonOptions,
2120
+ entry: { bin: 'src/cli/bin.ts' },
2121
+ banner: '#!/usr/bin/env node',
2122
+ clean: false,
2123
+ noExternal: ['yaml', 'commander', 'picocolors', 'zod'],
2124
+ inlineOnly: false,
2125
+ },
2126
+ // CJS bootstrap (enables compile cache before ESM loads)
2127
+ {
2128
+ format: ['cjs'] as 'cjs'[],
2129
+ platform: 'node' as const,
2130
+ target: 'node24' as const,
2131
+ sourcemap: true,
2132
+ dts: false,
2133
+ entry: { 'bin-bootstrap': 'src/cli/bin-bootstrap.cjs' },
2134
+ banner: '#!/usr/bin/env node',
2135
+ clean: false,
2136
+ },
2137
+ ]);
2138
+ ```
2139
+
2140
+ **Key patterns**:
2141
+
2142
+ 1. **`commonOptions` object**: Avoids duplication across configs
2143
+ 2. **`clean: true` only on first config**: Prevents later configs from deleting earlier
2144
+ output
2145
+ 3. **Separate DTS generation**: Only library entry points need `.d.mts` files
2146
+ 4. **Different `noExternal` per config**: Bundle deps for CLI, leave unbundled for
2147
+ library
2148
+
2149
+ **Assessment**: This pattern provides optimal output for each use case without
2150
+ compromise.
2151
+
2152
+ * * *
2153
+
2154
+ #### Conditional Build Script
2155
+
2156
+ **Status**: Recommended
2157
+
2158
+ **Details**:
2159
+
2160
+ Pre-push hooks should avoid unnecessary rebuilds.
2161
+ A `build-if-needed` script checks whether the build output is up-to-date before running
2162
+ the full build:
2163
+
2164
+ ```json
2165
+ {
2166
+ "scripts": {
2167
+ "build:check": "node packages/my-cli/scripts/build-if-needed.mjs"
2168
+ }
2169
+ }
2170
+ ```
2171
+
2172
+ The script compares modification times of `src/` files against `dist/` output and only
2173
+ triggers a build if source is newer.
2174
+ This makes pre-push hooks near-instant when the build is already current.
2175
+
2176
+ * * *
2177
+
1720
2178
  ### 14. Private Package Distribution
1721
2179
 
1722
2180
  #### Option A: GitHub Packages (Recommended)
@@ -2123,6 +2581,17 @@ than discovering them when users try to use the library in browser/edge contexts
2123
2581
  This eliminates “did I forget to build?”
2124
2582
  confusion.
2125
2583
 
2584
+ 20. **Use CJS bootstrap for CLI startup**: Enable Node.js compile cache via a CJS
2585
+ bootstrap file that runs before ESM module loading.
2586
+ This significantly improves repeated CLI invocation times on Node.js 22.8+.
2587
+
2588
+ 21. **Bundle CLI dependencies**: Use tsdown’s `noExternal` to bundle runtime deps into
2589
+ the CLI binary for faster startup (no `node_modules` resolution at runtime).
2590
+
2591
+ 22. **Add guard tests for node-free core**: If your library entry point should be
2592
+ node-free, add automated tests that verify no `node:` imports leak into the public
2593
+ API surface.
2594
+
2126
2595
  * * *
2127
2596
 
2128
2597
  ## Open Research Questions
@@ -2335,7 +2804,7 @@ ready for public release.
2335
2804
  {
2336
2805
  "name": "project-workspace",
2337
2806
  "private": true,
2338
- "packageManager": "pnpm@10.27.0",
2807
+ "packageManager": "pnpm@10.28.2",
2339
2808
  "engines": {
2340
2809
  "node": ">=24"
2341
2810
  },
@@ -2362,7 +2831,7 @@ ready for public release.
2362
2831
  "@eslint/js": "^9.0.0",
2363
2832
  "eslint": "^9.0.0",
2364
2833
  "eslint-config-prettier": "^10.0.0",
2365
- "lefthook": "^2.0.0",
2834
+ "lefthook": "^2.0.15",
2366
2835
  "npm-check-updates": "^19.0.0",
2367
2836
  "prettier": "^3.0.0",
2368
2837
  "typescript": "^5.9.0",
@@ -2656,7 +3125,52 @@ For CLI packages, consider restricting console usage to centralized output funct
2656
3125
  }
2657
3126
  ```
2658
3127
 
2659
- ### Appendix D: tsdown Config Example
3128
+ **Project-Specific Restricted Imports**:
3129
+
3130
+ Use `@typescript-eslint/no-restricted-imports` to enforce project-specific patterns.
3131
+ For example, requiring atomic file writes:
3132
+
3133
+ ```javascript
3134
+ {
3135
+ files: ['**/*.ts'],
3136
+ rules: {
3137
+ '@typescript-eslint/no-restricted-imports': [
3138
+ 'error',
3139
+ {
3140
+ paths: [
3141
+ {
3142
+ name: 'node:fs/promises',
3143
+ importNames: ['writeFile'],
3144
+ message: 'Use writeFile from "atomically" instead for atomic writes.',
3145
+ },
3146
+ ],
3147
+ },
3148
+ ],
3149
+ },
3150
+ }
3151
+ ```
3152
+
3153
+ **CLI Command Handler Relaxations**:
3154
+
3155
+ Commander.js command handlers often have async signatures and unused parameters.
3156
+ Relax strict rules for these files:
3157
+
3158
+ ```javascript
3159
+ {
3160
+ files: ['**/cli/commands/**/*.ts'],
3161
+ rules: {
3162
+ '@typescript-eslint/require-await': 'off',
3163
+ '@typescript-eslint/no-unused-vars': [
3164
+ 'error',
3165
+ { argsIgnorePattern: '^_|^options$|^id$|^query$' },
3166
+ ],
3167
+ },
3168
+ }
3169
+ ```
3170
+
3171
+ ### Appendix D: tsdown Config Examples
3172
+
3173
+ #### Simple Library (Single Config)
2660
3174
 
2661
3175
  ```typescript
2662
3176
  // tsdown.config.ts
@@ -2678,6 +3192,62 @@ export default defineConfig({
2678
3192
  });
2679
3193
  ```
2680
3194
 
3195
+ #### CLI/Library Hybrid (Multi-Config with Bundling)
3196
+
3197
+ For packages that are both a library and a CLI tool, use an array of configs to optimize
3198
+ each output separately:
3199
+
3200
+ ```typescript
3201
+ // tsdown.config.ts
3202
+ import { defineConfig } from 'tsdown';
3203
+ import { getGitVersion } from './scripts/git-version.mjs';
3204
+
3205
+ const version = getGitVersion();
3206
+
3207
+ const commonOptions = {
3208
+ format: ['esm'] as 'esm'[],
3209
+ platform: 'node' as const,
3210
+ target: 'node24' as const,
3211
+ sourcemap: true,
3212
+ dts: true,
3213
+ define: {
3214
+ __VERSION__: JSON.stringify(version),
3215
+ },
3216
+ };
3217
+
3218
+ export default defineConfig([
3219
+ // Library entry points (unbundled, with type declarations)
3220
+ {
3221
+ ...commonOptions,
3222
+ entry: {
3223
+ index: 'src/index.ts',
3224
+ cli: 'src/cli/cli.ts',
3225
+ },
3226
+ clean: true,
3227
+ },
3228
+ // CLI binary (bundled deps for fast startup, shebang banner)
3229
+ {
3230
+ ...commonOptions,
3231
+ entry: { bin: 'src/cli/bin.ts' },
3232
+ banner: '#!/usr/bin/env node',
3233
+ clean: false,
3234
+ noExternal: ['yaml', 'commander', 'picocolors', 'zod'],
3235
+ inlineOnly: false,
3236
+ },
3237
+ // CJS bootstrap (enables compile cache before ESM loads)
3238
+ {
3239
+ format: ['cjs'] as 'cjs'[],
3240
+ platform: 'node' as const,
3241
+ target: 'node24' as const,
3242
+ sourcemap: true,
3243
+ dts: false,
3244
+ entry: { 'bin-bootstrap': 'src/cli/bin-bootstrap.cjs' },
3245
+ banner: '#!/usr/bin/env node',
3246
+ clean: false,
3247
+ },
3248
+ ]);
3249
+ ```
3250
+
2681
3251
  ### Appendix E: Complete lefthook.yml Example
2682
3252
 
2683
3253
  ```yaml