Bläddra i källkod

Allow multiple named defines that are visible to the current module. Allows distributing single file JS files in node.

jrburke 12 år sedan
förälder
incheckning
6c719d58eb
6 ändrade filer med 301 tillägg och 146 borttagningar
  1. 240 141
      amdefine.js
  2. 2 0
      tests/all.js
  3. 0 1
      tests/basic/a.js
  4. 1 4
      tests/basic/sub/nested/d.js
  5. 42 0
      tests/named/lib.js
  6. 16 0
      tests/named/named-tests.js

+ 240 - 141
amdefine.js

@@ -8,190 +8,289 @@
 /*global module, process */
 'use strict';
 
-var path = require('path'),
-    loaderCache = {},
-    makeRequire;
+var path = require('path');
 
 /**
- * Given a relative module name, like ./something, normalize it to
- * a real name that can be mapped to a path.
- * @param {String} name the relative name
- * @param {String} baseName a real name that the name arg is relative
- * to.
- * @returns {String} normalized name
- */
-function normalize(name, baseName) {
-    return path.normalize(path.join(baseName, name));
-}
-
-/**
- * Create the normalize() function passed to a loader plugin's
- * normalize method.
+ * Creates a define for node.
+ * @param {Object} module the "module" object that is defined by Node for the
+ * current module.
+ * @param {Function} [require]. Node's require function for the current module.
+ * It only needs to be passed in Node versions before 0.5, when module.require
+ * did not exist.
+ * @returns {Function} a define function that is usable for the current node
+ * module.
  */
-function makeNormalize(relName) {
-    return function (name) {
-        return normalize(name, relName);
-    };
-}
+function amdefine(module, require) {
+    var defineCache = {},
+        loaderCache = {},
+        alreadyCalled = false,
+        makeRequire, stringRequire;
 
-function makeLoad(id) {
-    function load(value) {
-        loaderCache[id] = value;
+    /**
+     * Trims the . and .. from an array of path segments.
+     * It will keep a leading path segment if a .. will become
+     * the first path segment, to help with module name lookups,
+     * which act like paths, but can be remapped. But the end result,
+     * all paths that use this function should look normalized.
+     * NOTE: this method MODIFIES the input array.
+     * @param {Array} ary the array of path segments.
+     */
+    function trimDots(ary) {
+        var i, part;
+        for (i = 0; ary[i]; i+= 1) {
+            part = ary[i];
+            if (part === '.') {
+                ary.splice(i, 1);
+                i -= 1;
+            } else if (part === '..') {
+                if (i === 1 && (ary[2] === '..' || ary[0] === '..')) {
+                    //End of the line. Keep at least one non-dot
+                    //path segment at the front so it can be mapped
+                    //correctly to disk. Otherwise, there is likely
+                    //no path mapping for a path starting with '..'.
+                    //This can still fail, but catches the most reasonable
+                    //uses of ..
+                    break;
+                } else if (i > 0) {
+                    ary.splice(i - 1, 2);
+                    i -= 2;
+                }
+            }
+        }
     }
 
-    load.fromText = function (id, text) {
-        //This one is difficult because the text can/probably uses
-        //define, and any relative paths and requires should be relative
-        //to that id was it would be found on disk. But this would require
-        //bootstrapping a module/require fairly deeply from node core.
-        //Not sure how best to go about that yet.
-        throw new Error('amdefine does not implement load.fromText');
-    };
-
-    return load;
-}
+    function normalize(name, baseName) {
+        var baseParts;
 
-function stringRequire(module, require, id) {
-    //Split the ID by a ! so that
-    var index = id.indexOf('!'),
-        relId = path.dirname(module.filename),
-        prefix, plugin;
-
-    if (index === -1) {
-        //Straight module lookup. If it is one of the special dependencies,
-        //deal with it, otherwise, delegate to node.
-        if (id === 'require') {
-            return makeRequire(module, require);
-        } else if (id === 'exports') {
-            return module.exports;
-        } else if (id === 'module') {
-            return module;
-        } else {
-            return require(id);
+        //Adjust any relative paths.
+        if (name && name.charAt(0) === '.') {
+            //If have a base name, try to normalize against it,
+            //otherwise, assume it is a top-level require that will
+            //be relative to baseUrl in the end.
+            if (baseName) {
+                baseParts = baseName.split('/');
+                baseParts = baseParts.slice(0, baseParts.length - 1);
+                baseParts = baseParts.concat(name.split('/'));
+                trimDots(baseParts);
+                name = baseParts.join('/');
+            }
         }
-    } else {
-        //There is a plugin in play.
-        prefix = id.substring(0, index);
-        id = id.substring(index + 1, id.length);
 
-        plugin = require(prefix);
-
-        if (plugin.normalize) {
-            id = plugin.normalize(id, makeNormalize(relId));
-        } else {
-            //Normalize the ID normally.
-            id = normalize(id, relId);
-        }
+        return name;
+    }
 
-        if (loaderCache[id]) {
-            return loaderCache[id];
-        } else {
-            plugin.load(id, makeRequire(module, require), makeLoad(id), {});
+    /**
+     * Create the normalize() function passed to a loader plugin's
+     * normalize method.
+     */
+    function makeNormalize(relName) {
+        return function (name) {
+            return normalize(name, relName);
+        };
+    }
 
-            return loaderCache[id];
+    function makeLoad(id) {
+        function load(value) {
+            loaderCache[id] = value;
         }
-    }
-}
 
-makeRequire = function (module, require) {
-    function amdRequire(deps, callback) {
-        if (typeof deps === 'string') {
-            //Synchronous, single module require('')
-            return stringRequire(module, require, deps);
-        } else {
-            //Array of dependencies with a callback.
+        load.fromText = function (id, text) {
+            //This one is difficult because the text can/probably uses
+            //define, and any relative paths and requires should be relative
+            //to that id was it would be found on disk. But this would require
+            //bootstrapping a module/require fairly deeply from node core.
+            //Not sure how best to go about that yet.
+            throw new Error('amdefine does not implement load.fromText');
+        };
 
-            //Convert the dependencies to modules.
-            deps = deps.map(function (depName) {
-                return stringRequire(module, require, depName);
-            });
+        return load;
+    }
 
-            //Wait for next tick to call back the require call.
-            process.nextTick(function () {
-                callback.apply(null, deps);
-            });
+    makeRequire = function (systemRequire, exports, module, relId) {
+        function amdRequire(deps, callback) {
+            if (typeof deps === 'string') {
+                //Synchronous, single module require('')
+                return stringRequire(systemRequire, exports, module, deps, relId);
+            } else {
+                //Array of dependencies with a callback.
 
-            //Keeps strict checking in komodo happy.
-            return undefined;
-        }
-    }
+                //Convert the dependencies to modules.
+                deps = deps.map(function (depName) {
+                    return stringRequire(systemRequire, exports, module, depName, relId);
+                });
 
-    amdRequire.toUrl = function (filePath) {
-        if (filePath.indexOf('.') === 0) {
-            return normalize(filePath, path.dirname(module.filename));
-        } else {
-            return filePath;
+                //Wait for next tick to call back the require call.
+                process.nextTick(function () {
+                    callback.apply(null, deps);
+                });
+            }
         }
-    };
 
-    return amdRequire;
-};
+        amdRequire.toUrl = function (filePath) {
+            if (filePath.indexOf('.') === 0) {
+                return normalize(filePath, path.dirname(module.filename));
+            } else {
+                return filePath;
+            }
+        };
 
-/**
- * Creates a define for node.
- * @param {Object} module the "module" object that is defined by Node for the
- * current module.
- * @param {Function} [require]. Node's require function for the current module.
- * It only needs to be passed in Node versions before 0.5, when module.require
- * did not exist.
- * @returns {Function} a define function that is usable for the current node
- * module.
- */
-function amdefine(module, require) {
-    var alreadyCalled = false;
+        return amdRequire;
+    };
 
     //Favor explicit value, passed in if the module wants to support Node 0.4.
     require = require || function req() {
         return module.require.apply(module, arguments);
     };
 
-    //Create a define function specific to the module asking for amdefine.
-    function define() {
-
-        var args = arguments,
-            factory = args[args.length - 1],
-            isFactoryFunction = (typeof factory === 'function'),
-            deps, result;
+    function runFactory(id, deps, factory) {
+        var r, e, m, result;
 
-        //Only support one define call per file
-        if (alreadyCalled) {
-            throw new Error('amdefine cannot be called more than once per file.');
-        }
-        alreadyCalled = true;
-
-        //Grab array of dependencies if it is there.
-        if (args.length > 1) {
-            deps = args[args.length - 2];
-            if (!Array.isArray(deps)) {
-                //deps is not an array, may be an ID. Discard it.
-                deps = null;
+        if (id) {
+            e = loaderCache[id] = {};
+            m = {
+                id: id,
+                uri: __filename,
+                exports: e
+            };
+            r = makeRequire(undefined, e, m, id);
+        } else {
+            //Only support one define call per file
+            if (alreadyCalled) {
+                throw new Error('amdefine with no module ID cannot be called more than once per file.');
             }
+            alreadyCalled = true;
+
+            //Use the real variables from node
+            //Use module.exports for exports, since
+            //the exports in here is amdefine exports.
+            e = module.exports;
+            m = module;
+            r = makeRequire(require, e, m, module.id);
         }
 
         //If there are dependencies, they are strings, so need
         //to convert them to dependency values.
         if (deps) {
             deps = deps.map(function (depName) {
-                return stringRequire(module, require, depName);
+                return r(depName);
             });
-        } else if (isFactoryFunction) {
-            //Pass in the standard require, exports, module
-            deps = [makeRequire(module, require), module.exports, module];
         }
 
-        if (!isFactoryFunction) {
-            //Factory is an object that should just be used for the define call.
-            module.exports = factory;
-        } else {
-            //Call the factory with the right dependencies.
+        //Call the factory with the right dependencies.
+        if (typeof factory === 'function') {
             result = factory.apply(module.exports, deps);
+        } else {
+            result = factory;
+        }
 
-            if (result !== undefined) {
-                module.exports = result;
+        if (result !== undefined) {
+            m.exports = result;
+            if (id) {
+                loaderCache[id] = m.exports;
             }
         }
     }
 
+    stringRequire = function (systemRequire, exports, module, id, relId) {
+        //Split the ID by a ! so that
+        var index = id.indexOf('!'),
+            originalId = id,
+            prefix, plugin;
+
+        if (index === -1) {
+            id = normalize(id, relId);
+
+            //Straight module lookup. If it is one of the special dependencies,
+            //deal with it, otherwise, delegate to node.
+            if (id === 'require') {
+                return makeRequire(systemRequire, exports, module, relId);
+            } else if (id === 'exports') {
+                return exports;
+            } else if (id === 'module') {
+                return module;
+            } else if (loaderCache.hasOwnProperty(id)) {
+                return loaderCache[id];
+            } else if (defineCache[id]) {
+                runFactory.apply(null, defineCache[id]);
+                return loaderCache[id];
+            } else {
+                if(systemRequire) {
+                    return systemRequire(originalId);
+                } else {
+                    throw new Error('No module with ID: ' + id);
+                }
+            }
+        } else {
+            //There is a plugin in play.
+            prefix = id.substring(0, index);
+            id = id.substring(index + 1, id.length);
+
+            plugin = stringRequire(systemRequire, exports, module, prefix, relId);
+
+            if (plugin.normalize) {
+                id = plugin.normalize(id, makeNormalize(relId));
+            } else {
+                //Normalize the ID normally.
+                id = normalize(id, relId);
+            }
+
+            if (loaderCache[id]) {
+                return loaderCache[id];
+            } else {
+                plugin.load(id, makeRequire(systemRequire, exports, module, relId), makeLoad(id), {});
+
+                return loaderCache[id];
+            }
+        }
+    };
+
+    //Create a define function specific to the module asking for amdefine.
+    function define(id, deps, factory) {
+        if (Array.isArray(id)) {
+            factory = deps;
+            deps = id;
+            id = undefined;
+        } else if (typeof id !== 'string') {
+            factory = id;
+            id = deps = undefined;
+        }
+
+        if (deps && !Array.isArray(deps)) {
+            factory = deps;
+            deps = undefined;
+        }
+
+        if (!deps) {
+            deps = ['require', 'exports', 'module'];
+        }
+
+        //Set up properties for this module. If an ID, then use
+        //internal cache. If no ID, then use the external variables
+        //for this node module.
+        if (id) {
+            //Put the module in deep freeze until there is a
+            //require call for it.
+            defineCache[id] = [id, deps, factory];
+        } else {
+            runFactory(id, deps, factory);
+        }
+    }
+
+    //define.require, which has access to all the values in the
+    //cache. Useful for AMD modules that all have IDs in the file,
+    //but need to finally export a value to node based on one of those
+    //IDs.
+    define.require = function (id) {
+        if (loaderCache[id]) {
+            return loaderCache[id];
+        }
+
+        if (defineCache[id]) {
+            runFactory.apply(null, defineCache[id]);
+            return loaderCache[id];
+        }
+    };
+
     define.amd = {};
 
     return define;

+ 2 - 0
tests/all.js

@@ -26,6 +26,8 @@ load("doh/runner.js");
 load('doh/_' + env + 'Runner.js');
 
 require("./basic/basic-tests");
+require("./named/named-tests");
+
 require("./plugins/relative/relative-tests");
 
 //Cannot handle load.fromText for plugins yet, so commented out.

+ 0 - 1
tests/basic/a.js

@@ -1,4 +1,3 @@
-
 if (typeof define !== 'function') { var define = require('../../amdefine')(module) }
 
 define(['./b', './sub/nested/d'], function (b, d) {

+ 1 - 4
tests/basic/sub/nested/d.js

@@ -1,9 +1,6 @@
 if (typeof define !== 'function') { var define = (require('../../../../amdefine'))(module); }
 
-//Define's a named module, but one that does not match the current module name
-//expected by node. amdefine should just ignore this ID and use the ID expected
-//by node.
-define('whatever', function (require, exports, module) {
+define(function (require, exports, module) {
     var c = require('../c'),
         e = require('./e');
 

+ 42 - 0
tests/named/lib.js

@@ -0,0 +1,42 @@
+if (typeof define !== 'function') { var define = require('../../amdefine')(module) }
+
+define('sub/nested/d', function (require, exports, module) {
+    var c = require('../c'),
+        e = require('./e');
+
+    return {
+        name: 'd',
+        e: e,
+        cName: c.name
+    };
+});
+
+
+define('sub/nested/e', function (require, exports) {
+    exports.name = 'e';
+});
+
+define('b', {
+    name: 'b'
+});
+
+define('sub/c', function (require, exports, module) {
+
+    //A fake out, modify the exports, but still prefer the
+    //return value as the module value.
+    exports.name = 'badc';
+
+    return {
+        name: 'c'
+    };
+});
+
+define('lib', ['./b', './sub/nested/d'], function (b, d) {
+    return {
+        name: 'lib',
+        b: b,
+        d: d
+    };
+});
+
+module.exports = define.require('lib');

+ 16 - 0
tests/named/named-tests.js

@@ -0,0 +1,16 @@
+doh.register(
+    "named",
+    [
+        function named(t){
+            var lib = require('./lib');
+
+            t.is('lib', lib.name);
+            t.is('b', lib.b.name);
+            t.is('d', lib.d.name);
+            t.is('c', lib.d.cName);
+            t.is('e', lib.d.e.name);
+        }
+    ]
+);
+
+doh.run();