my-ts-util
v1.1.0
Published
A tiny TypeScript utility bundled with Rollup and published via semantic-release
Maintainers
Readme
⚙️ TypeScript utility bundled with Rollup and published to npm via semantic-release
This starter gives you a minimal-yet-production-ready setup to build a TypeScript utility, bundle it with Rollup into both ESM and CJS, generate type declarations, and publish automatically to the public npm registry using semantic-release.
Works great for internal “artifactory-style” publishing to npmjs with versioning controlled by Conventional Commits.
1) Project Structure
my-ts-util/
├─ src/
│ └─ index.ts
├─ .releaserc.json
├─ rollup.config.ts
├─ tsconfig.json
├─ package.json
├─ .npmrc # (CI only — use token env var)
├─ .gitignore
├─ README.md
└─ .github/
└─ workflows/
└─ release.yml2) Files — copy/paste
package.json
{
"name": "@your-scope/my-ts-util",
"version": "0.0.0-development",
"description": "A tiny TypeScript utility bundled with Rollup and published via semantic-release",
"keywords": ["typescript", "rollup", "semantic-release", "utility"],
"license": "MIT",
"author": "Your Name <[email protected]>",
"repository": {
"type": "git",
"url": "git+https://github.com/your-org/my-ts-util.git"
},
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist"
],
"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && rollup -c",
"lint": "eslint .",
"test": "echo 'add tests' && exit 0",
"prepare": "npm run build",
"release": "semantic-release"
},
"engines": {
"node": ">=18"
},
"devDependencies": {
"@commitlint/cli": "^19.5.0",
"@commitlint/config-conventional": "^19.5.0",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-typescript": "^12.1.3",
"@types/node": "^22.5.4",
"eslint": "^9.10.0",
"rimraf": "^6.0.1",
"rollup": "^4.21.2",
"rollup-plugin-dts": "^6.3.2",
"semantic-release": "^24.2.0",
"tslib": "^2.7.0",
"typescript": "^5.6.2",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^10.1.7",
"@semantic-release/npm": "^12.0.1"
}
}Notes
type: moduleenables ESM by default; we also emit a CJS build for Node consumers.prepareruns in CI under semantic-release to build before publishing.
tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020"],
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"declaration": false,
"sourceMap": true,
"inlineSources": true,
"noEmit": true,
"skipLibCheck": true,
"types": ["node" ]
},
"include": ["src"]
}rollup.config.ts
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import typescript from '@rollup/plugin-typescript';
import dts from 'rollup-plugin-dts';
import { defineConfig } from 'rollup';
// Treat all non-relative imports as external (dependencies/peerDependencies)
const external = (id: string) => !id.startsWith('.') && !id.startsWith('/') && !id.includes('\\');
export default defineConfig([
// JS bundles (ESM + CJS)
{
input: 'src/index.ts',
output: [
{ file: 'dist/index.mjs', format: 'esm', sourcemap: true },
{ file: 'dist/index.cjs', format: 'cjs', sourcemap: true, exports: 'named' }
],
external,
plugins: [
resolve({ preferBuiltins: true }),
commonjs(),
json(),
typescript({ tsconfig: './tsconfig.json' })
]
},
// Types bundle
{
input: 'src/index.ts',
output: [{ file: 'dist/index.d.ts', format: 'es' }],
plugins: [dts()],
}
]);src/index.ts
/**
* A tiny demo utility — replace with your real logic.
*/
export type GreetOptions = { name?: string };
export function greet(options: GreetOptions = {}): string {
const name = options.name?.trim() || 'world';
return `hello, ${name}!`;
}.releaserc.json
{
"branches": [
"+([0-9])?(.{+([0-9]),x}).x",
"main",
"next",
{ "name": "beta", "prerelease": true },
{ "name": "alpha", "prerelease": true }
],
"plugins": [
["@semantic-release/commit-analyzer", { "preset": "conventionalcommits" }],
["@semantic-release/release-notes-generator", { "preset": "conventionalcommits" }],
["@semantic-release/changelog", { "changelogFile": "CHANGELOG.md" }],
["@semantic-release/npm", { "tarballDir": "dist" }],
["@semantic-release/github", { "assets": ["dist/*.tgz"] }],
["@semantic-release/git", {
"assets": ["package.json", "CHANGELOG.md"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}]
]
}If you only want the npm publish (no GitHub assets/changelog commits), you can remove the
githubandgitplugins.
.github/workflows/release.yml
name: Release
on:
push:
branches:
- main
permissions:
contents: write
issues: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm run build
- name: Semantic Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release.npmrc (CI environment only)
//registry.npmjs.org/:_authToken=${NPM_TOKEN}Do not commit a real token. Commit the line above; inject
NPM_TOKENthrough CI Secrets.
.gitignore
node_modules
.DS_Store
*.log
coverage
.nyc_output
.dist
/dist
CHANGELOG.mdYou may choose to not ignore
CHANGELOG.mdif you want it tracked; semantic-release/git will update it.
README.md
# @your-scope/my-ts-util
A tiny TypeScript utility bundled with Rollup and published to npm via semantic-release.
## Install
```bash
npm i @your-scope/my-ts-utilUsage
import { greet } from '@your-scope/my-ts-util';
console.log(greet({ name: 'Koustubh' }));
// → hello, Koustubh!Contributing
- Follow Conventional Commits (feat:, fix:, perf:, docs:, refactor:, chore:, etc.)
- All merges to
maintrigger an automated release if there are releasable commits.
---
## 3) One-time bootstrap
```bash
# Create + init
mkdir my-ts-util && cd my-ts-util
npm init -y
# Install dev deps
npm i -D \
typescript tslib rollup @rollup/plugin-typescript rollup-plugin-dts \
@rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-json \
semantic-release @semantic-release/{npm,github,git,changelog} \
@commitlint/{cli,config-conventional} eslint rimraf @types/node
# Optional: commitlint (enforce Conventional Commits)
echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.cjs
npm pkg set scripts.commitlint="commitlint -E HUSKY_GIT_PARAMS"For local pre-commit hooks you can add Husky, but CI-only is fine for many teams.
4) Configure tokens
In GitHub → Settings → Secrets and variables → Actions → New repository secret:
NPM_TOKEN: an npm automation token with publish rights for the package scope.
Ensure your package name is available and scope has publish access.
If using an internal registry mirror, set
registry-url:accordingly in the workflow.
5) Release flow (semantic-release)
- You merge a PR with a Conventional Commit message, e.g.
feat: add range parser. - GitHub Action runs on
main, builds, and invokessemantic-release. - semantic-release analyzes commits → bumps version → publishes to npm → updates
CHANGELOG.md→ creates GitHub release notes. package.jsonremains0.0.0-developmentin source; actual version is tagged in the release.
6) Local smoke test
npm run build
node -e "console.log(require('./dist/index.cjs').greet({name: 'test'}))"
node -e "import('./dist/index.mjs').then(m=>console.log(m.greet({name:'esm'})))" --input-type=module7) Tweaks you might want
- Peer deps: if you depend on React, RxJS, etc., add them as
peerDependenciesand keep them external in Rollup. - Multi-entry: extend Rollup config to bundle multiple entry points.
- Tree-shaking: keep modules small and side-effect-free for optimal DCE.
- Private/internal: set
"publishConfig": { "access": "public" }(for scoped public packages) or"private": false.
8) Conventional Commit cheatsheet
feat: ...→ minor releasefix: ...→ patch releaseperf: ...→ patch releasefeat!: ...orBREAKING CHANGE: ...in body → major releasedocs:, chore:, refactor:, test:→ no release unless paired with BREAKING change
You're ready. Commit, push to main, and your first release will cut automatically once a releasable commit lands. ✅
