From 9967c369c9272335bb0343558673b689725c6d7c Mon Sep 17 00:00:00 2001 From: isaacs Date: Sun, 12 Jun 2011 10:36:05 -0700 Subject: [PATCH] AMD compatibility for node, with docs and tests Closes #1173 Closes #1170 --- doc/api/modules.markdown | 32 +++++++++++++++++ lib/module.js | 39 ++++++++++++++++++++- src/node.cc | 4 +-- src/node.js | 2 +- test/fixtures/amd-modules/extra-args.js | 3 ++ test/fixtures/amd-modules/module-exports.js | 3 ++ test/fixtures/amd-modules/object.js | 1 + test/fixtures/amd-modules/regular.js | 14 ++++++++ test/fixtures/amd-modules/return.js | 3 ++ test/simple/test-module-loading.js | 20 +++++++++++ 10 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/amd-modules/extra-args.js create mode 100644 test/fixtures/amd-modules/module-exports.js create mode 100644 test/fixtures/amd-modules/object.js create mode 100644 test/fixtures/amd-modules/regular.js create mode 100644 test/fixtures/amd-modules/return.js diff --git a/doc/api/modules.markdown b/doc/api/modules.markdown index eec27b2d839..1bb185e1766 100644 --- a/doc/api/modules.markdown +++ b/doc/api/modules.markdown @@ -330,6 +330,38 @@ Because `module` provides a `filename` property (normally equivalent to `__filename`), the entry point of the current application can be obtained by checking `require.main.filename`. +## AMD Compatibility + +Node's modules have access to a function named `define`, which may be +used to specify the module's return value. This is not necessary in node +programs, but is present in the node API in order to provide +compatibility with module loaders that use the Asynchronous Module +Definition pattern. + +The example module above could be structured like so: + + define(function (require, exports, module) { + var PI = Math.PI; + + exports.area = function (r) { + return PI * r * r; + }; + + exports.circumference = function (r) { + return 2 * PI * r; + }; + }); + +* Only the last argument to `define()` matters. Other module loaders + sometimes use a `define(id, [deps], cb)` pattern, but since this is + not relevant in node programs, the other arguments are ignored. +* If the `define` callback returns a value other than `undefined`, then + that value is assigned to `module.exports`. +* **Important**: Despite being called "AMD", the node module loader **is + in fact synchronous**, and using `define()` does not change this fact. + Node executes the callback immediately, so please plan your programs + accordingly. + ## Addenda: Package Manager Tips The semantics of Node's `require()` function were designed to be general diff --git a/lib/module.js b/lib/module.js index 308fd4e8e30..06fa80fe9c8 100644 --- a/lib/module.js +++ b/lib/module.js @@ -381,6 +381,7 @@ Module.prototype._compile = function(content, filename) { require.cache = Module._cache; var dirname = path.dirname(filename); + var define = makeDefine(require, self); if (Module._contextLoad) { if (self.id !== '.') { @@ -397,6 +398,7 @@ Module.prototype._compile = function(content, filename) { sandbox.module = self; sandbox.global = sandbox; sandbox.root = root; + sandbox.define = define; return runInNewContext(content, sandbox, filename, true); } @@ -408,6 +410,7 @@ Module.prototype._compile = function(content, filename) { global.__filename = filename; global.__dirname = dirname; global.module = self; + global.define = define; return runInThisContext(content, filename, true); } @@ -419,10 +422,44 @@ Module.prototype._compile = function(content, filename) { if (filename === process.argv[1] && global.v8debug) { global.v8debug.Debug.setBreakPoint(compiledWrapper, 0, 0); } - var args = [self.exports, require, self, filename, dirname]; + + var args = [self.exports, require, self, filename, dirname, define]; return compiledWrapper.apply(self.exports, args); }; +// AMD compatibility +function makeDefine(require, module) { + var called = false; + function define() { + if (called) { + throw new Error("define() may only be called once."); + } + called = true; + + // only care about the last argument + var cb = arguments[ arguments.length - 1 ]; + + // set exports immediately: + // define({ foo: "bar" }) + if (typeof cb !== 'function') { + module.exports = cb; + return; + } + + var ret = cb(require, module.exports, module); + + if (ret !== undefined) { + // set exports with return statement + // define(function () { return { foo: "bar" } }) + module.exports = ret; + } + } + + return define; +} + + + // Native extension for .js Module._extensions['.js'] = function(module, filename) { var content = NativeModule.require('fs').readFileSync(filename, 'utf8'); diff --git a/src/node.cc b/src/node.cc index 4f01727fcd1..65dcf676ad8 100644 --- a/src/node.cc +++ b/src/node.cc @@ -1287,7 +1287,7 @@ void DisplayExceptionLine (TryCatch &try_catch) { // // When reporting errors on the first line of a script, this wrapper // function is leaked to the user. This HACK is to remove it. The length - // of the wrapper is 62. That wrapper is defined in src/node.js + // of the wrapper is 70. That wrapper is defined in src/node.js // // If that wrapper is ever changed, then this number also has to be // updated. Or - someone could clean this up so that the two peices @@ -1295,7 +1295,7 @@ void DisplayExceptionLine (TryCatch &try_catch) { // // Even better would be to get support into V8 for wrappers that // shouldn't be reported to users. - int offset = linenum == 1 ? 62 : 0; + int offset = linenum == 1 ? 70 : 0; fprintf(stderr, "%s\n", sourceline_string + offset); // Print wavy underline (GetUnderline is deprecated). diff --git a/src/node.js b/src/node.js index 1f512a5f6f1..581bb94b843 100644 --- a/src/node.js +++ b/src/node.js @@ -436,7 +436,7 @@ }; NativeModule.wrapper = [ - '(function (exports, require, module, __filename, __dirname) { ', + '(function (exports, require, module, __filename, __dirname, define) { ', '\n});' ]; diff --git a/test/fixtures/amd-modules/extra-args.js b/test/fixtures/amd-modules/extra-args.js new file mode 100644 index 00000000000..a81d48c3b24 --- /dev/null +++ b/test/fixtures/amd-modules/extra-args.js @@ -0,0 +1,3 @@ +define(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, function(r, e, m) { + exports.ok = require("./regular.js").ok; +}); diff --git a/test/fixtures/amd-modules/module-exports.js b/test/fixtures/amd-modules/module-exports.js new file mode 100644 index 00000000000..496440eedf7 --- /dev/null +++ b/test/fixtures/amd-modules/module-exports.js @@ -0,0 +1,3 @@ +define(function(require, exports, module) { + module.exports = { ok: require("./regular.js").ok }; +}); diff --git a/test/fixtures/amd-modules/object.js b/test/fixtures/amd-modules/object.js new file mode 100644 index 00000000000..93b840b61bf --- /dev/null +++ b/test/fixtures/amd-modules/object.js @@ -0,0 +1 @@ +define({ ok: require("./regular.js").ok }); diff --git a/test/fixtures/amd-modules/regular.js b/test/fixtures/amd-modules/regular.js new file mode 100644 index 00000000000..013a6cfd433 --- /dev/null +++ b/test/fixtures/amd-modules/regular.js @@ -0,0 +1,14 @@ +var R = require; +var E = exports; +var M = module; + +define(function(require, exports, module, nothingHere) { + if (R !== require) throw new Error("invalid require in AMD cb"); + if (E !== exports) throw new Error("invalid exports in AMD cb"); + if (M !== module) throw new Error("invalid module in AMD cb"); + if (nothingHere !== undefined) { + throw new Error("unknown args to AMD cb"); + } + + exports.ok = { ok: true }; +}); diff --git a/test/fixtures/amd-modules/return.js b/test/fixtures/amd-modules/return.js new file mode 100644 index 00000000000..6ad01321c2e --- /dev/null +++ b/test/fixtures/amd-modules/return.js @@ -0,0 +1,3 @@ +define(function() { + return { ok: require("./regular.js").ok }; +}); diff --git a/test/simple/test-module-loading.js b/test/simple/test-module-loading.js index 5f648f0c784..421fcbf8446 100644 --- a/test/simple/test-module-loading.js +++ b/test/simple/test-module-loading.js @@ -187,6 +187,26 @@ try { assert.equal(require(loadOrder + 'file8').file8, 'file8/index.reg', msg); assert.equal(require(loadOrder + 'file9').file9, 'file9/index.reg2', msg); + +// test the async module definition pattern modules +var amdFolder = '../fixtures/amd-modules'; +var amdreg = require(amdFolder + '/regular.js'); +assert.deepEqual(amdreg.ok, {ok: true}, 'regular amd module failed'); + +// make sure they all get the same 'ok' object. +var amdModuleExports = require(amdFolder + '/module-exports.js'); +assert.equal(amdModuleExports.ok, amdreg.ok, 'amd module.exports failed'); + +var amdReturn = require(amdFolder + '/return.js'); +assert.equal(amdReturn.ok, amdreg.ok, 'amd return failed'); + +var amdObj = require(amdFolder + '/object.js'); +assert.equal(amdObj.ok, amdreg.ok, 'amd object literal failed'); + +var amdExtraArgs = require(amdFolder + '/extra-args.js'); +assert.equal(amdExtraArgs.ok, amdreg.ok, 'amd extra args failed'); + + process.addListener('exit', function() { assert.ok(common.indirectInstanceOf(a.A, Function)); assert.equal('A done', a.A());