Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // 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 */
- })();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement