zshy: a bundler-free build tool for TypeScript libraries
🐒 Bundler-free build tool for TypeScript libraries. Powered by tsc.
At a glance
- What is it?
- zshy builds dual ESM and CommonJS output for TypeScript libraries by driving tsc through the TypeScript Compiler API, with no bundler and no extra config file. It was built for Zod and reads its entrypoints from package.json.
- Who is it for?
- zshy fits a maintainer of a TypeScript library who wants dual ESM and CJS output, type checking, and a generated exports map without adding a bundler or a second configuration file to the repository. It does not fit a project that needs bundling features such as code splitting or asset inlining, nor one that cannot accept a build step that rewrites package.json.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The tsc limitation that forces library authors to bundle
zshy exists because of a specific gap in the TypeScript compiler. Vanilla `tsc` does not perform extension rewriting; it only ever transpiles a `.ts` file to a `.js` file, never to `.cjs` or `.mjs`. For a library that has to ship both module formats, that single behaviour pushes authors toward bundlers or bundler-powered tools such as `tsup`, `tsdown` or `unbuild`.
zshy works around it with the official TypeScript Compiler API, using its hooks to customise file extensions during the build. The result is that each `.ts` source file is transpiled twice, once to `.js` with `.d.ts` declarations for ESM, and once to `.cjs` with `.d.cts` declarations for CommonJS. The README shows the resulting layout of a `dist` directory containing `index.js`, `index.cjs`, `index.d.ts` and `index.d.cts`.
Relative import and export statements are rewritten to carry the right extension for each format, so `from "./util"` becomes `from "./util.js"` in the ESM output and `from "./util.cjs"` in the CommonJS output. Other tools do the same rewriting, the README notes, but they require a bundler to do so.
This is a library publishing tool, not an application build tool, and that distinction is the whole design.
Entrypoints live in package.json, not a new config file
zshy reads from your existing `package.json` and `tsconfig.json`, and the project describes itself as unopinionated, meaning you can use any file structure or import extension syntax you like. The entrypoint map is declarative and lives under the `zshy` key in `package.json`.
A single entrypoint is one string:
{
"name": "my-pkg",
"version": "1.0.0",
"zshy": "./src/index.ts"
}Multiple entrypoints use an `exports` object, and the map accepts both wildcard and deep wildcard patterns:
{
"name": "my-pkg",
"version": "1.0.0",
"zshy": {
"exports": {
".": "./src/index.ts",
"./utils": "./src/utils.ts",
"./plugins/*": "./src/plugins/*",
"./components/**/*": "./src/components/**/*"
}
}
}Because the map sits in the same file you already version, there is no second configuration file to drift out of sync. The cost is that `package.json` becomes the place where build behaviour lives, which some maintainers like and others find crowded.
Installing zshy and running the first build
The install step is a dev dependency, and the README lists all three package managers:
npm install --save-dev zshy
yarn add --dev zshy
pnpm add --save-dev zshyThen run the build with npx, where the dry run flag lets you see what would happen without writing or updating any file:
npx zshy --dry-runThe build output shows the detected project root, the `package.json` and `tsconfig.json` it read, the cleanup of the outDir, a table of subpaths and their entrypoints, and the resolved rootDir and outDir. It also reports whether the package is an ES module based on the `type` field, then builds CommonJS first, rewriting `.ts` to `.cjs` and `.d.cts`, and ESM second.
Two keys in `package.json` are updated as a side effect: `exports` and `bin`. The exports map that zshy writes looks like this:
{
"exports": {
".": {
"types": "./dist/index.d.cts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}The types condition points at the CommonJS declaration file, the import condition at the ESM build, and the require condition at the CommonJS build.
The README also suggests adding a `build` script so the project has one entry point for building:
{
"scripts": {
"build": "zshy"
}
}After that, `npm run build` is all you need.
What the flags give you and what the CLI refuses to do
The CLI surface is small, and the help output lists every option:
Usage: zshy [options]
Options:
-h, --help Show this help message
-p, --project <path> Path to tsconfig (default: ./tsconfig.json)
--verbose Enable verbose output
--dry-run Don't write any files or update package.json
--fail-threshold <threshold> When to exit with non-zero error code
"error" (default)
"warn"
"never"The `--project` flag matters for monorepo layouts, where each package has its own tsconfig and the tool needs to be pointed at the right one. The `--fail-threshold` flag controls when the process exits non-zero, with `error` as the default and `warn` and `never` as the alternatives, which is useful in a pipeline where type errors are reported but should not stop a publish.
The `--dry-run` flag is the one to reach for first. Because zshy writes to `package.json`, a dry run is how you inspect the proposed exports map and outDir before anything in your published package changes.
Dual module output, JSX, assets and bin entries
Beyond the core transpile-and-rewrite cycle, zshy handles several packaging details that library authors usually deal with by hand. It builds ESM and CJS outputs from a single TypeScript source file, which is the feature the project markets hardest, and it copies non-JS assets to the output directory rather than dropping them.
TypeScript sources with `.tsx` are supported and rewritten to `.js`, `.cjs` or `.mjs` according to the `jsx` settings in your `tsconfig.json`, so a React component library can use the same pipeline as a plain utility library.
Binary entry points are a first-class concern rather than an afterthought, and the CLI-friendly `bin` support means a package that ships a command line tool can be built by the same invocation as the library itself. The zshy repository dogfoods this: its own `package.json` declares `"bin": "./src/index.ts"` under the `zshy` key, and its published `main` points at `./dist/index.cjs` with `types` at `./dist/index.d.cts`.
The one advertised feature that is switched off is React Native support. The README describes a flat build mode designed for bundlers that do not support `package.json#/exports`, but that line is commented out in the current documentation, so treat it as unavailable.
Version 0.8, a peer dependency on TypeScript, and an MIT licence
The release tags move in small increments: v0.7.2 on 2026-04-29, v0.7.3 on 2026-06-12, and v0.8.0 on 2026-09-07. The repository is not archived and the last push was on 2026-09-30, so the project is still being worked on.
zshy is a peer dependency consumer of TypeScript, and the manifest states the peer range as `typescript: ">5.5.0"` with `typescript: ^5.8.3` in devDependencies. Because zshy drives the compiler through the Compiler API rather than shelling out, that floor is a hard requirement for anyone installing it, and it means the TypeScript version your library develops against is also the version zshy compiles with.
The dependency list is short: `arg` for flag parsing, `fast-glob` for file discovery, and `table` for the entrypoint and resolved-path tables in the build output. There is no bundler in the tree, which is the point of the project.
The licence is MIT, which is the permissive choice you want in a build tool that sits in other people's release pipelines. The repository also carries a `pnpm-lock.yaml`, a `biome.jsonc` configuration, a `vitest.config.ts`, and separate `tsconfig.json` and `tsconfig.build.json` files, with a `test` script that runs `vitest run`.
Editorial conclusion
zshy fits a maintainer of a TypeScript library who wants dual ESM and CJS output, type checking, and a generated exports map without adding a bundler or a second configuration file to the repository. It does not fit a project that needs bundling features such as code splitting or asset inlining, nor one that cannot accept a build step that rewrites package.json. Before switching, run `npx zshy --dry-run` on your package to see the proposed outDir and exports without touching any file, then check the generated exports map against the type condition ordering the project expects.
Frequently asked questions
What does zshy do and why was it created?
It is a bundler-free build tool for transpiling TypeScript libraries, originally created as an internal build tool for Zod. It was made to work around the fact that vanilla tsc only transpiles .ts to .js and never rewrites extensions to .cjs or .mjs.
How do I install and run zshy?
Install it as a dev dependency with `npm install --save-dev zshy`, or the yarn or pnpm equivalent, then run `npx zshy`. The `--dry-run` flag shows what the build would do without writing any files or updating package.json.
Where does zshy read the entrypoints from?
From the `zshy` key in package.json, either as a single string or as an `exports` object that supports wildcard and deep wildcard patterns. It reads tsconfig.json for compiler settings and needs no separate config file.
Does zshy modify my package.json?
Yes. A normal build writes the `exports` map and the `bin` field directly into package.json, pointing types at the .d.cts file, import at the .js build and require at the .cjs build. Use `--dry-run` to preview it first.
What TypeScript version does zshy require?
The manifest declares a peer dependency of typescript ">5.5.0" and develops against typescript ^5.8.3. Because zshy drives tsc through the TypeScript Compiler API, that floor applies to your project as well.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/colinhacks-zshy)Was this useful?