Last active
September 6, 2016 04:06
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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