{"id":29079413,"url":"https://github.com/colinhacks/zshy","last_synced_at":"2025-07-12T15:33:24.408Z","repository":{"id":301405008,"uuid":"1008874029","full_name":"colinhacks/zshy","owner":"colinhacks","description":null,"archived":false,"fork":false,"pushed_at":"2025-06-26T18:16:44.000Z","size":58,"stargazers_count":11,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-06-26T18:20:16.894Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/colinhacks.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-06-26T08:18:07.000Z","updated_at":"2025-06-26T18:16:47.000Z","dependencies_parsed_at":"2025-06-26T18:32:24.515Z","dependency_job_id":null,"html_url":"https://github.com/colinhacks/zshy","commit_stats":null,"previous_names":["colinhacks/zshy"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/colinhacks/zshy","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/colinhacks%2Fzshy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/colinhacks%2Fzshy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/colinhacks%2Fzshy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/colinhacks%2Fzshy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/colinhacks","download_url":"https://codeload.github.com/colinhacks/zshy/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/colinhacks%2Fzshy/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262137148,"owners_count":23264674,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-06-27T17:02:36.450Z","updated_at":"2025-07-12T15:33:24.397Z","avatar_url":"https://github.com/colinhacks.png","language":"TypeScript","readme":"\u003cp align=\"center\"\u003e\n\n  \u003ch1 align=\"center\"\u003e🐒\u003cbr/\u003e\u003ccode\u003ezshy\u003c/code\u003e\u003c/h1\u003e\n  \u003cp align=\"center\"\u003eThe no-bundler build tool for TypeScript libraries. Powered by \u003ccode\u003etsc\u003c/code\u003e.\n    \u003cbr/\u003e\n    by \u003ca href=\"https://x.com/colinhacks\"\u003e@colinhacks\u003c/a\u003e\n  \u003c/p\u003e\n\u003c/p\u003e\n\u003cbr/\u003e\n\n\u003cp align=\"center\"\u003e\n\u003c!-- \u003ca href=\"https://github.com/colinhacks/zshy/actions?query=branch%3Amain\"\u003e\u003cimg src=\"https://github.com/colinhacks/zshy/actions/workflows/test.yml/badge.svg?event=push\u0026branch=main\" alt=\"zshy CI status\" /\u003e\u003c/a\u003e --\u003e\n\u003ca href=\"https://opensource.org/licenses/MIT\" rel=\"nofollow\"\u003e\u003cimg src=\"https://img.shields.io/github/license/colinhacks/zshy\" alt=\"License\"\u003e\u003c/a\u003e\n\u003ca href=\"https://www.npmjs.com/package/zshy\" rel=\"nofollow\"\u003e\u003cimg src=\"https://img.shields.io/npm/dw/zshy.svg\" alt=\"npm\"\u003e\u003c/a\u003e\n\u003ca href=\"https://github.com/colinhacks/zshy\" rel=\"nofollow\"\u003e\u003cimg src=\"https://img.shields.io/github/stars/colinhacks/zshy\" alt=\"stars\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003c!-- \u003cdiv align=\"center\"\u003e\n  \u003ca href=\"https://github.com/colinhacks/zshy\"\u003eGitHub\u003c/a\u003e\n  \u003cspan\u003e\u0026nbsp;\u0026nbsp;•\u0026nbsp;\u0026nbsp;\u003c/span\u003e\n  \u003ca href=\"https://twitter.com/colinhacks\"\u003e𝕏\u003c/a\u003e\n  \u003cspan\u003e\u0026nbsp;\u0026nbsp;•\u0026nbsp;\u0026nbsp;\u003c/span\u003e\n  \u003ca href=\"https://bsky.app/profile/colinhacks.com\"\u003eBluesky\u003c/a\u003e\n  \u003cbr /\u003e\n\u003c/div\u003e --\u003e\n\n\u003cbr/\u003e\n\u003cbr/\u003e\n\u003cbr/\u003e\n\n\u003c!-- ## What is `zshy`? --\u003e\n\n\u003ch2 align=\"center\"\u003eWhat is \u003ccode\u003ezshy\u003c/code\u003e?\u003c/h2\u003e\n\n`zshy` (zee-shy) is a bundler-free batteries-included build tool for transpiling TypeScript libraries. It was originally created as an internal build tool for [Zod](https://github.com/colinhacks/zod) but is now available as a general-purpose tool for TypeScript libraries.\n\n- 🧱 **Dual-module builds** — Builds ESM and CJS outputs from a single TypeScript source file\n- 👑 **Powered by `tsc`** — The gold standard for TypeScript transpilation\n- 📦 **Bundler-free** — No bundler or bundler configs involved\n- 🟦 **No config file** — Reads from your `package.json` and `tsconfig.json`\n- 📝 **Declarative entrypoint map** — Specify your TypeScript entrypoints in `package.json#/zshy`\n- 🤖 **Auto-generated `\"exports\"`** — Writes `\"exports\"` map directly into your `package.json`\n- 📂 **Unopinionated** — Use any file structure or import extension syntax you like\n- 📦 **Asset handling** — Non-JS assets are copied to the output directory\n- ⚛️ **Supports `.tsx`** — Rewrites to `.js/.cjs/.mjs` per your `tsconfig.json#/jsx*` settings\n- 🐚 **CLI-friendly** — First-class `\"bin\"` support\n- 🐌 **Blazing fast** — Just kidding, it's slow. But [it's worth it](#is-it-fast)\n\n\u003c!-- - 📱 **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` --\u003e\n\n\u003cbr/\u003e\n\u003cbr/\u003e\n\u003cbr/\u003e\n\n\u003ch2 align=\"center\"\u003eQuickstart\u003c/h2\u003e\n\n\u003cbr/\u003e\n\n### 1. Install `zshy` as a dev dependency:\n\n```bash\nnpm install --save-dev zshy\nyarn add --dev zshy\npnpm add --save-dev zshy\n```\n\n\u003cbr/\u003e\n\n### 2. Specify your entrypoint(s) in `package.json#zshy`:\n\n```diff\n{\n  \"name\": \"my-pkg\",\n  \"version\": \"1.0.0\",\n\n  // with a single entrypoint\n+ \"zshy\": \"./src/index.ts\"\n\n  // with multiple entrypoints (subpaths, wildcards, deep wildcards)\n+ \"zshy\": {\n+   \"exports\": {\n+     \".\": \"./src/index.ts\",\n+     \"./utils\": \"./src/utils.ts\",\n+     \"./plugins/*\": \"./src/plugins/*\"\n+     \"./components/**/*\": \"./src/components/**/*\"\n+   }\n+ }\n}\n```\n\n\u003cbr/\u003e\n\n### 3. Run a build\n\nRun a build with `npx zshy`:\n\n```bash\n$ npx zshy # use --dry-run to try it out without writing/updating files\n\n→  Starting zshy build 🐒\n→  Detected project root: /Users/colinmcd94/Documents/projects/zshy\n→  Reading package.json from ./package.json\n→  Reading tsconfig from ./tsconfig.json\n→  Cleaning up outDir...\n→  Determining entrypoints...\n   ╔════════════╤════════════════╗\n   ║ Subpath    │ Entrypoint     ║\n   ╟────────────┼────────────────╢\n   ║ \"my-pkg\"   │ ./src/index.ts ║\n   ╚════════════╧════════════════╝\n→  Resolved build paths:\n   ╔══════════╤════════════════╗\n   ║ Location │ Resolved path  ║\n   ╟──────────┼────────────────╢\n   ║ rootDir  │ ./src          ║\n   ║ outDir   │ ./dist         ║\n   ╚══════════╧════════════════╝\n→  Package is an ES module (package.json#/type is \"module\")\n→  Building CJS... (rewriting .ts -\u003e .cjs/.d.cts)\n→  Building ESM...\n→  Updating package.json#/exports...\n→  Updating package.json#/bin...\n→  Build complete! ✅\n```\n\n\u003e **Add a `\"build\"` script to your `package.json`**\n\u003e\n\u003e ```diff\n\u003e {\n\u003e   // ...\n\u003e   \"scripts\": {\n\u003e +   \"build\": \"zshy\"\n\u003e   }\n\u003e }\n\u003e ```\n\u003e\n\u003e Then, to run a build:\n\u003e\n\u003e ```bash\n\u003e $ npm run build\n\u003e ```\n\n\u003cbr/\u003e\n\n\u003cbr/\u003e\n\n\u003ch2 align=\"center\"\u003eHow it works\u003c/h2\u003e\n\nVanilla `tsc` does not perform _extension rewriting_; it will only ever transpile a `.ts` file to a `.js` file (never `.cjs` or `.mjs`). This is the fundamental limitation that forces library authors to use bundlers or bundler-powered tools like `tsup`, `tsdown`, or `unbuild`...\n\n...until now! `zshy` works around this limitation using the official [TypeScript Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API), which provides some powerful (and criminally under-utilized) hooks for customizing file extensions during the `tsc` build process.\n\nUsing these hooks, `zshy` transpiles each `.ts` file to `.js/.d.ts` (ESM) and `.cjs/.d.cts` (CommonJS):\n\n```bash\n$ tree .\n├── package.json\n├── src\n│   └── index.ts\n└── dist # generated\n  ├── index.js\n  ├── index.cjs\n  ├── index.d.ts\n  └── index.d.cts\n```\n\nSimilarly, all relative `import`/`export` statements are rewritten to include the appropriate file extension. (Other tools like `tsup` or `tsdown` do the same, but they require a bundler to do so.)\n\n| Original path      | Result (ESM)       | Result (CJS)        |\n| ------------------ | ------------------ | ------------------- |\n| `from \"./util\"`    | `from \"./util.js\"` | `from \"./util.cjs\"` |\n| `from \"./util.ts\"` | `from \"./util.js\"` | `from \"./util.cjs\"` |\n| `from \"./util.js\"` | `from \"./util.js\"` | `from \"./util.cjs\"` |\n\nFinally, `zshy` automatically writes `\"exports\"` into your `package.json`:\n\n```diff\n{\n  // ...\n  \"zshy\": {\n    \"exports\": \"./src/index.ts\"\n  },\n+ \"exports\": { // auto-generated by zshy\n+   \".\": {\n+     \"types\": \"./dist/index.d.cts\",\n+     \"import\": \"./dist/index.js\",\n+     \"require\": \"./dist/index.cjs\"\n+   }\n+ }\n}\n```\n\nThe result is a tool that I consider to be the \"holy grail\" of TypeScript library build tools:\n\n- performs dual-module (ESM + CJS) builds\n- type checks your code\n- leverages `tsc` for gold-standard transpilation\n- doesn't require a bundler\n- doesn't require another config file (just `package.json` and `tsconfig.json`)\n\n\u003cbr/\u003e\n\u003cbr/\u003e\n\n\u003ch2 align=\"center\"\u003eUsage\u003c/h2\u003e\n\n\u003cbr/\u003e\n\n### Flags\n\n```sh\n$ npx zshy --help\nUsage: zshy [options]\n\nOptions:\n  -h, --help                        Show this help message\n  -p, --project \u003cpath\u003e              Path to tsconfig (default: ./tsconfig.json)\n      --verbose                     Enable verbose output\n      --dry-run                     Don't write any files or update package.json\n      --fail-threshold \u003cthreshold\u003e  When to exit with non-zero error code\n                                      \"error\" (default)\n                                      \"warn\"\n                                      \"never\"\n```\n\n\u003cbr/\u003e\n\n### Subpaths and wildcards\n\nMulti-entrypoint packages can specify subpaths or wildcard exports in `package.json#/zshy/exports`:\n\n```jsonc\n{\n  \"name\": \"my-pkg\",\n  \"version\": \"1.0.0\",\n\n  \"zshy\": {\n    \"exports\": {\n      \".\": \"./src/index.ts\", // root entrypoint\n      \"./utils\": \"./src/utils.ts\", // subpath\n      \"./plugins/*\": \"./src/plugins/*\" // wildcard\n    }\n  }\n}\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eView typical build output\u003c/summary\u003e\n\nWhen you run a build, you'll see something like this:\n\n```bash\n$ npx zshy\n\n→  Starting zshy build... 🐒\n→  Detected project root: /path/to/my-pkg\n→  Reading package.json from ./package.json\n→  Reading tsconfig from ./tsconfig.json\n→  Determining entrypoints...\n   ╔════════════════════╤═════════════════════════════╗\n   ║ Subpath            │ Entrypoint                  ║\n   ╟────────────────────┼─────────────────────────────╢\n   ║ \"my-pkg\"           │ ./src/index.ts              ║\n   ║ \"my-pkg/utils\"     │ ./src/utils.ts              ║\n   ║ \"my-pkg/plugins/*\" │ ./src/plugins/* (5 matches) ║\n   ╚════════════════════╧═════════════════════════════╝\n→  Resolved build paths:\n   ╔══════════╤════════════════╗\n   ║ Location │ Resolved path  ║\n   ╟──────────┼────────────────╢\n   ║ rootDir  │ ./src          ║\n   ║ outDir   │ ./dist         ║\n   ╚══════════╧════════════════╝\n→  Package is ES module (package.json#/type is \"module\")\n→  Building CJS... (rewriting .ts -\u003e .cjs/.d.cts)\n→  Building ESM...\n→  Updating package.json exports...\n→  Build complete! ✅\n```\n\nAnd the generated `\"exports\"` map will look like this:\n\n```diff\n// package.json\n{\n  // ...\n+ \"exports\": {\n+   \".\": {\n+     \"types\": \"./dist/index.d.cts\",\n+     \"import\": \"./dist/index.js\",\n+     \"require\": \"./dist/index.cjs\"\n+   },\n+   \"./utils\": {\n+     \"types\": \"./dist/utils.d.cts\",\n+     \"import\": \"./dist/utils.js\",\n+     \"require\": \"./dist/utils.cjs\"\n+   },\n+   \"./plugins/*\": {\n+     \"types\": \"./dist/src/plugins/*\",\n+     \"import\": \"./dist/src/plugins/*\",\n+     \"require\": \"./dist/src/plugins/*\"\n+   }\n+ }\n}\n```\n\n\u003c/details\u003e\n\n\u003cbr/\u003e\n\n### Building CLIs (`\"bin\"` support)\n\nIf 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.\n\n```jsonc\n{\n  // package.json\n  \"name\": \"my-cli\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"zshy\": {\n    \"bin\": \"./src/cli.ts\" // 👈 specify CLI entrypoint\n  }\n}\n```\n\nThe `\"bin\"` field is automatically written into your `package.json`:\n\n```diff\n{\n  // package.json\n  \"name\": \"my-cli\",\n  \"version\": \"1.0.0\",\n  \"zshy\": {\n    \"exports\": \"./src/index.ts\",\n    \"bin\": \"./src/cli.ts\"\n  },\n+ \"bin\": {\n+   \"my-cli\": \"./dist/cli.cjs\" // CommonJS entrypoint\n+ }\n}\n```\n\nBe sure to include a [shebang](\u003chttps://en.wikipedia.org/wiki/Shebang_(Unix)\u003e) as the first line of your CLI entrypoint file:\n\n```ts\n#!/usr/bin/env node\n\n// CLI code here\n```\n\n\u003cbr/\u003e\n\u003cbr/\u003e\n\u003cbr/\u003e\n\n\u003ch2 align=\"center\"\u003eFAQ for nerds\u003c/h2\u003e\n\n\u003cbr/\u003e\n\n### How does `zshy` resolve entrypoints?\n\nIt reads your `package.json#/zshy` config:\n\n```jsonc\n// package.json\n{\n  \"name\": \"my-pkg\",\n  \"version\": \"1.0.0\",\n  \"zshy\": {\n    \"exports\": {\n      \".\": \"./src/index.ts\",\n      \"./utils\": \"./src/utils.ts\",\n      \"./plugins/*\": \"./src/plugins/*\", // shallow match {.ts,.tsx,.cts,.mts} files\n      \"./components/*\": \"./src/components/**/*\" // deep match *.{.ts,.tsx,.cts,.mts} files\n    }\n  }\n}\n```\n\nA few important notes about `package.json#/zshy/exports`:\n\n- All keys should start with `\"./\"`\n- All values should be relative paths to source files (resolved relative to the `package.json` file)\n\nA few notes on wildcards exports:\n\n- The _key_ should always end in `\"/*\"`\n- The _value_ should correspond to a glob-like path value that ends in either `\"/*\"` (shallow match) or `\"/**/*\"` (deep match)\n- Do not include a file extensions! `zshy` matches source files with the following extensions:\n  - `.ts`, `.tsx`, `.cts`, `.mts`\n- A shallow match (`./\u003cdir\u003e/*`) will match both:\n  - `./\u003cdir\u003e/*.{ts,tsx,cts,mts}`\n  - `./\u003cdir\u003e/*/index.{ts,tsx,cts,mts}`.\n- A deep match (`./\u003cdir\u003e/**/*`) will match all files recursively in the specified directory, including subdirectories:\n  - `./\u003cdir\u003e/**/*.{ts,tsx,cts,mts}`\n  - `./\u003cdir\u003e/**/*/index.{ts,tsx,cts,mts}`\n\n**Note** — Since `zshy` computes an exact set of resolved entrypoints, your `\"files\"`, `\"include\"`, and `\"exclude\"` settings in `tsconfig.json` are ignored during the build.\n\n\u003cbr/\u003e\n\n### Does `zshy` respect my `tsconfig.json` compiler options?\n\nYes! With some strategic overrides:\n\n- **`module`**: Overridden (`\"commonjs\"` for CJS build, `\"esnext\"` for ESM build)\n- **`moduleResolution`**: Overridden (`\"node10\"` for CJS, `\"bundler\"` for ESM)\n- **`declaration`/`noEmit`/`emitDeclarationOnly`**: Overridden to ensure proper output\n- **`verbatimModuleSyntax`**: Set to `false` to allow multiple build formats\n- **`esModuleInterop`**: Set to `true` (it's a best practice)\n\nAll other options are respected as defined, though `zshy` will also set the following reasonable defaults if they are not explicitly set:\n\n- `outDir` (defaults to `./dist`)\n- `declarationDir` (defaults to `outDir` — you probably shouldn't set this explicitly)\n- `target` (defaults to `es2020`)\n\n\u003cbr/\u003e\n\n### Do I need to use a specific file structure?\n\nNo. You can organize your source however you like; `zshy` will transpile your entrypoints and all the files they import, respecting your `tsconfig.json` settings.\n\n\u003e **Comparison to `tshy`** — `tshy` requires you to put your source in a `./src` directory, and always builds to `./dist/esm` and `./dist/cjs`.\n\n\u003cbr/\u003e\n\n### What files does `zshy` create?\n\nIt depends on your `package.json#/type` field. If your package is ESM (that is, `\"type\": \"module\"` in `package.json`):\n\n- `.js` + `.d.ts` (ESM)\n- `.cjs` + `.d.cts` (CJS)\n\n```bash\n$ tree dist\n\n.\n├── package.json # if type == \"module\"\n├── src\n│   └── index.ts\n└── dist\n    ├── index.js\n    ├── index.d.ts\n    ├── index.cjs\n    └── index.d.cts\n```\n\nOtherwise, the package is considered _default-CJS_ and the ESM build files will be rewritten as `.mjs`/`.d.mts`.\n\n- `.mjs` + `.d.mts` (ESM)\n- `.js` + `.d.ts` (CJS)\n\n```bash\n$ tree dist\n.\n├── package.json # if type != \"module\"\n├── src\n│   └── index.ts\n└── dist\n    ├── index.js\n    ├── index.d.ts\n    ├── index.mjs\n    └── index.d.mts\n```\n\n\u003e **Comparison to `tshy`** — `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).\n\n\u003cbr/\u003e\n\n### How does extension rewriting work?\n\n`zshy` uses the [TypeScript Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API) to rewrite file extensions during the `tsc` emit step.\n\n- If `\"type\": \"module\"`\n  - `.ts` becomes `.js`/`.d.ts` (ESM) and `.cjs`/`.d.cts` (CJS)\n- Otherwise:\n  - `.ts` becomes `.mjs`/`.d.mts` (ESM) and `.js`/`.d.ts` (CJS)\n\nSimilarly, all relative `import`/`export` statements are rewritten to account for the new file extensions.\n\n| Original path      | Result (ESM)       | Result (CJS)        |\n| ------------------ | ------------------ | ------------------- |\n| `from \"./util\"`    | `from \"./util.js\"` | `from \"./util.cjs\"` |\n| `from \"./util.ts\"` | `from \"./util.js\"` | `from \"./util.cjs\"` |\n| `from \"./util.js\"` | `from \"./util.js\"` | `from \"./util.cjs\"` |\n\nTypeScript's Compiler API provides dedicated hooks for performing such transforms (though they are criminally under-utilized).\n\n- **`ts.TransformerFactory`**: Provides AST transformations to rewrite import/export extensions before module conversion\n- **`ts.CompilerHost#writeFile`**: Handles output file extension changes (`.js` → `.cjs`/`.mjs`)\n\n\u003e **Comparison to `tshy`** — `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.\n\n\u003cbr/\u003e\n\n### Can I use extension-less imports?\n\nYes! `zshy` supports whatever import style you prefer:\n\n- `from \"./utils\"`: classic extensionless imports\n- `from \"./utils.js\"`: ESM-friendly extensioned imports\n- `from \"./util.ts\"`: recently supported natively via[`rewriteRelativeImportExtensions`](https://www.typescriptlang.org/tsconfig/#rewriteRelativeImportExtensions)\n\nUse whatever you like; `zshy` will rewrite all imports/exports properly during the build process.\n\n\u003e **Comparison to `tshy`** — `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.\n\n\u003cbr/\u003e\n\n### What about `package.json#/exports`?\n\nYour exports map is automatically written into your `package.json` when you run `zshy`. The generated exports map looks like this:\n\n```diff\n{\n  \"zshy\": {\n    \"exports\": {\n      \".\": \"./src/index.ts\",\n      \"./utils\": \"./src/utils.ts\",\n      \"./plugins/*\": \"./src/plugins/*\"\n    }\n  },\n+ \"exports\": { // auto-generated by zshy\n+   \".\": {\n+     \"types\": \"./dist/index.d.cts\",\n+     \"import\": \"./dist/index.js\",\n+     \"require\": \"./dist/index.cjs\"\n+   },\n+   \"./utils\": {\n+     \"types\": \"./dist/utils.d.cts\",\n+     \"import\": \"./dist/utils.js\",\n+     \"require\": \"./dist/utils.cjs\"\n+   },\n+   \"./plugins/*\": {\n+     \"import\": \"./dist/src/plugins/*\",\n+     \"require\": \"./dist/src/plugins/*\"\n+   }\n+ }\n}\n```\n\n\u003cbr/\u003e\n\n### Why `.d.cts` for `\"types\"`?\n\nThe `\"types\"` field always points to the CJS declaration file (`.d.cts`). This is an intentional design choice. **It solves the \"Masquerading as ESM\" issue**. You've likely seen this dreaded error before:\n\n```ts\nimport mod from \"pkg\";         ^^^^^\n//              ^ 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.\n```\n\nSimply put, an ESM file can `import` CommonJS, but CommonJS files can't `require` ESM. By having `\"types\"` point to the `.d.cts` declarations, we can always avoid the error above. Technically we're tricking TypeScript into thinking our code is CommonJS; in practice, this has no real consequences and maximizes compatibility.\n\nTo learn more, read the [\"Masquerading as ESM\"](https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseESM.md) writeup from ATTW.\n\n\u003e **Comparison to `tshy`** — `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.\n\n\u003cbr/\u003e\n\n### Why do I see \"Masquerading as CJS\"?\n\nThis is expected behavior when running the \"Are The Types Wrong\" tool. This warning does not cause any resolution issues (unlike \"Masquerading as ESM\"). Technically, we're tricking TypeScript into thinking our code is CommonJS; when in fact it may be ESM. The ATTW tool is very rigorous and flags this; in practice, this has no real consequences and maximizes compatibility (Zod has relied on the CJS masquerading trick since it's earliest days.)\n\nTo learn more, read the [\"Masquerading as CJS\"](https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md) writeup from ATTW.\n\n\u003cbr/\u003e\n\n### How are default exports transpiled?\n\n**CJS interop transform** — When a file contains a single `export default ...` and _no named exports_...\n\n```ts\nfunction hello() {\n  console.log('hello');\n}\n\nexport default hello;\n```\n\n...the built `.cjs` code will assign the exported value directly to `module.exports`:\n\n```ts\nfunction hello() {\n  console.log('hello');\n}\nexports.default = hello;\nmodule.exports = exports.default;\n```\n\n...and the associated `.d.cts` files will use `export =` syntax:\n\n```ts\ndeclare function hello(): void;\nexport = hello;\n```\n\nThe ESM build is not impacted by this transform.\n\n**ESM interop transform** — Similarly, if a source `.ts` file contains the following syntax:\n\n```ts\nexport = ...\n```\n\n...the generated _ESM_ build will transpile to the following syntax:\n\n```ts\nexport default ...\n```\n\n\u003cbr/\u003e\n\n### Can it support React Native or non-Node.js environments?\n\nYes! This is one of the key reasons `zshy` was originally developed. Many environments don't support `package.json#/exports` yet:\n\n- Node.js v12.7 or earlier\n- React Native - The Metro bundler does not support `\"exports\"` by default\n- TypeScript projects with legacy configs — e.g. `\"module\": \"commonjs\"`\n\nThis causes issues for packages that want to use subpath imports to structure their package. Fortunately `zshy` unlocks a workaround I call a _flat build_:\n\n1. Remove `\"type\": \"module\"` from your `package.json` (if present)\n2. Set `outDir: \".\"` in your `tsconfig.json`\n3. Configure `\"exclude\"` in `package.json` to exclude all source files:\n\n   ```jsonc\n   {\n     // ...\n     \"exclude\": [\"**/*.ts\", \"**/*.tsx\", \"**/*.cts\", \"**/*.mts\", \"node_modules\"]\n   }\n   ```\n\nWith this setup, your build outputs (`index.js`, etc) will be written to the package root. 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.\n\n\u003cbr/\u003e\n\n### How to include custom conditions in `package.json#/exports`?\n\nTo tell `zshy` to specify a custom condition pointing to your _source files_, use `\"sourceDialects\"`:\n\n```diff\n{\n  \"zshy\": {\n    \"exports\": {\n      \".\": \"./src/index.ts\"\n    },\n+   \"sourceDialects\": [\"my-source\"] // 👈 add this\n  }\n}\n```\n\nWith this addition, `zshy` will add the `\"my-source\"` condition to the generated `\"exports\"` map:\n\n```diff\n// package.json\n{\n  \"exports\": {\n    \".\": {\n+     \"my-source\": \"./src/index.ts\",\n      \"types\": \"./dist/index.d.cts\",\n      \"import\": \"./dist/index.js\",\n      \"require\": \"./dist/index.cjs\"\n    }\n  }\n}\n```\n\nSpecifying additional dialects for `\"import\"` and `\"require\"` is not yet supported (create an issue if you need this).\n\n\u003cbr /\u003e\n\n### Is it fast?\n\nNot really. It uses `tsc` to typecheck your codebase, which is a lot slower than using a bundler that strips types. That said:\n\n1. You _should_ be type checking your code during builds\n2. TypeScript is [about to get 10x faster](https://devblogs.microsoft.com/typescript/typescript-native-port/)\n\n\u003cbr/\u003e\n\u003cbr/\u003e\n\n\u003ch2 align=\"center\"\u003eAcknowledgements\u003c/h2\u003e\n\nThe DX of `zshy` was heavily inspired by [tshy](https://github.com/isaacs/tshy) by [@isaacs](https://x.com/izs), particularly its declarative entrypoint map and auto-updating of `package.json#/exports`. It proved that there's a modern way to transpile libraries using pure `tsc` (and various `package.json` hacks). Unfortunately its approach necessarily involved certain constraints that made it unworkable for Zod (described in the FAQ in more detail). `zshy` borrows elements of `tshy`'s DX while using the Compiler API to relax these constraints and provide a more \"batteries included\" experience.\n","funding_links":[],"categories":["Utilities","TypeScript"],"sub_categories":["Build Tools"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcolinhacks%2Fzshy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcolinhacks%2Fzshy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcolinhacks%2Fzshy/lists"}