Browse Source

Add amdefine/intercept and rev to 0.1.0

jrburke 10 years ago
parent
commit
d6f9819dd3

+ 51 - 1
README.md

@@ -10,7 +10,7 @@ requiring those other programs to use AMD.
 
 ```javascript
     "dependencies": {
-        "amdefine": ">=0.0.8"
+        "amdefine": ">=0.1.0"
     }
 ```
 
@@ -39,6 +39,55 @@ with npm, then just download the latest release and refer to it using a relative
 
 [Latest Version](https://github.com/jrburke/amdefine/raw/latest/amdefine.js)
 
+### amdefine/intercept
+
+Instead of pasting the piece of text for the amdefine setup of a `define`
+variable in each module you create or cosunme, you can use `amdefine/intercept`
+instead. It will automatically insert the above snippet in each .js file loaded
+by Node.
+
+**Warning**: you should only use this if you are creating an application that
+is consuming AMD style defined()'d modules that are distributed via npm and want
+to run that code in Node.
+
+For library code where you are not sure if it will be used by others in Node or
+in the browser, then explicitly depending on amdefine and placing the code
+snippet above is suggested path, instead of using `amdefine/intercept`. The
+intercept module affects all .js files loaded in the Node app, and it is
+inconsiderate to modify global state like that unless you are also controlling
+the top level app.
+
+#### Why distribute AMD-style nodes via npm?
+
+npm has a lot of weaknesses for front-end use (installed layout is not great,
+should have better support for the `baseUrl + moduleID + '.js' style of loading,
+single file JS installs), but some people want a JS package manager and are
+willing to live with those constraints. If that is you, but still want to author
+in AMD style modules to get dynamic require([]), better direct source usage and
+powerful loader plugin support in the browser, then this tool can help.
+
+#### amdefine/intercept usage
+
+Just require it in your top level app module (for example index.js, server.js):
+
+```javascript
+require('amdefine/intercept');
+```
+
+The module does not return a value, so no need to assign the result to a local
+variable.
+
+Then just require() code as you normally would with Node's require(). Any .js
+loaded after the intercept require will have the amdefine check injected in
+the .js source as it is loaded. It does not modify the source on disk, just
+prepends some content to the text of the module as it is loaded by Node.
+
+#### How amdefine/intercept works
+
+It overrides the `Module._extensions['.js']` in Node to automatically prepend
+the amdefine snippet above. So, it will affect any .js file loaded by your
+app.
+
 ## define() usage
 
 It is best if you use the anonymous forms of define() in your module:
@@ -112,6 +161,7 @@ To run the tests, cd to **tests** and run:
 
 ```
 node all.js
+node all-intercept.js
 ```
 
 ## License

+ 1 - 1
amdefine.js

@@ -1,5 +1,5 @@
 /** vim: et:ts=4:sw=4:sts=4
- * @license amdefine 0.0.8 Copyright (c) 2011, The Dojo Foundation All Rights Reserved.
+ * @license amdefine 0.1.0 Copyright (c) 2011, The Dojo Foundation All Rights Reserved.
  * Available via the MIT or new BSD license.
  * see: http://github.com/jrburke/amdefine for details
  */

+ 36 - 0
intercept.js

@@ -0,0 +1,36 @@
+/*jshint node: true */
+var inserted,
+    Module = require('module'),
+    fs = require('fs'),
+    existingExtFn = Module._extensions['.js'],
+    amdefineRegExp = /amdefine\.js/;
+
+inserted  = "if (typeof define !== 'function') {var define = require('amdefine')(module)}";
+
+//From the node/lib/module.js source:
+function stripBOM(content) {
+    // Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
+    // because the buffer-to-string conversion in `fs.readFileSync()`
+    // translates it to FEFF, the UTF-16 BOM.
+    if (content.charCodeAt(0) === 0xFEFF) {
+        content = content.slice(1);
+    }
+    return content;
+}
+
+//Also adapted from the node/lib/module.js source:
+function intercept(module, filename) {
+    var content = stripBOM(fs.readFileSync(filename, 'utf8'));
+
+    if (!amdefineRegExp.test(module.id)) {
+        content = inserted + content;
+    }
+
+    module._compile(content, filename);
+}
+
+intercept._id = 'amdefine/intercept';
+
+if (!existingExtFn._id || existingExtFn._id !== intercept._id) {
+    Module._extensions['.js'] = intercept;
+}

+ 1 - 1
package.json

@@ -1,7 +1,7 @@
 {
     "name": "amdefine",
     "description": "Provide AMD's define() API for declaring modules in the AMD format",
-    "version": "0.0.8",
+    "version": "0.1.0",
     "homepage": "http://github.com/jrburke/amdefine",
     "author": "James Burke <jrburke@gmail.com> (http://github.com/jrburke)",
     "licenses": [

+ 33 - 0
tests/all-intercept.js

@@ -0,0 +1,33 @@
+/*jslint strict: false, evil: true */
+/*global Packages: false, process: false, require: true, define: true, doh: false */
+
+//A hack to doh to avoid dojo setup stuff in doh/runner.js
+var skipDohSetup = true,
+    fs, vm, load, env;
+
+require('../intercept');
+
+(function () {
+    if (typeof Packages !== 'undefined') {
+        env = 'rhino';
+    } else if (typeof process !== 'undefined') {
+        env = 'node';
+
+        fs = require('fs');
+        vm = require('vm');
+
+        load = function (path) {
+            return vm.runInThisContext(fs.readFileSync(path, 'utf8'), path);
+        };
+    }
+
+}());
+
+//Load the tests.
+load("doh/runner.js");
+load('doh/_' + env + 'Runner.js');
+
+require("./basic-intercept/basic-tests");
+
+//Print out the final report
+doh.run();

+ 7 - 0
tests/basic-intercept/a.js

@@ -0,0 +1,7 @@
+define(['./b', './sub/nested/d'], function (b, d) {
+    return {
+        name: 'a',
+        b: b,
+        d: d
+    };
+});

+ 3 - 0
tests/basic-intercept/b.js

@@ -0,0 +1,3 @@
+define({
+    name: 'b'
+});

+ 16 - 0
tests/basic-intercept/basic-tests.js

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

+ 10 - 0
tests/basic-intercept/sub/c.js

@@ -0,0 +1,10 @@
+define(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'
+    };
+});

+ 10 - 0
tests/basic-intercept/sub/nested/d.js

@@ -0,0 +1,10 @@
+define(function (require, exports, module) {
+    var c = require('../c'),
+        e = require('./e');
+
+    return {
+        name: 'd',
+        e: e,
+        cName: c.name
+    };
+});

+ 4 - 0
tests/basic-intercept/sub/nested/e.js

@@ -0,0 +1,4 @@
+//Just testing a plain exports case.
+define(function (require, exports) {
+    exports.name = 'e';
+});

+ 299 - 0
tests/node_modules/amdefine.js

@@ -0,0 +1,299 @@
+/** vim: et:ts=4:sw=4:sts=4
+ * @license amdefine 0.0.8 Copyright (c) 2011, The Dojo Foundation All Rights Reserved.
+ * Available via the MIT or new BSD license.
+ * see: http://github.com/jrburke/amdefine for details
+ */
+
+/*jslint node: true */
+/*global module, process */
+'use strict';
+
+/**
+ * Creates a define for node.
+ * @param {Object} module the "module" object that is defined by Node for the
+ * current module.
+ * @param {Function} [requireFn]. 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, requireFn) {
+    'use strict';
+    var defineCache = {},
+        loaderCache = {},
+        alreadyCalled = false,
+        path = require('path'),
+        makeRequire, stringRequire;
+
+    /**
+     * 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;
+                }
+            }
+        }
+    }
+
+    function normalize(name, baseName) {
+        var baseParts;
+
+        //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('/');
+            }
+        }
+
+        return name;
+    }
+
+    /**
+     * Create the normalize() function passed to a loader plugin's
+     * normalize method.
+     */
+    function makeNormalize(relName) {
+        return function (name) {
+            return normalize(name, relName);
+        };
+    }
+
+    function makeLoad(id) {
+        function load(value) {
+            loaderCache[id] = value;
+        }
+
+        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;
+    }
+
+    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.
+
+                //Convert the dependencies to modules.
+                deps = deps.map(function (depName) {
+                    return stringRequire(systemRequire, exports, module, depName, relId);
+                });
+
+                //Wait for next tick to call back the require call.
+                process.nextTick(function () {
+                    callback.apply(null, deps);
+                });
+            }
+        }
+
+        amdRequire.toUrl = function (filePath) {
+            if (filePath.indexOf('.') === 0) {
+                return normalize(filePath, path.dirname(module.filename));
+            } else {
+                return filePath;
+            }
+        };
+
+        return amdRequire;
+    };
+
+    //Favor explicit value, passed in if the module wants to support Node 0.4.
+    requireFn = requireFn || function req() {
+        return module.require.apply(module, arguments);
+    };
+
+    function runFactory(id, deps, factory) {
+        var r, e, m, result;
+
+        if (id) {
+            e = loaderCache[id] = {};
+            m = {
+                id: id,
+                uri: __filename,
+                exports: e
+            };
+            r = makeRequire(requireFn, 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(requireFn, 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 r(depName);
+            });
+        }
+
+        //Call the factory with the right dependencies.
+        if (typeof factory === 'function') {
+            result = factory.apply(m.exports, deps);
+        } else {
+            result = factory;
+        }
+
+        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;
+}
+
+module.exports = amdefine;