Skip to main content

Sooner or later, every Javascript developer encounters the Module Pattern. It isn't always used for modules in the conventional sense, but even when it is used for modules, the files in which those modules reside are still often loaded by carefully hand-listing script-elements, in dependency order, in a HTML page.

/*!
 * Modules library
 *
 * @example
 * To trigger project loading in our HTML client page, all we need to do is to
 * write an inline module, listing the dependencies we would like to use, and
 * the code we would like to call, and our modules library will do the rest:
 *
 *     <html>
 *     <head>
 *     <script src="modules.js"></script>
 *     </head>
 *     <body>
 *     <script>
 *     module('client',['main.js','debug.js'], function(main, debug) {
 *         var result = main.doSomething();
 *         debug.log(result);
 *     });
 *     </script>
 *     </body>
 *     </html>
 *
 * @link https://libraryinstitute.wordpress.com/2010/12/01/loading-javascript-modules/
 */
(function () {
    //'use strict';

    var modules = {}; // private record of module data

    // modules are functions with additional information
    function module(name, imports, mod) {

        // record module information
        window.console.log('found module ' + name);
        modules[name] = {
            name: name,
            imports: imports,
            mod: mod
        };

        // trigger loading of import dependencies
        for (var imp in imports) {
            if (imports.hasOwnProperty(imp)) {
                loadModule(imports[imp]);
            }
        }

        // check whether this was the last module to be loaded
        // in a given dependency group
        loadedModule(name);
    }

    // trigger module loading by adding script element
    function loadModule(mod) {

        if (modules[mod]) {
            return; // don't load the same module twice
        } else {
            modules[mod] = {}; // mark module as currently loading
        }

        // add a script element to document head, with module as src
        var element = document.createElement('script');
        element.setAttribute('type', 'text/javascript');
        element.setAttribute('src', mod);
        document.getElementsByTagName('head')[0].appendChild(element);
    }

    // check whether this was the last module to be loaded
    // in a given dependency group;
    // if yes, start linking and running modules
    function loadedModule(mod) {
        window.console.log('finished loading: ' + mod);

        // collect modules marked as currently loading
        var pending = [];
        for (var m in modules) {
            if (modules.hasOwnProperty(m)) {
                if (!modules[m].name) {
                    pending.push(m);
                }
            }
        }

        // if no more modules need to be loaded, we can start
        // linking the modules together
        if (pending.length === 0) {
            window.console.log('all done loading');
            linkModules();
        } else {
            window.console.log('loads pending: ' + pending.join(', '));
        }
    }

    // Sort modules by dependencies (dependents last),
    // returning sorted list of module names.
    function dependencySort(modules) {

        var pending = [], // modules remaining to be sorted
            sorted = [], // modules already sorted
            beenHere = {}; // remember length of pending list for each module
        // (if we revisit a module without pending
        //  getting any shorter, we are stuck in a loop)

        // preparation: linked modules do not need to be sorted,
        //              all others go into pending
        for (var name in modules)
            if (modules[name].linked) {
                sorted.push(name); // allready linked by a previous run
            } else {
                pending.push(name); // sort for linking (after its dependencies)
            }

            // has mod been sorted already?
        function issorted(mod) {
            var result = false;
            for (var s in sorted) {
                if (sorted.hasOwnProperty(s)) {
                    result = result || (sorted[s] === mod);
                }
            }
            return result;
        }

        // have all dependencies deps been sorted already?
        function aresorted(deps) {
            var result = true;
            for (var d in deps) {
                if (deps.hasOwnProperty(d)) {
                    result = result && (issorted(deps[d]));
                }
            }
            return result;
        }

        // repeat while there are modules pending
        while (pending.length > 0) {

            // consider the next pending module
            var m = pending.shift();

            // if we've been here and have not made any progress, we are looping
            // (no support for cyclic module dependencies)
            if (beenHere[m] && beenHere[m] <= pending.length) {
                throw ('can\'t sort dependencies: ' + sorted + ' < ' + m + ' < ' + pending);
            } else {
                beenHere[m] = pending.length;
            }

            // consider the current module's import dependencies
            var deps = modules[m].imports;
            if (aresorted(deps))
                sorted.push(m); // dependencies done; module done
            else
                pending.push(m); // some dependencies still pending;
            // revisit module later
        }

        return sorted;
    }

    // link and run loaded modules, keep record of results
    function linkModules() {
        window.console.log('linking modules');

        // sort modules in dependency order
        var sortedNames = dependencySort(modules);

        // link modules in dependency order
        for (var nextName in sortedNames) {
            if (sortedNames.hasOwnProperty(nextName)) {
                var name = sortedNames[nextName];
                var module = modules[name];
                var imports = module.imports;

                if (module.linked) {
                    window.console.log('already linked ' + name);
                    continue;
                }
                window.console.log('linking module ' + name);

                // collect import dependencies
                var deps = [];
                for (var i in imports) {
                    if (imports.hasOwnProperty(i)) {
                        deps.push(modules[imports[i]].linked);
                    }
                }

                // execute module code, pass imports, record exports
                modules[name].linked = module.mod.apply(null, deps);
            }

        }
    }

    // export module wrapper
    window.module = module;

    // trigger module loading for our project
    // NOTE: we assume that loadModule calls are executed before the
    //       script elements they create; otherwise, we have a possible
    //       race condition (linkModules could be called early, because
    //       not all dependencies are marked as currently loading yet)
    // loadModule('utils.js');
    // loadModule('debug.js');
    // loadModule('module1.js');
    // loadModule('module2.js');
    // loadModule('main.js');

    // just calling linkModules here would not work, as we have only
    // added the script elements, the scripts could still be loading;

    // calling linkModules in document onload would not work
    // in browsers which do not stop parsing while script-inserted
    // external scripts are loading;

    // therefore, we call linkModules when all modules in a dependency
    // group have been loaded, as checked by loadedModule;

})();