Skip to content
This repository has been archived by the owner on Apr 22, 2023. It is now read-only.

Commit

Permalink
AMD compatibility for node, with docs and tests
Browse files Browse the repository at this point in the history
Closes #1173
Closes #1170
  • Loading branch information
isaacs committed Jun 13, 2011
1 parent 2eb1274 commit 9967c36
Show file tree
Hide file tree
Showing 10 changed files with 117 additions and 4 deletions.
32 changes: 32 additions & 0 deletions doc/api/modules.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 38 additions & 1 deletion lib/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 !== '.') {
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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');
Expand Down
4 changes: 2 additions & 2 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1287,15 +1287,15 @@ 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
// don't need to be changed.
//
// 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).
Expand Down
2 changes: 1 addition & 1 deletion src/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@
};

NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'(function (exports, require, module, __filename, __dirname, define) { ',
'\n});'
];

Expand Down
3 changes: 3 additions & 0 deletions test/fixtures/amd-modules/extra-args.js
Original file line number Diff line number Diff line change
@@ -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;
});
3 changes: 3 additions & 0 deletions test/fixtures/amd-modules/module-exports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
define(function(require, exports, module) {
module.exports = { ok: require("./regular.js").ok };
});
1 change: 1 addition & 0 deletions test/fixtures/amd-modules/object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
define({ ok: require("./regular.js").ok });
14 changes: 14 additions & 0 deletions test/fixtures/amd-modules/regular.js
Original file line number Diff line number Diff line change
@@ -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 };
});
3 changes: 3 additions & 0 deletions test/fixtures/amd-modules/return.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
define(function() {
return { ok: require("./regular.js").ok };
});
20 changes: 20 additions & 0 deletions test/simple/test-module-loading.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down

5 comments on commit 9967c36

@tlrobinson
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lame, but I give up.

@subtleGradient
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tlrobinson What do you mean?

@tlrobinson
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just not a fan of AMD for anything except a browser transport mechanism, but I'm not going to fight it any more.

@isaacs
Copy link
Author

@isaacs isaacs commented on 9967c36 Jun 14, 2011

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, that's all it should really be used for. Personally, I think that the modules themselves is a bad place for boilerplate. But a lot of people have been asking for this pretty incessantly for the last year or so, and rival AMD specs have gone around and around. This is at least a specific pattern that authors or platforms can target, which node will support natively.

A goal is that it may knock down a few bikesheds. TBD whether or not it'll have that effect.

[Edit: double-negative typo, corrected]

@drewlesueur
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this. I like that it works like the following as well:

// contents of circle.js
define(function () {
  var PI = Math.PI;
  var circle = {};
  circle.area = function (r) {
    return PI * r * r;
  };

  circle.circumference = function (r) {
    return 2 * PI * r;
  };
  return circle;
});

Please sign in to comment.