diff --git a/bin/jsx b/bin/jsx index 2a6437d535b..fc08bad79d3 100755 --- a/bin/jsx +++ b/bin/jsx @@ -32,5 +32,23 @@ require("commoner").resolve(function(id) { // Constant propagation means removing any obviously dead code after // replacing constant expressions with literal (boolean) values. - return propagate(constants, source); + source = propagate(constants, source); + + // Make sure there is exactly one newline at the end of the module. + source = source.replace(/\s+$/m, "\n"); + + if (constants.__MOCK__) { + return context.getProvidedP().then(function(idToPath) { + if (id !== "mock-modules" && + id !== "mocks" && + idToPath.hasOwnProperty("mock-modules")) { + return source + '\nrequire("mock-modules").register(' + + JSON.stringify(id) + ', module);\n'; + } + + return source; + }); + } + + return source; }); diff --git a/grunt/config/browserify.js b/grunt/config/browserify.js index 7594e955442..837dd0500b8 100644 --- a/grunt/config/browserify.js +++ b/grunt/config/browserify.js @@ -49,6 +49,60 @@ function simpleBannerify(src) { '\n' + src; } +var TEST_PRELUDE = [ + "(function(modules, cache, entry) {", + " var hasOwn = cache.hasOwnProperty;", + " function require(name) {", + " if (!hasOwn.call(cache, name)) {", + " var m = cache[name] = { exports: {} };", + " modules[name][0].call(m.exports, function(relID) {", + " var id = modules[name][1][relID];", + " return require(id ? id : relID);", + " }, m, m.exports);", + " }", + " return cache[name].exports;", + " }", + "", + " require.dumpCache = function() { cache = {} };", + "", + " for(var i = 0, len = entry.length; i < len; ++i)", + " require(entry[i]);", + "", + " return require;", + "})" +].join("\n"); + +function monkeyPatchDumpCache(src) { + var ch, start = 0, depth = 0; + + // Parsing the entire source with Esprima would be cleaner but also + // prohibitively expensive (multiple seconds), and regular expressions + // don't balance parentheses well. So we take a hybrid parenthesis + // counting approach that fails noisily if things go awry. + for (var i = 0, len = src.length; i < len; ++i) { + ch = src.charAt(i); + if (ch === "(") { + if (depth++ === 0) { + var expected = "(function"; + if (src.slice(i, i + expected.length) !== expected) { + throw new Error( + "Refusing to replace react-test.js prelude because first " + + "opening parenthesis did not begin a function expression: " + + src.slice(i, 100) + "..." + ); + } + start = i; + } + } else if (ch === ")") { + if (--depth === 0) { + return src.slice(0, start) + TEST_PRELUDE + src.slice(i + 1); + } + } + } + + throw new Error("Unable to replace react-test.js prelude."); +} + // Our basic config which we'll add to to make our other builds var basic = { entries: [ @@ -96,7 +150,8 @@ var test = { ], outfile: './build/react-test.js', debug: false, - standalone: false + standalone: false, + after: [monkeyPatchDumpCache] }; module.exports = { diff --git a/grunt/config/jsx/debug.json b/grunt/config/jsx/debug.json index 1596c562e85..d4099010b2f 100644 --- a/grunt/config/jsx/debug.json +++ b/grunt/config/jsx/debug.json @@ -1,6 +1,7 @@ { "debug": true, "constants": { + "__MOCK__": true, "__DEV__": true } } diff --git a/src/test/mock-modules.js b/src/test/mock-modules.js index 6c0efd95f87..12daff56a4d 100644 --- a/src/test/mock-modules.js +++ b/src/test/mock-modules.js @@ -16,18 +16,86 @@ * @providesModule mock-modules */ -var global = Function("return this")(); -require('test/mock-timers').installMockTimers(global); +var mocks = require("mocks"); +var exportsRegistry = {}; + +function getMock(exports) { + try { + return mocks.generateFromMetadata( + mocks.getMetadata(exports)); + } catch (err) { + console.warn(err); + return exports; + } +} + +// This function should be called at the bottom of any module that might +// need to be mocked, after the final value of module.exports is known. +exports.register = function(id, module) { + var directive = exportsRegistry[id]; + + exportsRegistry[id] = { + module: module, + actual: module.exports, + mocked: null // Filled in lazily later. + }; + + // If doMock or doNotMock was called earlier, before the module was + // registered, then we ought to have recorded the choice as a string + // literal. Now that the module is registered, we can finally fulfill + // the request. + if (directive === "doNotMock") { + doNotMock(id); + } else if (directive === "doMock") { + doMock(id); + } + + return exports; +}; exports.dumpCache = function() { + // This .clear() call intelligently resets all mocked properties that + // have been modified, so we don't have to recreate every mock exports + // object or invalidate the exportsRegistry. require("mocks").clear(); + global.require.dumpCache(); return exports; }; -exports.dontMock = function() { +// Call this function to ensure that require(id) returns the actual +// exports object created by the module. +function doNotMock(id) { + var entry = exportsRegistry[id]; + if (entry && entry.module && entry.actual) { + entry.module.exports = entry.actual; + } else { + // When this module is first registered, make sure NOT to mock it. + exportsRegistry[id] = "doNotMock"; + } + return exports; -}; +} + +// Call this function to ensure that require(id) returns a mock exports +// object based on the actual exports object created by the module. +function doMock(id) { + var entry = exportsRegistry[id]; + if (entry && entry.module && entry.actual) { + entry.module.exports = entry.mocked || ( + // Because mocking can be expensive, create the mock exports + // object on demand, the first time doMock is called. + entry.mocked = getMock(entry.actual)); + } else { + // When this module is first registered, make sure TO mock it. + exportsRegistry[id] = "doMock"; + } -exports.mock = function() { return exports; -}; +} + +var global = Function("return this")(); +require('test/mock-timers').installMockTimers(global); + +// Exported names are different for backwards compatibility. +exports.dontMock = doNotMock; +exports.mock = doMock; diff --git a/src/test/mocks.js b/src/test/mocks.js index d0d60b0f398..65fec086755 100644 --- a/src/test/mocks.js +++ b/src/test/mocks.js @@ -305,7 +305,8 @@ function removeUnusedRefs(metadata) { }); } -var dirtyMocks = []; +var global = Function("return this")(); +global.dirtyMocks = global.dirtyMocks || []; module.exports = { /** @@ -313,8 +314,8 @@ module.exports = { * called since the last time clear was called. */ clear: function() { - var old = dirtyMocks; - dirtyMocks = []; + var old = global.dirtyMocks; + global.dirtyMocks = []; old.forEach(function(mock) { mock.mockClear(); });