This blog post describes how to use TypeScript to create npm packages with CommonJS modules. All the artifacts that are shown can be downloaded via the GitHub repository ts-demo-npm-cjs
(I deliberately have not published it to npm).
Required knowledge: You should be familiar with how npm packages work.
We are using what TypeScript currently supports best:
.js
.Especially on Node.js, TypeScript currently doesn’t really support ECMAScript modules and filename extensions other than .js
.
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:
index.ts
: the actual code of the packageindex_test.ts
: a test for index.ts
tsconfig.json
: configuration data for the TypeScript compilerpackage.json
contains scripts for compiling:
ts/
(TypeScript code)dist/
(CommonJS modules; the directory doesn’t yet exist in the repository)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
.gitignore
This file lists the directories that we don’t want to check into git:
node_modules/
dist/
Explanations:
node_modules/
is set up via npm install
.dist/
are created by the TypeScript compiler (more on that later)..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:
dist/
).ts/
).Note that npm ignores the directory node_modules/
by default.
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:
type
: The value "commonjs"
means that .js
files are interpreted as CommonJS modules.main
: When using a bare import with just the name of the package, this is the module that will be imported.types
points to the bundled type declaration file.The next two subsections cover the remaining properties.
Property scripts
defines various commands for building that can be invoked via npm run clean
(etc.):
clean
: Use the cross-platform package shx
to delete the compilation results. shx
provides a variety of shell commands with the benefit of not needing a separate package for each command we may want to use.build
, watch
: Use the TypeScript compiler tsc
to compile the TypeScript files according to tsconfig.json
. tsc
must be installed globally or locally (then package typescript
would be a dev dependency).test
, testall
: Use the unit test framework Mocha to run one test or all tests.prepack
: This script is run run before a tarball is packed (due to npm pack
, npm publish
, and when installing dependencies from git).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
.
package.json
explain various properties of that file.scripts
explain the package.json
property scripts
.dependencies
vs. devDependencies
dependencies
should only contain the packages that are needed when importing a package. That excludes packages that are needed for running tests etc.
Packages whose names start with @types/
provide TypeScript type definitions (which enable auto-completion even for JavaScript) for packages that don’t include them. Are these normal dependencies or dev dependencies? It depends:
If the type definitions of our package refer to type definitions in another package, that package is a normal dependency.
Otherwise, the package is only needed during development time and a dev dependency.
tsconfig.json
{
"compilerOptions": {
"rootDir": "ts",
"outDir": "dist",
"target": "es2019",
"lib": [
"es2019"
],
"module": "commonjs",
"esModuleInterop": true,
"strict": true,
"declaration": true,
"sourceMap": true
}
}
rootDir
: Where are our TypeScript files located?outDir
: Where should the compilation results be put?target
: What is the targeted ECMAScript version? Features that the targeted version does not support will be compiled to features that it does support. Therefore, if we target ES3, many features with be compiled.lib
: What platform features should TypeScript be aware of? Possibilities includes the ECMAScript standard library and the DOM of browsers. The Node.js API is supported differently, via the package @types/node
.module
: Specifies the format of the compilation output.The remaining options are explained by the official documentation for tsconfig.json
.
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.
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
t
is an abbreviation for the npm command test
.test
is an abbreviation for run test
(which runs that script from package.json
).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.