├── .gitignore ├── .jshintrc ├── Gruntfile.js ├── LICENSE-MIT ├── README.md ├── dist ├── ba-hooker.js └── ba-hooker.min.js ├── lib ├── .jshintrc └── hooker.js ├── package.json └── test └── hooker_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": "nofunc", 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": "vars", 11 | "boss": true, 12 | "eqnull": true, 13 | "node": true 14 | } 15 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | // Metadata. 6 | pkg: grunt.file.readJSON('package.json'), 7 | banner: '/*! JavaScript Hooker - v<%= pkg.version %> - ' + 8 | '<%= grunt.template.today("yyyy-mm-dd") %>\n' + 9 | '<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' + 10 | '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + 11 | ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */\n', 12 | clean: { 13 | dist: 'dist', 14 | }, 15 | concat: { 16 | options: { 17 | banner: '<%= banner %>\n', 18 | stripBanners: true, 19 | }, 20 | dist: { 21 | src: ['lib/<%= pkg.name %>.js'], 22 | dest: 'dist/ba-<%= pkg.name %>.js', 23 | }, 24 | }, 25 | uglify: { 26 | options: { 27 | banner: '<%= banner %>', 28 | }, 29 | dist: { 30 | expand: true, 31 | src: '<%= concat.dist.dest %>', 32 | ext: '.min.js', 33 | // dest: 'dist/<%= pkg.name %>.min.js', 34 | } 35 | }, 36 | nodeunit: { 37 | files: ['test/**/*_test.js'] 38 | }, 39 | jshint: { 40 | build: { 41 | options: {jshintrc: '.jshintrc'}, 42 | src: ['Gruntfile.js'], 43 | }, 44 | lib: { 45 | options: {jshintrc: 'lib/.jshintrc'}, 46 | src: ['lib/**/*.js'], 47 | }, 48 | test: { 49 | options: {jshintrc: 'lib/.jshintrc'}, 50 | src: ['test/**/*.js'], 51 | }, 52 | }, 53 | watch: { 54 | build: { 55 | files: '<%= jshint.build.src %>', 56 | tasks: ['jshint:build'] 57 | }, 58 | lib: { 59 | files: '<%= jshint.lib.src %>', 60 | tasks: ['jshint:lib', 'nodeunit'] 61 | }, 62 | test: { 63 | files: '<%= jshint.test.src %>', 64 | tasks: ['jshint:test', 'nodeunit'] 65 | }, 66 | }, 67 | }); 68 | 69 | grunt.loadNpmTasks('grunt-contrib-jshint'); 70 | grunt.loadNpmTasks('grunt-contrib-nodeunit'); 71 | grunt.loadNpmTasks('grunt-contrib-clean'); 72 | grunt.loadNpmTasks('grunt-contrib-concat'); 73 | grunt.loadNpmTasks('grunt-contrib-uglify'); 74 | grunt.loadNpmTasks('grunt-contrib-watch'); 75 | 76 | // Default task. 77 | grunt.registerTask('default', ['test', 'build']); 78 | 79 | grunt.registerTask('test', ['jshint', 'nodeunit']); 80 | grunt.registerTask('build', ['clean', 'concat', 'uglify']); 81 | 82 | }; 83 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 "Cowboy" Ben Alman 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript Hooker 2 | 3 | Monkey-patch (hook) functions for debugging and stuff. 4 | 5 | ## Getting Started 6 | 7 | This code should work just fine in Node.js: 8 | 9 | First, install the module with: `npm install hooker` 10 | 11 | ```javascript 12 | var hooker = require('hooker'); 13 | hooker.hook(Math, "max", function() { 14 | console.log(arguments.length + " arguments passed"); 15 | }); 16 | Math.max(5, 6, 7) // logs: "3 arguments passed", returns 7 17 | ``` 18 | 19 | Or in the browser: 20 | 21 | ```html 22 | 23 | 29 | ``` 30 | 31 | In the browser, you can attach Hooker's methods to any object. 32 | 33 | ```html 34 | 37 | 38 | 44 | ``` 45 | 46 | ## Documentation 47 | 48 | ### hooker.hook 49 | Monkey-patch (hook) one or more methods of an object. 50 | #### Signature: 51 | `hooker.hook(object, [ props, ] [options | prehookFunction])` 52 | #### `props` 53 | The optional `props` argument can be a method name, array of method names or null. If null (or omitted), all enumerable methods of `object` will be hooked. 54 | #### `options` 55 | * `pre` - (Function) a pre-hook function to be executed before the original function. Arguments passed into the method will be passed into the pre-hook function as well. 56 | * `post` - (Function) a post-hook function to be executed after the original function. The original function's result is passed into the post-hook function as its first argument, followed by the method arguments. 57 | * `once` - (Boolean) if true, auto-unhook the function after the first execution. 58 | * `passName` - (Boolean) if true, pass the name of the method into the pre-hook function as its first arg (preceding all other arguments), and into the post-hook function as the second arg (after result but preceding all other arguments). 59 | 60 | #### Returns: 61 | An array of hooked method names. 62 | 63 | ### hooker.unhook 64 | Un-monkey-patch (unhook) one or more methods of an object. 65 | #### Signature: 66 | `hooker.unhook(object [, props ])` 67 | #### `props` 68 | The optional `props` argument can be a method name, array of method names or null. If null (or omitted), all methods of `object` will be unhooked. 69 | #### Returns: 70 | An array of unhooked method names. 71 | 72 | ### hooker.orig 73 | Get a reference to the original method from a hooked function. 74 | #### Signature: 75 | `hooker.orig(object, props)` 76 | 77 | ### hooker.override 78 | When a pre- or post-hook returns the result of this function, the value 79 | passed will be used in place of the original function's return value. Any 80 | post-hook override value will take precedence over a pre-hook override value. 81 | #### Signature: 82 | `hooker.override(value)` 83 | 84 | ### hooker.preempt 85 | When a pre-hook returns the result of this function, the value passed will 86 | be used in place of the original function's return value, and the original 87 | function will NOT be executed. 88 | #### Signature: 89 | `hooker.preempt(value)` 90 | 91 | ### hooker.filter 92 | When a pre-hook returns the result of this function, the context and 93 | arguments passed will be applied into the original function. 94 | #### Signature: 95 | `hooker.filter(context, arguments)` 96 | 97 | 98 | ## Examples 99 | See the unit tests for more examples. 100 | 101 | ```javascript 102 | var hooker = require('hooker'); 103 | // Simple logging. 104 | hooker.hook(Math, "max", function() { 105 | console.log(arguments.length + " arguments passed"); 106 | }); 107 | Math.max(5, 6, 7) // logs: "3 arguments passed", returns 7 108 | 109 | hooker.unhook(Math, "max"); // (This is assumed between all further examples) 110 | Math.max(5, 6, 7) // 7 111 | 112 | // Returning hooker.override(value) overrides the original value. 113 | hooker.hook(Math, "max", function() { 114 | if (arguments.length === 0) { 115 | return hooker.override(9000); 116 | } 117 | }); 118 | Math.max(5, 6, 7) // 7 119 | Math.max() // 9000 120 | 121 | // Auto-unhook after one execution. 122 | hooker.hook(Math, "max", { 123 | once: true, 124 | pre: function() { 125 | console.log("Init something here"); 126 | } 127 | }); 128 | Math.max(5, 6, 7) // logs: "Init something here", returns 7 129 | Math.max(5, 6, 7) // 7 130 | 131 | // Filter `this` and arguments through a pre-hook function. 132 | hooker.hook(Math, "max", { 133 | pre: function() { 134 | var args = [].map.call(arguments, function(num) { 135 | return num * 2; 136 | }); 137 | return hooker.filter(this, args); // thisValue, arguments 138 | } 139 | }); 140 | Math.max(5, 6, 7) // 14 141 | 142 | // Modify the original function's result with a post-hook function. 143 | hooker.hook(Math, "max", { 144 | post: function(result) { 145 | return hooker.override(result * 100); 146 | } 147 | }); 148 | Math.max(5, 6, 7) // 700 149 | 150 | // Hook every Math method. Note: if Math's methods were enumerable, the second 151 | // argument could be omitted. Since they aren't, an array of properties to hook 152 | // must be explicitly passed. Non-method properties will be skipped. 153 | // See a more generic example here: http://bit.ly/vvJlrS 154 | hooker.hook(Math, Object.getOwnPropertyNames(Math), { 155 | passName: true, 156 | pre: function(name) { 157 | console.log("=> Math." + name, [].slice.call(arguments, 1)); 158 | }, 159 | post: function(result, name) { 160 | console.log("<= Math." + name, result); 161 | } 162 | }); 163 | 164 | var result = Math.max(5, 6, 7); 165 | // => Math.max [ 5, 6, 7 ] 166 | // <= Math.max 7 167 | result // 7 168 | 169 | result = Math.ceil(3.456); 170 | // => Math.ceil [ 3.456 ] 171 | // <= Math.ceil 4 172 | result // 4 173 | ``` 174 | 175 | ## Contributing 176 | In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using [grunt](https://github.com/cowboy/grunt). 177 | 178 | _Also, please don't edit files in the "dist" subdirectory as they are generated via grunt. You'll find source code in the "lib" subdirectory!_ 179 | 180 | ## Release History 181 | 2012/01/09 - v0.2.3 - First official release. 182 | 183 | ## License 184 | Copyright (c) 2012 "Cowboy" Ben Alman 185 | Licensed under the MIT license. 186 | 187 | -------------------------------------------------------------------------------- /dist/ba-hooker.js: -------------------------------------------------------------------------------- 1 | /*! JavaScript Hooker - v0.2.3 - 1/22/2012 2 | * http://github.com/cowboy/javascript-hooker 3 | * Copyright (c) 2012 "Cowboy" Ben Alman; Licensed MIT */ 4 | 5 | (function(exports) { 6 | // Get an array from an array-like object with slice.call(arrayLikeObject). 7 | var slice = [].slice; 8 | // Get an "[object [[Class]]]" string with toString.call(value). 9 | var toString = {}.toString; 10 | 11 | // I can't think of a better way to ensure a value is a specific type other 12 | // than to create instances and use the `instanceof` operator. 13 | function HookerOverride(v) { this.value = v; } 14 | function HookerPreempt(v) { this.value = v; } 15 | function HookerFilter(c, a) { this.context = c; this.args = a; } 16 | 17 | // When a pre- or post-hook returns the result of this function, the value 18 | // passed will be used in place of the original function's return value. Any 19 | // post-hook override value will take precedence over a pre-hook override 20 | // value. 21 | exports.override = function(value) { 22 | return new HookerOverride(value); 23 | }; 24 | 25 | // When a pre-hook returns the result of this function, the value passed will 26 | // be used in place of the original function's return value, and the original 27 | // function will NOT be executed. 28 | exports.preempt = function(value) { 29 | return new HookerPreempt(value); 30 | }; 31 | 32 | // When a pre-hook returns the result of this function, the context and 33 | // arguments passed will be applied into the original function. 34 | exports.filter = function(context, args) { 35 | return new HookerFilter(context, args); 36 | }; 37 | 38 | // Execute callback(s) for properties of the specified object. 39 | function forMethods(obj, props, callback) { 40 | var prop; 41 | if (typeof props === "string") { 42 | // A single prop string was passed. Create an array. 43 | props = [props]; 44 | } else if (props == null) { 45 | // No props were passed, so iterate over all properties, building an 46 | // array. Unfortunately, Object.keys(obj) doesn't work everywhere yet, so 47 | // this has to be done manually. 48 | props = []; 49 | for (prop in obj) { 50 | if (obj.hasOwnProperty(prop)) { 51 | props.push(prop); 52 | } 53 | } 54 | } 55 | // Execute callback for every method in the props array. 56 | var i = props.length; 57 | while (i--) { 58 | // If the property isn't a function... 59 | if (toString.call(obj[props[i]]) !== "[object Function]" || 60 | // ...or the callback returns false... 61 | callback(obj, props[i]) === false) { 62 | // ...remove it from the props array to be returned. 63 | props.splice(i, 1); 64 | } 65 | } 66 | // Return an array of method names for which the callback didn't fail. 67 | return props; 68 | } 69 | 70 | // Monkey-patch (hook) a method of an object. 71 | exports.hook = function(obj, props, options) { 72 | // If the props argument was omitted, shuffle the arguments. 73 | if (options == null) { 74 | options = props; 75 | props = null; 76 | } 77 | // If just a function is passed instead of an options hash, use that as a 78 | // pre-hook function. 79 | if (typeof options === "function") { 80 | options = {pre: options}; 81 | } 82 | 83 | // Hook the specified method of the object. 84 | return forMethods(obj, props, function(obj, prop) { 85 | // The original (current) method. 86 | var orig = obj[prop]; 87 | // The new hooked function. 88 | function hooked() { 89 | var result, origResult, tmp; 90 | 91 | // Get an array of arguments. 92 | var args = slice.call(arguments); 93 | 94 | // If passName option is specified, prepend prop to the args array, 95 | // passing it as the first argument to any specified hook functions. 96 | if (options.passName) { 97 | args.unshift(prop); 98 | } 99 | 100 | // If a pre-hook function was specified, invoke it in the current 101 | // context with the passed-in arguments, and store its result. 102 | if (options.pre) { 103 | result = options.pre.apply(this, args); 104 | } 105 | 106 | if (result instanceof HookerFilter) { 107 | // If the pre-hook returned hooker.filter(context, args), invoke the 108 | // original function with that context and arguments, and store its 109 | // result. 110 | origResult = result = orig.apply(result.context, result.args); 111 | } else if (result instanceof HookerPreempt) { 112 | // If the pre-hook returned hooker.preempt(value) just use the passed 113 | // value and don't execute the original function. 114 | origResult = result = result.value; 115 | } else { 116 | // Invoke the original function in the current context with the 117 | // passed-in arguments, and store its result. 118 | origResult = orig.apply(this, arguments); 119 | // If the pre-hook returned hooker.override(value), use the passed 120 | // value, otherwise use the original function's result. 121 | result = result instanceof HookerOverride ? result.value : origResult; 122 | } 123 | 124 | if (options.post) { 125 | // If a post-hook function was specified, invoke it in the current 126 | // context, passing in the result of the original function as the 127 | // first argument, followed by any passed-in arguments. 128 | tmp = options.post.apply(this, [origResult].concat(args)); 129 | if (tmp instanceof HookerOverride) { 130 | // If the post-hook returned hooker.override(value), use the passed 131 | // value, otherwise use the previously computed result. 132 | result = tmp.value; 133 | } 134 | } 135 | 136 | // Unhook if the "once" option was specified. 137 | if (options.once) { 138 | exports.unhook(obj, prop); 139 | } 140 | 141 | // Return the result! 142 | return result; 143 | } 144 | // Re-define the method. 145 | obj[prop] = hooked; 146 | // Fail if the function couldn't be hooked. 147 | if (obj[prop] !== hooked) { return false; } 148 | // Store a reference to the original method as a property on the new one. 149 | obj[prop]._orig = orig; 150 | }); 151 | }; 152 | 153 | // Get a reference to the original method from a hooked function. 154 | exports.orig = function(obj, prop) { 155 | return obj[prop]._orig; 156 | }; 157 | 158 | // Un-monkey-patch (unhook) a method of an object. 159 | exports.unhook = function(obj, props) { 160 | return forMethods(obj, props, function(obj, prop) { 161 | // Get a reference to the original method, if it exists. 162 | var orig = exports.orig(obj, prop); 163 | // If there's no original method, it can't be unhooked, so fail. 164 | if (!orig) { return false; } 165 | // Unhook the method. 166 | obj[prop] = orig; 167 | }); 168 | }; 169 | }(typeof exports === "object" && exports || this)); 170 | -------------------------------------------------------------------------------- /dist/ba-hooker.min.js: -------------------------------------------------------------------------------- 1 | /*! JavaScript Hooker - v0.2.3 - 1/22/2012 2 | * http://github.com/cowboy/javascript-hooker 3 | * Copyright (c) 2012 "Cowboy" Ben Alman; Licensed MIT */ 4 | (function(a){function d(a){this.value=a}function e(a){this.value=a}function f(a,b){this.context=a,this.args=b}function g(a,b,d){var e;if(typeof b=="string")b=[b];else if(b==null){b=[];for(e in a)a.hasOwnProperty(e)&&b.push(e)}var f=b.length;while(f--)(c.call(a[b[f]])!=="[object Function]"||d(a,b[f])===!1)&&b.splice(f,1);return b}var b=[].slice,c={}.toString;a.override=function(a){return new d(a)},a.preempt=function(a){return new e(a)},a.filter=function(a,b){return new f(a,b)},a.hook=function(c,h,i){return i==null&&(i=h,h=null),typeof i=="function"&&(i={pre:i}),g(c,h,function(c,g){function j(){var j,k,l,m=b.call(arguments);return i.passName&&m.unshift(g),i.pre&&(j=i.pre.apply(this,m)),j instanceof f?k=j=h.apply(j.context,j.args):j instanceof e?k=j=j.value:(k=h.apply(this,arguments),j=j instanceof d?j.value:k),i.post&&(l=i.post.apply(this,[k].concat(m)),l instanceof d&&(j=l.value)),i.once&&a.unhook(c,g),j}var h=c[g];c[g]=j;if(c[g]!==j)return!1;c[g]._orig=h})},a.orig=function(a,b){return a[b]._orig},a.unhook=function(b,c){return g(b,c,function(b,c){var d=a.orig(b,c);if(!d)return!1;b[c]=d})}})(typeof exports=="object"&&exports||this) -------------------------------------------------------------------------------- /lib/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": "nofunc", 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": "vars", 11 | "boss": true, 12 | "eqnull": true, 13 | "globals": { 14 | "module": false, 15 | "exports": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/hooker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JavaScript Hooker 3 | * http://github.com/cowboy/javascript-hooker 4 | * 5 | * Copyright (c) 2012 "Cowboy" Ben Alman 6 | * Licensed under the MIT license. 7 | * http://benalman.com/about/license/ 8 | */ 9 | 10 | (function(exports) { 11 | // Get an array from an array-like object with slice.call(arrayLikeObject). 12 | var slice = [].slice; 13 | // Get an "[object [[Class]]]" string with toString.call(value). 14 | var toString = {}.toString; 15 | 16 | // I can't think of a better way to ensure a value is a specific type other 17 | // than to create instances and use the `instanceof` operator. 18 | function HookerOverride(v) { this.value = v; } 19 | function HookerPreempt(v) { this.value = v; } 20 | function HookerFilter(c, a) { this.context = c; this.args = a; } 21 | 22 | // When a pre- or post-hook returns the result of this function, the value 23 | // passed will be used in place of the original function's return value. Any 24 | // post-hook override value will take precedence over a pre-hook override 25 | // value. 26 | exports.override = function(value) { 27 | return new HookerOverride(value); 28 | }; 29 | 30 | // When a pre-hook returns the result of this function, the value passed will 31 | // be used in place of the original function's return value, and the original 32 | // function will NOT be executed. 33 | exports.preempt = function(value) { 34 | return new HookerPreempt(value); 35 | }; 36 | 37 | // When a pre-hook returns the result of this function, the context and 38 | // arguments passed will be applied into the original function. 39 | exports.filter = function(context, args) { 40 | return new HookerFilter(context, args); 41 | }; 42 | 43 | // Execute callback(s) for properties of the specified object. 44 | function forMethods(obj, props, callback) { 45 | var prop; 46 | if (typeof props === "string") { 47 | // A single prop string was passed. Create an array. 48 | props = [props]; 49 | } else if (props == null) { 50 | // No props were passed, so iterate over all properties, building an 51 | // array. Unfortunately, Object.keys(obj) doesn't work everywhere yet, so 52 | // this has to be done manually. 53 | props = []; 54 | for (prop in obj) { 55 | if (obj.hasOwnProperty(prop)) { 56 | props.push(prop); 57 | } 58 | } 59 | } 60 | // Execute callback for every method in the props array. 61 | var i = props.length; 62 | while (i--) { 63 | // If the property isn't a function... 64 | if (toString.call(obj[props[i]]) !== "[object Function]" || 65 | // ...or the callback returns false... 66 | callback(obj, props[i]) === false) { 67 | // ...remove it from the props array to be returned. 68 | props.splice(i, 1); 69 | } 70 | } 71 | // Return an array of method names for which the callback didn't fail. 72 | return props; 73 | } 74 | 75 | // Monkey-patch (hook) a method of an object. 76 | exports.hook = function(obj, props, options) { 77 | // If the props argument was omitted, shuffle the arguments. 78 | if (options == null) { 79 | options = props; 80 | props = null; 81 | } 82 | // If just a function is passed instead of an options hash, use that as a 83 | // pre-hook function. 84 | if (typeof options === "function") { 85 | options = {pre: options}; 86 | } 87 | 88 | // Hook the specified method of the object. 89 | return forMethods(obj, props, function(obj, prop) { 90 | // The original (current) method. 91 | var orig = obj[prop]; 92 | // The new hooked function. 93 | function hooked() { 94 | var result, origResult, tmp; 95 | 96 | // Get an array of arguments. 97 | var args = slice.call(arguments); 98 | 99 | // If passName option is specified, prepend prop to the args array, 100 | // passing it as the first argument to any specified hook functions. 101 | if (options.passName) { 102 | args.unshift(prop); 103 | } 104 | 105 | // If a pre-hook function was specified, invoke it in the current 106 | // context with the passed-in arguments, and store its result. 107 | if (options.pre) { 108 | result = options.pre.apply(this, args); 109 | } 110 | 111 | if (result instanceof HookerFilter) { 112 | // If the pre-hook returned hooker.filter(context, args), invoke the 113 | // original function with that context and arguments, and store its 114 | // result. 115 | origResult = result = orig.apply(result.context, result.args); 116 | } else if (result instanceof HookerPreempt) { 117 | // If the pre-hook returned hooker.preempt(value) just use the passed 118 | // value and don't execute the original function. 119 | origResult = result = result.value; 120 | } else { 121 | // Invoke the original function in the current context with the 122 | // passed-in arguments, and store its result. 123 | origResult = orig.apply(this, arguments); 124 | // If the pre-hook returned hooker.override(value), use the passed 125 | // value, otherwise use the original function's result. 126 | result = result instanceof HookerOverride ? result.value : origResult; 127 | } 128 | 129 | if (options.post) { 130 | // If a post-hook function was specified, invoke it in the current 131 | // context, passing in the result of the original function as the 132 | // first argument, followed by any passed-in arguments. 133 | tmp = options.post.apply(this, [origResult].concat(args)); 134 | if (tmp instanceof HookerOverride) { 135 | // If the post-hook returned hooker.override(value), use the passed 136 | // value, otherwise use the previously computed result. 137 | result = tmp.value; 138 | } 139 | } 140 | 141 | // Unhook if the "once" option was specified. 142 | if (options.once) { 143 | exports.unhook(obj, prop); 144 | } 145 | 146 | // Return the result! 147 | return result; 148 | } 149 | // Re-define the method. 150 | obj[prop] = hooked; 151 | // Fail if the function couldn't be hooked. 152 | if (obj[prop] !== hooked) { return false; } 153 | // Store a reference to the original method as a property on the new one. 154 | obj[prop]._orig = orig; 155 | }); 156 | }; 157 | 158 | // Get a reference to the original method from a hooked function. 159 | exports.orig = function(obj, prop) { 160 | return obj[prop]._orig; 161 | }; 162 | 163 | // Un-monkey-patch (unhook) a method of an object. 164 | exports.unhook = function(obj, props) { 165 | return forMethods(obj, props, function(obj, prop) { 166 | // Get a reference to the original method, if it exists. 167 | var orig = exports.orig(obj, prop); 168 | // If there's no original method, it can't be unhooked, so fail. 169 | if (!orig) { return false; } 170 | // Unhook the method. 171 | obj[prop] = orig; 172 | }); 173 | }; 174 | }(typeof exports === "object" && exports || this)); 175 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hooker", 3 | "description": "Monkey-patch (hook) functions for debugging and stuff.", 4 | "version": "0.2.3", 5 | "homepage": "http://github.com/cowboy/javascript-hooker", 6 | "author": { 7 | "name": "\"Cowboy\" Ben Alman", 8 | "url": "http://benalman.com/" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/cowboy/javascript-hooker.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/cowboy/javascript-hooker/issues" 16 | }, 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "https://github.com/cowboy/javascript-hooker/blob/master/LICENSE-MIT" 21 | } 22 | ], 23 | "dependencies": {}, 24 | "devDependencies": { 25 | "grunt": "~0.4.2", 26 | "grunt-contrib-jshint": "~0.8.0", 27 | "grunt-contrib-nodeunit": "~0.3.0", 28 | "grunt-contrib-uglify": "~0.3.2", 29 | "grunt-contrib-watch": "~0.5.3", 30 | "grunt-contrib-concat": "~0.3.0", 31 | "grunt-contrib-clean": "~0.5.0" 32 | }, 33 | "keywords": [ 34 | "patch", 35 | "hook", 36 | "function", 37 | "debug", 38 | "aop" 39 | ], 40 | "engines": { 41 | "node": "*" 42 | }, 43 | "main": "lib/hooker", 44 | "scripts": { 45 | "test": "grunt test" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/hooker_test.js: -------------------------------------------------------------------------------- 1 | /*global require:true */ 2 | var hooker = require('../lib/hooker'); 3 | 4 | exports['hook'] = { 5 | setUp: function(done) { 6 | this.order = []; 7 | this.track = function() { 8 | [].push.apply(this.order, arguments); 9 | }; 10 | 11 | this.prop = 1; 12 | this.add = function(a, b) { 13 | this.track("add", this.prop, a, b); 14 | return this.prop + a + b; 15 | }; 16 | 17 | this.obj = { 18 | that: this, 19 | prop: 1, 20 | add1: function(a, b) { 21 | this.that.track("add1", this.prop, a, b); 22 | return this.prop + a + b; 23 | }, 24 | add2: function(a, b) { 25 | this.that.track("add2", this.prop, a, b); 26 | return this.prop + a + b; 27 | }, 28 | add3: function(a, b) { 29 | this.that.track("add3", this.prop, a, b); 30 | return this.prop + a + b; 31 | } 32 | }; 33 | 34 | done(); 35 | }, 36 | 'orig': function(test) { 37 | test.expect(1); 38 | var orig = this.add; 39 | hooker.hook(this, "add", function() {}); 40 | test.strictEqual(hooker.orig(this, "add"), orig, "should return a refernce to the original function."); 41 | test.done(); 42 | }, 43 | 'once': function(test) { 44 | test.expect(5); 45 | var orig = this.add; 46 | hooker.hook(this, "add", { 47 | once: true, 48 | pre: function(a, b) { 49 | // Arguments are passed into pre-hook as specified. 50 | this.track("before", this.prop, a, b); 51 | } 52 | }); 53 | test.strictEqual(this.add(2, 3), 6, "should return the original function's result."); 54 | test.deepEqual(this.order, ["before", 1, 2, 3, "add", 1, 2, 3], "functions should execute in-order."); 55 | test.strictEqual(this.add, orig, "should automatically unhook when once is specified."); 56 | this.order = []; 57 | test.strictEqual(this.add(2, 3), 6, "should return the original function's result."); 58 | test.deepEqual(this.order, ["add", 1, 2, 3], "only the original function should execute."); 59 | test.done(); 60 | }, 61 | 'pre-hook (simple syntax)': function(test) { 62 | test.expect(3); 63 | // Pre-hook. 64 | var result = hooker.hook(this, "add", function(a, b) { 65 | // Arguments are passed into pre-hook as specified. 66 | this.track("before", this.prop, a, b); 67 | }); 68 | test.deepEqual(result, ["add"], "add should have been hooked."); 69 | test.strictEqual(this.add(2, 3), 6, "should return the original function's result."); 70 | test.deepEqual(this.order, ["before", 1, 2, 3, "add", 1, 2, 3], "functions should execute in-order."); 71 | test.done(); 72 | }, 73 | 'pre-hook': function(test) { 74 | test.expect(3); 75 | // Pre-hook. 76 | var result = hooker.hook(this, "add", { 77 | pre: function(a, b) { 78 | // Arguments are passed into pre-hook as specified. 79 | this.track("before", this.prop, a, b); 80 | } 81 | }); 82 | test.deepEqual(result, ["add"], "add should have been hooked."); 83 | test.strictEqual(this.add(2, 3), 6, "should return the original function's result."); 84 | test.deepEqual(this.order, ["before", 1, 2, 3, "add", 1, 2, 3], "functions should execute in-order."); 85 | test.done(); 86 | }, 87 | 'post-hook': function(test) { 88 | test.expect(3); 89 | // Post-hook. 90 | var result = hooker.hook(this, "add", { 91 | post: function(result, a, b) { 92 | // Arguments to post-hook are the original function's return value, 93 | // followed by the specified function arguments. 94 | this.track("after", this.prop, a, b, result); 95 | } 96 | }); 97 | test.deepEqual(result, ["add"], "add should have been hooked."); 98 | test.strictEqual(this.add(2, 3), 6, "should return the original function's result."); 99 | test.deepEqual(this.order, ["add", 1, 2, 3, "after", 1, 2, 3, 6], "functions should execute in-order."); 100 | test.done(); 101 | }, 102 | 'pre- & post-hook': function(test) { 103 | test.expect(2); 104 | // Pre- & post-hook. 105 | hooker.hook(this, "add", { 106 | pre: function(a, b) { 107 | // Arguments are passed into pre-hook as specified. 108 | this.track("before", this.prop, a, b); 109 | }, 110 | post: function(result, a, b) { 111 | // Arguments to post-hook are the original function's return value, 112 | // followed by the specified function arguments. 113 | this.track("after", this.prop, a, b, result); 114 | } 115 | }); 116 | test.strictEqual(this.add(2, 3), 6, "should return the original function's result."); 117 | test.deepEqual(this.order, ["before", 1, 2, 3, "add", 1, 2, 3, "after", 1, 2, 3, 6], "functions should execute in-order."); 118 | test.done(); 119 | }, 120 | 121 | 'pre-hook, return value override': function(test) { 122 | test.expect(2); 123 | // Pre-hook. 124 | hooker.hook(this, "add", { 125 | pre: function(a, b) { 126 | // Arguments are passed into pre-hook as specified. 127 | this.track("before", this.prop, a, b); 128 | // This return value will override the original function's return value. 129 | return hooker.override("b" + this.prop + a + b); 130 | } 131 | }); 132 | test.strictEqual(this.add(2, 3), "b123", "should return the overridden result."); 133 | test.deepEqual(this.order, ["before", 1, 2, 3, "add", 1, 2, 3], "functions should execute in-order."); 134 | test.done(); 135 | }, 136 | 'post-hook, return value override': function(test) { 137 | test.expect(2); 138 | // Post-hook. 139 | hooker.hook(this, "add", { 140 | post: function(result, a, b) { 141 | // Arguments to post-hook are the original function's return value, 142 | // followed by the specified function arguments. 143 | this.track("after", this.prop, a, b, result); 144 | // This return value will override the original function's return value. 145 | return hooker.override("a" + this.prop + a + b + result); 146 | } 147 | }); 148 | test.strictEqual(this.add(2, 3), "a1236", "should return the post-hook overridden result."); 149 | test.deepEqual(this.order, ["add", 1, 2, 3, "after", 1, 2, 3, 6], "functions should execute in-order."); 150 | test.done(); 151 | }, 152 | 'pre- & post-hook, return value override': function(test) { 153 | test.expect(2); 154 | // Pre- & post-hook. 155 | hooker.hook(this, "add", { 156 | pre: function(a, b) { 157 | // Arguments are passed into pre-hook as specified. 158 | this.track("before", this.prop, a, b); 159 | // This return value will override the original function's return value. 160 | return hooker.override("b" + this.prop + a + b); 161 | }, 162 | post: function(result, a, b) { 163 | // Arguments to post-hook are the original function's return value, 164 | // followed by the specified function arguments. 165 | this.track("after", this.prop, a, b, result); 166 | // This return value will override the original function's return value 167 | // AND the pre-hook's return value. 168 | return hooker.override("a" + this.prop + a + b + result); 169 | } 170 | }); 171 | test.strictEqual(this.add(2, 3), "a1236", "should return the overridden result, and post-hook result should take precedence over pre-hook result."); 172 | test.deepEqual(this.order, ["before", 1, 2, 3, "add", 1, 2, 3, "after", 1, 2, 3, 6], "functions should execute in-order."); 173 | test.done(); 174 | }, 175 | 176 | 'pre-hook, filtering arguments': function(test) { 177 | test.expect(2); 178 | // Pre-hook. 179 | hooker.hook(this, "add", { 180 | pre: function(a, b) { 181 | // Arguments are passed into pre-hook as specified. 182 | this.track("before", this.prop, a, b); 183 | // Return hooker.filter(context, arguments) and they will be passed into 184 | // the original function. The "track" and "order" propterites are just 185 | // set here for the same of this unit test. 186 | return hooker.filter({prop: "x", track: this.track, order: this.order}, ["y", "z"]); 187 | } 188 | }); 189 | test.strictEqual(this.add(2, 3), "xyz", "should return the original function's result, given filtered context and arguments."); 190 | test.deepEqual(this.order, ["before", 1, 2, 3, "add", "x", "y", "z"], "functions should execute in-order."); 191 | test.done(); 192 | }, 193 | 'pre- & post-hook, filtering arguments': function(test) { 194 | test.expect(2); 195 | // Pre- & post-hook. 196 | hooker.hook(this, "add", { 197 | pre: function(a, b) { 198 | // Arguments are passed into pre-hook as specified. 199 | this.track("before", this.prop, a, b); 200 | // Return hooker.filter(context, arguments) and they will be passed into 201 | // the original function. The "track" and "order" propterites are just 202 | // set here for the same of this unit test. 203 | return hooker.filter({prop: "x", track: this.track, order: this.order}, ["y", "z"]); 204 | }, 205 | post: function(result, a, b) { 206 | // Arguments to post-hook are the original function's return value, 207 | // followed by the specified function arguments. 208 | this.track("after", this.prop, a, b, result); 209 | } 210 | }); 211 | test.strictEqual(this.add(2, 3), "xyz", "should return the original function's result, given filtered context and arguments."); 212 | test.deepEqual(this.order, ["before", 1, 2, 3, "add", "x", "y", "z", "after", 1, 2, 3, "xyz"], "functions should execute in-order."); 213 | test.done(); 214 | }, 215 | 'pre- & post-hook, filtering arguments, return value override': function(test) { 216 | test.expect(2); 217 | // Pre- & post-hook. 218 | hooker.hook(this, "add", { 219 | pre: function(a, b) { 220 | // Arguments are passed into pre-hook as specified. 221 | this.track("before", this.prop, a, b); 222 | // Return hooker.filter(context, arguments) and they will be passed into 223 | // the original function. The "track" and "order" propterites are just 224 | // set here for the same of this unit test. 225 | return hooker.filter({prop: "x", track: this.track, order: this.order}, ["y", "z"]); 226 | }, 227 | post: function(result, a, b) { 228 | // Arguments to post-hook are the original function's return value, 229 | // followed by the specified function arguments. 230 | this.track("after", this.prop, a, b, result); 231 | // This return value will override the original function's return value 232 | // AND the pre-hook's return value. 233 | return hooker.override("a" + this.prop + a + b + result); 234 | } 235 | }); 236 | test.strictEqual(this.add(2, 3), "a123xyz", "should return the post-hook overridden result."); 237 | test.deepEqual(this.order, ["before", 1, 2, 3, "add", "x", "y", "z", "after", 1, 2, 3, "xyz"], "functions should execute in-order."); 238 | test.done(); 239 | }, 240 | 241 | 'pre-hook, preempt original function': function(test) { 242 | test.expect(2); 243 | // Pre-hook. 244 | hooker.hook(this, "add", { 245 | pre: function(a, b) { 246 | // Arguments are passed into pre-hook as specified. 247 | this.track("before", this.prop, a, b); 248 | // Returning hooker.preempt will prevent the original function from being 249 | // invoked and optionally set a return value. 250 | return hooker.preempt(); 251 | } 252 | }); 253 | test.strictEqual(this.add(2, 3), undefined, "should return the value passed to preempt."); 254 | test.deepEqual(this.order, ["before", 1, 2, 3], "functions should execute in-order."); 255 | test.done(); 256 | }, 257 | 'pre-hook, preempt original function with value': function(test) { 258 | test.expect(2); 259 | // Pre-hook. 260 | hooker.hook(this, "add", { 261 | pre: function(a, b) { 262 | // Arguments are passed into pre-hook as specified. 263 | this.track("before", this.prop, a, b); 264 | // Returning hooker.preempt will prevent the original function from being 265 | // invoked and optionally set a return value. 266 | return hooker.preempt(9000); 267 | } 268 | }); 269 | test.strictEqual(this.add(2, 3), 9000, "should return the value passed to preempt."); 270 | test.deepEqual(this.order, ["before", 1, 2, 3], "functions should execute in-order."); 271 | test.done(); 272 | }, 273 | 'pre- & post-hook, preempt original function with value': function(test) { 274 | test.expect(2); 275 | // Pre- & post-hook. 276 | hooker.hook(this, "add", { 277 | pre: function(a, b) { 278 | // Arguments are passed into pre-hook as specified. 279 | this.track("before", this.prop, a, b); 280 | // Returning hooker.preempt will prevent the original function from being 281 | // invoked and optionally set a return value. 282 | return hooker.preempt(9000); 283 | }, 284 | post: function(result, a, b) { 285 | // Arguments to post-hook are the original function's return value, 286 | // followed by the specified function arguments. 287 | this.track("after", this.prop, a, b, result); 288 | } 289 | }); 290 | test.strictEqual(this.add(2, 3), 9000, "should return the value passed to preempt."); 291 | test.deepEqual(this.order, ["before", 1, 2, 3, "after", 1, 2, 3, 9000], "functions should execute in-order."); 292 | test.done(); 293 | }, 294 | 'pre- & post-hook, preempt original function with value, return value override': function(test) { 295 | test.expect(2); 296 | // Pre- & post-hook. 297 | hooker.hook(this, "add", { 298 | pre: function(a, b) { 299 | // Arguments are passed into pre-hook as specified. 300 | this.track("before", this.prop, a, b); 301 | // Returning hooker.preempt will prevent the original function from being 302 | // invoked and optionally set a return value. 303 | return hooker.preempt(9000); 304 | }, 305 | post: function(result, a, b) { 306 | // Arguments to post-hook are the original function's return value, 307 | // followed by the specified function arguments. 308 | this.track("after", this.prop, a, b, result); 309 | // This return value will override any preempt value set in pre-hook. 310 | return hooker.override("a" + this.prop + a + b + result); 311 | } 312 | }); 313 | test.strictEqual(this.add(2, 3), "a1239000", "should return the overridden result, and post-hook result should take precedence over preempt value."); 314 | test.deepEqual(this.order, ["before", 1, 2, 3, "after", 1, 2, 3, 9000], "functions should execute in-order."); 315 | test.done(); 316 | }, 317 | 'pre- & post-hook, some properties': function(test) { 318 | test.expect(7); 319 | // Pre- & post-hook. 320 | var result = hooker.hook(this.obj, ["add1", "add2"], { 321 | pre: function(a, b) { 322 | // Arguments are passed into pre-hook as specified. 323 | this.that.track("before", this.prop, a, b); 324 | }, 325 | post: function(result, a, b) { 326 | // Arguments to post-hook are the original function's return value, 327 | // followed by the specified function arguments. 328 | this.that.track("after", this.prop, a, b, result); 329 | } 330 | }); 331 | test.deepEqual(result.sort(), ["add1", "add2"], "both functions should have been hooked."); 332 | test.strictEqual(this.obj.add1(2, 3), 6, "should return the original function's result."); 333 | test.deepEqual(this.order, ["before", 1, 2, 3, "add1", 1, 2, 3, "after", 1, 2, 3, 6], "functions should execute in-order."); 334 | this.order = []; 335 | test.strictEqual(this.obj.add2(2, 3), 6, "should return the original function's result."); 336 | test.deepEqual(this.order, ["before", 1, 2, 3, "add2", 1, 2, 3, "after", 1, 2, 3, 6], "functions should execute in-order."); 337 | this.order = []; 338 | test.strictEqual(this.obj.add3(2, 3), 6, "should return the original function's result."); 339 | test.deepEqual(this.order, ["add3", 1, 2, 3], "functions should execute in-order."); 340 | test.done(); 341 | }, 342 | 'pre- & post-hook, all properties': function(test) { 343 | test.expect(7); 344 | // Pre- & post-hook. 345 | var result = hooker.hook(this.obj, { 346 | pre: function(a, b) { 347 | // Arguments are passed into pre-hook as specified. 348 | this.that.track("before", this.prop, a, b); 349 | }, 350 | post: function(result, a, b) { 351 | // Arguments to post-hook are the original function's return value, 352 | // followed by the specified function arguments. 353 | this.that.track("after", this.prop, a, b, result); 354 | } 355 | }); 356 | test.deepEqual(result.sort(), ["add1", "add2", "add3"], "all functions should have been hooked."); 357 | test.strictEqual(this.obj.add1(2, 3), 6, "should return the original function's result."); 358 | test.deepEqual(this.order, ["before", 1, 2, 3, "add1", 1, 2, 3, "after", 1, 2, 3, 6], "functions should execute in-order."); 359 | this.order = []; 360 | test.strictEqual(this.obj.add2(2, 3), 6, "should return the original function's result."); 361 | test.deepEqual(this.order, ["before", 1, 2, 3, "add2", 1, 2, 3, "after", 1, 2, 3, 6], "functions should execute in-order."); 362 | this.order = []; 363 | test.strictEqual(this.obj.add3(2, 3), 6, "should return the original function's result."); 364 | test.deepEqual(this.order, ["before", 1, 2, 3, "add3", 1, 2, 3, "after", 1, 2, 3, 6], "functions should execute in-order."); 365 | test.done(); 366 | }, 367 | 'pre- & post-hook, all properties, passName': function(test) { 368 | test.expect(6); 369 | // Pre- & post-hook. 370 | hooker.hook(this.obj, { 371 | passName: true, 372 | pre: function(name, a, b) { 373 | // Arguments are passed into pre-hook as specified. 374 | this.that.track("before", this.prop, name, a, b); 375 | }, 376 | post: function(result, name, a, b) { 377 | // Arguments to post-hook are the original function's return value, 378 | // followed by the specified function arguments. 379 | this.that.track("after", this.prop, name, a, b, result); 380 | } 381 | }); 382 | test.strictEqual(this.obj.add1(2, 3), 6, "should return the original function's result."); 383 | test.deepEqual(this.order, ["before", 1, "add1", 2, 3, "add1", 1, 2, 3, "after", 1, "add1", 2, 3, 6], "functions should execute in-order."); 384 | this.order = []; 385 | test.strictEqual(this.obj.add2(2, 3), 6, "should return the original function's result."); 386 | test.deepEqual(this.order, ["before", 1, "add2", 2, 3, "add2", 1, 2, 3, "after", 1, "add2", 2, 3, 6], "functions should execute in-order."); 387 | this.order = []; 388 | test.strictEqual(this.obj.add3(2, 3), 6, "should return the original function's result."); 389 | test.deepEqual(this.order, ["before", 1, "add3", 2, 3, "add3", 1, 2, 3, "after", 1, "add3", 2, 3, 6], "functions should execute in-order."); 390 | test.done(); 391 | }, 392 | 'unhook one property': function(test) { 393 | test.expect(5); 394 | var orig = this.add; 395 | hooker.hook(this, "add", function() {}); 396 | var result = hooker.unhook(this, "add"); 397 | test.deepEqual(result, ["add"], "one function should have been unhooked."); 398 | test.strictEqual(this.add, orig, "should have unhooked, restoring the original function"); 399 | result = hooker.unhook(this, "add"); 400 | test.deepEqual(result, [], "nothing should have been unhooked."); 401 | test.strictEqual(this.add, orig, "shouldn't explode if already unhooked"); 402 | test.strictEqual(this.add.orig, undefined, "original function shouldn't have an orig property"); 403 | test.done(); 404 | }, 405 | 'unhook some properties': function(test) { 406 | test.expect(6); 407 | var add1 = this.obj.add1; 408 | var add2 = this.obj.add2; 409 | hooker.hook(this.obj, ["add1", "add2"], function() {}); 410 | test.strictEqual(hooker.orig(this.obj, "add1"), add1, "should return a refernce to the original function"); 411 | test.strictEqual(hooker.orig(this.obj, "add2"), add2, "should return a refernce to the original function"); 412 | test.strictEqual(hooker.orig(this.obj, "add3"), undefined, "should not have been hooked, so should not have an original function"); 413 | var result = hooker.unhook(this.obj, ["add1", "add2"]); 414 | test.deepEqual(result.sort(), ["add1", "add2"], "both functions should have been unhooked."); 415 | test.strictEqual(this.obj.add1, add1, "should have unhooked, restoring the original function"); 416 | test.strictEqual(this.obj.add2, add2, "should have unhooked, restoring the original function"); 417 | test.done(); 418 | }, 419 | 'unhook all properties': function(test) { 420 | test.expect(7); 421 | var add1 = this.obj.add1; 422 | var add2 = this.obj.add2; 423 | var add3 = this.obj.add3; 424 | hooker.hook(this.obj, function() {}); 425 | test.strictEqual(hooker.orig(this.obj, "add1"), add1, "should return a refernce to the original function"); 426 | test.strictEqual(hooker.orig(this.obj, "add2"), add2, "should return a refernce to the original function"); 427 | test.strictEqual(hooker.orig(this.obj, "add3"), add3, "should return a refernce to the original function"); 428 | var result = hooker.unhook(this.obj); 429 | test.deepEqual(result.sort(), ["add1", "add2", "add3"], "all functions should have been unhooked."); 430 | test.strictEqual(this.obj.add1, add1, "should have unhooked, restoring the original function"); 431 | test.strictEqual(this.obj.add2, add2, "should have unhooked, restoring the original function"); 432 | test.strictEqual(this.obj.add3, add3, "should have unhooked, restoring the original function"); 433 | test.done(); 434 | } 435 | }; 436 | --------------------------------------------------------------------------------