create-node-lib 2.11.2 → 2.12.0

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/AGENTS.md CHANGED
@@ -21,7 +21,7 @@ The scaffolding engine that prompts users and generates projects:
21
21
  The EJS template files that become the generated project. These use placeholders like `<%= projectName %>`, `<%= author %>`, `<%= email %>`, `<%= username %>`, `<%= projectRepository %>`, and helpers like `<%= npmClientInstall(npmClient) %>` and `<%- changesetRepo({ projectRepository }) %>`.
22
22
 
23
23
  The generated project includes:
24
- - TypeScript source in `src/` with dual ESM/CJS output via tsup
24
+ - TypeScript source in `src/` with dual ESM/CJS output via tsdown
25
25
  - Node.js built-in test runner with c8 coverage
26
26
  - ESLint (neostandard) + eslint-plugin-security + Prettier
27
27
  - Changesets for versioning and releases
@@ -57,7 +57,7 @@ The generated project includes:
57
57
 
58
58
  | Command | Purpose |
59
59
  |---------|---------|
60
- | `npm run build` | `tsc && tsup` — compile TypeScript to ESM + CJS |
60
+ | `npm run build` | `tsc && tsdown` — compile TypeScript to ESM + CJS |
61
61
  | `npm test` | `c8 node --import tsx --test` — run tests with coverage |
62
62
  | `npm run lint` | ESLint + lockfile-lint + markdownlint |
63
63
  | `npm run lint:fix` | ESLint with auto-fix |
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [2.12.0](https://github.com/lirantal/create-node-lib/compare/v2.11.2...v2.12.0) (2026-03-13)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * gitignore should include dist/ ([33bcda7](https://github.com/lirantal/create-node-lib/commit/33bcda76bde22c7ac6828b129814aab8044ef43c))
7
+ * README had missing closing tags for h1 ([02302f6](https://github.com/lirantal/create-node-lib/commit/02302f6854b15c467e471f6e353824a96fe3406d))
8
+ * run directly with tsx ([17d82e5](https://github.com/lirantal/create-node-lib/commit/17d82e5bf503406571cee81f2947f91792674201))
9
+
10
+
11
+ ### Features
12
+
13
+ * migrate from tsup to tsdown ([0b0be21](https://github.com/lirantal/create-node-lib/commit/0b0be2147fd632cdee79ae463ba2add3b92d809c))
14
+
1
15
  ## [2.11.2](https://github.com/lirantal/create-node-lib/compare/v2.11.1...v2.11.2) (2026-03-09)
2
16
 
3
17
 
@@ -0,0 +1,333 @@
1
+ # Migrating a Node.js CLI from tsup to tsdown
2
+
3
+ A step-by-step guide for migrating a TypeScript Node.js CLI project from
4
+ [tsup](https://tsup.egoist.dev/) to [tsdown](https://tsdown.dev/). Based on a
5
+ real migration and cross-referenced with the
6
+ [official tsdown migration guide](https://tsdown.dev/guide/migrate-from-tsup)
7
+ and the
8
+ [Turborepo tsup-to-tsdown PR](https://github.com/vercel/turborepo/pull/11649).
9
+
10
+ ## Prerequisites
11
+
12
+ - Node.js >= 20.19 (required by tsdown)
13
+ - pnpm (guide uses pnpm commands; adapt for npm/yarn as needed)
14
+ - Existing tsup-based project with a `tsup.config.ts` and dual ESM/CJS output
15
+
16
+ ## Overview of changes
17
+
18
+ | File | Action |
19
+ | --- | --- |
20
+ | `package.json` | Swap `tsup` for `tsdown` in devDependencies, update build script |
21
+ | `tsup.config.ts` | Delete |
22
+ | `tsdown.config.ts` | Create with equivalent config |
23
+ | `package.json` exports/types | Update `.d.ts` references to `.d.mts` / `.d.cts` |
24
+ | `pnpm-workspace.yaml` | Allow native build scripts for rolldown dependencies |
25
+ | ESLint config | Ensure `dist/` is excluded from linting |
26
+
27
+ ---
28
+
29
+ ## Step 1: Swap the dependency and build script
30
+
31
+ In `package.json`:
32
+
33
+ 1. Remove `tsup` from `devDependencies`.
34
+ 2. Add `tsdown` to `devDependencies`.
35
+ 3. Update the `build` script to call `tsdown` instead of `tsup`.
36
+
37
+ ```diff
38
+ "scripts": {
39
+ - "build": "tsc && tsup",
40
+ + "build": "tsc && tsdown",
41
+ },
42
+ "devDependencies": {
43
+ - "tsup": "^8.1.0",
44
+ + "tsdown": "^0.9.0",
45
+ }
46
+ ```
47
+
48
+ ## Step 2: Replace the config file
49
+
50
+ Delete `tsup.config.ts` and create `tsdown.config.ts`. The config uses
51
+ `defineConfig` imported from `tsdown` instead of `tsup`.
52
+
53
+ ### Option mapping reference
54
+
55
+ Use this table to translate your tsup config options into tsdown equivalents:
56
+
57
+ | tsup option | tsdown equivalent | Notes |
58
+ | --- | --- | --- |
59
+ | `entryPoints` | `entry` | Different key name, same purpose |
60
+ | `format: ['cjs', 'esm']` | `format: ['cjs', 'esm']` | Identical syntax. tsdown defaults to `esm` only, so always set this explicitly for dual output |
61
+ | `dts: true` | `dts: true` | Auto-detected if `package.json` has a `types` field, but explicit is fine |
62
+ | `outDir` | `outDir` | Identical |
63
+ | `clean: true` | `clean: true` | Enabled by default in tsdown |
64
+ | `sourcemap: false` | `sourcemap: false` | Disabled by default in tsdown. Note: if your `tsconfig.json` has `declarationMap: true`, tsdown will force sourcemaps on for declaration files regardless of this setting |
65
+ | `bundle: true` | _(default)_ | tsdown bundles by default; use `unbundle: true` to mirror input structure |
66
+ | `splitting: false` | _(N/A)_ | tsdown does not have a `splitting` option; single-entry chunks do not split by default |
67
+ | `outExtension` | `outExtensions` | Note the plural "s". Different function signature (see below). Often unnecessary when using `fixedExtension: true` |
68
+ | `treeshake: false` | `treeshake: false` | tsdown defaults to `true`, so set explicitly if you want it off |
69
+ | `target` | `target` | Identical. tsdown also auto-reads from `engines.node` in `package.json` if not set |
70
+ | `platform: 'node'` | `platform: 'node'` | Identical |
71
+ | `tsconfig` | `tsconfig` | Identical |
72
+ | `cjsInterop: true` | `cjsDefault: true` | tsdown's equivalent for CJS default export interop |
73
+ | `keepNames: true` | _(N/A)_ | No direct equivalent in tsdown; drop this option |
74
+ | `skipNodeModulesBundle` | _(default)_ | tsdown handles externals via the `deps` config; default behavior is equivalent |
75
+ | `minify: false` | `minify: false` | Disabled by default in tsdown |
76
+
77
+ ### The `fixedExtension` option
78
+
79
+ When `platform` is set to `'node'`, tsdown defaults `fixedExtension` to `true`.
80
+ This means output files always use `.cjs` / `.mjs` extensions (and `.d.cts` /
81
+ `.d.mts` for declarations) regardless of the package `type` field. This
82
+ typically makes a custom `outExtensions` function unnecessary.
83
+
84
+ If you previously had a tsup `outExtension` function like:
85
+
86
+ ```typescript
87
+ outExtension(ctx) {
88
+ return {
89
+ dts: '.d.ts',
90
+ js: ctx.format === 'cjs' ? '.cjs' : '.mjs',
91
+ }
92
+ }
93
+ ```
94
+
95
+ You can replace it with just `fixedExtension: true` in tsdown (or omit it
96
+ entirely for `platform: 'node'` since it's the default).
97
+
98
+ ### Example config
99
+
100
+ **Before** (`tsup.config.ts`):
101
+
102
+ ```typescript
103
+ import { defineConfig } from 'tsup'
104
+
105
+ export default defineConfig([
106
+ {
107
+ entryPoints: ['src/main.ts', 'src/bin/cli.ts'],
108
+ format: ['cjs', 'esm'],
109
+ dts: true,
110
+ minify: false,
111
+ outDir: 'dist/',
112
+ clean: true,
113
+ sourcemap: false,
114
+ bundle: true,
115
+ splitting: false,
116
+ outExtension(ctx) {
117
+ return {
118
+ dts: '.d.ts',
119
+ js: ctx.format === 'cjs' ? '.cjs' : '.mjs',
120
+ }
121
+ },
122
+ treeshake: false,
123
+ target: 'es2022',
124
+ platform: 'node',
125
+ tsconfig: './tsconfig.json',
126
+ cjsInterop: true,
127
+ keepNames: true,
128
+ skipNodeModulesBundle: false,
129
+ },
130
+ ])
131
+ ```
132
+
133
+ **After** (`tsdown.config.ts`):
134
+
135
+ ```typescript
136
+ import { defineConfig } from 'tsdown'
137
+
138
+ export default defineConfig({
139
+ entry: ['src/main.ts', 'src/bin/cli.ts'],
140
+ format: ['cjs', 'esm'],
141
+ dts: true,
142
+ outDir: 'dist/',
143
+ clean: true,
144
+ sourcemap: false,
145
+ treeshake: false,
146
+ target: 'es2022',
147
+ platform: 'node',
148
+ tsconfig: './tsconfig.json',
149
+ cjsDefault: true,
150
+ fixedExtension: true,
151
+ minify: false,
152
+ })
153
+ ```
154
+
155
+ Key differences:
156
+
157
+ - No array wrapper needed (single config object, not `defineConfig([...])`)
158
+ - `entryPoints` becomes `entry`
159
+ - `cjsInterop` becomes `cjsDefault`
160
+ - `bundle`, `splitting`, `keepNames`, `skipNodeModulesBundle` are dropped
161
+ - `outExtension` function replaced by `fixedExtension: true`
162
+
163
+ ### CommonJS packages
164
+
165
+ If your package has `"type": "commonjs"` in `package.json`, name the config file
166
+ `tsdown.config.mts` instead of `tsdown.config.ts` (as noted in the
167
+ [Turborepo migration PR](https://github.com/vercel/turborepo/pull/11649)).
168
+
169
+ ## Step 3: Update declaration file references in package.json
170
+
171
+ tsdown with `fixedExtension: true` produces `.d.mts` for ESM declarations and
172
+ `.d.cts` for CJS declarations. tsup typically produced `.d.ts` for ESM. You need
173
+ to update any references in `package.json`:
174
+
175
+ ```diff
176
+ -"types": "dist/main.d.ts",
177
+ +"types": "dist/main.d.mts",
178
+ ```
179
+
180
+ ```diff
181
+ "exports": {
182
+ ".": {
183
+ "import": {
184
+ - "types": "./dist/main.d.ts",
185
+ + "types": "./dist/main.d.mts",
186
+ "default": "./dist/main.mjs"
187
+ },
188
+ "require": {
189
+ "types": "./dist/main.d.cts",
190
+ "default": "./dist/main.cjs"
191
+ }
192
+ }
193
+ }
194
+ ```
195
+
196
+ The `bin` field pointing to `.cjs` files does not need to change -- tsdown
197
+ produces `.cjs` files with the shebang (`#!/usr/bin/env node`) intact and
198
+ automatically grants execute permission.
199
+
200
+ ## Step 4: Allow native build scripts (pnpm)
201
+
202
+ tsdown depends on [rolldown](https://rolldown.rs/) (Rust-based bundler) and
203
+ other native packages that need to run postinstall scripts. If your
204
+ `pnpm-workspace.yaml` uses `onlyBuiltDependencies` or you have strict install
205
+ policies, add these packages:
206
+
207
+ ```yaml
208
+ onlyBuiltDependencies:
209
+ - esbuild
210
+ - rolldown
211
+ - unrs-resolver
212
+ ```
213
+
214
+ Without this, `pnpm install` will skip the native binary downloads and tsdown
215
+ will fail at runtime.
216
+
217
+ ## Step 5: Ensure dist/ is excluded from ESLint
218
+
219
+ tsdown's output code style differs from tsup's (tabs vs spaces, semicolons,
220
+ variable naming, etc.). If your ESLint config lints `dist/` files, you will get
221
+ false lint failures. Make sure `dist/**` is in your ESLint ignores:
222
+
223
+ ```javascript
224
+ // eslint.config.js (flat config)
225
+ export default [
226
+ {
227
+ ignores: ['dist/**'],
228
+ },
229
+ // ... rest of config
230
+ ]
231
+ ```
232
+
233
+ Or for legacy `.eslintrc`:
234
+
235
+ ```json
236
+ {
237
+ "ignorePatterns": ["dist/**"]
238
+ }
239
+ ```
240
+
241
+ This is good practice regardless of bundler -- build output should never be
242
+ linted.
243
+
244
+ ## Step 6: Install and verify
245
+
246
+ ```bash
247
+ # Update the lockfile and install tsdown
248
+ pnpm install --no-frozen-lockfile
249
+
250
+ # Run the build
251
+ pnpm run build
252
+
253
+ # Verify output files exist
254
+ ls dist/
255
+
256
+ # Expected output structure for a dual ESM/CJS CLI:
257
+ # dist/main.mjs (ESM entry)
258
+ # dist/main.cjs (CJS entry)
259
+ # dist/main.d.mts (ESM declarations)
260
+ # dist/main.d.cts (CJS declarations)
261
+ # dist/bin/cli.mjs (ESM CLI)
262
+ # dist/bin/cli.cjs (CJS CLI, with shebang)
263
+
264
+ # Test the CLI binary
265
+ node dist/bin/cli.cjs
266
+
267
+ # Run lint
268
+ pnpm run lint
269
+
270
+ # Run tests
271
+ pnpm run test
272
+ ```
273
+
274
+ ## Behavioral differences to be aware of
275
+
276
+ ### Declaration file extensions
277
+
278
+ tsdown uses `.d.mts` / `.d.cts` with `fixedExtension: true`, whereas tsup
279
+ typically used `.d.ts` / `.d.cts`. Update all `types` references in
280
+ `package.json` accordingly.
281
+
282
+ ### Declaration maps
283
+
284
+ If your `tsconfig.json` has `declarationMap: true`, tsdown will generate
285
+ `.d.mts.map` and `.d.cts.map` files and will force sourcemaps on for the
286
+ declaration build, regardless of the `sourcemap` setting in `tsdown.config.ts`.
287
+
288
+ ### Chunk naming
289
+
290
+ tsdown may produce internal chunks with hashed filenames (e.g.,
291
+ `main-DM5FYoS9.cjs`). These are implementation details invisible to package
292
+ consumers -- the public entry points (`main.cjs`, `main.mjs`) re-export from
293
+ them.
294
+
295
+ ### Target auto-detection
296
+
297
+ If you omit `target`, tsdown reads from `engines.node` in `package.json`. For
298
+ example, `"node": ">=24.0.0"` would auto-set the target to `node24`. Set
299
+ `target` explicitly if you want a specific ES version like `es2022`.
300
+
301
+ ### Shebang and execute permissions
302
+
303
+ tsdown automatically detects CLI entry points, preserves the `#!/usr/bin/env
304
+ node` shebang, and grants execute permission to the output file. No special
305
+ configuration needed.
306
+
307
+ ### No `keepNames` equivalent
308
+
309
+ tsdown does not have a `keepNames` option (which preserves `Function.name` and
310
+ `Class.name`). If your code relies on `Function.name` at runtime, verify that
311
+ the bundled output still works correctly.
312
+
313
+ ## Quick checklist
314
+
315
+ - [ ] Replace `tsup` with `tsdown` in `devDependencies`
316
+ - [ ] Update build script from `tsup` to `tsdown`
317
+ - [ ] Delete `tsup.config.ts`
318
+ - [ ] Create `tsdown.config.ts` (or `.mts` for CJS packages) with mapped options
319
+ - [ ] Update `types` and `exports.*.types` in `package.json` (`.d.ts` to `.d.mts`)
320
+ - [ ] Allow native build scripts in pnpm (`onlyBuiltDependencies`)
321
+ - [ ] Ensure `dist/` is excluded from ESLint
322
+ - [ ] Run `pnpm install --no-frozen-lockfile`
323
+ - [ ] Run `pnpm run build` and verify output files
324
+ - [ ] Run `pnpm run lint` and confirm no regressions
325
+ - [ ] Run `pnpm run test` and confirm all tests pass
326
+ - [ ] Test the CLI binary manually (`node dist/bin/cli.cjs`)
327
+
328
+ ## References
329
+
330
+ - [tsdown documentation](https://tsdown.dev/guide/)
331
+ - [tsdown UserConfig API reference](https://tsdown.dev/reference/api/Interface.UserConfig)
332
+ - [tsdown migration guide from tsup](https://tsdown.dev/guide/migrate-from-tsup)
333
+ - [Turborepo tsup-to-tsdown PR #11649](https://github.com/vercel/turborepo/pull/11649)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-node-lib",
3
- "version": "2.11.2",
3
+ "version": "2.12.0",
4
4
  "description": "Scaffolding out a Node.js library module",
5
5
  "bin": "./bin/cli.js",
6
6
  "engines": {
@@ -1,8 +1,10 @@
1
1
  <!-- markdownlint-disable -->
2
2
 
3
- <p align="center"><h1 align="center">
4
- <%= projectName %>
5
- </h1>
3
+ <p align="center">
4
+ <h1 align="center">
5
+ <%= projectName %>
6
+ </h1>
7
+ </p>
6
8
 
7
9
  <p align="center">
8
10
  <%= description %>
@@ -3,7 +3,7 @@ import neostandard, { resolveIgnoresFromGitignore, plugins } from 'neostandard'
3
3
 
4
4
  export default [
5
5
  ...neostandard({
6
- ignores: ['__tests__/**/*.ts', ...resolveIgnoresFromGitignore()],
6
+ ignores: ['__tests__/**/*.ts', 'dist/**', ...resolveIgnoresFromGitignore()],
7
7
  ts: true, // Enable TypeScript support,
8
8
  filesTs: ['src/**/*.ts', '__tests__/**/*.ts']
9
9
  }),
@@ -11,6 +11,9 @@ pids
11
11
  *.seed
12
12
  *.pid.lock
13
13
 
14
+ # dist
15
+ dist/
16
+
14
17
  # Directory for instrumented libs generated by jscoverage/JSCover
15
18
  lib-cov
16
19
 
@@ -2,7 +2,7 @@
2
2
  "name": "<%= projectName %>",
3
3
  "version": "0.0.1",
4
4
  "description": "<%= description %>",
5
- "types": "dist/main.d.ts",
5
+ "types": "dist/main.d.mts",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "<%= projectName %>": "./dist/bin/cli.cjs"
@@ -10,7 +10,7 @@
10
10
  "exports": {
11
11
  ".": {
12
12
  "import": {
13
- "types": "./dist/main.d.ts",
13
+ "types": "./dist/main.d.mts",
14
14
  "default": "./dist/main.mjs"
15
15
  },
16
16
  "require": {
@@ -20,7 +20,7 @@
20
20
  "default": "./dist/main.mjs"
21
21
  },
22
22
  "./dist/*": {
23
- "types": "./dist/*.d.ts",
23
+ "types": "./dist/*.d.mts",
24
24
  "import": "./dist/*.mjs",
25
25
  "require": "./dist/*.cjs"
26
26
  }
@@ -35,8 +35,8 @@
35
35
  "bin"
36
36
  ],
37
37
  "scripts": {
38
- "start": "node --import tsx src/bin/cli.ts",
39
- "build": "tsc && tsup",
38
+ "start": "tsx src/bin/cli.ts",
39
+ "build": "tsc && tsdown",
40
40
  "lint": "eslint . && npm run lint:lockfile && npm run lint:markdown",
41
41
  "lint:markdown": "npx -y markdownlint-cli@0.48.0 -c .github/.markdownlint.yml -i '.git' -i '__tests__' -i '.github' -i '.changeset' -i 'CODE_OF_CONDUCT.md' -i 'CHANGELOG.md' -i 'docs/**' -i 'node_modules' -i 'dist' '**/**.md' --fix",
42
42
  "lint:fix": "eslint . --fix",
@@ -80,7 +80,7 @@
80
80
  "lint-staged": "^16.2.7",
81
81
  "lockfile-lint": "^4.14.0",
82
82
  "neostandard": "^0.12.2",
83
- "tsup": "^8.1.0",
83
+ "tsdown": "^0.9.0",
84
84
  "tsx": "^4.19.4",
85
85
  "typescript": "^5.5.3",
86
86
  "validate-conventional-commit": "^1.0.4"
@@ -12,3 +12,8 @@ trustPolicy: no-downgrade
12
12
  # Ignore the check for packages published more than 30 days ago (pnpm 10.27+)
13
13
  # Useful for older packages that pre-date provenance support
14
14
  trustPolicyIgnoreAfter: 43200 # minutes (30 days)
15
+
16
+ onlyBuiltDependencies:
17
+ - esbuild
18
+ - rolldown
19
+ - unrs-resolver
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from 'tsdown'
2
+
3
+ export default defineConfig({
4
+ entry: ['src/main.ts', 'src/bin/cli.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ outDir: 'dist/',
8
+ clean: true,
9
+ sourcemap: false,
10
+ treeshake: false,
11
+ target: 'es2022',
12
+ platform: 'node',
13
+ tsconfig: './tsconfig.json',
14
+ cjsDefault: true,
15
+ fixedExtension: true,
16
+ minify: false,
17
+ })
@@ -1,29 +0,0 @@
1
- import { defineConfig } from 'tsup'
2
-
3
- export default defineConfig([
4
- {
5
- entryPoints: ['src/main.ts', 'src/bin/cli.ts'],
6
- format: ['cjs', 'esm'],
7
- dts: true,
8
- minify: false,
9
- outDir: 'dist/',
10
- clean: true,
11
- sourcemap: false,
12
- bundle: true,
13
- splitting: false,
14
- outExtension (ctx) {
15
- return {
16
- dts: '.d.ts',
17
- js: ctx.format === 'cjs' ? '.cjs' : '.mjs',
18
- }
19
- },
20
- treeshake: false,
21
- target: 'es2022',
22
- platform: 'node',
23
- tsconfig: './tsconfig.json',
24
- cjsInterop: true,
25
- keepNames: true,
26
- skipNodeModulesBundle: false,
27
- },
28
-
29
- ])