ImportJSv3.0

Configuration-free, modular JavaScript.

What is ImportJS?

ImportJS is a JavaScript library that allows you to write modular JavaScript with little to no configuration. It works both in a Node.js environment as well as the browser, and is reminiscent of how one might write import statements in a language like ActionScript or Java. It can easily handle things like circular dependencies, and comes with a built-in code preloader and plugin system for easy extensibility.

How does it work?

ImportJS works under the principle of "load everything first, execute later". When you write an application for the desktop, all of the code is immediately available to the end-user. But on the web there is latency in the time it takes for all of your scripts to download, so you can run into problems with code execution order. ImportJS attempts to solve this problem by providing a very simple mechanism for delaying the execution of your code until you're certain everything has been loaded. Let's take a look at an example, shall we?

Create some modules:

        //Main.js
        ImportJS.pack('Main', function (module, exports)  {
          //Dependencies
          var Foo = this.import('utils.Foo');

          //Code
          var Main = function () {};
          Main.prototype.toString = function () {
            return 'Hello world! I know about Foo: ' + Foo.value;
          };

          //Expose the Main 'class'
          module.exports = Main;
        });

        //utils/Foo.js
        ImportJS.pack('utils.Foo', function (module, exports)  {
          module.exports = { value: 'bar' };
        });
        

This might look familiar to Node.js developers, the module and exports variables work exactly as you'd expect. The module argument is an Object that contains an empty 'exports' Object that you can use to expose methods and properties to the outside world. The exports argument acts a shortcut to this 'module.exports' object, however you can override the exports object entirely like in the above example if you desire. The exports object of a module is then retrieved via the import() call.

Finally, you can initialize your code as follows:

        ImportJS.compile(); //<-Assuming Main.js and Foo.js are already loaded
        var Main = ImportJS.unpack('Main'); //Global version of this.import()
        var app = new Main();
        //Prints "Hello world! I know about Foo: bar" to your browser console
        console.log(app.toString());
        

As long as the previous code has been loaded, you can write this anywhere and it will simply work. This gives you the flexibility to use whatever script loading mechanisms you want, though the built-in pre-loader for ImportJS is pretty convenient on its own:

        ImportJS.preload({
          baseUrl: 'js/',
          packages: ['Main.js'],
          autoCompile: true, 
          entryPoint: 'Main:new'
        });
        

That one single function preloads Main.js, automatically discovers the Foo.js dependency by running a regular expression on its source code, loads Foo after reverse-domain naming its module name for its file path, and declares a "new Main()" instance to act as the application's entry point once everything has been loaded. You can alternatively specify all the dependencies manually if you wanted, or even simply concatenate it all together into one build.js file. As long as the code is loaded, you choose how you want to handle the rest.

The reason ImportJS can load files this way is due to the fact that it does not execute the wrapped code within loaded modules until you tell it to. The thought process behind this is that in most cases, the typical JS developer wants to make a single self-contained application whether files are loaded asynchronously or bundled into one package. The mechanisms ImportJS provides are more akin to how a traditional desktop application might work as one package, but offer more flexibility in where modules are stored in your file hierarchy. As such, you could skip the preload() entirely by simply concatenating all of your module files together in one file and calling compile(). This comes with the added benefit of being able to use the same identifiers for modules regardless of file path, since all module names are simple key-value lookups (except in the place of plug-ins which is described further down)

Installation & Usage

ImportJS can work in the browser or a Node.js environment. Its behavior is mainly the same regardless, however Node.js will only load code synchronously.

For use in Node.js:

 $ npm install importjs
        //Recommended to assign to a global to save a line of code across other files
        var ImportJS = global.ImportJS = require('importjs');
        

For use in the browser:

Download either the development or minified version of the code from the project's GitHub page.

The browser version will make "ImportJS" a global variable that you can access from anywhere.

ImportJS in action:

Define modules with the pack() function:

        ImportJS.pack('my.module.name', function (module, exports) {
          //Your code here

          //These are equivalents
          exports.foo = 'bar';
          module.exports.foo = 'bar';
        });
        

Pull modules into outer scope via the unpack() function:

        var MyModuleName = ImportJS.unpack('my.module.name');
        

Import and inject modules internally via the import() and inject() functions:

        ImportJS.pack('my.module.name', function (module, exports) {
          var NeedNow = this.import('module.i.need.right.now');
          var NeedEventually;
          this.inject(function () {
            //Gets called once all JS has been preloaded
            NeedEventually = this.import('module.i.need.eventually');
          });

          //Code here
        });
        

As you can see in the above example, the function wrapper exposes these special functions on its this reference. Although this.import() might look like a reserved keyword, it's actually just a method attached to the function in this case. Generally there is no need to interact with a function in this way, so ImportJS takes advantage of it to make the code more concise.

Lastly, the this.inject() function offers an easy mechanism to hoist up dependencies only once the application has been fully loaded. In many other JS loaders this is typically a tricky thing to deal with since they don't handle this by default. Circular dependencies are not best practice, however if you run into a situation where you need them then ImportJS makes it easy!

The Reverse Domain Naming System

This optional naming approach is supported by ImportJS and offers a potential way to organize the file structure of your modules. This is a concept that should be familiar to Flash and Java developers. The format looks something like this:

        App.js = com.myproject.core.App
        Helpers.js = com.myproject.util.Helpers
        

In the above example, you would store App.js under the folder path com/myproject/core/ relative the base folder for your scripts. The Helpers.js file would go under com/myproject/util/. When you load your project via ImportJS's preload() function, you get a couple of extra features:

        ImportJS.preload({
          baseUrl: 'js/',
          strict: true,
          parse: false, //<-Optional, to disable the regex parsing
          packages: ['com/myproject/core/App.js', 'com/myproject/util/Helpers.js'],
          /* OR */
          packages: {
            com: {
              myproject: {
                core: {
                  "App": "App.js"
                },
                util: {
                  "Helpers": "Helpers.js"
                }
              }
            }
          }
        });
        

There are a couple of things happening above. First, is the use of the strict parameter. Enabling this will have ImportJS enforce modules to follow the reverse domain naming convention each time a file is loaded. This means if "App.js" were named "SomethingElse.js", ImportJS would throw an error. Next, if you would like to use a different file name for your module you can explicitly state it by providing an Object to the "packages" parameter describing the module hierarchy. Each module is a key value pair, where the key specifies the module name and the value specifies the file name.

This offers an alternative way to use ImportJS if you would like to enforce such a feature. Otherwise "strict" is set to false by default, and you can rely on ImportJS's regex parser to pick up dependencies for you so that you don't have to specify them all explicitly. Keep in mind that without "strict", you would be responsible for ensuring your module names are consistent with your files on your own.

Plugin System

Version 3.0 includes a newly added plugin system, which allows you to write extensible libraries for inclusion in ImportJS projects. Below is a simple example that creates a plugin and includes it in a project.

        /* js/plugins/myplugin/myplugin.js */
        ImportJS.pack('myplugin', function (module, exports) {
          //Any imports in here are relative to the myplugin/ folder

          module.exports = { 
            doSomething: function () { 
              console.log("This is my plugin."); 
            }
          };
        });

        /* js/main.js */
        ImportJS.pack('main', function (module, exports) {
          var myplugin = this.plugin('myplugin');

          exports.run = function () {
            myplugin.doSomething();
          };
        });

        /* js/initialize.js */
        //The plugin will be detected automatically via regex
        ImportJS.preload({ 
          baseUrl: 'js/',
          packages: ['main.js'],
          entryPoint: 'main:run'
        });
        

In order to use the plugin system you must load your library via the preload() function and allow function source parsing in order to pick up the dependencies (enabled by default). This will allow you to load plugins that have their own file hierarchy under a relative "plugins/" folder that is independent of your project. The naming convention goes "plugins/pluginName/pluginName.js". Plugins can have potentially infinite depth, so you can build out modules that have their own versions of specific dependencies. Node.js devs can think of this as a "node_modules" folder for the browser!

Want more info?

Check out this article by the project's author for details on the motivation behind ImportJS.

Also be sure to check out ImportJS's cousin library AS3JS, which is a transpiler that converts ActionScript 3.0 to JavaScript in a similar format as ImportJS.

And finally, it's open source! So be sure to visit the project's Github page.