├── .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 |
--------------------------------------------------------------------------------