Another post that’s sat in draft for a while – native JavaScript module implementations are now supported in all modern browsers, but I think it’s still interesting to look at the variations in module systems.
JavaScript doesn’t (as such) come with a built in module system. Originally it was primarily used for script elements within an HTML document (inline scripting or even scripts per file) not a full blown framework/application as we see nowadays with the likes of React, Angular, Vue etc.
The concept of modules was introduced as a means to enable developers to separate their code into separate files and this ofcourse aids in reuse of code, maintainability etc. This is not all that modules offer. In the early days of JavaScript you could store your scripts in separate files but these would then end up in the global namespace which is not ideal, especially if you start sharing your scripts with others (or using other’s scripts), then there’s the potential of name collisions, i.e. more than one function with the same name in the global namespace.
If you come from a language such as C#, Java or the likes then you may find yourself being slightly surprised that this is a big deal in JavaScript, after all, these languages (and languages older than JavaScript) seemed to have solved this problems already. This is simply the way it was with JavaScript because of it’s original scripting background.
What becomes more confusing is that there isn’t a single solution to modules and how they should work, several groups have created there own versions of modules over the life of JavaScript.
Supported by TypeScript
This post is not a history of JavaScript modules so we’re not going to cover every system every created but instead primarily concentrate on those supported by the TypeScript transpiler, simply because this where my interest in the different modules came from.
TypeScript supports the following module types, “none”, “commonjs”, “amd”, “system”, “umd”, “es2015” and “ESNext”. These are the one’s that you can set in the tsconfig.json’s compilerOptions, module.
To save me looking at writing my own examples of the JavaScript code, we’ll simply let the TypeScript transpiler generate code for us and look at how that looks/works.
Here’s a simple TypeScript (Demo.ts) file with some code (for simplicity we’re not going to worry about lint rules such as one class per file or the likes).
export class MyClass {
}
export default class MyDefaultClass {
}
Now my tsconfig.json looks like this
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"strict": true
},
"include": ["Demo.ts"]
}
And I’ll simply change the “module” to each of the support module kinds and we’ll look at the resultant JavaScript source. Alternatively we can use this index.js
var ts = require('typescript');
let code = 'export class MyClass {' +
'}' +
'export default class MyDefaultClass {' +
'}';
let result = ts.transpile(code, { module: ts.ModuleKind.CommonJS});
console.log(result);
and change the ModuleKind the review the console output. Either way we should get a chance to look at what style of output we get.
Module CommonJS
Setting tsconfig module to CommonJS results in the following JavaScript being generated. The Object.defineProperty simple assigns the property __esModule to the exports. Other than this the class definitions are very similar to our original code. The main difference is in how the exports are created.
The __esModule is set to true to let “importing” modules know that this code is is a transpiled ES module. It appears this matters specifically with regards to default exports. See module.ts
The exports are made directly to the exports map and available via the name/key. For example exports[‘default’] would supply the MyDefaultClass, like wise exports[‘MyClass’] would return the MyClass type.
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class MyClass {
}
exports.MyClass = MyClass;
class MyDefaultClass {
}
exports.default = MyDefaultClass;
We can import the exports using require, for example
var demo = require('./DemoCommon');
var o = new demo.MyClass();
Module AMD
The Asynchronous Module Definition was designed to allow the module to be asynchronously loaded. CommonJS is a synchronous module style and hence when it’s being loaded the application will be blocked/halted. With the AMD module style expects the define function which passes arguments into a callback function.
As you can see the code within the define function is basically our CommonJS file wrapped in the callback.
define(["require", "exports"], function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class MyClass {
}
exports.MyClass = MyClass;
class MyDefaultClass {
}
exports.default = MyDefaultClass;
});
To import/require AMD modules we need to use requirejs, for example assuming we created a file from the above generated code named DemoAmd.js, then we can access the module using
var requirejs = require('requirejs');
requirejs.config({
baseUrl: __dirname,
nodeRequire: require
});
var demo = requirejs('./DemoAmd');
var o = new demo.MyClass();
Module System
System.register([], function (exports_1, context_1) {
"use strict";
var MyClass, MyDefaultClass;
var __moduleName = context_1 && context_1.id;
return {
setters: [],
execute: function () {
MyClass = class MyClass {
};
exports_1("MyClass", MyClass);
MyDefaultClass = class MyDefaultClass {
};
exports_1("default", MyDefaultClass);
}
};
});
Module UMD
UMD or Universal Module Definition is a module style which can be used in place of CommonJS or AMD in that it uses if/else to generate modules in either CommonJS or AMD style. This means you can write your modules in a way that can be imported into a system expecting either CommonJS or AMD.
(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 });
class MyClass {
}
exports.MyClass = MyClass;
class MyDefaultClass {
}
exports.default = MyDefaultClass;
});
We can import UMD modules using either require (as shown in our CommonJS code) or requirejs (as shown in our AMD code) as UMD modules represent modules in both styles.
Module es2015 and ESNext
As you might expect as the TypeScript we’ve written is pretty standard for ES2015 the resultant code looks identical.
export class MyClass {
}
export default class MyDefaultClass {
}
Module None
I’ve left this one until last as it needs further investigation as module None suggests that code is created which is not part of the module system, however the code below works quite happily with require form of import. This is possibly a lack of understanding of it’s use by me. It’s included for completeness.
"use strict";
exports.__esModule = true;
var MyClass = /** @class */ (function () {
function MyClass() {
}
return MyClass;
}());
exports.MyClass = MyClass;
var MyDefaultClass = /** @class */ (function () {
function MyDefaultClass() {
}
return MyDefaultClass;
}());
exports["default"] = MyDefaultClass;
Inspecting modules
function printModule(m, indent = 0) {
let indents = ' '.repeat(indent);
console.log(`${indents}Filename: ${m.filename}`);
console.log(`${indents}Id: ${m.id}`);
let hasParent = m.parent !== undefined && m.parent !== null;
console.log(`${indents}HasParent: ${hasParent}`);
console.log(`${indents}Loaded: ${m.loaded}`);
if(m.export !== []) {
console.log(`${indents}Exports`);
for(const e of Object.keys(m.exports)) {
console.log(`${indents} ${e}`);
}
}
if(m.paths !== []) {
console.log(`${indents}Paths`);
for(const p of m.paths) {
console.log(`${indents} ${p}`);
}
}
if(m.children !== []) {
console.log(`${indents}Children`);
for (const child of m.children) {
printModule(child, indent + 3);
}
}
}
console.log(Module._nodeModulePaths(Path.dirname('')));
printModule(module);