Developing your first npm library - Build, CI and Publish
Prerequisites
Node.js
Package manager:
npm
(or any alternative likepnpm
)Github account
npm account
Project structure
Create three folders for library source(src), testing(test), and compiled(dist).
project-root
|
+---dist
+---src
\---test
2
3
4
5
Init repository
Init your repository and add .gitignore
git init
dist
node_modules
2
Init package.json
npm init -y
Add package name for your project.
// package.json
{
"name": "My first npm package",
}
2
3
4
Add LICENSE
Install license
for generating a LICENSE
.
npm install license -D
Add MIT license or add other license, the author name is the same as the user.name in current git config.
npx license MIT
# or
npx license
2
3
Add TypeScript
Add as devDependency
npm install -D typescript
tsconfig.json
npx tsc -init
// tsconfig.json
{
"include": ["./src/**/*.ts"],
"exclude": ["./node_modules"],
"compilerOptions": {
"target": "es2016",
"rootDir": "./src",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"noEmit": true
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Add tsup
tsup
is a zero-config TypeScript bundler with a focus on simplicity and speed. It is designed to make it easy to bundle TypeScript code for the web without needing extensive configuration. The name "tsup" is derived from "TypeScript micro bundler."
npm install -D tsup
Add scripts in package.json
Add a script that calls tsup
to help us build the source. And lint
for get syntax checking.
// package.json
{
"scripts": {
"build": "tsup ./src/index.ts -d ./dist --format cjs,esm --dts",
"lint": "tsc"
}
}
2
3
4
5
6
7
Build using tsup
Now add ./src/index.ts
just for demonstration.
export const hello = () => console.log('Hello!');
Then build it. And check compiled file in ./dist
npm run build
Add vitest
for testing
Vitest testing framework powered by Vite. It aims to position itself as the Test Runner of choice for Vite projects, and as a solid alternative even for projects not using Vite.
npm install -D vitest
Create a test
Add ./test/index.test.ts
for demonstration.
import { describe, expect, it } from "vitest";
describe("Whatever", () => {
it("should pass CI", () => {
expect(1).toBe(1);
});
});
2
3
4
5
6
7
Add scripts for testing
// package.json
{
"scripts": {
"build": "tsup ./src/index.ts -d ./dist --format cjs,esm --dts",
"lint": "tsc",
"dev": "vitest", // watch the project
"test": "vitest run"
},
}
2
3
4
5
6
7
8
9
Add script for CI
When CI runs, we should do syntax check first, then testing, finally build our library.
// package.json
{
"build": "tsup ./src/index.ts -d ./dist --format cjs,esm --dts",
"lint": "tsc",
"dev": "vitest",
"test": "vitest run",
"ci": "npm run lint && npm run test && npm run build"
}
2
3
4
5
6
7
8
Add CI action
With "ci"
script, we can now set our workflow. Add .github/workflows/main.yml
in project root.
# .github/workflows/main.yml
name: CI
on:
push:
branches:
- "**"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
cache: npm
- run: npm install
- run: npm run ci
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Now commit files and publish the branch to github, the workflow will auto run.
Set entries for your package
Compiled files in ./dist
include index.js
, index.mjs
, index.d.ts
and index.d.mts
. "main"
for commonjs
entry, "module"
for esm
entry. This are the entries for other users to import your code. Also, include type info from index.d.ts
.
{
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts"
}
2
3
4
5
Alpha version
In the early stage of development, we set the first version as 0.0.1
. Working with changeset
, each version will be automatically changed.
// package.json
{
"version": "0.0.1"
}
2
3
4
Access token from npm account
- Generate new automation token for your npm account.
- Open repository setting, find
security-Secrets and variables-Actions
, add new repository secret with copied token value, and name the secret asNPM_TOKEN
.
Add @changesets/cli
The changesets workflow is designed to help when people are making changes, all the way through to publishing. It lets contributors declare how their changes should be released, then we automate updating package versions, and changelogs, and publishing new versions of packages based on the provided information.
npm install @changesets/cli -D
changeset init
npx changeset init
After initialization, .changeset/config.json
is added in project root. Please check if "baseBranch"
is same as your branch.
/* .changeset/config.json */
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "master",
"updateInternalDependencies": "patch",
"ignore": []
}
2
3
4
5
6
7
8
9
10
11
12
Ready to release? changeset
your version
Once you are ready to release the initial version, do
npx changeset
Publish automation
Add "release"
script
For releasing, we first do CI
. If everything is fine, changeset
publishes it.
{
"scripts": {
"build": "tsup ./src/index.ts -d ./dist --format cjs,esm --dts",
"lint": "tsc",
"dev": "vitest",
"test": "vitest run",
"ci": "npm run lint && npm run test && npm run build",
"release": "npm run ci && npx changeset publish"
}
}
2
3
4
5
6
7
8
9
10
Add publish
action
Add publish.yml
in .github/workflows/
# .github/workflows/publish.yml
name: Publish
on:
push:
branches:
- "master"
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
cache: npm
- run: npm install
- name: Create Release Pull Request or Publish
id: changesets
uses: changesets/action@v1
with:
publish: npm run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
What files to release? - Add .npmignore
.npmignore
excludes the files we don't want to release in npm as a package. We should exclude all develop-stage files but compiled files in ./dist
and package.json
, CHANGELOG.md
as well as LICENSE
.
src
.changeset
.github
.editorconfig
package-lock.json
tsconfig.json
2
3
4
5
6
Publish your package
Make sure changeset
release your library as public.
// .changeset/config.json
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "master",
"updateInternalDependencies": "patch",
"ignore": []
}
2
3
4
5
6
7
8
9
10
11
12
Before you commit, check if workflow has permission to perform actions.
Then commit the current stage, sync to repository, when publish action completed, an auto generated pull request is right there.
Now simply merge the pull request, the CI
action will run to release your library in npm!
Add more info into package.json
// package.json
{
"author": "sharpchen",
"repository": {
"type": "git",
"url": "https://github.com/sharpchen/myfirst-npm-package/"
},
"homepage": "https://github.com/sharpchen/myfirst-npm-package/",
"keywords": [
// ...keywords for searching
],
"license": "MIT",
"description": "what this package do",
}
2
3
4
5
6
7
8
9
10
11
12
13
14
How to work with it
Regular develop-stage
During regular development stage, we just do commit and sync to repository.
Ready to release a new version
If a version is ready, do
npx changeset
Then
npx changeset version
And finally commit those changes, actions will start to work and publish the new version in npm!