Browse Source

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

jrburke 12 năm trước cách đây
mục cha
commit
6c719d58eb
6 tập tin đã thay đổi với 301 bổ sung146 xóa
  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();