This is a start kit for developing ECMAScript standard EM Modules format library with TypeScript.
ES Module | UMD | CommonJS |
---|---|---|
your-library.esm.js | your-library.js | your-library.common.js |
In this chapter, we will explain how to implement libraries in three formats: ECMAScript standard, CommonJS, and UMD.
-
Create project.
mkdir your-library && cd $_;
-
Create project configuration file.
Execute the following command.
This will create package.json at the root of the project.npm init -y;
Open package.json and edit as follows.
... "main": "dist/your-library.common.js", "module": "dist/your-library.esm.js", "browser": "dist/your-library.js", "types": "types/your-library.d.ts", ...
Name Value Description main dist/your-library.common.js Library name to output in CommonJS format. module dist/your-library.esm.js Library name to output in ES Modules format. browser dist/your-library.js Library name output in UMD format. types types/your-library.d.ts Set the typescript declaration file. -
Install required packages.
npm i -D \ typescript \ ts-node \ tsconfig-paths \ rollup \ rollup-plugin-typescript2 \ rollup-plugin-terser \ jest \ @types/jest \ ts-jest;
Name Description typescript Used to compile TypeScript source code into JavaScript. ts-node Used to execute TypeScript code on a node and immediately check the result. tsconfig-paths Used to resolve paths (alias) in tsconfig.json at runtime with ts-node. rollup Rollup is a module bundler.
Used to bundle ES Modules, CommonJS, and UMD libraries for distribution to clients.rollup-plugin-typescript2 Plug-in for processing typescript with rollup. rollup-plugin-terser Used to compress bundle files. jest Jest is a library for testing JavaScript code. @types/jest Jest's type declaration. ts-jest A TypeScript preprocessor required to test projects written in TypeScript using Jest. -
Create a TypeScript compilation configuration.
Create TypeScript compilation configuration file.
touch tsconfig.json;
Add content:
{ "compilerOptions": { "target": "ESNext", "module": "ESNext", "declarationDir": "./types", "declaration": true, "outDir": "./dist", "rootDir": "./src", "strict": true, "noImplicitAny": true, "baseUrl": "./", "paths": {"~/*": ["src/*"]}, "esModuleInterop": true }, "include": [ "src/**/*" ], "exclude": [ "node_modules", "**/*.test.ts" ] }
Option Value Description compilerOptions target ESNext Specify ECMAScript target version.
"ESNext" targets latest supported ES proposed features.module ESNext Specify module code generation.
"ESNext" is an ECMAScript standard, and import/export in typescript is output as import/export.declarationDir ./types Output directory for generated declaration files. declaration true Generates corresponding .d.ts file. outDir ./dist Output directory for compiled files. rootDir ./src Specifies the root directory of input files. baseUrl ./ Base directory to resolve non-relative module names. paths {
"~/*": ["src/*"]
}List of path mapping entries for module names to locations relative to the baseUrl.
Set the alias of "/ src" directly under the root with "~ /".
e.g. import Awesome from '~/components/Awesome';include [
"src/**/*"
]A list of glob patterns that match the files to be included in the compilation.
Set the source directory.exclude [
"node_modules",
"**/*.test.ts"
]A list of files to exclude from compilation.
Set node_modules and unit test code. -
Create a library module with a type script.
Create a directory to store source files.
mkdir src;
Submodule that calculates the subtraction.
// src/add.ts /** * Sum two values */ export default function(a:number, b:number):number { return a + b; }
Submodule that calculates the addition.
// src/sub.ts /** * Diff two values */ export default function(a:number, b:number):number { return a - b; }
Main module that imports multiple modules and exports a single library.
// src/your-library.ts import add from '~/add'; import sub from '~/sub'; export {add, sub};
-
Let's run the library on node.
Run the following command.
To run on node, it is important to set the module option to CommonJS.npx ts-node -r tsconfig-paths/register -P tsconfig.json -O '{"module":"commonjs"}' -e "\ import {add} from '~/your-library'; console.log('1+2=' + add(1,2));";# 1+2=3
-
Setting up and running unit tests.
Create unit test configuration file.
touch jest.config.js;
Add content:
const { pathsToModuleNameMapper } = require('ts-jest/utils'); const { compilerOptions } = require('./tsconfig.json'); module.exports = { roots: [ '<rootDir>/src', '<rootDir>/tests/' ], transform: { '^.+\\.tsx?$': 'ts-jest' }, testRegex: '(/tests/.*|(\\.|/)(test|spec))\\.tsx?$', moduleFileExtensions: [ 'ts', 'js' ], moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths , { prefix: '<rootDir>/' }) // moduleNameMapper: { // '^~/(.+)': '<rootDir>/src/$1' // } }
Name Description roots A list of paths to directories that Jest should use to search for files in.
Specify the directory path where the source code (./src) and test code (./tests) files are located.transform Instruct Jest to transpile TypeScript code with ts-jest. testRegex Specify the file and directory to be teste. moduleFileExtensions Specifies the extension of the file to be tested. moduleNameMapper Apply alias setting of actual path set in "baseUrl" and "paths" of tsconfig.json. Create a tests directory to store test code at the root of the project.
mkdir tests;
Then, Create add.test.ts and sub.test.ts files in the tests directory.
This will contain our actual test.// tests/add.test.ts import {add} from '~/your-library'; test('Add 1 + 2 to equal 3', () => { expect(add(1, 2)).toBe(3); });
// tests/sub.test.ts import {sub} from '~/your-library'; test('Subtract 1 - 2 to equal -1', () => { expect(sub(1, 2)).toBe(-1); });
Open your package.json and add the following script.
... "scripts": { "test": "jest" ...
Run the test.
npm run test;
Jest will print this message.
You just successfully wrote your first test.PASS tests/add.test.ts PASS tests/sub.test.ts Test Suites: 2 passed, 2 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 1.332s, estimated 3s Ran all test suites.
-
Run the build.
There is one caveat.
Convert UMD library names in global namespace from snake case to camel case. In the case of e.g.your-library, it will be window.yourLibrary.Create a build configuration file.
touch rollup.config.js;
Add content:
import typescript from 'rollup-plugin-typescript2'; import { terser } from "rollup-plugin-terser"; import pkg from './package.json'; export default { external: Object.keys(pkg['dependencies'] || []), input: './src/your-library.ts', plugins: [ typescript({ tsconfigDefaults: { compilerOptions: {} }, tsconfig: "tsconfig.json", tsconfigOverride: { compilerOptions: {} }, useTsconfigDeclarationDir: true }), terser() ], output: [ // ES module (for bundlers) build. { format: 'esm', file: pkg.module }, // CommonJS (for Node) build. { format: 'cjs', file: pkg.main }, // browser-friendly UMD build { format: 'umd', file: pkg.browser, name: pkg.browser .replace(/^.*\/|\.js$/g, '') .replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace('-', '').replace('_', '')) } ] }
Name Description external Comma-separate list of module IDs to exclude. input The bundle's entry point(s) (e.g. your main.js or app.js or index.js). plugins Plugins allow you to customise Rollup's behaviour by, for example,
transpiling code before bundling, or finding third-party modules in your node_modules folder.
Use rollup-plugin-typescript2 and rollup-plugin-terser.
rollup-plugin-typescript2 is a TypeScript loader, and this plugin reads "tsconfig.json" by default.
rollup-plugin-terser compresses source code.output The output destination of the bundle.
Three types of libraries, ES Modules, CommonJS, and UMD, are output.Open your package.json and add the following script.
... "scripts": { "build": "rollup -c" } ...
Run the build.
npm run build;
The library compilation result and declaration file are output to the project root.
You just built successfully.. -- dist/ -- your-library.esm.js -- your-library.common.js -- your-library.js -- types/ -- your-library.d.ts -- add.d.ts -- sub.d.ts
-
Create an NPM user locally.
When the command is executed, a '~/.npmrc' file is created and the entered information is stored.npm set init.author.name 'Your name'; npm set init.author.email '[email protected]'; npm set init.author.url 'https://your-url.com'; npm set init.license 'MIT'; npm set init.version '1.0.0';
-
Create a user on npm.
If the user is not registered yet, enter the new user information to be registered in npm.
If an npm user has already been created, enter the user information and log in.npm adduser;
-
Create a repository on GitHub and clone.
git clone https://github.com/your-user/your-repository.git;
-
Setting files to be excluded from publishing
Create an .npmignore file at the root of the project.
.npmignore
Add node_modules and package-lock.json to .npmignore not to publish.
node_modules/ package-lock.json
-
Create v1.0.0 tag on GitHub.
git tag -a v1.0.0 -m 'My first version v1.0.0'; git push origin v1.0.0;
-
Publish to npm
npm publish;
-
Push program changes to github
git commit -am 'Update something'; git push;
-
Increase version
npm version patch -m "Update something";
-
Create a new version tag on Github
git push --tags;
-
Publish to npm
npm publish;
-
Create project.
mkdir myapp && cd $_;
-
Create project configuration file.
npm init -y;
-
Install this library.
npm i -S esm-and-other-format-libraries-starter;
-
Create HTML.
touch index.html;
-
Try the library.
-
For ES Modules:
The ES Modules library can be run in the browser immediately without compiling.
Add the following code to myapp/index.html and check with your browser.
<script type="module"> import { add } from './node_modules/esm-and-other-format-libraries-starter/dist/mylib.esm.js'; console.log(`1+2=${add(1,2)}`);// 1+2=3 </script>
-
For CommonJS:
The CommonJS library cannot be executed in the browser as it is, so it must be compiled into a format that can be executed in the browser.
Install webpack for build.
npm i -D webpack webpack-cli;
Create a module that runs the library.
Prepare myapp/app.js and add the following code.import {add} from 'esm-and-other-format-libraries-starter'; console.log(`1+2=${add(1,2)}`);// 1+2=3
Compile "myapp/app.js" into a format that can be executed by a browser.
The compilation result is output to "myapp/app.budle.js".npx webpack app.js -o bundle.js;
Add the following code to myapp/index.html and check with your browser.
<script src="bundle.js"></script>
-
For UMD:
The UMD library can be executed globally. It's very easy, but I don't like it because it makes module dependencies unclear.
Add the following code to myapp/index.html and check with your browser.
<script src="node_modules/esm-and-other-format-libraries-starter/dist/mylib.js"></script> <script> console.log(`1+2=${mylib.add(1,2)}`);// 1+2=3 </script>
-
Check what JavaScript code is generated according to the setting value of module of tsconfig.
-
The following is a module written in TypeScript used in the experiment.
Main module.
// ./src/app.ts import add from './add'; const result = add(1, 2);
Sub module.
// ./src/add.ts export default function (a:number, b:number):number { return a + b; }
-
Experimental results
-
'target' is 'ESNext' and 'module' is 'es2015' or 'ESNext':
// ./dist/app.js import add from './add'; const result = add(1, 2);
// ./dist/add.js export default function (a, b) { return a + b; }
-
'target' is 'ESNext' and 'module' is 'none' or 'commonjs':
// ./dist/app.js "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const add_1 = __importDefault(require("./add")); const result = add_1.default(1, 2);
// ./dist/add.js "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); function default_1(a, b) { return a + b; } exports.default = default_1;
-
'target' is 'ESNext' and 'module' is 'amd':
./dist/app.js var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; define(["require", "exports", "./add"], function (require, exports, add_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); add_1 = __importDefault(add_1); const result = add_1.default(1, 2); });
// ./dist/add.js define(["require", "exports"], function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); function default_1(a, b) { return a + b; } exports.default = default_1; });
-
'target' is 'ESNext' and 'module' is 'system':
// ./dist/app.js System.register(["./add"], function (exports_1, context_1) { "use strict"; var add_1, result; var __moduleName = context_1 && context_1.id; return { setters: [ function (add_1_1) { add_1 = add_1_1; } ], execute: function () { result = add_1.default(1, 2); } }; });
// ./dist/add.js System.register([], function (exports_1, context_1) { "use strict"; var __moduleName = context_1 && context_1.id; function default_1(a, b) { return a + b; } exports_1("default", default_1); return { setters: [], execute: function () { } }; });
-
'target' is 'ESNext' and 'module' is 'umd':
// ./dist/app.js var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; (function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "./add"], factory); } })(function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const add_1 = __importDefault(require("./add")); const result = add_1.default(1, 2); });
// ./dist/add.js (function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports"], factory); } })(function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); function default_1(a, b) { return a + b; } exports.default = default_1; });
-
- Twitter: @TakuyaMotoshima
- Github: TakuyaMotoshima mail to: [email protected]