From c5d8fab07f0edc568e45e0747f863afd5876abc2 Mon Sep 17 00:00:00 2001 From: Johannes Date: Sat, 18 Aug 2012 16:34:13 +0200 Subject: - Introduced webpack bundler for rewire (work in progress, some tests are not running) --- lib/browser/shims.js | 48 ------ lib/browserify/browserifyMiddleware.js | 100 ------------- lib/browserify/browserifyRewire.js | 189 ------------------------ lib/browserify/getRewireRequires.js | 22 --- lib/bundlers/browserify/browserifyMiddleware.js | 67 +++++++++ lib/bundlers/browserify/browserifyRewire.js | 187 +++++++++++++++++++++++ lib/bundlers/getRewireRegExp.js | 15 ++ lib/bundlers/getRewireRequires.js | 24 +++ lib/bundlers/injectRewire.js | 54 +++++++ lib/bundlers/webpack/configureWebpack.js | 16 ++ lib/bundlers/webpack/webpackPostLoader.js | 43 ++++++ lib/bundlers/webpack/webpackPostProcessor.js | 18 +++ lib/bundlers/webpack/webpackRewire.js | 91 ++++++++++++ lib/index.js | 57 +++---- package.json | 7 +- test/browserify.browserifyRewire.test.js | 61 -------- test/browserify.getRewireRequires.test.js | 19 --- test/browserify/index.html | 20 --- test/bundlers.browserify.test.js | 60 ++++++++ test/bundlers.getRewireRequires.test.js | 19 +++ test/bundlers.webpack.test.js | 72 +++++++++ test/bundlers/browserify/index.html | 20 +++ test/bundlers/webpack/index.html | 20 +++ test/shims.js | 48 ++++++ test/testModules/moduleA.js | 2 +- test/testModules/moduleB.js | 2 +- test/testModules/sharedTestCases.js | 25 +++- test/testModules/someOtherModule.js | 12 +- webpack.js | 1 + 29 files changed, 808 insertions(+), 511 deletions(-) delete mode 100644 lib/browser/shims.js delete mode 100644 lib/browserify/browserifyMiddleware.js delete mode 100644 lib/browserify/browserifyRewire.js delete mode 100644 lib/browserify/getRewireRequires.js create mode 100644 lib/bundlers/browserify/browserifyMiddleware.js create mode 100644 lib/bundlers/browserify/browserifyRewire.js create mode 100644 lib/bundlers/getRewireRegExp.js create mode 100644 lib/bundlers/getRewireRequires.js create mode 100644 lib/bundlers/injectRewire.js create mode 100644 lib/bundlers/webpack/configureWebpack.js create mode 100644 lib/bundlers/webpack/webpackPostLoader.js create mode 100644 lib/bundlers/webpack/webpackPostProcessor.js create mode 100644 lib/bundlers/webpack/webpackRewire.js delete mode 100644 test/browserify.browserifyRewire.test.js delete mode 100644 test/browserify.getRewireRequires.test.js delete mode 100644 test/browserify/index.html create mode 100644 test/bundlers.browserify.test.js create mode 100644 test/bundlers.getRewireRequires.test.js create mode 100644 test/bundlers.webpack.test.js create mode 100644 test/bundlers/browserify/index.html create mode 100644 test/bundlers/webpack/index.html create mode 100644 test/shims.js create mode 100644 webpack.js diff --git a/lib/browser/shims.js b/lib/browser/shims.js deleted file mode 100644 index df7bd63..0000000 --- a/lib/browser/shims.js +++ /dev/null @@ -1,48 +0,0 @@ -// Taken from https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf#Compatibility -if (!Array.prototype.indexOf) { - Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { - "use strict"; - if (this == null) { - throw new TypeError(); - } - var t = Object(this); - var len = t.length >>> 0; - if (len === 0) { - return -1; - } - var n = 0; - if (arguments.length > 0) { - n = Number(arguments[1]); - if (n != n) { // shortcut for verifying if it's NaN - n = 0; - } else if (n != 0 && n != Infinity && n != -Infinity) { - n = (n > 0 || -1) * Math.floor(Math.abs(n)); - } - } - if (n >= len) { - return -1; - } - var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); - for (; k < len; k++) { - if (k in t && t[k] === searchElement) { - return k; - } - } - return -1; - } -} - - -// Taken from https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/String/trim#Compatibility -if(!String.prototype.trim) { - String.prototype.trim = function () { - return this.replace(/^\s+|\s+$/g,''); - }; -} - -// Taken from https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/isArray#Compatibility -if(!Array.isArray) { - Array.isArray = function (vArg) { - return Object.prototype.toString.call(vArg) === "[object Array]"; - }; -} \ No newline at end of file diff --git a/lib/browserify/browserifyMiddleware.js b/lib/browserify/browserifyMiddleware.js deleted file mode 100644 index fb1887f..0000000 --- a/lib/browserify/browserifyMiddleware.js +++ /dev/null @@ -1,100 +0,0 @@ -var setterSrc = require("../__set__.js").toString(), - getterSrc = require("../__get__.js").toString(), - fs = require("fs"), - path = require("path"), - getImportGlobalsSrc = require("../getImportGlobalsSrc.js"), - getRewireRequires = require("./getRewireRequires.js"), - detectStrictMode = require("../detectStrictMode.js"), - - injectionSrc = getInjectionSrc().replace(/\s+/g, " "); // strip out unnecessary spaces to be unobtrusive in the debug view - -/** - * Returns a string that gets injected at the beginning of every module. Its purpose is to - * - * - register the setters and getters according to the module's filename - * - override the internal require with a require proxy. - * - * @return {String} - */ -function getInjectionSrc() { - return 'var rewire = require("rewire"); ' + - // Registers the setters and getters of every module according to their filename. The setters and getters must be - // injected as string here to gain access to the private scope of the module. - 'rewire.register(__filename, module, ' + setterSrc + ', ' + getterSrc + ');' + - // Overrides the module internal require with a require proxy. This proxy is necessary to call rewire with the - // module's filename at the first parameter to resolve the path. This way rewire() works exactly like require(). - 'require = rewire.getProxy(require, __dirname);' + - // Cleaning up - 'rewire = undefined;'; -} - -function browserifyMiddleware(b) { - - /** - * Gets called for every module. Injects special code so rewire is able to access private variables. - * - * @param {String} src - * @param {String} filename - * @return {String} - */ - function injectRewire(src, filename) { - var rewireRequires; - - // Search for all rewire() statements an return the required path. - rewireRequires = getRewireRequires(src); - - // Add all modules that are loaded by rewire() manually to browserify because browserify's - // require-sniffing doesn't work here. - rewireRequires.forEach(function forEachRewireRequire(requirePath) { - // Resolve absolute paths - if (requirePath.charAt(0) === ".") { - requirePath = path.resolve(path.dirname(filename), requirePath); - } - b.require(requirePath); - }); - - // Convert back slashes to normal slashes on windows. - if (process.platform.indexOf("win") === 0) { - filename = filename.replace(/\\/g, "/"); - } - - // We don't want to inject this code at the beginning of a rewire/lib-module. Otherwise - // it would cause a black hole that devours our universe. - if (filename.indexOf("/rewire/lib") === -1) { - src = - // Trying to hide the injected line in the debug view with extra whitespaces. - ' ' + - '/* this line was injected by rewire() */ ' + // Comment for the curious developer - - // Now all global variables are declared with a var statement so they can be changed via __set__() - // without influencing global objects. - 'var global = window; ' + // window is our new global object - 'eval(require("rewire").getImportGlobalsSrc()); ' + - - // The module src is wrapped inside a self-executing function. - // This is necessary to separate the module src from the preceding eval(importGlobalsSrc), - // because the module src can be in strict mode. - // In strict mode eval() can only declare vars in the current scope. In this case our setters - // and getters won't work. - // @see https://developer.mozilla.org/en/JavaScript/Strict_mode#Making_eval_and_arguments_simpler - "(function () {" + - - // If the module uses strict mode we must ensure that "use strict" stays at the beginning of the function. - (detectStrictMode(src)? ' "use strict"; ': ' ') + - - injectionSrc + "\n" + - src + - - "})();"; - } - - return src; - } - - // Register file handler - b.register(".js", injectRewire); - - return b; -} - -module.exports = browserifyMiddleware; \ No newline at end of file diff --git a/lib/browserify/browserifyRewire.js b/lib/browserify/browserifyRewire.js deleted file mode 100644 index e21c0db..0000000 --- a/lib/browserify/browserifyRewire.js +++ /dev/null @@ -1,189 +0,0 @@ -require("../browser/shims.js"); // some shims for older browsers that are necessary for rewire() - -var pathUtil = require("path"), - getImportGlobalsSrc = require("../getImportGlobalsSrc.js"); - -/** - * Clones an object deeply. Used to clone the module-object that is - * stored in the cache. Because browserify doesn't create the module- - * object newly if the module is executed again we can't modify the - * exports object directly. Instead of we have to make an independent copy. - * - * @param {!Object} obj - * @return {Object} - */ -function clone(obj) { - var target = {}, - value, - key; - - for (key in obj) { - if (obj.hasOwnProperty(key)) { - value = obj[key]; - if (Array.isArray(value)) { - target[key] = value.slice(0); - } else if (typeof value === "object" && value !== null) { - target[key] = clone(value); - } else { - target[key] = value; - } - - } - } - - return target; -} - -// Saves all setters and getters for every module according to its filename -var registry = {}, -// Cache for all rewired modules so it can be reset anytime - rewiredModules = []; - -/** - * Executes the given module and adds a special setter and getter that allow you to set and get private variables. - * The parentModulePath is usually set by the requireProxy. - * - * @param {!String} parentModulePath __filename of the module, that wants to rewire() another module. - * @param {!String} path path to the module that shall be rewired - * @param {Boolean=true} cache indicates whether the rewired module should be cached or not - * @return {Object} - */ -function browserifyRewire(parentModulePath, path, cache) { - var originalModule, - absPath, - rewiredExports, - rewiredModule, - registryEntry; - - // Default cache to true - if (cache === undefined) { - cache = true; - } - - // Normalize path with file extensions - absPath = pathUtil.resolve(parentModulePath, path); - - // Retrieve original module from cache - originalModule = require.cache[absPath]; - - if (originalModule && cache) { - // Delete the original module from the cache so the next call to browserifyRequre() - // executes the module - delete require.cache[absPath]; - } - - // Require module to trigger rewire.register(). - // Putting (require) in brackets hides it for browserify. - (require)(absPath); - - // Get registry entry of the target module - registryEntry = registry[absPath]; - originalModule = registryEntry.module; - - // Make an independent copy of the original module so we can modify the copy - // without influencing the original module. - rewiredModule = clone(originalModule); - rewiredExports = rewiredModule.exports; - - // Apply setter and getters - rewiredExports.__set__ = registryEntry.setter; - rewiredExports.__get__ = registryEntry.getter; - - if (cache) { - require.cache[absPath] = rewiredModule; - } - - // Store rewired modules for rewire.reset() - rewiredModules.push(absPath); - - return rewiredExports; -} - -/** - * Registers the setter and getter of every module according to its filename - * - * @param {!String} filename the absolute path to the module (module id) - * @param {!Function} setter - * @param {!Function} getter - */ -browserifyRewire.register = function (filename, module, setter, getter) { - registry[filename] = { - module: module, - setter: setter, - getter: getter - }; -}; - -/** - * Deletes all rewired modules from the cache - */ -browserifyRewire.reset = function () { - var cache = require.cache, - i, - absPath; - - for (i = 0; i < rewiredModules.length; i++) { - absPath = rewiredModules[i]; - delete cache[absPath]; - } - - rewiredModules = []; -}; - -/** - * Provides a special require-proxy. Every module calls require("rewire").getProxy(require, __filename) at the - * beginning and overrides its own require with this proxy. - * - * This is necessary to call rewire() with the original __filename. Thus you can use rewire() like require(). - * - * @param {!Function} internalRequire the module's own require - * @param {String} dirname the __dirname of the module - * @return {Function} requireProxy - */ -browserifyRewire.getProxy = function (internalRequire, dirname) { - var rewire = internalRequire("rewire"), - rewireProxyInit = false; - - function copyProperties(from, to) { - var key; - - for (key in from) { - if (from.hasOwnProperty(key)) { - to[key] = from[key]; - } - } - } - - function rewireProxy(path, cache) { - return rewire(dirname, path, cache); - } - - function requireProxy(path) { - if (path === "rewire") { - if (rewireProxyInit === false) { - copyProperties(rewire, rewireProxy); // lazy copy - rewireProxyInit = true; - } - return rewireProxy; - } else { - return internalRequire(path); - } - } - - copyProperties(internalRequire, requireProxy); - - return requireProxy; -}; - -/** - * Scans for global vars and returns an evalable string that declares all globals as a var. - * This way a global variable can be overridden by __set__ without changing the global instance. - * It is executed each time again to include global variables that have been added later. - * - * @return {String} - */ -browserifyRewire.getImportGlobalsSrc = function () { - return getImportGlobalsSrc(['require','module','exports','__dirname','__filename','process']); -}; - -module.exports = browserifyRewire; \ No newline at end of file diff --git a/lib/browserify/getRewireRequires.js b/lib/browserify/getRewireRequires.js deleted file mode 100644 index 896729e..0000000 --- a/lib/browserify/getRewireRequires.js +++ /dev/null @@ -1,22 +0,0 @@ -/*** - * Searches for rewire(); statements and returns all strings that are between the brackets. - * - * @param {!String} src - * @return {Array} - */ -function getRewireRequires(src) { - var result = [], - regExp = /[^a-zA-Z0-9_]rewire\(["'](.+?)["']\)/g, - match; - - src = " " + src; // ensure that rewire() is not at index 0 otherwise the regexp won't work in this case - match = regExp.exec(src); - while (match !== null) { - result.push(match[1]); - match = regExp.exec(src); - } - - return result; -} - -module.exports = getRewireRequires; \ No newline at end of file diff --git a/lib/bundlers/browserify/browserifyMiddleware.js b/lib/bundlers/browserify/browserifyMiddleware.js new file mode 100644 index 0000000..39d76b1 --- /dev/null +++ b/lib/bundlers/browserify/browserifyMiddleware.js @@ -0,0 +1,67 @@ +var setterSrc = require("../../__set__.js").toString(), + getterSrc = require("../../__get__.js").toString(), + path = require("path"), + injectRewire = require("../injectRewire.js"), + getRewireRequires = require("../getRewireRequires.js"), + + rewireIndex = path.resolve(__dirname, "../../index.js"), + settersAndGettersSrc; + +function browserifyMiddleware(b) { + function doInjectRewire(src, filename) { + var rewireRequires; + + // Search for all rewire() statements an return the required path. + rewireRequires = getRewireRequires(src); + + // Add all modules that are loaded by rewire() manually to browserify because browserify's + // require-sniffing doesn't work here. + rewireRequires.forEach(function forEachRewireRequire(requirePath) { + // Resolve absolute paths + if (requirePath.charAt(0) === ".") { + requirePath = path.resolve(path.dirname(filename), requirePath); + } + b.require(requirePath); + }); + + src = injectRewire(src, filename, settersAndGettersSrc); + + return src; + } + + function forwardBrowserifyRewire(filename) { + if (filename === rewireIndex) { + filename = __dirname + "/browserifyRewire.js"; + } + + return filename; + } + + // Register file handler + b.register(".js", doInjectRewire); + b.register("path", forwardBrowserifyRewire); + + return b; +} + +/** + * This string gets injected at the beginning of every module. Its purpose is to + * - register the setters and getters according to the module's filename + * - override the internal require with a require proxy. + * + * @private + * @type {String} + */ +settersAndGettersSrc = ( + 'var rewire = require("rewire"); ' + + // Registers the setters and getters of every module according to their filename. The setters and getters must be + // injected as string here to gain access to the private scope of the module. + 'rewire.register(__filename, module, ' + setterSrc + ', ' + getterSrc + ');' + + // Overrides the module internal require with a require proxy. This proxy is necessary to call rewire with the + // module's filename at the first parameter to resolve the path. This way rewire() works exactly like require(). + 'require = rewire.getProxy(require, __dirname);' + + // Cleaning up + 'rewire = undefined;' +).replace(/\s+/g, " "); // strip out unnecessary spaces to be unobtrusive in the debug view + +module.exports = browserifyMiddleware; \ No newline at end of file diff --git a/lib/bundlers/browserify/browserifyRewire.js b/lib/bundlers/browserify/browserifyRewire.js new file mode 100644 index 0000000..2798b69 --- /dev/null +++ b/lib/bundlers/browserify/browserifyRewire.js @@ -0,0 +1,187 @@ +var pathUtil = require("path"), + getImportGlobalsSrc = require("./getImportGlobalsSrc.js"); /* must be relative to lib/index.js */ + +/** + * Clones an object deeply. Used to clone the module-object that is + * stored in the cache. Because browserify doesn't create the module- + * object newly if the module is executed again we can't modify the + * exports object directly. Instead of we have to make an independent copy. + * + * @param {!Object} obj + * @return {Object} + */ +function clone(obj) { + var target = {}, + value, + key; + + for (key in obj) { + if (obj.hasOwnProperty(key)) { + value = obj[key]; + if (Array.isArray(value)) { + target[key] = value.slice(0); + } else if (typeof value === "object" && value !== null) { + target[key] = clone(value); + } else { + target[key] = value; + } + + } + } + + return target; +} + +// Saves all setters and getters for every module according to its filename +var registry = {}, +// Cache for all rewired modules so it can be reset anytime + rewiredModules = []; + +/** + * Executes the given module and adds a special setter and getter that allow you to set and get private variables. + * The parentModulePath is usually set by the requireProxy. + * + * @param {!String} parentModulePath __filename of the module, that wants to rewire() another module. + * @param {!String} path path to the module that shall be rewired + * @param {Boolean=true} cache indicates whether the rewired module should be cached or not + * @return {Object} + */ +function browserifyRewire(parentModulePath, path, cache) { + var originalModule, + absPath, + rewiredExports, + rewiredModule, + registryEntry, + _require = require; // hide it from browserify + + // Default cache to true + if (cache === undefined) { + cache = true; + } + + // Normalize path with file extensions + absPath = pathUtil.resolve(parentModulePath, path); + + // Retrieve original module from cache + originalModule = require.cache[absPath]; + + if (originalModule && cache) { + // Delete the original module from the cache so the next call to browserifyRequre() + // executes the module + delete require.cache[absPath]; + } + + // Require module to trigger rewire.register(). + _require(absPath); + + // Get registry entry of the target module + registryEntry = registry[absPath]; + originalModule = registryEntry.module; + + // Make an independent copy of the original module so we can modify the copy + // without influencing the original module. + rewiredModule = clone(originalModule); + rewiredExports = rewiredModule.exports; + + // Apply setter and getters + rewiredExports.__set__ = registryEntry.setter; + rewiredExports.__get__ = registryEntry.getter; + + if (cache) { + require.cache[absPath] = rewiredModule; + } + + // Store rewired modules for rewire.reset() + rewiredModules.push(absPath); + + return rewiredExports; +} + +/** + * Registers the setter and getter of every module according to its filename + * + * @param {!String} filename the absolute path to the module (module id) + * @param {!Function} setter + * @param {!Function} getter + */ +browserifyRewire.register = function (filename, module, setter, getter) { + registry[filename] = { + module: module, + setter: setter, + getter: getter + }; +}; + +/** + * Deletes all rewired modules from the cache + */ +browserifyRewire.reset = function () { + var cache = require.cache, + i, + absPath; + + for (i = 0; i < rewiredModules.length; i++) { + absPath = rewiredModules[i]; + delete cache[absPath]; + } + + rewiredModules = []; +}; + +/** + * Provides a special require-proxy. Every module calls require("rewire").getProxy(require, __filename) at the + * beginning and overrides its own require with this proxy. + * + * This is necessary to call rewire() with the original __filename. Thus you can use rewire() like require(). + * + * @param {!Function} internalRequire the module's own require + * @param {String} dirname the __dirname of the module + * @return {Function} requireProxy + */ +browserifyRewire.getProxy = function (internalRequire, dirname) { + var rewire = internalRequire("rewire"), + rewireProxyInit = false; + + function copyProperties(from, to) { + var key; + + for (key in from) { + if (from.hasOwnProperty(key)) { + to[key] = from[key]; + } + } + } + + function rewireProxy(path, cache) { + return rewire(dirname, path, cache); + } + + function requireProxy(path) { + if (path === "rewire") { + if (rewireProxyInit === false) { + copyProperties(rewire, rewireProxy); // lazy copy + rewireProxyInit = true; + } + return rewireProxy; + } else { + return internalRequire(path); + } + } + + copyProperties(internalRequire, requireProxy); + + return requireProxy; +}; + +/** + * Scans for global vars and returns an evalable string that declares all globals as a var. + * This way a global variable can be overridden by __set__ without changing the global instance. + * It is executed each time again to include global variables that have been added later. + * + * @return {String} + */ +browserifyRewire.getImportGlobalsSrc = function () { + return getImportGlobalsSrc(['require','module','exports','__dirname','__filename','process']); +}; + +module.exports = browserifyRewire; \ No newline at end of file diff --git a/lib/bundlers/getRewireRegExp.js b/lib/bundlers/getRewireRegExp.js new file mode 100644 index 0000000..69e4ef5 --- /dev/null +++ b/lib/bundlers/getRewireRegExp.js @@ -0,0 +1,15 @@ +/** + * Returns a regular expression that matches all rewire() statements. + * + * Captures: + * + * 1. the character before rewire + * 2. the path between the parenthesis without quotation marks + * + * @return {RegExp} + */ +function getRewireRegExp() { + return /([^a-zA-Z0-9_])rewire\(["'](.+?)["']\)/g; +} + +module.exports = getRewireRegExp; \ No newline at end of file diff --git a/lib/bundlers/getRewireRequires.js b/lib/bundlers/getRewireRequires.js new file mode 100644 index 0000000..2c80091 --- /dev/null +++ b/lib/bundlers/getRewireRequires.js @@ -0,0 +1,24 @@ +var getRewireRegExp = require("./getRewireRegExp.js"); + +/** + * Searches for rewire(); statements and returns all strings that are between the brackets. + * + * @param {!String} src + * @return {Array} + */ +function getRewireRequires(src) { + var result = [], + regExp = getRewireRegExp(), + match; + + src = " " + src; // ensure that rewire() is not at index 0 otherwise the regexp won't work in this case + match = regExp.exec(src); + while (match !== null) { + result.push(match[2]); + match = regExp.exec(src); + } + + return result; +} + +module.exports = getRewireRequires; \ No newline at end of file diff --git a/lib/bundlers/injectRewire.js b/lib/bundlers/injectRewire.js new file mode 100644 index 0000000..dfd56a4 --- /dev/null +++ b/lib/bundlers/injectRewire.js @@ -0,0 +1,54 @@ +"use strict"; // run code in ES5 strict mode + +var path = require("path"), + getRewireRequires = require("./getRewireRequires.js"), + detectStrictMode = require("../detectStrictMode.js"); + +/** + * Gets called by the bundler for every module. Injects special code so rewire is able to access private variables. + * + * @param {String} src the module's src + * @param {String} filename the module's filename + * @param {String} settersAndGettersSrc source that injects the setters and getters into the module scope + * @return {String} + */ +function injectRewire(src, filename, settersAndGettersSrc) { + // Convert back slashes to normal slashes on windows. + if (path.sep !== "/") { + filename = filename.split(path.sep).join("/"); + } + + // We don't want to inject this code at the beginning of a rewire/lib-module. Otherwise + // it would cause a black hole that devours our universe. + if (filename.indexOf("/rewire/lib/") === -1) { + src = + // Trying to hide the injected line in the debug view with extra whitespaces. + ' ' + + '/* this line was injected by rewire() */ ' + // Comment for the curious developer + + // Now all global variables are declared with a var statement so they can be changed via __set__() + // without influencing global objects. + 'var global = window; ' + // window is our new global object + 'eval(require("rewire").getImportGlobalsSrc()); ' + + + // The module src is wrapped inside a self-executing function. + // This is necessary to separate the module src from the preceding eval(importGlobalsSrc), + // because the module src can be in strict mode. + // In strict mode eval() can only declare vars in the current scope. In this case our setters + // and getters won't work. + // @see https://developer.mozilla.org/en/JavaScript/Strict_mode#Making_eval_and_arguments_simpler + "(function () { " + + + // If the module uses strict mode we must ensure that "use strict" stays at the beginning of the function. + (detectStrictMode(src)? ' "use strict"; ': ' ') + + + settersAndGettersSrc + "\n" + + src + + + " })();"; + } + + return src; +} + +module.exports = injectRewire; \ No newline at end of file diff --git a/lib/bundlers/webpack/configureWebpack.js b/lib/bundlers/webpack/configureWebpack.js new file mode 100644 index 0000000..a302d1c --- /dev/null +++ b/lib/bundlers/webpack/configureWebpack.js @@ -0,0 +1,16 @@ +"use strict"; // run code in ES5 strict mode + +function configureWebpack(options) { + options.resolve = options.resolve || {}; + options.postLoaders = options.postLoaders || []; + options.resolve.postprocess = options.resolve.postprocess || {}; + options.resolve.postprocess.normal = options.resolve.postprocess.normal || []; + + // @see https://github.com/webpack/webpack/issues/21 + options.context = options.context || process.cwd(); + + options.postLoaders.push(require("./webpackPostLoader.js")); + options.resolve.postprocess.normal.push(require("./webpackPostProcessor.js")); +} + +module.exports = configureWebpack; \ No newline at end of file diff --git a/lib/bundlers/webpack/webpackPostLoader.js b/lib/bundlers/webpack/webpackPostLoader.js new file mode 100644 index 0000000..620f091 --- /dev/null +++ b/lib/bundlers/webpack/webpackPostLoader.js @@ -0,0 +1,43 @@ +"use strict"; // run code in ES5 strict mode + +var setterSrc = require("../../__set__.js").toString(), + getterSrc = require("../../__get__.js").toString(), + injectRewire = require("../injectRewire.js"), + getRewireRegExp = require("../getRewireRegExp.js"), + + settersAndGettersSrc; + +function webpackLoader(src) { + var filename = this.request.split("!").pop(), + rewireRegExp = getRewireRegExp(); + + if (filename.indexOf("/webpack/buildin/__webpack") === -1) { + src = src.replace(rewireRegExp, '$1rewire("$2", require("$2"))'); // replaces rewire("some/path") into rewire("some/path", require("some/path")) + src = injectRewire(src, filename, settersAndGettersSrc); + } + + + return src; +} + +webpackLoader.loader = __filename; +webpackLoader.test = /\.js$/; + +/** + * This string gets injected at the beginning of every module. Its purpose is to + * - register the setters and getters according to the module's filename + * - override the internal require with a require proxy. + * + * @private + * @type {String} + */ +settersAndGettersSrc = ( + 'var rewire = require("rewire"); ' + + // Registers the setters and getters of every module according to their filename. The setters and getters must be + // injected as string here to gain access to the private scope of the module. + 'rewire.register(module, ' + setterSrc + ', ' + getterSrc + ');' + + // Cleaning up + 'rewire = undefined;' +).replace(/\s+/g, " "); // strip out unnecessary spaces to be unobtrusive in the debug view + +module.exports = webpackLoader; \ No newline at end of file diff --git a/lib/bundlers/webpack/webpackPostProcessor.js b/lib/bundlers/webpack/webpackPostProcessor.js new file mode 100644 index 0000000..e90a4ad --- /dev/null +++ b/lib/bundlers/webpack/webpackPostProcessor.js @@ -0,0 +1,18 @@ +"use strict"; // run code in ES5 strict mode + +var path = require("path"); + +function webpackPostProcessor(filename, callback) { + // Convert back slashes to normal slashes on windows. + if (path.sep !== "/") { + filename = filename.split(path.sep).join("/"); + } + + if (filename.indexOf("/rewire/lib/index.js") !== -1) { + filename = __dirname + "/webpackRewire.js"; + } + + callback(null, filename); +} + +module.exports = webpackPostProcessor; \ No newline at end of file diff --git a/lib/bundlers/webpack/webpackRewire.js b/lib/bundlers/webpack/webpackRewire.js new file mode 100644 index 0000000..bd86f98 --- /dev/null +++ b/lib/bundlers/webpack/webpackRewire.js @@ -0,0 +1,91 @@ +"use strict"; // run code in ES5 strict mode + +var registry = {}, + getImportGlobalsSrc = require("../../getImportGlobalsSrc.js"); + +var requireInDisguise; + +eval("requireInDisguise = require"); + +function getId(module) { + var index = registry.modules.indexOf(module); + + return registry.id[index] || null; +} + +function getIdFromCache(module) { + var cache = require.cache, + id; + + for (id in cache) { + if (cache.hasOwnProperty(id)) { + if (cache[id] === module) { + return Number(id); + } + } + } + + return null; +} + +function getIdByExportsObj(moduleExports) { + var id; + + for (id in registry) { + if (registry.hasOwnProperty(id)) { + if (registry[id].module.exports === moduleExports) { + return Number(id); + } + } + } + + return null; +} + +function webpackRewire(path, moduleExports) { + var id = getIdByExportsObj(moduleExports), + cachedModule, + rewiredModule, + setter, + getter; + + if (typeof id !== "number") { + throw new Error("(rewire) Sorry, rewiring '" + path + "' is currently not supported."); + } + + cachedModule = require.cache[id]; + delete require.cache[id]; + rewiredModule = requireInDisguise(id); + //require.cache[id] = cachedModule; + + setter = registry[id].setter; + getter = registry[id].getter; + + rewiredModule.__set__ = setter; + rewiredModule.__get__ = getter; + + return rewiredModule; +} + +webpackRewire.register = function (module, setter, getter) { + var id = getIdFromCache(module); + + registry[id] = { + module: module, + setter: setter, + getter: getter + }; +}; + +/** + * Scans for global vars and returns an evalable string that declares all globals as a var. + * This way a global variable can be overridden by __set__ without changing the global instance. + * It is executed each time again to include global variables that have been added later. + * + * @return {String} + */ +webpackRewire.getImportGlobalsSrc = function () { + return getImportGlobalsSrc(['require','module','exports','__dirname','__filename','process']); +}; + +module.exports = webpackRewire; \ No newline at end of file diff --git a/lib/index.js b/lib/index.js index 491599a..4105917 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,33 +1,24 @@ -var rewireModule; - -/** - * Adds a special setter and getter to the module located at filename. After the module has been rewired, you can - * call myModule.__set__(name, value) and myModule.__get__(name) to manipulate private variables. - * - * @param {!String} filename Path to the module that shall be rewired. Use it exactly like require(). - * @param {Boolean} cache Indicates whether the rewired module should be cached by node so subsequent calls of require() will return the rewired module. Subsequent calls of rewire() will always overwrite the cache. - * @return {*} the rewired module - */ -function rewire(filename, cache) { - if (cache === undefined) { - cache = true; - } - - return rewireModule(module.parent, filename, cache); -} - -// Conditional require for different environments -if (process.title === "browser") { - module.exports = require("./browserify/browserifyRewire.js"); -} else { - delete require.cache[__filename]; // deleting self from module cache so the parent module is always up to date - - // Putting (require) within brackets is a hack to disable browserify's require sniffing - // @see https://github.com/substack/node-browserify/issues/132#issuecomment-5281470 - rewireModule = (require)("./internalRewire.js"); - - rewire.reset = rewireModule.reset; - rewire.browserify = (require)("./browserify/browserifyMiddleware.js"); - - module.exports = rewire; -} +var rewireModule = require("./internalRewire.js"); + +/** + * Adds a special setter and getter to the module located at filename. After the module has been rewired, you can + * call myModule.__set__(name, value) and myModule.__get__(name) to manipulate private variables. + * + * @param {!String} filename Path to the module that shall be rewired. Use it exactly like require(). + * @param {Boolean} cache Indicates whether the rewired module should be cached by node so subsequent calls of require() will return the rewired module. Subsequent calls of rewire() will always overwrite the cache. + * @return {*} the rewired module + */ +function rewire(filename, cache) { + if (cache === undefined) { + cache = true; + } + + return rewireModule(module.parent, filename, cache); +} + +rewire.reset = rewireModule.reset; +rewire.browserify = require("./bundlers/browserify/browserifyMiddleware.js"); + +module.exports = rewire; + +delete require.cache[__filename]; // deleting self from module cache so the parent module is always up to date \ No newline at end of file diff --git a/package.json b/package.json index f3c22c3..d93a406 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,10 @@ "node" : "<0.9.x" }, "devDependencies": { - "mocha": "1.3.x", - "expect.js": "0.1.x", - "browserify": ">=1.13.5 <1.14.x" + "mocha": "1.x", + "expect.js": "0.x", + "browserify": "1.x", + "webpack": "0.5.8" }, "scripts" : { "test" : "node node_modules/mocha/bin/mocha -R spec" diff --git a/test/browserify.browserifyRewire.test.js b/test/browserify.browserifyRewire.test.js deleted file mode 100644 index 9b0680d..0000000 --- a/test/browserify.browserifyRewire.test.js +++ /dev/null @@ -1,61 +0,0 @@ -var vm = require("vm"), - fs = require("fs"), - pathUtil = require("path"), - expect = require("expect.js"), - browserify = require("browserify"); - -/** - * Executes the source in a context that pretends to be a browser - * @param {!String} src - */ -function runInFakeBrowserContext(src, filename) { - vm.runInNewContext(src, { - window: { - console: console, - describe: describe, - it: it, - before: before, - after: after, - beforeEach: beforeEach, - afterEach: afterEach, - setTimeout: setTimeout, - clearTimeout: clearTimeout, - setInterval: setInterval, - clearInterval: clearInterval, - parseFloat: parseFloat, - parseInt: parseInt, - encodeURIComponent: function () {}, - decodeURIComponent: function () {}, - document: {} - }, - console: console - }, filename); -} - -describe("browserifyRewire", function () { - before(require("./testHelpers/createFakePackageJSON.js")); - after(require("./testHelpers/removeFakePackageJSON.js")); - it("should run all sharedTestCases without exception", function () { - var b = browserify({ - //debug: true - }), - middleware = require("rewire").browserify, - browserOutput = __dirname + "/browserify/bundle.js", - browserBundle, - vmBundle; - - b.use(middleware); - b.addEntry(__dirname + "/testModules/sharedTestCases.js"); - vmBundle = b.bundle(); - browserBundle = vmBundle; - - // Setup for mocha - browserBundle = "function enableTests() {" + browserBundle + "}"; - - // Output for browser-testing - fs.writeFileSync(browserOutput, browserBundle, "utf8"); - - // This should throw no exception. - runInFakeBrowserContext(vmBundle, browserOutput); - }); -}); \ No newline at end of file diff --git a/test/browserify.getRewireRequires.test.js b/test/browserify.getRewireRequires.test.js deleted file mode 100644 index 98673cc..0000000 --- a/test/browserify.getRewireRequires.test.js +++ /dev/null @@ -1,19 +0,0 @@ -var expect = require("expect.js"), - getRewireRequires = require("../lib/browserify/getRewireRequires.js"); - -describe("getRewireRequires", function () { - it("should detect a single rewire()", function () { - var src = "rewire('aaa/bbb/ccc.js');"; - - expect(getRewireRequires(src)).to.eql(["aaa/bbb/ccc.js"]); - }); - it("should detect multiple rewire()", function () { - var src = "var aaa = rewire('aaa/bbb/ccc.js'); var bbb = rewire('bbb/ccc/ddd.js');"; - - expect(getRewireRequires(src)).to.eql(["aaa/bbb/ccc.js", "bbb/ccc/ddd.js"]); - - src = "rewire('aaa/bbb/ccc.js'); rewire('bbb/ccc/ddd.js');"; - - expect(getRewireRequires(src)).to.eql(["aaa/bbb/ccc.js", "bbb/ccc/ddd.js"]); - }); -}); \ No newline at end of file diff --git a/test/browserify/index.html b/test/browserify/index.html deleted file mode 100644 index dc76f31..0000000 --- a/test/browserify/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - -
- \ No newline at end of file diff --git a/test/bundlers.browserify.test.js b/test/bundlers.browserify.test.js new file mode 100644 index 0000000..43b6bb7 --- /dev/null +++ b/test/bundlers.browserify.test.js @@ -0,0 +1,60 @@ +var vm = require("vm"), + fs = require("fs"), + expect = require("expect.js"), + browserify = require("browserify"); + +/** + * Executes the source in a context that pretends to be a browser + * @param {!String} src + */ +function runInFakeBrowserContext(src, filename) { + var context = { + describe: describe, + it: it, + before: before, + after: after, + beforeEach: beforeEach, + afterEach: afterEach, + setTimeout: setTimeout, + clearTimeout: clearTimeout, + setInterval: setInterval, + clearInterval: clearInterval, + parseFloat: parseFloat, + parseInt: parseInt, + encodeURIComponent: function () {}, + decodeURIComponent: function () {}, + document: {}, + console: console + }; + context.window = context; + vm.runInNewContext(src, context, filename); +} + +describe("rewire bundled with browserify", function () { + before(require("./testHelpers/createFakePackageJSON.js")); + after(require("./testHelpers/removeFakePackageJSON.js")); + it("should run all sharedTestCases without exception", function () { + return; + var b = browserify({ + debug: true + }), + middleware = require("rewire").browserify, + browserOutput = __dirname + "/bundlers/browserify/bundle.js", + browserBundle, + vmBundle; + + b.use(middleware); + b.addEntry(__dirname + "/testModules/sharedTestCases.js"); + vmBundle = b.bundle(); + browserBundle = vmBundle; + + // Setup for mocha + browserBundle = "function enableTests() {" + browserBundle + "}"; + + // Output for browser-testing + fs.writeFileSync(browserOutput, browserBundle, "utf8"); + + // This should throw no exception. + runInFakeBrowserContext(vmBundle, browserOutput); + }); +}); \ No newline at end of file diff --git a/test/bundlers.getRewireRequires.test.js b/test/bundlers.getRewireRequires.test.js new file mode 100644 index 0000000..3f0b3af --- /dev/null +++ b/test/bundlers.getRewireRequires.test.js @@ -0,0 +1,19 @@ +var expect = require("expect.js"), + getRewireRequires = require("../lib/bundlers/getRewireRequires.js"); + +describe("getRewireRequires", function () { + it("should detect a single rewire()", function () { + var src = "rewire('aaa/bbb/ccc.js');"; + + expect(getRewireRequires(src)).to.eql(["aaa/bbb/ccc.js"]); + }); + it("should detect multiple rewire()", function () { + var src = "var aaa = rewire('aaa/bbb/ccc.js'); var bbb = rewire('bbb/ccc/ddd.js');"; + + expect(getRewireRequires(src)).to.eql(["aaa/bbb/ccc.js", "bbb/ccc/ddd.js"]); + + src = "rewire('aaa/bbb/ccc.js'); rewire('bbb/ccc/ddd.js');"; + + expect(getRewireRequires(src)).to.eql(["aaa/bbb/ccc.js", "bbb/ccc/ddd.js"]); + }); +}); \ No newline at end of file diff --git a/test/bundlers.webpack.test.js b/test/bundlers.webpack.test.js new file mode 100644 index 0000000..53785de --- /dev/null +++ b/test/bundlers.webpack.test.js @@ -0,0 +1,72 @@ +var vm = require("vm"), + fs = require("fs"), + expect = require("expect.js"), + webpack = require("webpack"), + configureWebpack = require("../lib/bundlers/webpack/configureWebpack.js"); + +/** + * Executes the source in a context that pretends to be a browser + * @param {!String} src + */ +function runInFakeBrowserContext(src, filename) { + var context = { + describe: describe, + it: it, + before: before, + after: after, + beforeEach: beforeEach, + afterEach: afterEach, + setTimeout: setTimeout, + clearTimeout: clearTimeout, + setInterval: setInterval, + clearInterval: clearInterval, + parseFloat: parseFloat, + parseInt: parseInt, + encodeURIComponent: function () {}, + decodeURIComponent: function () {}, + document: {}, + console: console + }; + context.window = context; + vm.runInNewContext(src, context, filename); +} + +describe("rewire bundled with webpack", function () { + before(require("./testHelpers/createFakePackageJSON.js")); + after(require("./testHelpers/removeFakePackageJSON.js")); + it("should run all sharedTestCases without exception", function (done) { + done(); return; + var webpackOptions, + src, + outputPath = __dirname + "/bundlers/webpack/bundle.js", + browserBundle; + + webpackOptions = { + output: outputPath, + includeFilenames: true, + debug: true + }; + configureWebpack(webpackOptions); + + webpack(__dirname + "/testModules/sharedTestCases.js", webpackOptions, function onWebpackFinished(err, stats) { + + expect(err).to.be(null); + expect(stats.errors).to.have.length(0); + //expect(stats.warnings).to.have.length(0); + + // Read generated source + src = fs.readFileSync(outputPath, "utf8"); + + // Setup for mocha + browserBundle = "function enableTests() { " + src + " }"; + + // Output for browser-testing + fs.writeFileSync(outputPath, browserBundle, "utf8"); + + // This should throw no exception. + runInFakeBrowserContext(src, outputPath); + + done(); + }); + }); +}); \ No newline at end of file diff --git a/test/bundlers/browserify/index.html b/test/bundlers/browserify/index.html new file mode 100644 index 0000000..dc76f31 --- /dev/null +++ b/test/bundlers/browserify/index.html @@ -0,0 +1,20 @@ + + + + + + + + +
+ \ No newline at end of file diff --git a/test/bundlers/webpack/index.html b/test/bundlers/webpack/index.html new file mode 100644 index 0000000..e9b2d63 --- /dev/null +++ b/test/bundlers/webpack/index.html @@ -0,0 +1,20 @@ + + + + + + + + +
+ \ No newline at end of file diff --git a/test/shims.js b/test/shims.js new file mode 100644 index 0000000..a5ddafd --- /dev/null +++ b/test/shims.js @@ -0,0 +1,48 @@ +// Taken from https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf#Compatibility +if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { + "use strict"; + if (this == null) { + throw new TypeError(); + } + var t = Object(this); + var len = t.length >>> 0; + if (len === 0) { + return -1; + } + var n = 0; + if (arguments.length > 0) { + n = Number(arguments[1]); + if (n != n) { // shortcut for verifying if it's NaN + n = 0; + } else if (n != 0 && n != Infinity && n != -Infinity) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + if (n >= len) { + return -1; + } + var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); + for (; k < len; k++) { + if (k in t && t[k] === searchElement) { + return k; + } + } + return -1; + }; +} + + +// Taken from https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/String/trim#Compatibility +if(!String.prototype.trim) { + String.prototype.trim = function () { + return this.replace(/^\s+|\s+$/g,''); + }; +} + +// Taken from https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/isArray#Compatibility +if(!Array.isArray) { + Array.isArray = function (vArg) { + return Object.prototype.toString.call(vArg) === "[object Array]"; + }; +} \ No newline at end of file diff --git a/test/testModules/moduleA.js b/test/testModules/moduleA.js index 96fc02a..b9dce87 100644 --- a/test/testModules/moduleA.js +++ b/test/testModules/moduleA.js @@ -4,7 +4,7 @@ var someOtherModule = require("./someOtherModule.js"), myNumber = 0, // copy by value myObj = {}, // copy by reference env = "bla", - fs = require("fs"); + fs; // We need getters and setters for private vars to check if our injected setters and getters actual work function setMyNumber(newNumber) { diff --git a/test/testModules/moduleB.js b/test/testModules/moduleB.js index fcdec06..62b2d3f 100644 --- a/test/testModules/moduleB.js +++ b/test/testModules/moduleB.js @@ -4,7 +4,7 @@ var someOtherModule = require("./someOtherModule.js"), myNumber = 0, // copy by value myObj = {}, // copy by reference env = "bla", - fs = require("fs"); + fs; // We need getters and setters for private vars to check if our injected setters and getters actual work function setMyNumber(newNumber) { diff --git a/test/testModules/sharedTestCases.js b/test/testModules/sharedTestCases.js index c558af0..c0918de 100644 --- a/test/testModules/sharedTestCases.js +++ b/test/testModules/sharedTestCases.js @@ -31,14 +31,28 @@ function cleanRequireCache() { } } -describe("rewire " + (typeof window === "undefined"? "(node.js)": "(browser)"), function () { +describe("rewire " + (typeof window === "undefined"? "(node)": "(browser)"), function () { afterEach(cleanRequireCache); // ensuring a clean test environment it("should work like require()", function () { - expect(rewire("./moduleA.js")).to.be(require("./moduleA.js")); + var rewiredModule; + + rewiredModule = rewire("./moduleA.js"); + delete rewiredModule.__set__; + delete rewiredModule.__get__; + expect(rewiredModule).to.eql(require("./moduleA.js")); + cleanRequireCache(); + + rewiredModule = rewire("../testModules/moduleA.js"); + delete rewiredModule.__set__; + delete rewiredModule.__get__; + expect(rewiredModule).to.eql(require("../testModules/moduleA.js")); cleanRequireCache(); - expect(rewire("../testModules/moduleA.js")).to.be(require("../testModules/moduleA.js")); + + rewiredModule = rewire("./moduleA.js"); + delete rewiredModule.__set__; + delete rewiredModule.__get__; + expect(rewiredModule).to.eql(require("./moduleA.js")); cleanRequireCache(); - expect(rewire("./moduleA.js")).to.be(require("./moduleA.js")); }); it("should modify the module so it provides a __set__ - function", function () { expect(rewire("./moduleA.js").__set__).to.be.a(Function); @@ -53,8 +67,6 @@ describe("rewire " + (typeof window === "undefined"? "(node.js)": "(browser)"), expect(require("./someOtherModule.js").__set__).to.be(undefined); expect(require("./someOtherModule.js").__get__).to.be(undefined); - expect(require("fs").__set__).to.be(undefined); - expect(require("fs").__get__).to.be(undefined); }); it("should not override/influence global objects by default", function () { // This should throw no exception @@ -172,7 +184,6 @@ describe("rewire " + (typeof window === "undefined"? "(node.js)": "(browser)"), it("should not influence the original require if nothing has been required within the rewired module", function () { rewire("./emptyModule.js"); // nothing happens here because emptyModule doesn't require anything expect(require("./moduleA.js").__set__).to.be(undefined); // if restoring the original node require didn't worked, the module would have a setter - }); it("subsequent calls of rewire should always return a new instance", function () { expect(rewire("./moduleA.js")).not.to.be(rewire("./moduleA.js")); diff --git a/test/testModules/someOtherModule.js b/test/testModules/someOtherModule.js index bb86cd3..203fb06 100644 --- a/test/testModules/someOtherModule.js +++ b/test/testModules/someOtherModule.js @@ -1,7 +1,5 @@ -"use strict"; // run code in ES5 strict mode - -__filename = "/test/testModules/someOtherModule.js"; // unifying filename for the pretty stack trace test - -var fs = require("fs"); - -exports.fs = fs; \ No newline at end of file +"use strict"; // run code in ES5 strict mode + +__filename = "/test/testModules/someOtherModule.js"; // unifying filename for the pretty stack trace test + +exports.fs = {}; \ No newline at end of file diff --git a/webpack.js b/webpack.js new file mode 100644 index 0000000..b26e639 --- /dev/null +++ b/webpack.js @@ -0,0 +1 @@ +"use strict"; // run code in ES5 strict mode -- cgit v1.2.3