Skip to content

Instantly share code, notes, and snippets.

@bradharms
Last active September 6, 2016 04:06
Show Gist options
  • Save bradharms/fbded076f53f532df95cc5f149264350 to your computer and use it in GitHub Desktop.
Save bradharms/fbded076f53f532df95cc5f149264350 to your computer and use it in GitHub Desktop.
An asynchronous module dependency manager that is loosely based on the AMD api. Requires an ES6 compatible browser.
/* jslint esversion: 6 */
/**
* # Module Manager
*
* An asynchronous module dependency manager that is loosely based on the AMD
* api.
*
* Requires an ES6 compatible browser.
*
* ## Usage Example:
*
* ```html
* <!-- index.html -->
* <head>
* <!-- Ex: Install using "/js/main.js" as the entry point -->
* <script
* src="/js/module-manager.js"
* data-main="/js/main.js"
* ></script>
* </head>
* ```
*
* ```js
* // /js/main.js
*
* define([
*
* 'console',
*
* ], (
* console
* ) => {
*
* console.log("Hello, world!");
*
* });
* ```
*
* ## Defining Modules
*
* Modules are defined using the global `define()` function that is only
* available from modules that are loaded by the module manager:
*
* ```js
* define(name, dependencyNames, factory)
* define(dependencyNames, factory)
* define(name, factory)
* define(factory)
* ```
*
* When a module doesn't define a name, the name will be determined from
* the value of the `src` attribute of the current `<script>` element. (See the
* `data-prefix` and `data-suffix` options below.)
*
* When a module omits the dependency list, it is assumed not to have any
* dependencies. (This is the same as an empty dependency list.)
*
* All modules must provide a factory function. This will be called exactly
* once for the whole application's execution when the module is first required.
* It will be supplied arguments positionally, with each one corresponding to
* the exports of the modules declared in the dependency names list. The names
* of the factory function's parameters not not significant.
*
* ## Loading Modules
*
* The module manager will initially load one module that is named using the
* `data-main` attribute. This is considered to be the application's main
* point of entry.
*
* From here, the module manager will begin loading module dependencies.
* When all of a module's dependencies have been loaded, the depending module
* will be initialized with all of the dependency module's exports
*
* There are no special requirements for the main module, but since it is the
* first to be loaded it cannot be directly declared as a dependency by other
* modules without creating a circular dependency. Therefore it is not important
* to export anything, and the name of the module is insignificant and may
* be omitted.
*
* ## Options
*
* Options to the module manager are passed as HTML5 data attributes. The
* available options are listed below.
*
* - `data-main` - Path to the source file for the entry point. This will be
* loaded first to bootstrap the application.
* - `data-prefix` - When given, this will be prefixed to the name of all
* modules to determine their source file paths. This means that portion can
* be omitted in the dependency lists and in the `data-main` option.
* For modules that omit their name from their definition, this subtring
* will be removed from the beginning of the file path to get the module's
* defined name.
* - `data-suffix` - Works the same way as `data-prefix`, but at the end of
* path instead of the beginning.
* - `data-timeout` - A time, given in miliseconds, to wait for a module to
* become available after it has been required by another module that is
* being loaded. If the module has not been defined in this time it is
* assumed to be unloadable and an error will be reported. The default
* is 10000 (10 seconds).
* - `data-debug` - If any value is provided to this option then "debug" mode
* will be enabled. This causes the module manager to verbosely report
* everything that it's doing into the browser console.
*
* ## Built-In Modules
*
* A number of built-in modules are defined and can be declared as dependencies
* of other modules, including the entry point.
*
* - `window` - The browser's global `window` object.
* - `debug` - The function used internally to report debug output. When in
* debug mode, this will output to the console. Otherwise it does nothing.
* - `require-async` - The asynchronous require function used to load modules.
* Returns an ES6-compatible Promise object that will resolve to the
* required module, or reject with an Error if the module fails to load
* within the configured timeout. This can be used to load modules that
* were not stated as dependencies in the module's definition.
*
* ## TODO
*
* - Detect circular dependencies and prevent them from locking the browser
*/
(({
window,
console,
Object,
Promise,
setTimeout,
clearTimeout,
}) => {
const document = window.document;
const script = document.currentScript;
const assign = Object.assign;
const modules = {};
let srcPrefix;
let srcSuffix;
let timeoutMs;
let mainName;
let debug;
function init() {
debug = (!!script.dataset.debug) ?
((...args) => console.log(...args)) : (() => {});
srcPrefix = script.dataset.srcPrefix || '';
srcSuffix = script.dataset.srcSuffix || '';
timeoutMs = parseInt(script.dataset.timeout || 10000);
mainName = script.dataset.main;
define('window', () => window);
define('debug', () => debug);
define('require-async', () => requireAsync);
requireAsync(mainName);
}
function define(...args) {
let name = (args[0] instanceof String || typeof args[0] == 'string') ?
args[0] : null;
if (!name) {
name = document.currentScript.getAttribute('src');
name = name.startsWith(srcPrefix) ?
name.substr(srcPrefix.length) : name;
name = name.endsWith(srcSuffix) ?
name.substr(0, name.length - srcSuffix.length) : name;
}
let depNames = (args[0] instanceof Array) ? args[0].slice() :
(args[1] instanceof Array) ? args[1] :
[];
let fn = (args[0] instanceof Function) ? args[0] :
(args[1] instanceof Function) ? args[1] :
(args[2] instanceof Function) ? args[2] :
(() => null);
if (modules[name] && modules[name].fn !== undefined) {
throw new Error(`${name}: Module already defined`);
} else {
modules[name] =
assign(modules[name] || { r: [] }, { depNames, fn });
debug(`${name}: Defined.`);
if (modules[name].r && modules[name].r.length) requireAsync(name);
}
}
function requireAsync(name) {
return (
!modules[name] || (
modules[name].script === undefined &&
modules[name].fn === undefined
)
) ?
new Promise((resolve, reject) => {
debug(`${name}: Downloading...`);
const src = `${srcPrefix}${name}${srcSuffix}`;
const timeout =
setTimeout(() => reject(`${name}: Timed out`), timeoutMs);
window.define = define;
modules[name] = modules[name] || { r: [] };
modules[name].r.push({ resolve, reject, timeout, name });
modules[name].script =
assign(document.createElement('script'), { src });
document.getElementsByTagName('body')[0]
.appendChild(modules[name].script);
})
: (modules[name].exports === undefined) ? (
debug(`${name}: Loading ${modules[name].depNames.length} dependencies...`),
Promise
.all(modules[name].depNames.map(depName => requireAsync(depName)))
.then(depModules => {
debug(`${name}: Initializing with`, depModules);
try {
modules[name].exports = modules[name].fn(...depModules);
modules[name].r.slice()
.forEach(({ resolve, timeout }, i) => {
resolve(modules[name].exports);
clearTimeout(timeout);
modules[name].r.splice(i, 1);
});
debug(`${name}: Loaded.`);
return modules[name].exports;
} catch (e) {
modules[name].r.slice()
.forEach(({ reject, timeout }, i) => {
reject(new Error('Dependency failed to load'));
clearTimeout(timeout);
modules[name].r.splice(i, 1);
});
throw e;
}
})
) : (
debug(`${name}: Already loaded.`),
Promise.resolve(modules[name].exports)
);
}
window.addEventListener('load', init);
})({
window,
console,
Object,
Promise,
setTimeout,
clearTimeout,
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment