Tackling TypeScript
Please support this book: buy it or donate
(Ad, please don’t block.)

8 Creating CommonJS-based npm packages via TypeScript



This chapter describes how to use TypeScript to create packages for the package manager npm that are based on the CommonJS module format.

  GitHub repository: ts-demo-npm-cjs

In this chapter, we are exploring the repository ts-demo-npm-cjs which can be downloaded on GitHub. (I deliberately have not published it as a package to npm.)

8.1 Required knowledge

You should be roughly familiar with:

8.2 Limitations

In this chapter, we are using what TypeScript currently supports best:

Especially on Node.js, TypeScript currently doesn’t really support ECMAScript modules and filename extensions other than .js.

8.3 The repository ts-demo-npm-cjs

This is how the repository ts-demo-npm-cjs is structured:

ts-demo-npm-cjs/
  .gitignore
  .npmignore
  dist/   (created on demand)
  package.json
  ts/
    src/
      index.ts
    test/
      index_test.ts
  tsconfig.json

Apart from the package.json for the package, the repository contains:

package.json contains scripts for compiling:

This is where the compilation results for the two TypeScript files are put:

ts/src/index.ts       --> dist/src/index.js
ts/test/index_test.ts --> dist/test/index_test.js

8.4 .gitignore

This file lists the directories that we don’t want to check into git:

node_modules/
dist/

Explanations:

8.5 .npmignore

When it comes to which files should and should not be uploaded to the npm registry, we have different needs than we did for git. Therefore, in addition to .gitignore, we also need the file .npmignore:

ts/

The two differences are:

Note that npm ignores the directory node_modules/ by default.

8.6 package.json

package.json looks like this:

{
  ···
  "type": "commonjs",
  "main": "./dist/src/index.js",
  "types": "./dist/src/index.d.ts",
  "scripts": {
    "clean": "shx rm -rf dist/*",
    "build": "tsc",
    "watch": "tsc --watch",
    "test": "mocha --ui qunit",
    "testall": "mocha --ui qunit dist/test",
    "prepack": "npm run clean && npm run build"
  },
  "// devDependencies": {
    "@types/node": "Needed for unit test assertions (assert.equal() etc.)",
    "shx": "Needed for development-time package.json scripts"
  },
  "devDependencies": {
    "@types/lodash": "···",
    "@types/mocha": "···",
    "@types/node": "···",
    "mocha": "···",
    "shx": "···"
  },
  "dependencies": {
    "lodash": "···"
  }
}

Let’s take a look at the properties:

The next two subsections cover the remaining properties.

8.6.1 Scripts

Property scripts defines various commands that can be invoked via npm run. For example, the script clean is invoked via npm run clean. The previous package.json contains the following scripts:

Note that when we are using an IDE, we don’t need the scripts build and watch because we can let the IDE build the artifacts. But they are needed for the script prepack.

8.6.2 dependencies vs. devDependencies

dependencies should only contain the packages that are needed when importing a package. That excludes packages that are used for running tests etc.

Packages whose names start with @types/ provide TypeScript type definitions for packages that don’t have any. Without the former, we can’t use the latter. Are these normal dependencies or dev dependencies? It depends:

8.6.3 More information on package.json

8.7 tsconfig.json

{
  "compilerOptions": {
    "rootDir": "ts",
    "outDir": "dist",
    "target": "es2019",
    "lib": [
      "es2019"
    ],
    "module": "commonjs",
    "esModuleInterop": true,
    "strict": true,
    "declaration": true,
    "sourceMap": true
  }
}

The remaining options are explained by the official documentation for tsconfig.json.

8.8 TypeScript code

8.8.1 index.ts

This file provides the actual functionality of the package:

import endsWith from 'lodash/endsWith';

export function removeSuffix(str: string, suffix: string) {
  if (!endsWith(str, suffix)) {
    throw new Error(JSON.stringify(suffix)} + ' is not a suffix of ' +
      JSON.stringify(str));
  }
  return str.slice(0, -suffix.length);
}

It uses function endsWith() of the library Lodash. That’s why Lodash is a normal dependency – it is needed at runtime.

8.8.2 index_test.ts

This file contains a unit test for index.ts:

import { strict as assert } from 'assert';
import { removeSuffix } from '../src/index';

test('removeSuffix()', () => {
  assert.equal(
    removeSuffix('myfile.txt', '.txt'),
    'myfile');
  assert.throws(() => removeSuffix('myfile.txt', 'abc'));
});

We can run the test like this:

npm t dist/test/index_test.js

As you can see, we are running the compiled version of the test (in directory dist/), not the TypeScript code.

For more information on the unit test framework Mocha, see its homepage.