Created
February 13, 2011 02:11
-
-
Save datchley/824352 to your computer and use it in GitHub Desktop.
A Javascript Module facility, similar to Perl's Modules/Exporter (and JSAN)
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
// Copyright @2011, David Atchley (david.m.atchley@gmail.com) | |
/** | |
* @fileOverview | |
* <p> | |
* JSM stands for JavaScript Module, and is based on Perl's module and exporter | |
* facilities. Combines a number of those ideas from Perl and from the existing | |
* JSAN facility, though with some slight changes. | |
* </p> | |
* | |
* <p> | |
* Provides a Package/Module management facility similar to Perl's Exporter | |
* style modules for JavaScript. Javascript modules can be created by including | |
* a simple Package = {...} declaration at the top of a javascript source file. | |
* The Package declaration defines information about the module, specifically: | |
* </p> | |
* | |
<code><pre> | |
Package = { | |
NAME: the Module's name | |
VERSION: the version of the module | |
DEFAULT: the default symbols available via Module name, e.g. Module.<symbol> | |
EXPORT: a set of default symbols to export to caller's namespace | |
(no Module prefix qualitifications) | |
EXPORT_OK: a list of symbols allowed to exported on request | |
EXPORT_TAGS: names that represent useful sets of symbols to export | |
} | |
</pre></code> | |
* | |
* <p>The module can then be used by importing it using this JSM singleton. There are | |
* various ways to import,</p> | |
* | |
<code><pre> | |
JSM.use('Foo'); // import module Foo | |
JSM.use('Foo', 1.0) // import module Foo, if version is >= 1.0 | |
// Each of the above creates a Foo object (namespace) that has the | |
// methods that Foo makes available (via DEFAULT). | |
JSM.use('Foo', [ 'bar', 'getters:', /^foo/ ]); | |
// This call loads the module Foo, which still makes all the default | |
// symbols available in the Foo namespace, but additionally exports | |
// the symbol 'bar', all symbols that are part of the 'getters' tag | |
// and all symbols matching the RegExp, e.g. those starting with 'foo' | |
</code></pre> | |
* | |
* <p> | |
* The Javascript file used a module can contain any style of coding and might be | |
* a useful set of functions (function library) or a set of classes (class library); | |
* but represents a reusable, modular set of functionality. The only requirement to | |
* be a Javascript Module is that it have the Package declaration at the top. | |
* </p> | |
* | |
* @author david.m.atchley@gmail.com, (Dave Atchley) | |
* @version 0.1 | |
*/ | |
/** | |
* <p> | |
* The JavaScript Module (JSM) class is a singleton allowing the | |
* management, loading and inclusion of JSM modules. JSM is designed | |
* in a similar fashion to Perl's Module and Exporter facilities. | |
* JSM will import modules, which can be any javascript file that | |
* includes a valid <b>Package</b> header. See the file overview | |
* for information on the Package declaration.</p> | |
* | |
* <p>The class can be used as follows:</p> | |
* | |
* | |
<code><pre> | |
// Import the module and it's default symbols | |
JSM.use('Math.Complex'); | |
// Import the module, but only if it's version is >= 2.0 | |
JSM.use('Math.Complex', 2.0); | |
// Import the module, request specific symbols exported to | |
// calling namespace (defaults still available via Module) | |
JSM.use('Math.Complex', [ 'add', 'mult']); | |
</pre></code> | |
* | |
* @class | |
*/ | |
var JSM = (function() { | |
/** @lends JSM */ | |
/** @private version of JSM being run */ | |
var version = 0.1, | |
/** @private The include path used by JSM to find modules, can be set with {@link setIncludePath} */ | |
inc = ['.', 'lib/js'], | |
/** @private timeout value (milliseconds) for waiting for modules to load */ | |
poll_timeout = 200, | |
/** @private number of attempts to try loading a given script url */ | |
tries = 3, | |
attempted = 3, // track attempts here | |
/** @private global namespace for exporting, can be set with {@link setGlobalScope} */ | |
globalcontext = self, | |
/** @private keep track of loaded files */ | |
loaded = []; | |
/** | |
* <p>Check to see if the XHR object has loaded the given url | |
* and whether we have exceeded our timeout thresh hold {@link poll_timeout}. | |
* If timeout hasn't been exceeded, reset the poll and continue. | |
* Otherwise throw an exception.</p> | |
* | |
* <p>JSM will poll {@link tries} times (in {@link poll_timeout} intervals) | |
* to see if the module is loaded. After that it bails.</p> | |
* | |
* @function | |
* @param {Object} xhrobj the XMLHTTPRequest object being used | |
* @param {String} url the url of the file it's attempting to load | |
*/ | |
var poll = function(url) { | |
setTimeout(function() { | |
attempted--; | |
if (loaded.indexOf(url) != -1) { | |
// NOOP: Loaded successfully, module script is in {@link loaded} | |
} | |
else if (attempted > 0) { | |
// Continue: keep trying ... | |
poll(url); | |
} | |
else { | |
// We've timed out looking/loading script, reset attempts and error out | |
attempted = tries; | |
throw new Error("JSM: (XHR): loading of " + url + " timed out"); | |
} | |
}, poll_timeout); | |
} | |
/** | |
* Will attempt to load the given url via XHR (synchronously). | |
* If the the requested javascript file is not found, it | |
* returns null. Otherwise it returns the javascript code as | |
* a string. | |
* <p>This is a synchronous XMLHTTPRequest call that uses polling | |
* to check on the state of things, throwing an exception if the | |
* {@link poll_timeout} value is reached.</p> | |
* | |
* @function | |
* @param {String} url the url of the javascript module to load | |
* @returns {String|null} the javascript contained in the file | |
* or null if not found. | |
*/ | |
var loadJSFromURL = function(url) { | |
if (typeof XMLHttpRequest != 'undefined') { | |
var ajax = new XMLHttpRequest(); | |
} | |
else { | |
var ajax = new ActiveXObject("Microsoft.XMLHTTP"); | |
} | |
ajax.open("GET", url, false); | |
try { | |
ajax.send(null); | |
var stat = ajax.status; | |
if (stat == 200 || stat == 304 || stat == 0 || stat == null) { | |
var responseText = ajax.responseText; | |
return responseText; | |
} | |
} | |
catch (e) { | |
// throw new Error("File not found: " + url); | |
return null; | |
} | |
// throw new Error("File not found: " + url); | |
return null; | |
} | |
/** | |
* Given a DEFAULT defined in the package header for a | |
* module, returns the list of default symbols to import | |
* for the module being processed. | |
* | |
* @function | |
* @param {Object} pkghdr package haeader from the module | |
* @returns {Array} list of default symbols to import | |
*/ | |
var getDefaultImportList = function(pkghdr) { | |
var DEFAULTS = pkghdr['DEFAULT'] || []; | |
var default_list = []; | |
// The DEFAULT is the always available interface | |
// provided by the module. | |
for (var i = 0; i < DEFAULTS.length; i++) { | |
default_list.push(DEFAULTS[i]); | |
} | |
return default_list; | |
} | |
/** | |
* Given the EXPORT* definitions in a package header, returns | |
* the available symbols for export for the module being processed. | |
* @function | |
* @param {Object} pkghdr package header from the module | |
* @param {Array} symbols an array of requested symbols to import | |
*/ | |
var getExportList = function(pkghdr, symbols) { | |
var EXPORTS = pkghdr['EXPORT'] || []; | |
var EXPORT_OK = pkghdr['EXPORT_OK'] || []; | |
var EXPORT_TAGS = pkghdr['EXPORT_TAGS'] || {}; | |
var ALL_EXPORTS = EXPORTS.concat(EXPORT_OK); | |
var export_list = []; | |
if (!symbols || symbols.length == 0) { | |
for (var i = 0; i < EXPORTS.length; i++) { | |
var symbol = EXPORTS[i]; | |
export_list.push(symbol); | |
} | |
} | |
else { | |
for (var i = 0; i < symbols.length; i++) { | |
var symbol = symbols[i]; | |
// TAG: add all symbols from the matching tag [ex., 'all:'] | |
if (!(symbol instanceof RegExp) && symbol.match(/.*:$/)) { | |
var symbol = symbol.replace(':',''); | |
for (tag in EXPORT_TAGS) { | |
if (tag == symbol) { | |
EXPORT_TAGS[tag].forEach(function(v,i) { | |
export_list.push(v); | |
}); | |
} | |
} | |
continue; | |
} | |
// REGEXP: add any symbols that match [ex., /^get/ ] | |
else if (symbol instanceof RegExp) { | |
ALL_EXPORTS.forEach(function(v,i) { | |
if (v.match(symbol)) { | |
export_list.push(v); | |
} | |
}); | |
continue; | |
} | |
// SYMBOL: add all matching symbols [ex., 'getTimer'] | |
else { | |
ALL_EXPORTS.forEach(function(v,i) { | |
if (v == symbol) { | |
export_list.push(v); | |
} | |
}); | |
continue; | |
} | |
} | |
} | |
return export_list; | |
} | |
/** | |
* <p>Import the package requested. Optionally, a specific | |
* version can be requested and specific symbols (ONLY) | |
* can also be requested for export.</p> | |
* | |
* <p>Calls {@link loadJSFromURL} to load the requested package.</p> | |
* | |
* @function | |
* @param {String} pkgname package name of the module to import | |
* @param {Number} [version] a specific version number of the module | |
* @param {Array} [symbols] a specific list of symbols to export | |
*/ | |
var import = function(pkgname, version, symbols) { | |
var scriptUrl = pkgname.replace('.','/','gi') + '.js'; | |
if (loaded.indexOf(scriptUrl) != -1) { | |
throw new Error("Module " + pkgname + " already loaded, skipping."); | |
} | |
// Try loading the script via each include path | |
for (var i = 0; i < inc.length; i++) { | |
var js, | |
url = inc[i] + '/' + scriptUrl; | |
try { | |
poll(scriptUrl); // Poll for the package being loaded | |
js = loadJSFromURL(url); | |
} | |
catch (e) { | |
if (i == (inc.length - 1)) { | |
// Only rethrow the error if we've depelted our include path search | |
throw e; | |
} | |
} | |
if (js) { | |
var ns = createNamespace(pkgname); | |
(function() { | |
// Eval the returned javascript making it available in local scope | |
eval(js); | |
// If we have the right Module and Version, continue building namespace | |
if (typeof Package != 'undefined' && Package['NAME'] == pkgname) { | |
if (version && Package['VERSION'] && Package['VERSION'] < version) { | |
throw new Error("module " + Package['NAME'] + '(' + Package['VERSION'] + "), requested version " + version + " or higher"); | |
} | |
// Modules should ALWAYS define some kind of interface in DEFAULT | |
if (!Package['DEFAULT'] || Package['DEFAULT'].length == 0) { | |
throw new Error("module " + Package['NAME'] + " does not have an available interface"); | |
} | |
// Track valid modules so we don't reload later | |
loaded.push(scriptUrl); | |
// Add Package info to our locally built namespace | |
ns['Package'] = Package; | |
// | |
// Import Default Namespace | |
// | |
var default_import_list = getDefaultImportList(Package); | |
default_import_list.forEach(function(v,i) { | |
if (typeof eval(v) != 'undefined') { | |
console.log("\t importing: " + v); | |
ns[v] = eval(v); // accessible via Module name qualifier | |
} | |
}); | |
// | |
// Export to Namespace | |
// | |
// Handle any symbols requested for 'export' | |
var export_symbols_list = getExportList(Package, symbols); | |
// Requested symbols are exported to caller's scope | |
export_symbols_list.forEach(function(v,i) { | |
if (typeof eval(v) != 'undefined') { | |
console.log("\t exporting: " + v); | |
globalcontext[v] = ns[v] = eval(v); // export to global scope here | |
} | |
}); | |
} | |
else { | |
throw new Error('Invalid JSM Package format in ' + scriptUrl); | |
} | |
})(); | |
return ns; | |
} | |
} | |
// I find your lack of script disturbing.... | |
throw new Error("Couldn't find script in JSM.inc = [" + inc + "]"); | |
} | |
/** | |
* Create a new namespace based on a package name of a module. | |
* @function | |
* @param {String} pkgname the name of the package containing the module | |
* @returns {Object} the new namespace to contain the module's symbols | |
*/ | |
var createNamespace = function(pkgname) { | |
var parts = name.split('.'); | |
var container = {}; | |
for (var i = 0; i < parts.length; i++) { | |
if (!container[parts[i]]) { | |
container[parts[i]] = {}; | |
} | |
else if (typeof container[parts[i]] != 'object') { | |
var n = parts.slice(0,i).join('.'); | |
throw new Error(n + " module already exists and is not an object"); | |
} | |
container = container[parts[i]]; | |
} | |
// Namespace is created in the global context | |
globalcontext[pkgname] = container; | |
return container; | |
} | |
return { | |
/** | |
* Add a new path to the JSM include path list | |
* @function | |
* @param {String|String[]} | |
*/ | |
addIncludePath: function(/* single path or array */) { | |
var paths = [].slice.apply(arguments, 0); | |
inc.concat(paths); | |
}, | |
/** | |
* Return the JSM include path list used to find modules | |
* @function | |
* @returns {Array} the JSM include path | |
*/ | |
getIncludePath: function() { | |
return inc; | |
}, | |
/** | |
* Set the global scope for exporting symbols. Call this | |
* prior to calling {@link JSM.use} | |
* @function | |
* @param {Object} context object to use when exporting symbols globally | |
*/ | |
setGlobalScope: function(context) { | |
if (typeof context !== 'undefined') | |
throw new TypeError; | |
globalcontext = context; | |
}, | |
/** | |
* Dynamically load and import a module from a javascript package. | |
* @function | |
* @param {String} pkgname the name of the module to import | |
* @param {Number} [version] require a specific version of this module | |
* @param {Array} [symbols] any specific symbols to export from module | |
* | |
* @returns {Object} reference to the local namespace for module | |
*/ | |
use: function(/* pkgname, version, [symbol, ...] */) { | |
var args = [].slice.call(arguments, 0); | |
// Check module name, first parameter: e.g., JSM.use('Mod') | |
if (!args[0] && typeof args[0] !== 'string') | |
throw new TypeError("expect module name as string in first parameter"); | |
// Get the package name and version requested (if any) | |
var pkgname = args.shift(); | |
if (args.length >= 1 && typeof args[0] == 'number') { | |
var version = args.shift(); | |
} | |
if (args.length >= 1 && typeof args[0] == 'object') { | |
var symbols = args.shift(); | |
} | |
// Import the script (with optional, specific version) | |
var ns = import(pkgname, version, symbols); | |
// Get symbols to import to caller's namespace (if any): eg., JSM.use('Mod', fn, fn2,...) | |
return ns; | |
} | |
} | |
/* end return */ | |
})(); |
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
// Package Definition // | |
var Package = { | |
NAME: 'Profiler', | |
VERSION: 0.1, | |
DEFAULT: ['addProfiling', 'removeProfiling', 'profileReport'], | |
EXPORT: [ ], | |
EXPORT_OK: ['_start','_stop','_init'], | |
EXPORT_TAGS: { } | |
}; | |
// Private Variables // | |
var _fnlist = {}; | |
/** | |
* Create a tracking object for a new function | |
* to be profiled. | |
* @function | |
*/ | |
function _init(fname, fn, scope) { | |
console.log("Adding " + fname + ": "); | |
if (typeof _fnlist[fname] == 'undefined') { | |
_fnlist[fname] = { | |
fn: fn, // save original function | |
scope: scope, // object owner of function (context) | |
ncalls: 0, // number of calls made | |
calls: {} // individual call profiling | |
}; | |
} | |
else | |
throw new Error(fname + " is already being profiled"); | |
} | |
/** | |
* Start timing this function, run prior to the | |
* actual function's execution. | |
* @function | |
*/ | |
function _start(fname, fn, args) { | |
_fnlist[fname].ncalls++; | |
var start = new Date().getTime(); | |
var id = start + "_" + _fnlist[fname].ncalls; | |
_fnlist[fname].calls[id] = { | |
start: start, | |
stop: null, | |
args: args, | |
total: 0 | |
}; | |
return id; // unique id for *this* call | |
} | |
/** | |
* Stop timing a function and calculate total | |
* elapsed time (milliseconds) | |
* @function | |
*/ | |
function _stop(fname, id) { | |
var ref = _fnlist[fname].calls[id]; | |
ref.stop = new Date().getTime(); | |
ref.total = ref.stop - ref.start; | |
} | |
/** | |
* Add profiling for a given function, identified | |
* by the given label. Returns the newly modified | |
* function to be assigned to the old function. | |
* @example addProfiling('func'); | |
* @example addProfiling('func', window); | |
* @function | |
*/ | |
function addProfiling(fname, scope) { | |
var scope = scope || window; | |
_init(fname, scope[fname], scope); | |
var tmpfn = scope[fname]; | |
scope[fname] = function() { | |
var id = _start(fname, tmpfn, arguments); | |
var rv = tmpfn.apply(tmpfn, arguments); | |
_stop(fname, id); | |
return rv; // return result of original function | |
} | |
} | |
/** | |
* Remove a function identified by the lable | |
* from being profiled. Returns original function | |
* to be reassigned. | |
* @example removeProfiling('func'); | |
* @function | |
*/ | |
function removeProfiling(fname) { | |
_fnlist[fname]['scope'][fname] = _fnlist[fname].fn; | |
delete _fnlist[fname]; | |
} | |
/** | |
* Report our profiling information via the console. | |
* @function | |
*/ | |
function profileReport() { | |
for (fn in _fnlist) { | |
var obj = _fnlist[fn]; | |
console.log("Function: " + fn + ":"); | |
console.dir(obj); | |
console.log("\tTotal Calls: " + obj['ncalls']); | |
var totaltime = 0; | |
for (call in obj['calls']) { | |
totaltime += obj['calls'][call].total; | |
} | |
console.log("\tTotal Time (sec): " + (totaltime/1000).toFixed(5)); | |
} | |
} |
I've added an example module for those that might want to download and play around with JSM. Plus, it gives a nice sample of the Package header definition for those interested in how it works.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Removed some dead code in the latest revision. I've also modified the XHR loading of the script to implement polling using setTimeout. JSM now has a poll_timeout value in milliseconds and a number of attempts. So, for each path in the include paths tried for a given modules script, JSM will essentially wait (poll_timeout * tries) milliseconds until giving up. Given that we need to use synchronous XHR instead of async, this needed to be added to handle server latency, missing files, etc. If there's a better way to handle this, let me know.