# antfu/starter-ts: the TypeScript library template that publishes itself on CI

> antfu/starter-ts is a starter template for a TypeScript library that will be published to npm, bundling tsdown, vitest, eslint, knip and a Trusted Publisher release flow. It is a scaffold with placeholder names still in place, so the value is in the wiring rather than in the code.

**antfu/starter-ts** — Starter template for TypeScript library

- Repository: https://github.com/antfu/starter-ts
- Stars: 928 · Forks: 193
- Language: TypeScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/antfu-starter-ts

## A starter for libraries that ship to npm, not for applications

antfu/starter-ts targets one narrow job: starting a TypeScript library that will be published to the npm registry rather than an application that ships as source. The repository is a template, and the giveaway sits at the top of package.json, where the name is pkg-placeholder, the description is _description_ and the version is 0.0.0. Nothing has been renamed on your behalf. The value sits in the surrounding scaffolding instead: a pnpm workspace file, a pnpm-lock.yaml, an eslint.config.js, a knip.json, a tsdown.config.ts, a vitest.config.ts, a src directory, a test directory and a .github directory. A team that would otherwise hand-write each of those files gets a working set of them, with the scripts already pointing at one another. The intended user is an engineer publishing their first or second npm package who wants the release path to run on CI rather than from a laptop.

## tsdown produces dist, and the exports map only points at one file

The published files are not produced by tsc. The build script is tsdown, and tsdown.config.ts sits at the repository root next to src, test, tsconfig.json and vitest.config.ts. The package.json exports map resolves the root export to ./dist/index.mjs, the types field resolves to ./dist/index.d.mts, and the files array is limited to dist, so the working tree does not travel inside the tarball. That is a narrow, deliberate shape: a consumer imports one ESM entry point and reads one matching declaration file. Two neighbouring settings change how the package behaves downstream. sideEffects is set to false, which lets bundlers discard imports whose exports are never used, and the package is declared as type module, so the .mjs extension on the entry point is consistent with the rest of the layout. During development the dev script runs tsdown --watch, so the dist output is rebuilt whenever a source file changes.

## One ci script chains lint, typecheck, knip and test

Continuous integration is expressed as a single command, which is the point of the scripts-info block that documents each script. The ci script runs lint, typecheck, knip and test in that order. typecheck is plain tsc, so type errors are caught independently of the bundler. test first runs the build and then vitest, which means a test run also proves that the bundle can be produced. Knip is the step that surprises newcomers: it is described as the script that finds unused files, exports and dependencies, and it is backed by a knip.json configuration file at the root. A template that leaves unused exports behind will fail this step, so the scaffold pushes toward a tight public surface from the first commit rather than after a cleanup sprint.

## The first npm publish is a manual step the template cannot automate

Getting the first release out takes two distinct phases, and the first one is not something the template can run for you. The README points developers at npm Trusted Publisher, where the release is done on CI so the published package never needs a long lived npm token. Setting it up begins with creating the package by hand, which the README asks you to do once:

```bash
pnpm publish
```

After the package exists on npm, the link between that package and your GitHub repository is made on the npm site, on the access page at https://www.npmjs.com/package/<your-package-name>/access. Once that connection is set, the release becomes a command, and the GitHub Actions workflow handles the rest:

```bash
pnpm run release
```

That script is bumpp, which handles the version bump and the publish step together. The release path also depends on the prepare script, which runs when dependencies are installed:

```json
"release": "bumpp",
"prepare": "git config core.hooksPath .githooks && simple-git-hooks && skills experimental_sync -y"
```

Because prepare points git at a hooks path inside the repository and installs simple-git-hooks, a fresh clone and a single install are enough to get the same hooks behaviour the author had.

## Every pkg-placeholder string has to be replaced before you publish

Renaming is the first task and the easiest one to get wrong, because the publishing steps will not catch it. Several fields still carry the placeholder name. The name field and description field are placeholders, the homepage and repository urls point at a path under antfu/pkg-placeholder, the bugs url does the same, and the badge links in README.md point at pkg-placeholder on npm and bundlephobia as well. pnpm publish would happily create a package under whatever the name field says, and the Trusted Publisher connection would then be configured for a package you never intended to release. The version field starts at 0.0.0, which is a further signal that nothing in this repository has been published as a package yet. Fixing the names early also makes the npm version and download badges in the README point at your own package instead of a name that will never exist.

## What the template leaves out, and what a GitHub Actions directory may not cover

Several things a library author might reasonably expect are absent. The repository has no GitHub releases, so there is no release history to read and no changelog file in the top level listing. No test is written for you: the test directory exists and vitest.config.ts is at the root, but a template cannot know what your library should assert, so the first meaningful test is yours to write. The eslint config, the knip config, the tsdown config and the vitest config are supplied as files you need to read before trusting them, and none of them are explained in the README. The README does not describe a rollback procedure for a bad publish, and it does not explain how to keep the generated declaration file in step with the source beyond rebuilding. AGENTS.md, CLAUDE.md, CONTRIBUTING.md and a .claude directory sit at the root, so there is guidance for contributors and for coding agents, but the listing alone does not tell you what those files require.

## Conclusion

antfu/starter-ts fits engineers about to publish a TypeScript library who want the release path on CI and the npm Trusted Publisher wiring already settled. It does not fit applications, monorepo services or libraries with a bespoke build step, because the scaffold assumes a single package, a single entry point and one build tool. Before you publish anything from it, replace every pkg-placeholder string in package.json, the badge links in README.md and the repository urls, then run the ci script and check that the dist output matches the exports map. The licence field on the repository says MIT, so the downstream licensing story is short, and the last push was on 2026-10-13.

## FAQ

### What is antfu/starter-ts and who is it for?

It is a starter template for a TypeScript library that will be published to npm, with the build, lint, typecheck, test and release scripts already wired together. It suits an engineer publishing a first or second npm package who wants the release to run on CI.

### How do I publish the first version of a package made from antfu/starter-ts?

The README asks you to run pnpm publish manually once to create the package on npm, then connect that package to your GitHub repository on the npm access page. After that, pnpm run release bumps the version and publishes, and the GitHub Actions workflow handles the release process.

### Does antfu/starter-ts build its published files with tsc?

No. The build script is tsdown, and the output lands in dist, with the root export pointing at ./dist/index.mjs and the types field at ./dist/index.d.mts. tsc is used separately by the typecheck script, and the test script runs the build before vitest.

### What licence does antfu/starter-ts use?

The licence field on the repository says MIT, and the README credits the MIT licence to Anthony Fu. The repository also ships a LICENSE.md file at the top level.

## Sources

- [antfu/starter-ts on GitHub](https://github.com/antfu/starter-ts)
- [Issues](https://github.com/antfu/starter-ts/issues)
- [License: MIT](https://github.com/antfu/starter-ts/blob/main/LICENSE)
- [README](https://github.com/antfu/starter-ts/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/antfu-starter-ts
