Node.js and TypeScript Modules: Internal, External... Shake it All About

Well, that was painful.

When I decided to write a JavaScript game, I knew I'd need to run the same logic on the server and the browser, so Node.js was an obvious choice. It's JavaScript on the server, right? Anything I write to run in a browser will be runnable on the server as well. Easy! Well... not so much.

The issues I came across boiled down to the way each JavaScript environment expects code to be organised. TypeScript's abstractions actually made it more difficult for me to figure out, but I stuck at it Dear Reader, and now hopefully you can benefit from the hours I spent slamming my head in a drawer. Not literally.

An 'Internal' TypeScript Module

Let's say we write the following TypeScript to run in a browser:

module Here.Is.Johnny {
    export class Axe {
        public swing(): void { }
    }
}

That gets compiled to the following JavaScript:

var Here;
(function (Here) {
    var Is;
    (function (Is) {
        var Johnny;
        (function (Johnny) {
            var Axe = (function () {
                function Axe() { }
                Axe.prototype.swing = function () { };
                return Axe;
            })();
            Johnny.Axe = Axe;
        })(Johnny = Is.Johnny || (Is.Johnny = {}));
    })(Is = Here.Is || (Here.Is = {}));
})(Here || (Here = {}));

This series of nested self-invoking functions creates a Here object with an Is property of type object which has a Johnny property of type object which has a Axe property which points to the Axe constructor function. When we run this in a browser the Here object is created (if it doesn't already exist) and gets added to the browser's window object to act as the root of the 'namespace' we've created. Subsequent references to Here in other files will point to window.Here which points to our root namespace object and everything works as you'd expect. Marvellous!

Now, despite the fact that:

  1. We've exported the Axe class with the export keyword, and
  2. The namespace's root Here object is automatically accessible in other TypeScript files

...Here.Is.Johnny is known as an Internal Module. Why is it 'internal'? What is it 'internal' to? I don't know. Umm... someone help me out?

A CommonJS External Module

To have the same class run happily in Node you have to write a CommonJS module. TypeScript puts a bit of sugar around that, which means you write something like this:

module Here.Is.Johnny {
    export class Axe {
        public swing(): void { }
    }
}

export = Here;

...which compiles to the following:

var Here;
(function (Here) {
    var Is;
    (function (Is) {
        var Johnny;
        (function (Johnny) {
            var Axe = (function () {
                function Axe() { }
                Axe.prototype.swing = function () { };
                return Axe;
            })();
            Johnny.Axe = Axe;
        })(Johnny = Is.Johnny || (Is.Johnny = {}));
    })(Is = Here.Is || (Here.Is = {}));
})(Here || (Here = {}));
module.exports = Here;

...that is, the same as before but with the last line assigning Here to the exports property of the magical module object to tell Node that the value of that variable is what should be returned when this module is required by another. Compare what happens with this to the browser example above: when we run it via Node the Here object is created (if it doesn't already exist) and gets added to the Node execution context to act as the root of the namespace we've created. Subsequent references to Here in other files will point to... their own Here object, not this one. To reference this Here object other modules will have to require it or have it injected into them.

So to summarise:

  • A set of internal modules share the same execution context (provided by the browser) and have transparent access to all members exported from a namespace
  • A set of external modules each have their own execution context (provided in this case by Node) and have no access to any members exported from a namespace or module without explicitly require-ing that module.

Accessing Internal Modules in Node

And so to the promise of write-once, run-anywhere JavaScript - a promise I invented and made to myself after not really looking into it quite as thoroughly as perhaps I should have.

Looking at the JavaScript generated for the browser and for Node and the module formats used by each, it's not difficult to see why we can't easily use internal modules from Node external ones. Writing a new external TypeScript module for Node, we can reference and use the internal module defined above like this:

import Johnny = Here.Is.Johnny;

var movieProps = {
    axe: Johnny.Axe
}

export = movieProps;

...which TypeScript merrily compiles to:

var Johnny = Here.Is.Johnny;
var movieProps = {
    axe: Johnny.Axe
};
module.exports = movieProps;

Unfortunately this errors at runtime because the execution context in which Node runs that code has no Here variable on which to read the Is property; I guess TypeScript compiles it anyway because it hasn't figured that the mystical boundary between internal and external modules has been breached. The issue is, just because we can reference Here.Is.Johnny in our pretty little editor doesn't make anything actually evaluate the code in which those objects are defined and add them to Node's execution context. Ah.

To do that, we have to import the internal module:

import Here = require("./InternalModule");
var johnny = Here.Is.Johnny;

var movieProps = {
    bigKnife: johnny.Axe
}

export = movieProps;

...but this doesn't compile because InternalModule is... an internal module, and so can't be required.

So how do you access all your lovely internal modules in Node? Well... you can't. At least not without...

An Internal Module Converter

The solution I came up with after much head-scratching, coffee-drinking, false starts, raging at Visual Studio for intermittently deciding not to debug Node, and reading and re-reading this StackOverflow question and answer was to write a class (in an external module) to combine internal modules into a single external one and export the root namespace. To this end the class performs the following steps:

  1. Finds all the JavaScript files in the current working directory and sub-directories
  2. Creates an object to represent each of the internal modules defined in those files
  3. Orders those internal modules based on which modules are dependent on which
  4. Combines the modules' source code into a single JavaScript file, ordering them such that each class is defined after the classes on which it depends
  5. Removes all the duplicate root namespace object declarations
  6. Appends a module.exports = statement to the bottom of the file, turning it into a CommonJS module.

...so with my module converter added to the project build using Grunt I now have all my internal modules in a single external module source file. requireing that file in my Node application's start up gives me a reference to the root namespace object from which I can instantiate any of the classes I've defined - and presto! I can run and debug the same code I wrote for the browser in Node! Hurrah! Smile

So would I recommend this class I've written to anyone else with the same problem to solve? Well... I guess... but not really. I've kind of thrown it together and it has a few provisos and conventions by which it expects code to be written which might make it awkward to use. This is also the kind of problem I'd like to think would be obsolete in a future TypeScript version, but we'll see. I know you can use Asynchronous Module Definition on the browser and there's a Node adapter for RequireJS so that may have been another way to go, but I'd already written lots of internal modules by the time I got to this point on the assumption that executing the same JavaScript in the browser and on Node wouldn't be a big deal. Oh well, it was a good learning experience, and I can now press on Smile

Print | posted @ Sunday, January 25, 2015 3:36 PM

Comments on this entry:

Gravatar # re: Node.js and TypeScript Modules: Internal, External... Shake it All About
by codeBelt at 4/7/2015 1:48 AM

I also had trouble with external modules. Your readers might learn something from my examples.

http://www.codebelt.com/typescript/typescript-internal-and-external-modules/
Post A Comment
Title:
Name:
Email:
Comment:
Verification: