I recently made a TypeScript module! It is a component library for work. I’ve never really known how to make a module, nevertheless I knew about the discrepancy between CommonJS and ECMAScript modules. As well, how to specify the difference in an NPM module and a CommonJS module was something that I didn’t know how to do. One thing I did know was that there needed to be some sort of entry file, but I didn’t know exactly how to do that either. I had previously made a crate for Rust, so I figured there must have been a handful of things to follow.

I did try to research, but I could never really find a straightforward answer on how to build a TypeScript module. I did find several sources hinting at how to do it, so I eventually assembled and picked what I thought I would need. I hope this post unifies any potential scattered information in building a module!

A Builder

I originally thought that I would make the module with all pure TypeScript APIs and not use a build tool. But the more I looked into it, the more I found that just using TypeScript APIs would actually be a little bit of a hassle for other software using the package. The preference, it would seem, would be to compile the TypeScript into JS with a build tool.

With the plethora of build tools in the JS ecosystem, which one could I choose? I am familiar with Webpack, Parcel, and Rollup, but maybe there’s a new one to try out? I ended up picking up Vite, because I heard it was gaining popularity. I found out later that Vite is a kind-of wrapper for rollup, but what it is wrapping exactly I don’t know. How it differs from Rollup, I don’t exactly know yet either, because I haven’t looked into it. Initially, I think it’s because Vite offers a more structured and opinionated build approach than Rollup.

Here are the following things that I took into account for the build configuration. Vite does come with built-in TypeScript support. I won’t have to pull in any additional module for it to detect TypeScript. The component library module that I built was a react component library, and Vite also can handle the JSX syntax. For module format, it would seem that backwards compatibility with CommonJS is preferred, seeing as some systems my not have stepped away from CommonJS. So with TypeScript and React, the build configuration would need to detect what I have in my TypeScript file, declare any types, have an input file, and output a CommonJS and an ECMAScript module. I have that in the following code snippet.

 1
 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
import { defineConfig } from 'vite'
import tsconfigPaths from 'vite-tsconfig-paths'
import dts from 'vite-plugin-dts'

import pkg from './package.json'

export default defineConfig({
  plugins: [tsconfigPaths(), dts()],
  build: {
    lib: {
      entry: 'src/index.ts',
      name: pkg.name,
    },
    rollupOptions: {
      external: ['react', 'react-dom'],
      output: [
        {
          format: 'esm',
          dir: 'dist/esm',
        },
        {
          format: 'cjs',
          dir: 'dist/cjs',
        },
      ],
    },
  },
})

The generated files are useless if NPM can’t detect them! I have all the generated files going into a dist/ folder. In the following code snipped, I have the filepaths for the related ECMAScript and CommonJS module builds. Addiitonally, a Vite plugin builds the types for me, and I specified that in the package.json file as well.

1
2
3
4
5
6
7
{
  // ...
  "module": "dist/esm/component-library.esm.js",
  "main": "dist/cjs/component-library.js",
  "types": "dist/index.d.ts",
  // ...
}

Packaging and Shipping

With the builds working, I needed to ship it to NPM. I elected to go with Github Actions, because it seemed to be the easiest and quickest way to ship things to NPM. I can include a workflow file straight in the Github repository, so I wouldn’t need to have any other CI/CD website to handle NPM publishing.

Anytime I publish a tag in the Github repository, this workflow will pick it up. Subsequently, it checks out the repo, sets up node and NPM, installs the dependencies, builds the package, and publishes to NPM. I found this workflow incredibly handy.

I do have some notes to share about Github Actions. The project is using FontAwesome Pro Icons and a private NPM package. So I had to specify the scoped NPM repository for FontAwesome and include the auth tokens for NPM and FontAwesome all within the .npmrc file. That in mind, I had to include environment secrets in local development and in Github Actions, that way the information in the .npmrc file could pick up the right tokens. With Github Actions, I put the tokens in the secrets of the repository, then I was able to reference those secrets to be injected into the build process.

 1
 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
29
30
name: NPM Release
on:
  release:
    types: [published]
jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: checkout repository
        uses: actions/checkout@v3

      - name: setup node
        uses: actions/setup-node@v3
        with:
          node-version: "18.x"
          registry-url: https://registry.npmjs.org

      - name: install dependencies
        run: npm ci
        env:
          FONTAWESOME_NPM_AUTH_TOKEN: ${{ secrets.FONTAWESOME_NPM_AUTH_TOKEN }}

      - name: build package
        run: npm run build-components

      - name: publish package npm
        run: npm publish
        env:
          NPM_ACCESS_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}

Arrival

I enjoyed learning how to make a package that is shipped with NPM, and I am grateful to having learned how to do it with Vite, TypeScript, and Github Actions. It seems to be a simple and straightforward process, and what I found and assembled makes sense. I hope to take this same information and apply it to any personal NPM packages, because I haven’t made any yet and I would like to do so!