zshy 0.0.4 → 0.0.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  <p align="center">
2
2
 
3
3
  <h1 align="center">⚜️<br/><code>zshy</code></h1>
4
- <p align="center">Gold-standard build tool for TypeScript libraries.
4
+ <p align="center">The ultimate build tool for TypeScript libraries. Powered by <code>tsc</code>.
5
5
  <br/>
6
6
  by <a href="https://x.com/colinhacks">@colinhacks</a>
7
7
  </p>
@@ -28,7 +28,6 @@
28
28
  <br/>
29
29
  <br/>
30
30
 
31
-
32
31
  <!-- ## What is `zshy`? -->
33
32
 
34
33
  <h2 align="center">What is <code>zshy</code>?</h2>
@@ -39,14 +38,15 @@
39
38
 
40
39
  - 🧱 **Dual-module builds** — Builds ESM and CJS outputs from a single TypeScript source file
41
40
  - 👑 **Powered by `tsc`** — No bundling, no extra configs, just good old-fashioned `tsc`
42
- - 📝 **Declarative config** — No build scripts, just a simple `"zshy"` field in your `package.json`
41
+ - 🟦 **No config file** — Reads only from your `package.json` and `tsconfig.json` (configurable)
42
+ - 📝 **Declarative entrypoint map** — Specify your TypeScript entrypoints in `package.json#/zshy`
43
43
  - 🤖 **Auto-generated `"exports"`** — Writes `"exports"` map directly into your `package.json`
44
- - 🟦 **Respects your `tsconfig.json`**
45
- - 📂 **Unopinionated about file structure** — Use any file structure you like
46
- - 🔗 **Unopinionated about import extensions** — Use any import syntax TypeScript supports: extensionless, `.js`, `.ts`
47
- - ⚛️ **Supports `.tsx`**
48
- - 📱 **Supports React Native**
49
- - 🐌 **Blazing fast** — Just kidding, it's slow. But `tsc` is about to get 10x faster!
44
+ - 🐚 **CLI-friendly** First-class `"bin"` support
45
+ - 📂 **Supports any file structure** — Use any file structure you like
46
+ - 🔗 **Supports extensionless imports** — Use any import syntax TypeScript supports: extensionless, `.js`, `.ts`
47
+ - ⚛️ **Supports `.tsx`** — Rewrites to `.js/.cjs/.mjs` per your `tsconfig.json#/jsx*` settings
48
+ - 📱 **Supports React Native** — Supports a [flat build mode](#can-it-support-react-native-legacy-or-non-nodejs-environments) designed for bundlers that don't support `package.json#/exports`
49
+ - 🐌 **Blazing fast** — Just kidding, it's slow. But [it's worth it](#is-it-fast).
50
50
 
51
51
  <br/>
52
52
  <br/>
@@ -72,11 +72,65 @@ Specify your package entrypoint with the `"zshy"` key in `package.json`.
72
72
  {
73
73
  "name": "my-pkg",
74
74
  "version": "1.0.0",
75
- "zshy": "./src/index.ts" // package entrypoint
75
+ "zshy": "./src/index.ts" // 👈 package entrypoint
76
76
  }
77
77
  ```
78
78
 
79
- More complicated packages can specify subpaths or wildcard exports with `"zshy.exports"`:
79
+ <br/>
80
+
81
+ ### 3. Run a build
82
+
83
+ ```bash
84
+ $ npx zshy
85
+
86
+ 💎 Starting zshy build...
87
+ ⚙️ Detected project root: /Users/colinmcd94/Documents/projects/zshy
88
+ 📦 Reading package.json from ./package.json
89
+ 📁 Reading tsconfig from ./tsconfig.json
90
+ 🗑️ Cleaning up outDir...
91
+ ➡️ Determining entrypoints...
92
+ ╔════════════╤════════════════╗
93
+ ║ Subpath │ Entrypoint ║
94
+ ╟────────────┼────────────────╢
95
+ ║ "my-pkg" │ ./src/index.ts ║
96
+ ╚════════════╧════════════════╝
97
+ 🔧 Resolved build paths:
98
+ ╔══════════╤═══════════════╗
99
+ ║ Location │ Resolved path ║
100
+ ╟──────────┼───────────────╢
101
+ ║ rootDir │ ./src ║
102
+ ║ outDir │ ./out ║
103
+ ╚══════════╧═══════════════╝
104
+ 🟨 Package is an ES module (package.json#/type is "module")
105
+ 🧱 Building CJS... (rewriting .ts -> .cjs/.d.cts)
106
+ 🧱 Building ESM...
107
+ 📦 Updating package.json#/exports...
108
+ 📦 Updating package.json#/bin...
109
+ 🎉 Build complete!
110
+ ```
111
+
112
+ Alernatively, add a `"build"` script to your `package.json`:
113
+
114
+ ```diff
115
+ {
116
+ // ...
117
+ "scripts": {
118
+ + "build": "zshy"
119
+ }
120
+ }
121
+ ```
122
+
123
+ Then, to run a build:
124
+
125
+ ```bash
126
+ $ npm run build
127
+ ```
128
+
129
+ <br/>
130
+
131
+ ### Subpaths and wildcards
132
+
133
+ Multi-entrypoint packages can specify subpaths or wildcard exports with `package.json#/zshy/exports`:
80
134
 
81
135
  ```jsonc
82
136
  {
@@ -93,9 +147,10 @@ More complicated packages can specify subpaths or wildcard exports with `"zshy.e
93
147
  }
94
148
  ```
95
149
 
96
- <br/>
150
+ <details>
151
+ <summary>View typical build output</summary>
97
152
 
98
- ### 3. Run a build
153
+ When you run a build, you'll see something like this:
99
154
 
100
155
  ```bash
101
156
  $ npx zshy
@@ -109,62 +164,110 @@ $ npx zshy
109
164
  ║ Subpath │ Entrypoint ║
110
165
  ╟────────────────────┼─────────────────────────────╢
111
166
  ║ "my-pkg" │ ./src/index.ts ║
112
- ╟────────────────────┼─────────────────────────────╢
113
167
  ║ "my-pkg/utils" │ ./src/utils.ts ║
114
- ╟────────────────────┼─────────────────────────────╢
115
168
  ║ "my-pkg/plugins/*" │ ./src/plugins/* (5 matches) ║
116
169
  ╚════════════════════╧═════════════════════════════╝
117
- 📂 Transpiling from ./src (rootDir) to ./dist (outDir)
118
- 🟨 Package is ES module (package.json#type is "module")
170
+ 🔧 Resolved build paths:
171
+ ╔══════════╤═══════════════╗
172
+ ║ Location │ Resolved path ║
173
+ ╟──────────┼───────────────╢
174
+ ║ rootDir │ ./src ║
175
+ ║ outDir │ ./out ║
176
+ ╚══════════╧═══════════════╝
177
+ 🟨 Package is ES module (package.json#/type is "module")
119
178
  🧱 Building CJS... (rewriting .ts -> .cjs/.d.cts)
120
179
  🧱 Building ESM...
121
180
  📦 Updating package.json exports...
122
- {
123
- ".": {
124
- "types": "./out/index.d.cts",
125
- "import": "./out/index.js",
126
- "require": "./out/index.cjs"
127
- },
128
- "./utils": {
129
- "types": "./out/utils.d.cts",
130
- "import": "./out/utils.js",
131
- "require": "./out/utils.cjs"
132
- },
133
- "./plugins/*": {
134
- "import": "./out/src/plugins/*",
135
- "require": "./out/src/plugins/*"
136
- }
137
- }
138
181
  🎉 Build complete!
139
182
  ```
140
183
 
141
- Alernatively, add a `"build"` script to your `package.json`:
184
+ And the generated `"exports"` map will look like this:
142
185
 
143
186
  ```diff
187
+ // package.json
144
188
  {
145
189
  // ...
146
- "scripts": {
147
- + "build": "zshy"
190
+ + "exports": {
191
+ + ".": {
192
+ + "types": "./out/index.d.cts",
193
+ + "import": "./out/index.js",
194
+ + "require": "./out/index.cjs"
195
+ + },
196
+ + "./utils": {
197
+ + "types": "./out/utils.d.cts",
198
+ + "import": "./out/utils.js",
199
+ + "require": "./out/utils.cjs"
200
+ + },
201
+ + "./plugins/*": {
202
+ + "types": "./out/src/plugins/*",
203
+ + "import": "./out/src/plugins/*",
204
+ + "require": "./out/src/plugins/*"
205
+ + }
206
+ + }
207
+ }
208
+ ```
209
+
210
+ </details>
211
+
212
+ <br/>
213
+
214
+ ### For CLIs (`"bin"` support)
215
+
216
+ If your package is a CLI, specify your CLI entrypoint in `package.json#/zshy/bin`. `zshy` will include this entrypoint in your builds and automatically set `"bin"` in your package.json.
217
+
218
+ ```json
219
+ {
220
+ // package.json
221
+ "name": "my-cli",
222
+ "version": "1.0.0",
223
+ "zshy": {
224
+ "bin": "./src/cli.ts" // 👈 specify CLI entrypoint
148
225
  }
149
226
  }
150
227
  ```
151
228
 
152
- Then, to run a build:
229
+ When you run `zshy`, it will automatically add the appropriate `"bin"` field to your `package.json`:
153
230
 
154
- ```bash
155
- $ npm run build
231
+ ```diff
232
+ {
233
+ // package.json
234
+ "name": "my-cli",
235
+ "version": "1.0.0",
236
+ "zshy": {
237
+ "exports": "./src/index.ts",
238
+ "bin": "./src/cli.ts"
239
+ },
240
+ + "bin": {
241
+ + "my-cli": "./out/cli.js" // CLI entrypoint
242
+ + }
243
+ }
156
244
  ```
157
245
 
246
+ <br/>
247
+
248
+ ### Flags
249
+
250
+ ```sh
251
+ $ npm zshy --help
252
+ Usage: zshy [options]
253
+
254
+ Options:
255
+ -h, --help Show this help message
256
+ -p, --project <path> Path to tsconfig.json file
257
+ --verbose Enable verbose output
258
+ --dry-run Don't write any files or update package.json
259
+ ```
158
260
 
159
261
  <br/>
160
262
  <br/>
263
+
161
264
  <h2 align="center">FAQ</h2>
162
265
 
163
266
  <br/>
164
267
 
165
268
  ### How does `zshy` resolve entrypoints?
166
269
 
167
- It reads your `package.json#zshy` config:
270
+ It reads your `package.json#/zshy` config:
168
271
 
169
272
  ```jsonc
170
273
  // package.json
@@ -214,72 +317,82 @@ No. You can organize your source however you like; `zshy` will transpile your en
214
317
 
215
318
  ### What files does `zshy` create?
216
319
 
217
- It depends on your `package.json#type` field. If your package is ESM (that is, `"type": "module"` in `package.json`), the CJS build files will be generated as `.cjs`/`.d.cts`:
320
+ It depends on your `package.json#/type` field. If your package is ESM (that is, `"type": "module"` in `package.json`):
218
321
 
219
322
  - `.js` + `.d.ts` (ESM)
220
323
  - `.cjs` + `.d.cts` (CJS)
221
324
 
222
325
  ```bash
223
- $ tree out
326
+ $ tree dist
224
327
 
225
328
  .
226
329
  ├── package.json # if type == "module"
227
330
  ├── src
228
331
  │   └── index.ts
229
- └── out
230
-    ├── index.js
332
+ └── dist
333
+    ├── index.js
231
334
     ├── index.d.ts
232
335
     ├── index.cts
233
336
     └── index.d.cts
234
337
  ```
235
338
 
236
- Otherwise, the package is considered *default-CJS* and the ESM build files will be rewritten as `.mjs`/`.d.mts`.
339
+ Otherwise, the package is considered _default-CJS_ and the ESM build files will be rewritten as `.mjs`/`.d.mts`.
237
340
 
238
341
  - `.mjs` + `.d.mts` (ESM)
239
342
  - `.js` + `.d.ts` (CJS)
240
343
 
241
344
  ```bash
242
- $ tree out
345
+ $ tree dist
243
346
  .
244
347
  ├── package.json # if type != "module"
245
348
  ├── src
246
349
  │   └── index.ts
247
- └── out
350
+ └── dist
248
351
     ├── index.js
249
352
     ├── index.d.ts
250
353
     ├── index.mjs
251
354
     └── index.d.mts
252
355
  ```
253
356
 
254
- > **Comparison** — `tshy` generates plain `.js`/`.d.ts` files into separate `dist/esm` and `dist/cjs` directories, each with a stub `package.json` to enable proper module resolution in Node.js. This is more convoluted than the flat file structure generated by `zshy`. It also causes issues with [Module Federation](https://github.com/colinhacks/zod/issues/4656).
357
+ > **Comparison** — `tshy` generates plain `.js`/`.d.ts` files into separate `dist/esm` and `dist/cjs` directories, each with a stub `package.json` to enable proper module resolution in Node.js. This is more convoluted than the flat file structure generated by `zshy`. It also causes issues with [Module Federation](https://github.com/colinhacks/zod/issues/4656).
255
358
 
256
359
  <br/>
257
360
 
258
361
  ### How does extension rewriting work?
259
362
 
260
- `zshy` uses the [TypeScript Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API) to perform extension rewriting during the `tsc` build process. TypeScript provides dedicated hooks for performing such transforms (though they are criminally under-utilized, until now!):
363
+ `zshy` uses the [TypeScript Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API) to rewrite file extensions during the `tsc` build process. This makes it possible to generate CJS and ESM build outputs side-by-side.
364
+
365
+ Depending on the build format being targeted, `zshy` will:
366
+
367
+ - Rewrite `.ts` imports/exports to `.js`/`.cjs`/`.mjs`
368
+ - Rewrite extensionless imports/exports to `.js`/`.cjs`/`.mjs`
369
+ - Rewrite `.js` imports/exports to `.cjs`/`.mjs`
370
+ - Rename build output files to `.cjs`/`.mjs`/`.d.cts`/`.d.mts`
371
+
372
+ TypeScript provides dedicated hooks for performing such transforms (though they are criminally under-utilized).
261
373
 
262
374
  - **`ts.TransformerFactory`**: Provides AST transformations to rewrite import/export extensions before module conversion
263
375
  - **`ts.CompilerHost#writeFile`**: Handles output file extension changes (`.js` → `.cjs`/`.mjs`)
264
376
 
265
- > **Comparison** — `tshy` was designed to enable dual-package builds powered by the `tsc` compiler. To make this work, it relies on a specific file structure and the creation of temporary `package.json` files to accommodate the various idiosyncrasies of Node.js module resolution. TypeScript provides a robust API for AST transformations (`ts.TransformerFactory`) that `tshy` does not take advantage of.
377
+ > **Comparison** — `tshy` was designed to enable dual-package builds powered by the `tsc` compiler. To make this work, it relies on a specific file structure and the creation of temporary `package.json` files to accommodate the various idiosyncrasies of Node.js module resolution. It also requires the use of separate `dist/esm` and `dist/cjs` build subdirectories.
266
378
 
267
379
  <br/>
268
380
 
269
381
  ### Can I use extension-less imports?
270
382
 
271
- Yes! `zshy` supports whatever import style you prefer:
383
+ Yes! `zshy` supports whatever import style you prefer:
384
+
272
385
  - `from "./utils"`: classic extensionless imports
273
386
  - `from "./utils.js"`: ESM-friendly extensioned imports
274
387
  - `from "./util.ts"`: recently supported natively via[`rewriteRelativeImportExtensions`](https://www.typescriptlang.org/tsconfig/#rewriteRelativeImportExtensions)
275
388
 
276
- Use whatever you like; `zshy` will rewrite extensionless and `.ts` imports/exports to have the appropriate file extension.
389
+ Use whatever you like; `zshy` will rewrite all imports/exports properly during the build process.
277
390
 
278
391
  > **Comparison** — `tshy` forces you to use `.js` imports throughout your codebase. While this is generally a good practice, it's not always feasible, and there are hundreds of thousands of existing TypeScript codebases reliant on extensionless imports.
279
392
 
280
393
  <br/>
281
394
 
282
- ### How does it generate `package.json#exports`?
395
+ ### How does it generate `package.json#/exports`?
283
396
 
284
397
  Your exports map is automatically written into your `package.json` when you run `zshy`. The generated exports map looks like this:
285
398
 
@@ -299,7 +412,7 @@ Your exports map is automatically written into your `package.json` when you run
299
412
  + "require": "./out/index.cjs"
300
413
  + },
301
414
  + "./utils": {
302
- + "types": "./out/utils.d.cts",
415
+ + "types": "./out/utils.d.cts",
303
416
  + "import": "./out/utils.js",
304
417
  + "require": "./out/utils.cjs"
305
418
  + },
@@ -324,7 +437,7 @@ import mod from "pkg"; ^^^^^
324
437
  // ^ The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("pkg")' call instead.
325
438
  ```
326
439
 
327
- By having `"types"` point to the `.d.cts` declarations, this error will never happen. Technically, we're lying to TypeScript and telling it to always assume our code is CommonJS; in practice, this has no real consequences and maximizes compatibility. To learn more, read the ["Masquerading as ESM"](https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseESM.md) and ["Masquerading as CJS"](https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md) writeups from Are The Types Wrong.
440
+ By having `"types"` point to the `.d.cts` declarations, this error will never happen. Technically, we're lying to TypeScript and telling it to always assume our code is CommonJS; in practice, this has no real consequences and maximizes compatibility. To learn more, read the ["Masquerading as ESM"](https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseESM.md) and ["Masquerading as CJS"](https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md) writeups from Are The Types Wrong.
328
441
 
329
442
  > **Comparison** — `tshy` generates independent (but identical) `.d.ts` files in `dist/esm` and `dist/cjs`. This can cause [Excessively Deep](https://github.com/colinhacks/zod/issues/4422) errors if users of the library use declaration merging (`declare module {}`) for plugins/extensions. [Zod](https://github.com/colinhacks/zod), [day.js](https://day.js.org/), and others rely on this pattern for plugins.
330
443
 
@@ -332,25 +445,33 @@ By having `"types"` point to the `.d.cts` declarations, this error will never ha
332
445
 
333
446
  ### Can it support React Native legacy or non-Node.js environments?
334
447
 
335
- Yes! This is one of the key reasons `zshy` was originally developed for Zod.
448
+ Yes! This is one of the key reasons `zshy` was originally developed. Many environments don't support `package.json#/exports` yet:
449
+
450
+ - Node.js v12.7 or earlier
451
+ - React Native - The Metro bundler does not support `"exports"` by default
452
+ - TypeScript projects with legacy configs — e.. `"module": "commonjs"`
336
453
 
337
- Many environments don't support `package.json#exports` yet (React Native, older bundlers, Node.js v10 or earlier, and many TypeScript projects using legacy configs). This causes issues for packages that want to use subpath imports to structure their package. Fortunately `zshy` unlocks a workaround I call a *flat build*:
454
+ This causes issues for packages that want to use subpath imports to structure their package. Fortunately `zshy` unlocks a workaround I call a _flat build_:
338
455
 
339
456
  1. Remove `"type": "module"` from your `package.json` (if present)
340
457
  2. Put your source files in your package root (not in a `src` directory)
341
458
  3. Set `outDir: "."` in your `tsconfig.json`
459
+ 4. Configure `"exclude"` in `package.json` to exclude all source files:
342
460
 
343
- With this setup, your build outputs (`index.js`, etc) will be written to disk right next to their corresponding source files. This lets you simulate subpath imports; imports like `"your-library/utils"` will generally resolve to `"your-library/utils/index.js"` in environments that predate modern module resolution. Zod uses this approach for broader compatibility with the following environments:
461
+ - ```jsonc
462
+ {
463
+ // ...
464
+ "exclude": ["**/*.ts", "**/*.tsx", "**/*.cts", "**/*.mts", "node_modules"]
465
+ }
466
+ ```
344
467
 
345
- 1. **Node.js v12.7 or older**
346
- 2. **TypeScript projects using legacy configs** - e.g. `"module": "commonjs"`
347
- 3. **React Native** - The Metro bundler does not support `"exports"` by default
468
+ With this setup, your build outputs (`index.js`, etc) will be written to disk alongside to their corresponding source files. Older environments will resolve imports like `"your-library/utils"` to `"your-library/utils/index.js"`, effectively simulating subpath imports in environments that don't support them.
348
469
 
349
- <br/>
470
+ <br/>
350
471
 
351
472
  ### Is it fast?
352
473
 
353
- Not really. Typechecking with `tsc` is a lot slower than using a bundler that strips types. That said:
354
- 1) you *should* be type checking your code during builds,
355
- 2) TypeScript is [about to get 10x faster](https://devblogs.microsoft.com/typescript/typescript-native-port/), and
356
- 3) you just spent the last hour staring at a Cursor spinner anyway 😇
474
+ Not really. It uses `tsc` to typecheck your codebase, which is a lot slower than using a bundler that strips types. That said:
475
+
476
+ 1. you _should_ be type checking your code during builds;
477
+ 2. TypeScript is [about to get 10x faster](https://devblogs.microsoft.com/typescript/typescript-native-port/)