Typescript Packaging Notes

Typescript packaging

If you are new to typescript the documentation may be a bit confusing about what it is doing around packaging your code. However, if you understand that typescript, sometimes, is a combination of transpiler, polyfill and packager, it makes much more sense.
My pipeline is typically, typescript => babel => webpack.
I use typescript as a transpiler, and sometimes it performs functions that babel does and sometimes things that webpack does. Webpack, and babel/typescript, add boilerplate to a bundled package when it bundles but it may be minor or significant depending on your target bundle size. Also, both typescript and babel can use global or "libary" polyfills to support different deployment models.
I tend to use babel for transpiling down to the target js version because I'm able to specify browser versions in .babelrc as targets and it automatically figures out what to do. typescript can do something similar by targeting a specific javascript version but its not as fine-grained.
We are using typescript 2.5+ in the examples below.

Namespacing

Namespacing existed before javacript new "modules" were implemented. Namespacing still exists and provides a direct implementation of a common "namespace" technique popular in hand-written javascript. Namespaces bridge the type and untyped world when you write types for a package that does not have any i.e. written in pure javascript. Namespaces are real objects only when there is a "value" declaration inside them, otherwise they are just a typescript construct to help with typing. In the below examples, we add values to the namespace in order to make them "real".
/** TS packaging test. */
namespace Foo {
    const x = 12
    const y = "blah"
}

namespace Foo.Child1 {
    const z = 40
}
if we use the following tsconfig.json
{
    "compilerOptions": {
        "jsx": "react",
        "target": "es5",
        "strictNullChecks": true
 ,"allowJs": true
 ,"importHelpers": true
 ,"sourceMap": true
 ,"experimentalDecorators": true
 ,"allowSyntheticDefaultImports": true
 ,"module": "commonjs"
 ,"typeRoots": ["./typings", "./node_modules/@types/"]
    }
    ,"exclude": ["node_modules"]
}
and run this via package.json
// partial package.json
"scripts": {
  ...
  "test1": "tsc test1.ts"
}
we get javascript that looks like the following and is very script tag inclusion friendly:
/** TS packaging test. */
var Foo;
(function (Foo) {
    var x = 12;
    var y = "blah";
})(Foo || (Foo = {}));
(function (Foo) {
    var Child1;
    (function (Child1) {
        var z = 40;
    })(Child1 = Foo.Child1 || (Foo.Child1 = {}));
})(Foo || (Foo = {}));
If we now add another source file that extends that namespace and reference the first one using "include" directives:
/** More of the namespace. */
/// <reference path="test1.ts"/>
namespace Foo {
    const x = 40
}

namespace Foo.Child1 {
    const zmore = 50
}

namespace Blah {
    const x = 100
}
and use a script to run it:
"scripts": {
 ...
        "test1-part2": "tsc --outFile test1-part2.js test1-part2.ts",
 ...
}
we get
/** TS packaging test. */
var Foo;
(function (Foo) {
    var x = 12;
    var y = "blah";
})(Foo || (Foo = {}));
(function (Foo) {
    var Child1;
    (function (Child1) {
        var z = 40;
    })(Child1 = Foo.Child1 || (Foo.Child1 = {}));
})(Foo || (Foo = {}));
/** More of the namespace. */
/// 
var Foo;
(function (Foo) {
    var x = 40;
})(Foo || (Foo = {}));
(function (Foo) {
    var Child1;
    (function (Child1) {
        var zmore = 50;
    })(Child1 = Foo.Child1 || (Foo.Child1 = {}));
})(Foo || (Foo = {}));
var Blah;
(function (Blah) {
    var x = 100;
})(Blah || (Blah = {}));
~                        
which shows that it concatenating both parts together, via the --outFile CLI option to tsc, and created a file that is easily included in a script or even packaged up.
/** TS packaging test. */
var Foo;
(function (Foo) {
    var x = 12;
    var y = "blah";
})(Foo || (Foo = {}));
(function (Foo) {
    var Child1;
    (function (Child1) {
        var z = 40;
    })(Child1 = Foo.Child1 || (Foo.Child1 = {}));
})(Foo || (Foo = {}));
/** More of the namespace. */
/// 
var Foo;
(function (Foo) {
    var x = 40;
})(Foo || (Foo = {}));
(function (Foo) {
    var Child1;
    (function (Child1) {
        var zmore = 50;
    })(Child1 = Foo.Child1 || (Foo.Child1 = {}));
})(Foo || (Foo = {}));
var Blah;
(function (Blah) {
    var x = 100;
})(Blah || (Blah = {}));
If we had not used --outFile we would have only had the output of test1-part2.ts in our test1-part2.js file and similarly test1.js which we would then need to load individually or include with multiple script tags.
If we use an advanced feature that is easily transpiled, typescript does that:
/** TS packaging test, with spread */
namespace Foo {
    const x = 12
    const y = "blah"
    const z = {x, y}
    const {x:x2} = z
}

namespace Foo.Child1 {
    const z = 40
}
becomes
/** TS packaging test, with spread */
var Foo;
(function (Foo) {
    var x = 12;
    var y = "blah";
    var z = { x: x, y: y };
    var x2 = z.x;
})(Foo || (Foo = {}));
(function (Foo) {
    var Child1;
    (function (Child1) {
        var z = 40;
    })(Child1 = Foo.Child1 || (Foo.Child1 = {}));
})(Foo || (Foo = {}));
But some transpiled features are more advanced and require more boilerplate that we might see issued from babel. In this case there is typescript tslib (npm i --save tslib) which acts much like babel-plugin-transform-runtime in that it factors out some boileplate from advanced features and instead of inserting boilerplace into each output, it references a common implementation of them--saves space.

Namespacing via webpack bundling

Since typescript's namespacing outputs a specific type of module, we see that it is really just something like webpack bundling (we are ignoring babel for the moment).
We can skip typescript namespacing and use webpack bundling with the output option set to "library." webpack adds some boilerplate to make "loading" per "part" cleanly separated. This helps with other webpack features such as hot module reloading (HMR) and other things such as bundling many different types of bundles together.
First we create two files: Foo.ts and Child1.ts. javascript modules are name after their files although you can rename them upon import. You cannot split the same module across multiple files by default but you fake it by using import/exports cleverly. The example below is a simple example of hierarchical assembling:
// Foo
export const x = 12
export const y = "blah"
export const z = 40
export const {x:x2} = {x,y,z}

export internalX = "some value"
and the child:
// Child1
export const z = 40
export const zmore = 50
With this setup we can import each separately into a consumer:
import * as Foo from "./Foo"
const * as Child1 from "./Child1"

console.log("imports", Foo, Child1)
If we had named Child1 "Foo.Child1" then the import looks like its hierachical but access is still through two separate variables, Foo and Child1 and two separate files.
We could simulate a hierarchy using an import and export:
// Foo
import * as Child1 from "./Child1"
export { Child1 as Child1 }
// in a consumer, Child1 available as Foo.Child1

export const x = 12
export const y = "blah"
export const z = 40
const combined = {x,y,z}
export const {x: x2} = combined

const internalX = "some value"
const blah = x2 + Child1.z
Now we need to tell webpack to put these together for us:
let webpack = require("webpack"),
    path = require("path"),
    srcdir = path.join(__dirname, "test1-webpack") // we put Foo,Child1 in this dir

const config = {
    entry: {
        Foo: path.join(srcdir, "Foo.ts")
    },
    output: {
        path: path.join(__dirname, "dist"),
        filename: "[name].js",
        library: "[name]",
        libraryTarget: "var"
    },
    target: "web",
    resolve: {
 extensions: [".ts"],
    },
    module: {
        rules: [
            {
                test: /\.ts$/,
                exclude: /node_modules/,
                use: [
                    /*{ loader: "babel-loader"},*/
                    { loader: "ts-loader" }
                ]
            },
        ]
    }
}
module.exports = config
and we get in the dist folder a larger-than-before Foo.js file, most of it is the webpack module handling code with our two modules at the end:
var Foo =
/******/ (function(modules) { // webpackBootstrap
/******/  // The module cache
/******/  var installedModules = {};
/******/
/******/  // The require function
/******/  function __webpack_require__(moduleId) {
/******/
/******/   // Check if module is in cache
/******/   if(installedModules[moduleId]) {
/******/    return installedModules[moduleId].exports;
/******/   }
/******/   // Create a new module (and put it into the cache)
/******/   var module = installedModules[moduleId] = {
/******/    i: moduleId,
/******/    l: false,
/******/    exports: {}
/******/   };
/******/
/******/   // Execute the module function
/******/   modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/   // Flag the module as loaded
/******/   module.l = true;
/******/
/******/   // Return the exports of the module
/******/   return module.exports;
/******/  }
/******/
/******/
/******/  // expose the modules object (__webpack_modules__)
/******/  __webpack_require__.m = modules;
/******/
/******/  // expose the module cache
/******/  __webpack_require__.c = installedModules;
/******/
/******/  // define getter function for harmony exports
/******/  __webpack_require__.d = function(exports, name, getter) {
/******/   if(!__webpack_require__.o(exports, name)) {
/******/    Object.defineProperty(exports, name, {
/******/     configurable: false,
/******/     enumerable: true,
/******/     get: getter
/******/    });
/******/   }
/******/  };
/******/
/******/  // getDefaultExport function for compatibility with non-harmony modules
/******/  __webpack_require__.n = function(module) {
/******/   var getter = module && module.__esModule ?
/******/    function getDefault() { return module['default']; } :
/******/    function getModuleExports() { return module; };
/******/   __webpack_require__.d(getter, 'a', getter);
/******/   return getter;
/******/  };
/******/
/******/  // Object.prototype.hasOwnProperty.call
/******/  __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/  // __webpack_public_path__
/******/  __webpack_require__.p = "";
/******/
/******/  // Load entry module and return exports
/******/  return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";

Object.defineProperty(exports, "__esModule", { value: true });
// Foo
const Child1 = __webpack_require__(1);
exports.Child1 = Child1;
exports.x = 12;
exports.y = "blah";
exports.z = 40;
const combined = { x: exports.x, y: exports.y, z: exports.z };
exports.x2 = combined.x;
const internalX = "some value";
const blah = exports.x2 + Child1.z;


/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";

Object.defineProperty(exports, "__esModule", { value: true });
// Child1
exports.z = 40;
exports.zmore = 50;


/***/ })
/******/ ]);
We had babel turned off as part of the loader since we knew that typescript could handle the transpilation directly.
In both the typescript namespace case and the webpack case (using the library option) we get a global variable called Foo.
But not all Foos are alike. While this output is good for "script" inclusion, we may want ot use this in node as well and this model will not work in node because of the way node loads content.
So, the exposure of a global variable means that it works well in a script tag. But in node, the load occurs in a different context and the exported "Foo" variable is not accessible. With webpack we can output a different type of module. In other words if we want isomorphic in our consuming environment we need to do something similar:
...
   libraryTarget: "umd"
...
There are a couple of ways to expose our content in node (for example, choose a "global" libraryTarget). For this example, we just chose to export our content as a UMD module which works on both the web and in node. Changing our output to "umd" gives the top of our output file a familiar UMD look:
(function webpackUniversalModuleDefinition(root, factory) {
        if(typeof exports === 'object' && typeof module === 'object')
                module.exports = factory();
        else if(typeof define === 'function' && define.amd)
                define([], factory);
        else if(typeof exports === 'object')
                exports["Foo"] = factory();
        else
                root["Foo"] = factory();
})(this, function() {
...
and we can use this in node easily as well as a script tag:
$ node
> var Foo = require("./dist/Foo")
undefined
> Foo
{ Child1: { z: 40, zmore: 50 }, x: 12, y: 'blah', z: 40, x2: 12 }
> Foo.Child1.z
40
> 
What's nice about UMD is that we can also provide this library, already processed, to other consumers who want to use the output as input into their programs as well as the raw source, both of which can be provided via a npm install process.
If the webpack header overhead fits into your bandwidth/byte budget, then using webpack is a flexible approach to creating modules.

Standard module import vs require

If you use the typescript compiler, it implements the new javascript loader so you can use import * as Child1 from "./test1-webpack/Child1" and it will load Child1 content from the specified location looking for .ts (then a .d.ts) file as described in the typescript manual under "module resolution."
As a backdoor, you can still use the "require" approach and have webpack handle importing. In fact, when typescript sees the require statement, it does not try to resolve it which lets standard webpack processing apply:
import * as Child1 from "./test1-webpack/Child1"
const R = require("ramda")
translates to:
"use strict";
exports.__esModule = true;
var R = require("ramda");
If we actually used Child2, which is pretty lean:
import * as Child1 from "./test1-webpack/Child1"
const R = require("ramda")

console.log("Child1.z", Child1.z)
we get
"use strict";
exports.__esModule = true;
var Child1 = require("./test1-webpack/Child1");
var R = require("ramda");
console.log("Child1.z", Child1.z);
By itself, when using require, we have to use something that implements/handles the "require" function call. Essentially, when you use a const/let/var and require, non-typescript resolution mechanisms are needed.
Typescript can use an import with require but it does not handle the require function call. This is bit of an escape hatch from typescript:
import * as Child1 from "./test1-webpack/Child1"
const R = require("ramda")
const moment = require("moment")

console.log("Child1.z", Child1.z)
console.log("R.curry", R.curry)
console.log("moment", moment.format())
Here we use the const=require pattern and the import=require patter (for moment like in the typescript docs).
The import=require format activates typescript based importing unlike the const=require approach. The output is
"use strict";
exports.__esModule = true;
var Child1 = require("./test1-webpack/Child1");
var R = require("ramda");
var moment = require("moment");
console.log("Child1.z", Child1.z);
console.log("R.curry", R.curry);
console.log("moment", moment.format());
Which indicates that a "const=require" import gets translated into a var moment=require("moment") which is what we wrote in the previous example.
But if the library is a proper ES module such as ramda, you can still use the newer import statement that typescript handles directly:
import * as Child1 from "./test1-webpack/Child1"
import * as R from "ramda"

console.log("Child1.z", Child1.z)
console.log("R.curry", R.curry)
You get:
"use strict";
exports.__esModule = true;
var Child1 = require("./test1-webpack/Child1");
var R = require("ramda");
console.log("Child1.z", Child1.z);
console.log("R.curry", R.curry);
The moment library is a UMD library (you know this by looking at the head of the file in node_modules/moment). It also comes with a .d.ts file that exports a namespace i.e. moment is really a global library as well as a function, moment. To consume it, we could use the import moment=require("moment") approach:
import * as Child1 from "./test1-webpack/Child1"
import * as R from "ramda"
import moment=require("moment")

console.log("Child1.z", Child1.z)
console.log("R.curry", R.curry)
const x = (): moment.Moment => moment()
console.log("moment", moment().format())
Note that since there is a both a function and a namsepace exported, the type Moment is exported from the namespace and the function allows us to call moment() to get an instance.
The output is:
"use strict";
exports.__esModule = true;
var Child1 = require("./test1-webpack/Child1");
var R = require("ramda");
var moment = require("moment");
console.log("Child1.z", Child1.z);
console.log("R.curry", R.curry);
var x = function () { return moment(); };
console.log("moment", moment().format());
and if we run our program inside of node since its a UMD module and hence isomporphic:
$ node test3-require.js 
Child1.z 40
R.curry function f1(a) {
    if (arguments.length === 0 || _isPlaceholder(a)) {
      return f1;
    } else {
      return fn.apply(this, arguments);
    }
  }
moment 2017-10-07T10:40:06-04:00
moment is imported in the final .js file using require. We had used that format before and typescript essentially ignored it.
Most importantly, when we used the "const=require" import approach directly in our .ts file, typescript ignored it and the types. When using the "import-from" model (and since moment had .d.ts file), we imported our types, including the moment function definition and the namespace. The different import methods boil down to the same thing but it we get the types. Thank goodness for the .d.ts file.
To summarize this, we can use the import * for things like ramda that are ES modules already but types may be elusive since this just uses node's or webpack bundling model. We can use the import moment = require("moment") or the import * as moment from "moment" model to import the module and types even if its written in a UMD format. We get the types from this style of import because statement started with the "import" statement and typescript handled it directly.
Also, note that since ramda exports an object by default with all the methods being members of that object, instead of a single named object (e.g. the default), we had to use import * as R from "ramda" or const R = require("ramda") or import R = require("ramda") instead of import R from "ramda". This last import form can be processed by babel/webpack correctly because those toolchains shim a simulated single object whose properties are everything that are exported and assign it to R. However, the last form will not work in typescript files because typescript import processing, activated by "import", does not insert a shim by default. Typescript has a compiler switch to insert a shim for these types of modules but it may cause other issues during compilation (vs the babel/webpack steps).

Transpilation of Advanced Feature

Both typescript and babel transpile eatures down to the target javascript version e.g. tsconfig.json's target setting or babel's present environments.
There are subtle interactions between tsconfig's target and --implicitHelpers and --noEmitHelpers options. Note that some of this is covered quite nicely at https://blog.mariusschulz.com/2016/12/16/typescript-2-1-external-helpers-library.
If you set the tsconfig's target to esnext, it assumes a fairly advanced environment and does not insert shims in place of advanced features and in fact "passes through" advance features to let the execution enviroment deal with it. Since we often use babel as the next stage of processing, babel handles shimming and additional transpilation but typescript can do some of this as well using --importHelpers and --noEmitHelpers.
The defaults are:
  • --importHelpers false => add tslib to the imports in your output file automatically
  • --noEmitHelpers false => do not emit helpers directly in the files, but reference them in a library called tslib
Let's see what we get from something easy:
/** Helper needs */

const x = {a:1, b:2, c:3}
const {b} = x

function doit({...rest}) {
    console.log(rest)
}
If we stick with esnext then typescript assumes the enviornment handles it:
// --target esnext
/** Helper needs */
const x = { a: 1, b: 2, c: 3 };
const { b } = x;
function doit({ ...rest }) {
    console.log(rest);
}
Its a passthrough.
We use es5 in the next few exmamples:
// --target es5 --noEmitHelpers false
var __rest = (this && this.__rest) || function (s, e) {
    var t = {};
    for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
        t[p] = s[p];
    if (s != null && typeof Object.getOwnPropertySymbols === "function")
        for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) if (e.indexOf(p[i]) < 0)
            t[p[i]] = s[p[i]];
    return t;
};
/** Helper needs */
var x = { a: 1, b: 2, c: 3 };
var b = x.b;
function doit(_a) {
    var rest = __rest(_a, []);
    console.log(rest);
}              
// --target es5 --noEmitHelpers true
/** Helper needs */
var x = { a: 1, b: 2, c: 3 };
var b = x.b;
function doit(_a) {
    var rest = __rest(_a, []);
    console.log(rest);
}
So we see that --noEmitHelpers true just issues the __rest method but does not inject the definition into the file, which is good for reducing code size. But this means that you will need to to include tslib (or tslib.es6) into your main entry point (or include it in a "script" tag) to ensure that __rest is defined. You can look at node_modules/tslib/*.js to see the definitions.
If you add --importHelpers then a require for "tslib" is added to the output:
// --target es5 --noEmitHelpers true --importHelpers
--noEmitHelpers true should be your default for large projects with multiple modules. Here's an example showing how much code gets inserted for a generator:
/** Advanced feature use. */
function *myGenerator() {
    yield 10
    yield "blah"
    yield "-1"
}
And we get:
// --target es5 --noEmitHelpers false
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [0, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
/** Advanced feature use. */
function myGenerator() {
    return __generator(this, function (_a) {
        switch (_a.label) {
            case 0: return [4 /*yield*/, 10];
            case 1:
                _a.sent();
                return [4 /*yield*/, "blah"];
            case 2:
                _a.sent();
                return [4 /*yield*/, "-1"];
            case 3:
                _a.sent();
                return [2 /*return*/];
        }
    });
vs
// --target es5 --noEmitHelpers true
//** Advanced feature use. */
function myGenerator() {
    return __generator(this, function (_a) {
        switch (_a.label) {
            case 0: return [4 /*yield*/, 10];
            case 1:
                _a.sent();
                return [4 /*yield*/, "blah"];
            case 2:
                _a.sent();
                return [4 /*yield*/, "-1"];
            case 3:
                _a.sent();
                return [2 /*return*/];
        }
    });
}
vs just using esnext
// --target esnext
/** Advanced feature use. */
function* myGenerator() {
    yield 10;
    yield "blah";
    yield "-1";
}
Of course with esnext we have to use babel to transpile it for our target environment:
// babel test4.js (the output of test4.ts compilation)
"use strict";

var _regenerator = require("babel-runtime/regenerator");

var _regenerator2 = _interopRequireDefault(_regenerator);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var _marked = /*#__PURE__*/_regenerator2.default.mark(myGenerator);

/** Advanced feature use. */
function myGenerator() {
    return _regenerator2.default.wrap(function myGenerator$(_context) {
        while (1) {
            switch (_context.prev = _context.next) {
                case 0:
                    _context.next = 2;
                    return 10;

                case 2:
                    _context.next = 4;
                    return "blah";

                case 4:
                    _context.next = 6;
                    return "-1";

                case 6:
                case "end":
                    return _context.stop();
            }
        }
    }, _marked, this);
}
So babel now sticks in all the polyfills using babel-runtime in this case as we have a .babelrc that says use the "library" mode instead of a global polyfill mode:
{
    "presets": [
        ["env", {
            "browsers": ["last 3 major Chrome versions"]
        }],
        "react",
 "flow"
    ],
    "plugins": [
        ["transform-runtime",
         {
             "useESModules": true
         }],
        "transform-object-rest-spread",
        "transform-class-properties"
    ]
}

Comments

  1. This post is so helpfull and informative.Keep updating with more information...
    AngularJS Software
    AngularJS Uses

    ReplyDelete

Post a Comment

Popular posts from this blog

quick note on scala.js, react hooks, monix, auth

zio environment and modules pattern: zio, scala.js, react, query management

user experience, scala.js, cats-effect, IO