Skip to content

This is a start kit for developing ECMAScript standard EM Modules format library with TypeScript.

License

Notifications You must be signed in to change notification settings

takuya-motoshima/esm-and-other-format-libraries-starter

Repository files navigation

esm-and-other-format-libraries-starter

This is a start kit for developing ECMAScript standard EM Modules format library with TypeScript.

Explanation of Build Files.

ES Module UMD CommonJS
your-library.esm.js your-library.js your-library.common.js

Create your library.

In this chapter, we will explain how to implement libraries in three formats: ECMAScript standard, CommonJS, and UMD.

  1. Create project.

    mkdir your-library && cd $_;
  2. 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.
  3. 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.
  4. 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.
  5. 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};
  6. 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
  7. 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.
  8. 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
    

How to publish a npm package

  1. 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';
  2. 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;
  3. Create a repository on GitHub and clone.

    git clone https://github.com/your-user/your-repository.git;
  4. 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
    
  5. 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;
  6. Publish to npm

    npm publish;

How to upgrade NPM packages

  1. Push program changes to github

    git commit -am 'Update something';
    git push;
  2. Increase version

    npm version patch -m "Update something";
  3. Create a new version tag on Github

    git push --tags;
  4. Publish to npm

    npm publish;

Try this library

  1. Create project.

    mkdir myapp && cd $_;
  2. Create project configuration file.

    npm init -y;
  3. Install this library.

    npm i -S esm-and-other-format-libraries-starter;
  4. Create HTML.

    touch index.html;
  5. 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>

Difference in tscofig module output results.

Check what JavaScript code is generated according to the setting value of module of tsconfig.

  1. 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;
    }
  2. 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;
      });

Reference

License

MIT

Author

About

This is a start kit for developing ECMAScript standard EM Modules format library with TypeScript.

Resources

License

Stars

Watchers

Forks

Packages

No packages published