How to Publish a Fully Production‑Ready TypeScript Package to npm
This step‑by‑step guide shows how to create, configure, test, and publish a TypeScript npm package using Git, Prettier, tsup, Vitest, GitHub Actions CI, and Changesets for versioning and release automation.
In this guide we start from an empty directory and walk through publishing a fully production‑ready npm package, covering Git version control, TypeScript, Prettier, export checks, building with tsup, testing with Vitest, CI with GitHub Actions, and versioning with Changesets.
Use Git for version control
Write code with TypeScript for type safety
Format code with Prettier
Check exports with @arethetypeswrong/cli
Compile TypeScript to CJS and ESM with tsup
Run tests with Vitest
Run CI with GitHub Actions
Version and publish with Changesets
.1:Initialize Repository
Run the following command to create a new Git repository:
<code>git init</code>1.2:Set .gitignore
Create a .gitignore file at the project root and add:
<code>node_modules</code>1.3:Create Initial Commit
Run the following commands to make the first commit:
<code>git add .
git commit -m "Initial commit"</code>1.4:Create a New Repository on GitHub
Using the GitHub CLI, create a new repository (example name tt-package-demo ):
<code>gh repo create tt-package-demo --source=. --public</code>1.5:Push to GitHub
Push your code to GitHub:
<code>git push --set-upstream origin main</code>Next we will create a package.json , add a license , a LICENSE file, and a README.md file.
2.1:Create package.json File
Create a package.json with the following content:
<code>{
"name": "tt-package-demo",
"version": "1.0.0",
"description": "A demo package for Total TypeScript",
"keywords": ["demo", "typescript"],
"homepage": "https://github.com/yourgithub/tt-package-demo",
"bugs": {"url": "https://github.com/yourgithub/tt-package-demo/issues"},
"author": "Matt Pocock <[email protected]> (https://totaltypescript.com)",
"repository": {"type": "git", "url": "git+https://github.com/yourgithub/tt-package-demo.git"},
"files": ["dist"],
"type": "module"
}
</code>name is the npm package name (must be unique).
version follows semver, e.g., 0.0.1 .
description and keywords help with npm search.
homepage points to the repo or docs.
bugs URL for issue reporting.
author identifies you; you can add contributors if needed.
repository links to the GitHub repo.
files lists files to include when publishing (here dist ).
type set to module indicates ESM.
2.2:Add license Field
Add a MIT license field to package.json :
<code>{
"license": "MIT"
}</code>2.3:Create LICENSE File
Create a LICENSE file with the MIT license text (replace [year] and [fullname] with the current year and your name).
<code>MIT License
Copyright (c) [year] [fullname]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
... (full license omitted for brevity) ...
</code>2.4:Create README.md File
Create a README.md describing the package, e.g.:
<code>**tt-package-demo**
A demo package for Total TypeScript.
</code>3.1:Install TypeScript
Install TypeScript as a dev dependency:
<code>npm install --save-dev typescript</code>3.2:Set Up tsconfig.json
Create a tsconfig.json with these options (core options shown):
<code>{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"module": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"sourceMap": true,
"declaration": true,
"declarationMap": true
}
}
</code>3.3:Configure DOM Types (optional)
If your code runs in the browser, skip this step. Otherwise add:
<code>{
"compilerOptions": {
"lib": ["es2022"]
}
}
</code>3.4:Create a Source File
Create src/utils.ts :
<code>export const add = (a: number, b: number) => a + b;
</code>3.5:Create an Index File
Create src/index.ts :
<code>export { add } from "./utils.js";
</code>3.6:Set build Script
Add a build script to package.json :
<code>{
"scripts": {
"build": "tsc"
}
}
</code>Running npm run build compiles the TypeScript to JavaScript in dist .
3.7:Run Build
<code>npm run build</code>3.8:Add dist to .gitignore
<code>dist</code>3.9:Set ci Script
<code>{
"scripts": {
"ci": "npm run build"
}
}
</code>4.1:Install Prettier
<code>npm install --save-dev prettier</code>4.2:Create .prettierrc
<code>{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80,
"tabWidth": 2
}
</code>4.3:Add format Script
<code>{
"scripts": {
"format": "prettier --write ."
}
}
</code>4.4:Run format Script
<code>npm run format</code>4.5:Add check-format Script
<code>{
"scripts": {
"check-format": "prettier --check ."
}
}
</code>4.6:Add check-format to CI
<code>{
"scripts": {
"ci": "npm run build && npm run check-format"
}
}
</code>5.1:Install @arethetypeswrong/cli
<code>npm install --save-dev @arethetypeswrong/cli</code>5.2:Add check-exports Script
<code>{
"scripts": {
"check-exports": "attw --pack ."
}
}
</code>5.3:Run check-exports
<code>npm run check-exports</code>Initially you will see resolution failures for Node and bundler.
5.4:Add main Field
<code>{
"main": "dist/index.js"
}
</code>5.5:Run check-exports Again
<code>npm run check-exports</code>Now only a warning for Node 16 (CJS) remains.
5.6:Fix CJS Warning (optional)
If you do not want to support CJS, change the script to ignore the rule:
<code>{
"scripts": {
"check-exports": "attw --pack . --ignore-rules=cjs-resolves-to-esm"
}
}
</code>5.7:Add check-exports to CI
<code>{
"scripts": {
"ci": "npm run build && npm run check-format && npm run check-exports"
}
}
</code>6.1:Install tsup
<code>npm install --save-dev tsup</code>6.2:Create tsup.config.ts
<code>import { defineConfig } from "tsup";
export default defineConfig({
entryPoints: ["src/index.ts"],
format: ["cjs", "esm"],
dts: true,
outDir: "dist",
clean: true,
});
</code>6.3:Change build Script to Use tsup
<code>{
"scripts": {
"build": "tsup"
}
}
</code>6.4:Add exports Field
<code>{
"exports": {
"./package.json": "./package.json",
".": {
"import": "./dist/index.js",
"default": "./dist/index.cjs"
}
}
}
</code>6.5:Run check-exports Again
<code>npm run check-exports</code>All checks should now be green.
6.6:Use tsup as Linter (add noEmit )
Add noEmit": true to compilerOptions in tsconfig.json to let TypeScript only type‑check.
6.6.2:Remove Unused Fields
Remove outDir , rootDir , sourceMap , declaration , and declarationMap from tsconfig.json .
6.6.3:Change module to Preserve
<code>{
"compilerOptions": {
"module": "Preserve"
}
}
</code>This allows imports without the .js extension.
6.6.4:Add lint Script
<code>{
"scripts": {
"lint": "tsc"
}
}
</code>6.6.5:Add lint to CI
<code>{
"scripts": {
"ci": "npm run build && npm run check-format && npm run check-exports && npm run lint"
}
}
</code>7.1:Install vitest
<code>npm install --save-dev vitest</code>7.2:Create Test File
<code>import { add } from "./utils";
import { test, expect } from "vitest";
test("add", () => {
expect(add(1, 2)).toBe(3);
});
</code>7.3:Add test Script
<code>{
"scripts": {
"test": "vitest run"
}
}
</code>7.4:Run Tests
<code>npm run test</code>7.5:Add dev Script for Watch Mode
<code>{
"scripts": {
"dev": "vitest"
}
}
</code>7.6:Add test to CI
<code>{
"scripts": {
"ci": "npm run build && npm run check-format && npm run check-exports && npm run lint && npm run test"
}
}
</code>8.1:Create GitHub Actions Workflow
<code>name: CI
on:
pull_request:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: npm install
- name: Run CI
run: npm run ci
</code>9.1:Install @changesets/cli
<code>npm install --save-dev @changesets/cli</code>9.2:Initialize Changesets
<code>npx changeset init</code>9.3:Make Changesets Public
Edit .changeset/config.json to set "access": "public" .
9.4:Enable Automatic Commit
Set "commit": true in .changeset/config.json .
9.5:Add Local Release Script
<code>{
"scripts": {
"local-release": "changeset version && changeset publish"
}
}
</code>9.6:Run CI Before Publishing
<code>{
"scripts": {
"prepublishOnly": "npm run ci"
}
}
</code>9.7:Add a Changeset
<code>npx changeset</code>Mark the version as patch and describe it (e.g., "initial release").
9.8:Commit Changes
<code>git add .
git commit -m "Prepare for initial release"
</code>9.9:Run Local Release Script
<code>npm run local-release</code>This runs the CI, versions the package, and publishes it to npm.
9.10:View Package on npm
<code>http://npmjs.com/package/<your-package-name></code>You should now see your published package.
Summary
You now have a fully configured TypeScript package with Prettier, export checks, tsup compilation, Vitest testing, GitHub Actions CI, and Changesets for versioning and publishing.
Code Mala Tang
Read source code together, write articles together, and enjoy spicy hot pot together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.