Modern JavaScript library starter
source link: https://advancedweb.hu/modern-javascript-library-starter/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Modern JavaScript library starter
How to publish a package with TypeScript, testing, GitHub Actions, and auto-publish to NPM
Publishing a library
Back then when I wanted to write and publish a JavaScript library, all I had to do is to create a new GitHub project, write a package.json with some basic details, add an index.js
, and publish to NPM via the CLI. But this simple setup misses a lot of new things that are considered essentials: no types, no CI/CD, no tests, to name a few.
So the last time I needed to start a new JavaScript library I spent some time setting up the basics and then realized that these steps are mostly generic and can be reused across different projects. This article is a documentation of the different aspects needed to develop and publish a modern library.
More specifically, I wanted these features:
- the library is written in TypeScript with types published in the package
- there are tests, also written in TypeScript
- a CI pipeline runs for commits building and running the tests
- a CD pipeline is run for every new version publishing to the NPM registry
Starting code
The important files are some configuration, the package source, and the tests:
Since there is a compile step, the sources and the compiled files are in different directories. While the .ts
files are in src/
, the target for the compilation go to dist/
.
The package.json
:
The files
define the dist
as only the compiled files will be packaged and pushed to the NPM registry. Then the main: "dist/index.js"
defines the entry point.
The tsconfig.json
configures the TypeScript compiler:
Depending on the project a lot of different configurations are possible, but the important parts are that the files in the src/
folder is included but not the tests, and the outDir
is dist
.
Then the index.ts
and the index.test.ts
files are simple, just to demonstrate that the library works:
Notice the import ... from "./index.js"
line. While the file has .ts
extension, importing is done using the .js
.
NPM scripts
Next, configure the scripts
in the package.json
.
First are the build
and clean
:
These simply call the tsc
to compile TypeScript to JavaScript:
Next, the prepare
script runs the build when the package is being published. This is a special name as npm
calls it at different parts of the lifecycle:
Tests
Next, configure automated tests. For this, I found that it's easier to not compile the test code but use a library that auto-complies TS files when needed. This is where the ts-node
dependency comes into play.
Because of this, the test
script does not need to run the build
:
The --loader ts-node/esm
attaches the ts-node
to the node module resolution process and that compiles .ts
files whenever they are imported. This makes testing setup super easy: no compilation, just running.
Continuous integration
Now that we have all the scripts in place for the library, it's time to setup GitHub Actions to run the build and the tests for every push.
Actions are configured in the .github/workflows
directory, where each YAML file describes a workflow.
Let's break down the interesting parts in this workflow!
The on: push, pull_requests
defines that the job will run on every push and pull request. You can define some filters here, such as to run tests only for certain branches, but it's not needed for now.
The build
job uses ubuntu-latest
which is a good all-around base for running scripts as it has a lot of preinstalled software.
The strategy/matrix
defines which node-version
to run the build with. This works like templating: the ${matrix.node-version}
placeholder will be filled with each value in this array and each configuration will bu run during the build.
The steps
are simple: checkout
gets the current code, the setup-node
installs the specific NodeJS version, then it runs npm ci
, npm run build
, and npm test
.
In action
The GitHub Actions page shows that the workflow runs for every push:
And each change shows the steps with the logs:
Moreover, a green checkmark shows that the actions were run successfully for a given commit:
This makes it very easy to see if tests are failing
Auto-deploy to NPM
Let's then implement the other half of CI/CD: automatic deployment!
For this, we'll configure a separate workflow:
The on/push/tags: ["*"]
defines that the workflow will be run for all top-level tags, such as 1.0.0
, v5.3.2
, but not for feature/ticket
or fix/bug-45
. This is a good default config: it does not force any versioning strategy but also allows any type of hierarchical branch names.
The build
step is the same as the other action, just to make sure that the library can be built with all the supported NodeJS versions and tests are passing.
The publish-npm
is the more interesting part: it checks out the code, sets up the correct NodeJS version, runs npm ci
, the publishes the package. The --provenance
adds extra metadata to the package and that is the reason for the permissions/id-token: write
config.
Provenance
Provenance is a modern feature of the NPM registry and its purpose is to provide a verifiable link from the published package to the source code that produced it.
Without it, nothing says that the code you see on GitHub is the same that the maintainer had when they built and published the package. And that means that even if you go the extra mile to audit the source code of the package it can still happen that it was changed.
Provenance solves this problem: GitHub Actions adds the metadata pointing to the code and the workflow then signs the package. With it, it is no longer possible that a malicious maintainer changes the code before publishing it.
When a version is published with provenance, it is shown on the package's page:
And also there is a green checkmark next to the version:
Secrets
An important link is still missing: how does NPM know that a package can be published from that GitHub Action? This is where the access tokens come into play.
NPM allows creating M2M (Machine-to-Machine) tokens that grant access to publish new versions. So to configure a workflow with publish access, configure a granular access token:
When adding a token, you can define which packages it has access to:
On the other end, add a repository secret to the GitHub repo:
Then the workflow can use this secret:
Publishing a new version
When everything is configured, publishing a new version is simple:
Then push the code and the new tag:
This triggers the workflows:
And the new version is pushed to the NPM registry:
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK