├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── assets ├── after.js ├── before.js ├── reporters.js ├── tracer.js ├── utils.js └── zone.js ├── client └── hijack.js ├── package.js ├── server └── inject.js ├── smart.json ├── tests ├── _both.js ├── _server.js ├── hijacks │ ├── collections.js │ ├── methods.js │ └── subscriptions.js ├── loader.js └── reporters.js └── versions.json /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | .npm 3 | packages 4 | smart.lock 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_install: 5 | - "curl -L http://git.io/ejPSng | /bin/sh" 6 | env: 7 | - METEOR_ENV=test TEST_COMMAND=meteor METEOR_RELEASE=1.1.0.2 8 | - TEST_COMMAND=mrt METEOR_RELEASE=0.8.3 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 meteorhacks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://api.travis-ci.org/meteorhacks/zones.svg)](https://travis-ci.org/meteorhacks/zones) 2 | # Zone.JS integration for meteor 3 | 4 | With [Zone.JS](https://github.com/angular/zone.js) integration, we can follow Meteor's async execution path (in client) and identify more information which is not possible before. 5 | 6 | As a result of that, error tracking can be improved and it can be used to capture stack traces over the async execution path. 7 | 8 | [![Demo: Zone.JS with Meteor](https://i.cloudup.com/uD_z8km2Xz.png)](http://zones-example.meteor.com/) 9 | 10 | ### Installation 11 | 12 | meteor add meteorhacks:zones 13 | 14 | // for older Meteor version 15 | mrt add zones 16 | 17 | That's all you've to do :) 18 | 19 | ### Integration with Kadira 20 | 21 | If you've added zones into a Meteor application which is being monitored with Kadira, error tracking on client can be improved dramatically. See following error trace: 22 | 23 | ![Kadira Error Tracking improved using Zones](https://cldup.com/-sxdlAvujw.png) 24 | 25 | For more information, visit Kadira's [error tracking docs](http://support.kadira.io/knowledgebase/articles/421158-client-side-error-tracking-with-zones). 26 | -------------------------------------------------------------------------------- /assets/after.js: -------------------------------------------------------------------------------- 1 | 2 | // initialize zone only if the browser is compatible 3 | // zone.js requires following ES5 features (or shims) 4 | if( window.JSON 5 | && Object.create 6 | && Object.defineProperties 7 | && Object.defineProperty 8 | && Object.freeze 9 | && Object.getOwnPropertyDescriptor 10 | && Object.keys 11 | && Array.prototype.forEach 12 | && Array.prototype.map 13 | && JSON.parse 14 | && JSON.stringify 15 | && isBrowserAllowed()) { 16 | Zone.init(); 17 | Zone.inited = true; 18 | restoreOriginals(); 19 | } 20 | 21 | function isBrowserAllowed() { 22 | var ieVersion = isIE(); 23 | if(!ieVersion) { 24 | return true; 25 | } else { 26 | return ieVersion > 9; 27 | } 28 | } 29 | 30 | function isIE () { 31 | var myNav = navigator.userAgent.toLowerCase(); 32 | return (myNav.indexOf('msie') != -1) ? parseInt(myNav.split('msie')[1]) : false; 33 | } -------------------------------------------------------------------------------- /assets/before.js: -------------------------------------------------------------------------------- 1 | backupOriginals(window, [ 2 | 'requestAnimationFrame', 3 | 'mozRequestAnimationFrame', 4 | 'webkitRequestAnimationFrame' 5 | ]); 6 | -------------------------------------------------------------------------------- /assets/reporters.js: -------------------------------------------------------------------------------- 1 | var Zone = window.Zone; 2 | var reporters = {}; 3 | 4 | Zone.Reporters = {}; 5 | 6 | Zone.Reporters.get = function(name) { 7 | return name ? reporters[name] : reporters; 8 | }; 9 | 10 | Zone.Reporters.add = function(name, reporter) { 11 | reporters[name] = reporter; 12 | }; 13 | 14 | Zone.Reporters.remove = function(name) { 15 | delete reporters[name]; 16 | }; 17 | 18 | Zone.Reporters.removeAll = function(name) { 19 | reporters = {}; 20 | }; 21 | 22 | Zone.Reporters.run = function(zone) { 23 | for(var name in reporters) { 24 | reporters[name](zone); 25 | } 26 | }; 27 | 28 | /* 29 | * Register default reporter 30 | */ 31 | 32 | Zone.Reporters.longStackTrace = function (zone) { 33 | var trace = []; 34 | var currZone = zone; 35 | var prevZone; 36 | var totalAsyncTime = 0; 37 | 38 | var errorMessage = Zone.Reporters.getErrorMessage(zone.erroredStack._e); 39 | trace.push("Error: " + errorMessage); 40 | trace.push(zone.erroredStack.get()); 41 | 42 | processZone(); 43 | 44 | function processZone() { 45 | if(currZone && currZone.currentStack) { 46 | var asyncTime = currZone.runAt - currZone.createdAt; 47 | 48 | if(prevZone) { 49 | // sometimes, there are gaps betweens zones 50 | // specially when handling with CPU intensive tasks and 51 | // with handling maxDepth 52 | var diff = prevZone.createdAt - currZone.runAt; 53 | asyncTime += diff; 54 | } 55 | 56 | if(asyncTime && asyncTime > 0) { 57 | totalAsyncTime += asyncTime; 58 | trace.push('\n> Before: ' + totalAsyncTime + 'ms (diff: ' + asyncTime + 'ms)'); 59 | } 60 | 61 | trace.push(currZone.currentStack.get()); 62 | prevZone = currZone; 63 | currZone = currZone.parent; 64 | 65 | setTimeout(processZone, 0); 66 | } else { 67 | console.log(trace.join('\n')); 68 | } 69 | } 70 | } 71 | 72 | // why? 73 | // in JavaScript, you can throw anything, not just errors 74 | // developers abuse this. even popular HighCharts does that 75 | // That's why we need ugly solutions like this 76 | Zone.Reporters.getErrorMessage = function(error) { 77 | if(!error) { 78 | return "Oops. sometimes went wrong with zones. There is no error." 79 | } else if(typeof error == 'string') { 80 | return error; 81 | } else if(error.message) { 82 | return error.message; 83 | } else { 84 | return error.toString(); 85 | } 86 | }; 87 | 88 | Zone.Reporters.add('longStackTrace', Zone.Reporters.longStackTrace); 89 | -------------------------------------------------------------------------------- /assets/tracer.js: -------------------------------------------------------------------------------- 1 | var Zone = window.Zone; 2 | 3 | nextZoneId = function() { 4 | var zoneIds = 0; 5 | return function () { 6 | return zoneIds++; 7 | }; 8 | }(); 9 | 10 | extendZone = function(fields) { 11 | for(var key in fields) { 12 | Zone.prototype[key] = fields[key]; 13 | } 14 | }; 15 | 16 | // extendZone with our own functionality 17 | 18 | extendZone({ 19 | maxDepth: 10, 20 | 21 | _fork: Zone.prototype.fork, 22 | 23 | onError: function (e) { 24 | this.erroredStack = new Stacktrace(e); 25 | Zone.Reporters.run(this); 26 | }, 27 | 28 | fork: function (locals) { 29 | var zone = this._fork(locals); 30 | 31 | // we don't need to get currentStack from the parent 32 | zone.currentStack = null; 33 | 34 | if(Zone.collectAllStacks) { 35 | zone.currentStack = getStacktrace(); 36 | } 37 | 38 | zone.createdAt = Date.now(); 39 | zone.id = nextZoneId(); 40 | // zone.currentStack = null; 41 | 42 | if(!zone.firstParent) { 43 | zone.firstParent = zone; 44 | } 45 | 46 | // setting depth and handling maxDepth 47 | zone.depth = (zone.depth)? zone.depth + 1 : 1; 48 | if(zone.depth > zone.maxDepth) { 49 | zone.run = zone._resetDepthAndRun; 50 | } 51 | 52 | // when creating a new zone, it will use's parent zone as __proto__ 53 | // that's why we can access ancesstor properties 54 | // but we are deleting eventMap just after zone ran 55 | // so, we need to create eventMap explicitely to stay it in the current zone 56 | zone.eventMap = zone.eventMap || {}; 57 | 58 | // infoMap is just like eventMap, but it deepCopy all the info 59 | // so only previous zone's info will be exists 60 | zone.infoMap = zone.infoMap || {}; 61 | if(zone.parent && zone.parent._info) { 62 | var parentInfo = zone.parent._info; 63 | zone.infoMap[zone.parent.id] = {}; 64 | for(var key in parentInfo) { 65 | zone.infoMap[zone.parent.id][key] = parentInfo[key]; 66 | } 67 | } 68 | 69 | // make sure owner doesn't get inherited 70 | zone.owner = undefined; 71 | return zone; 72 | }, 73 | 74 | beforeTask: function() { 75 | this.runAt = Date.now(); 76 | 77 | // create eventMap for the first time 78 | // eventMap will be deleted just after zone completed 79 | // but it will be available only in the errroed zone 80 | // so, in that zone, someone can access all the events happened on the 81 | // async call stack 82 | if(!this.eventMap) { 83 | this.eventMap = {}; 84 | this.infoMap = {}; 85 | } 86 | 87 | // _events will only be available during the zone running time only 88 | // an event can be run multiple times. So we can't maintain the events 89 | // in this array forever. 90 | // best option is to add it to eventMap, which carries events to the 91 | // top of the stack 92 | this._events = []; 93 | this.eventMap[this.id] = this._events; 94 | this._info = {}; 95 | this.infoMap[this.id] = this._info; 96 | 97 | // if there is _ownerArgs we need to add it as an event 98 | // after that we don't need to _ownerArgs 99 | if(this._ownerArgs) { 100 | this.addEvent({type: "owner-args", args: this._ownerArgs, at: Date.now()}); 101 | delete this._ownerArgs; 102 | } 103 | }, 104 | 105 | afterTask: function() { 106 | delete this._events; 107 | delete this._info; 108 | // we only keep eventMap in the errored zone only 109 | if(!this.erroredStack) { 110 | delete this.eventMap; 111 | delete this.infoMap; 112 | } 113 | }, 114 | 115 | addEvent: function(event) { 116 | // when zone completed _events will be removed 117 | // but actions may happen even after the zone completed 118 | // and we are not interested about those 119 | if(this._events) { 120 | event.time = this.getTime(); 121 | this._events.push(event); 122 | } 123 | }, 124 | 125 | setInfo: function(key, value) { 126 | if(this._info) { 127 | value.time = this.getTime(); 128 | this._info[key] = value; 129 | } 130 | }, 131 | 132 | // we can add it the zone direcly, because zone can be run many times 133 | // we can't add this as a event yet, since zone doesn't started yet 134 | // so we'll add this to a special fields and it's will added to the _events 135 | // when _events will be creating 136 | setOwnerArgs: function(args) { 137 | this._ownerArgs = args; 138 | }, 139 | 140 | setOwner: function(ownerInfo) { 141 | ownerInfo.time = this.getTime(); 142 | this.owner = ownerInfo; 143 | }, 144 | 145 | // validate and pick arguments 146 | // we don't need to capture event object as the argument 147 | // (then we can't to JSON stringify) 148 | // That's why we've this 149 | bind: function (func, skipEnqueue, ownerInfo, validateArgs) { 150 | validateArgs = validateArgs || function() {return []}; 151 | skipEnqueue || this.enqueueTask(func); 152 | var zone = this.fork(); 153 | 154 | if(ownerInfo) { 155 | zone.setOwner(ownerInfo); 156 | ownerInfo.zoneId = zone.id; 157 | } 158 | 159 | return function zoneBoundFn() { 160 | if(ownerInfo) { 161 | zone.setOwnerArgs(validateArgs(this, arguments)); 162 | } 163 | return zone.run(func, this, arguments); 164 | }; 165 | }, 166 | 167 | bindOnce: function (func, ownerInfo, validateArgs) { 168 | var boundZone = this; 169 | return this.bind(function() { 170 | var result = Zone._apply(func, this, arguments); 171 | boundZone.dequeueTask(func); 172 | return result; 173 | }, false, ownerInfo, validateArgs); 174 | }, 175 | 176 | getTime: function () { 177 | return Date.now(); 178 | }, 179 | 180 | _resetDepthAndRun: function(fn, applyTo, applyWith) { 181 | try { 182 | window._oldZone = window.zone; 183 | window.zone = this.firstParent || window._oldZone; 184 | return fn.apply(applyTo, applyWith); 185 | } catch(ex) { 186 | if(this.onError) { 187 | this.onError(ex); 188 | } else { 189 | throw ex; 190 | } 191 | } finally { 192 | window.zone = window._oldZone; 193 | } 194 | } 195 | }); 196 | 197 | /** 198 | * Create a stack trace 199 | */ 200 | 201 | function getStacktrace () { 202 | var stack = getStacktraceWithUncaughtError(); 203 | if (stack && stack._e.stack) { 204 | getStacktrace = getStacktraceWithUncaughtError; 205 | return stack; 206 | } else { 207 | getStacktrace = getStacktraceWithCaughtError; 208 | return getStacktrace(); 209 | } 210 | }; 211 | 212 | function getStacktraceWithUncaughtError () { 213 | return new Stacktrace(new Error()); 214 | } 215 | 216 | function getStacktraceWithCaughtError () { 217 | try { 218 | throw new Error(); 219 | } catch (e) { 220 | return new Stacktrace(e); 221 | } 222 | } 223 | 224 | /* 225 | * Wrapped stacktrace 226 | * 227 | * We need this because in some implementations, constructing a trace is slow 228 | * and so we want to defer accessing the trace for as long as possible 229 | */ 230 | 231 | function Stacktrace (e) { 232 | this._e = e; 233 | } 234 | 235 | Stacktrace.prototype.get = function() { 236 | if(this._e && typeof this._e.stack == 'string') { 237 | return this._e.stack 238 | .split('\n') 239 | .filter(this.stackFramesFilter) 240 | .join('\n'); 241 | } else { 242 | return ""; 243 | } 244 | }; 245 | 246 | Stacktrace.prototype.stackFramesFilter = function(line) { 247 | var filterRegExp = /\/packages\/(meteorhacks_zones|zones|local-test_meteorhacks_zones)\/assets\/|^Error$/; 248 | return !line.match(filterRegExp); 249 | }; 250 | -------------------------------------------------------------------------------- /assets/utils.js: -------------------------------------------------------------------------------- 1 | function hijackConnection(original, type) { 2 | return function () { 3 | var self = this; 4 | var args = Array.prototype.slice.call(arguments); 5 | 6 | // if this comes from a Method.call we don't need to track it 7 | var isFromCall = Zone.fromCall.get(); 8 | 9 | if(!isFromCall && args.length) { 10 | var callback = args[args.length - 1]; 11 | if(typeof callback === 'function') { 12 | var methodName = args[0]; 13 | var methodArgs = args.slice(1, args.length - 1); 14 | var ownerInfo = {type: type, name: methodName, args: methodArgs}; 15 | var zoneInfo = {type: type, name: methodName, args: methodArgs}; 16 | zone.setInfo(type, zoneInfo); 17 | args[args.length - 1] = function (argument) { 18 | var args = Array.prototype.slice.call(arguments); 19 | return Zone._apply(callback, this, args); 20 | } 21 | args[args.length - 1] = zone.bind(args[args.length - 1], false, ownerInfo, pickAllArgs); 22 | } 23 | } 24 | 25 | if(type == "Meteor.call") { 26 | // make sure this won't get tracked another time 27 | return Zone.fromCall.withValue(true, function() { 28 | return Zone._apply(original, self, args); 29 | }); 30 | } else { 31 | return Zone._apply(original, this, args); 32 | } 33 | } 34 | } 35 | 36 | function hijackSubscribe(originalFunction, type) { 37 | return function () { 38 | var args = Array.prototype.slice.call(arguments); 39 | if(args.length) { 40 | var callback = args[args.length - 1]; 41 | var subName = args[0]; 42 | var subArgs = args.slice(1, args.length - 1); 43 | if(typeof callback === 'function') { 44 | var ownerInfo = {type: type, name: subName, args: subArgs}; 45 | var zoneInfo = {type: type, name: subName, args: subArgs}; 46 | zone.setInfo(type, zoneInfo); 47 | args[args.length - 1] = function (argument) { 48 | var args = Array.prototype.slice.call(arguments); 49 | return Zone._apply(callback, this, args); 50 | } 51 | args[args.length - 1] = zone.bind(args[args.length - 1], false, ownerInfo, pickAllArgs); 52 | } else if(callback) { 53 | ['onReady', 'onError'].forEach(function (funName) { 54 | var ownerInfo = {type: type, name: subName, args: subArgs, callbackType: funName}; 55 | if(typeof callback[funName] === "function") { 56 | var zoneInfo = {type: type, name: subName, args: subArgs, callbackType: funName}; 57 | zone.setInfo(type, zoneInfo); 58 | var originalCallback = callback[funName]; 59 | callback[funName] = function (argument) { 60 | var args = Array.prototype.slice.call(arguments); 61 | return Zone._apply(originalCallback, this, args); 62 | } 63 | callback[funName] = zone.bind(callback[funName], false, ownerInfo, pickAllArgs); 64 | } 65 | }) 66 | } 67 | } 68 | return Zone._apply(originalFunction, this, args); 69 | } 70 | } 71 | 72 | function hijackCursor(Cursor) { 73 | 74 | hijackFunction('observe', [ 75 | 'added', 'addedAt', 'changed', 'changedAt', 76 | 'removed', 'removedAt', 'movedTo' 77 | ]); 78 | 79 | hijackFunction('observeChanges', [ 80 | 'added', 'addedBefore', 'changed', 81 | 'removed', 'movedBefore' 82 | ]); 83 | 84 | // hijack Cursor.fetch 85 | var originalCursorFetch = Cursor.fetch; 86 | Cursor.fetch = function () { 87 | var self = this; 88 | var args = Array.prototype.slice.call(arguments); 89 | var type = 'MongoCursor.fetch'; 90 | if(zone && !this._avoidZones) { 91 | var collection = this.collection && this.collection.name; 92 | var query = this.matcher && this.matcher._selector; 93 | var zoneInfo = {type: type, collection: collection, query: query}; 94 | zone.setInfo(type, zoneInfo) 95 | }; 96 | return Zone.notFromForEach.withValue(true, function() { 97 | return Zone._apply(originalCursorFetch, self, args); 98 | }); 99 | }; 100 | 101 | ['forEach', 'map'].forEach(function (name) { 102 | var original = Cursor[name]; 103 | Cursor[name] = function (callback, thisArg) { 104 | var self = thisArg || this; 105 | var args = Array.prototype.slice.call(arguments); 106 | var type = 'MongoCursor.' + name; 107 | var notFromForEach = Zone.notFromForEach.get(); 108 | if(!this._avoidZones 109 | && !notFromForEach 110 | && typeof callback === 'function') { 111 | args[0] = function (doc, index) { 112 | var args = Array.prototype.slice.call(arguments); 113 | var collection = self.collection && self.collection.name; 114 | var query = self.matcher && self.matcher._selector; 115 | var ownerInfo = {type: type, collection: collection, query: query}; 116 | var zoneInfo = {type: type, collection: collection, query: query, document: doc, index: index}; 117 | zone.setInfo(type, zoneInfo); 118 | callback = zone.bind(callback, false, ownerInfo, pickAllArgs); 119 | return Zone._apply(callback, this, args); 120 | }; 121 | } 122 | 123 | if(name !== 'forEach') { 124 | return Zone.notFromForEach.withValue(true, function() { 125 | return Zone._apply(original, self, args); 126 | }); 127 | } else { 128 | return Zone._apply(original, self, args); 129 | } 130 | } 131 | }); 132 | 133 | function hijackFunction(type, callbacks) { 134 | var original = Cursor[type]; 135 | Cursor[type] = function (options) { 136 | var self = this; 137 | var eventType = 'MongoCursor.' + type; 138 | // check this request comes from an observer call 139 | // if so, we don't need to track this request 140 | var isFromObserve = Zone.fromObserve.get(); 141 | 142 | if(!this._avoidZones && !isFromObserve && options) { 143 | callbacks.forEach(function (funName) { 144 | var callback = options[funName]; 145 | if(typeof callback === 'function') { 146 | var ownerInfo = { 147 | type: eventType, 148 | query: self.matcher._selector, 149 | callbackType: funName, 150 | collection: self.collection.name 151 | }; 152 | zone.setInfo(eventType, { 153 | type: eventType, 154 | query: self.matcher._selector, 155 | callbackType: funName, 156 | collection: self.collection.name 157 | }); 158 | options[funName] = zone.bind(callback, false, ownerInfo, pickAllArgs); 159 | } 160 | }); 161 | } 162 | 163 | if(type == 'observe') { 164 | // notify observeChanges to not to track again 165 | return Zone.fromObserve.withValue(true, function() { 166 | return original.call(self, options); 167 | }); 168 | } else { 169 | return original.call(this, options); 170 | } 171 | }; 172 | } 173 | } 174 | 175 | function hijackComponentEvents(original) { 176 | var type = 'Template.event'; 177 | return function (dict) { 178 | var self = this; 179 | var name = getTemplateName(this); 180 | for (var target in dict) { 181 | var handler = dict[target]; 182 | if (typeof handler === 'function') { 183 | dict[target] = prepareHandler(handler, target); 184 | } 185 | } 186 | 187 | return original.call(this, dict); 188 | 189 | function prepareHandler(handler, target) { 190 | return function () { 191 | var args = Array.prototype.slice.call(arguments); 192 | var ownerInfo = {type: type, event: target, template: name}; 193 | zone.owner = ownerInfo; 194 | Zone._apply(handler, this, args); 195 | }; 196 | } 197 | 198 | function getTemplateName(view) { 199 | if(view.__templateName) { 200 | // for Meteor 0.9.0 and older 201 | return view.__templateName; 202 | } else if(view.viewName) { 203 | // for Meteor 0.9.1 204 | return view.viewName.replace(/Template\./, ''); 205 | } else if(view.kind) { 206 | return view.kind.split('_')[1]; 207 | } 208 | } 209 | 210 | } 211 | } 212 | 213 | function hijackDepsFlush(original, type) { 214 | return function () { 215 | var args = Array.prototype.slice.call(arguments); 216 | if(zone.owner && window.zone.owner.type == 'setTimeout') { 217 | zone.owner = {type: type}; 218 | } 219 | return Zone._apply(original, this, args); 220 | } 221 | } 222 | 223 | function hijackSessionSet(original, type) { 224 | return function () { 225 | var args = Array.prototype.slice.call(arguments); 226 | zone.addEvent({type: type, key: args[0], value: args[1]}); 227 | return Zone._apply(original, this, args); 228 | } 229 | } 230 | 231 | var TemplateCoreFunctions = ['prototype', '__makeView', '__render']; 232 | 233 | function hijackTemplateHelpers(template, templateName) { 234 | _.each(template, function (hookFn, name) { 235 | template[name] = hijackHelper(hookFn, name, templateName); 236 | }); 237 | } 238 | 239 | function hijackNewTemplateHelpers(original, templateName) { 240 | return function (dict) { 241 | dict && _.each(dict, function (hookFn, name) { 242 | dict[name] = hijackHelper(hookFn, name, templateName); 243 | }); 244 | 245 | var args = Array.prototype.slice.call(arguments); 246 | return Zone._apply(original, this, args); 247 | } 248 | } 249 | 250 | function hijackHelper(hookFn, name, templateName) { 251 | if(hookFn 252 | && typeof hookFn === 'function' 253 | && _.indexOf(TemplateCoreFunctions, name) === -1) { 254 | // Assuming the value is a template helper 255 | return function () { 256 | var args = Array.prototype.slice.call(arguments); 257 | zone.setInfo('Template.helper', {name: name, template: templateName}); 258 | var result = Zone._apply(hookFn, this, args); 259 | if(result && typeof result.observe === 'function') { 260 | result._avoidZones = true; 261 | } 262 | return result; 263 | } 264 | } else { 265 | return hookFn; 266 | } 267 | } 268 | 269 | function hijackGlobalHelpers(helpers) { 270 | var _ = Package.underscore._; 271 | _(helpers || {}).each(function (helperFn, name) { 272 | helpers[name] = hijackGlobalHelper(helperFn, name) 273 | }); 274 | } 275 | 276 | function hijackNewGlobalHelpers (original) { 277 | return function (name, helperFn) { 278 | var args = Array.prototype.slice.call(arguments); 279 | args[1] = hijackGlobalHelper(helperFn, name); 280 | return Zone._apply(original, this, args); 281 | }; 282 | } 283 | 284 | function hijackGlobalHelper(helperFn, name) { 285 | var _ = Package.underscore._; 286 | if(helperFn 287 | && typeof helperFn === 'function' 288 | && _.indexOf(TemplateCoreFunctions, name) === -1) { 289 | return function () { 290 | var args = Array.prototype.slice.call(arguments); 291 | var result = Zone._apply(helperFn, this, args); 292 | if(result && typeof result.observe === 'function') { 293 | result._avoidZones = true; 294 | zone.setInfo('Global.helper', {name: name, args: args}); 295 | } else { 296 | var zoneInfo = {name: name, args: args, result: result}; 297 | zone.setInfo('Global.helper', zoneInfo); 298 | } 299 | return result; 300 | } 301 | } else { 302 | return helperFn; 303 | } 304 | } 305 | 306 | //--------------------------------------------------------------------------\\ 307 | 308 | var originalFunctions = []; 309 | function backupOriginals(obj, methodNames) { 310 | if(obj && methodNames && methodNames.length) { 311 | var backup = {obj: obj, methods: {}}; 312 | for(var i=0, l=methodNames.length; i 1 ? new WS(a, b) : new WS(a); 796 | var proxySocket; 797 | 798 | // Safari 7.0 has non-configurable own 'onmessage' and friends properties on the socket instance 799 | var onmessageDesc = Object.getOwnPropertyDescriptor(socket, 'onmessage'); 800 | if (onmessageDesc && onmessageDesc.configurable === false) { 801 | proxySocket = Object.create(socket); 802 | ['addEventListener', 'removeEventListener', 'send', 'close'].forEach(function(propName) { 803 | proxySocket[propName] = function() { 804 | return socket[propName].apply(socket, arguments); 805 | }; 806 | }); 807 | } else { 808 | // we can patch the real socket 809 | proxySocket = socket; 810 | } 811 | 812 | utils.patchProperties(proxySocket, ['onclose', 'onerror', 'onmessage', 'onopen']); 813 | 814 | return proxySocket; 815 | }; 816 | } 817 | 818 | module.exports = { 819 | apply: apply 820 | }; 821 | 822 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 823 | },{"../utils":13}],13:[function(require,module,exports){ 824 | (function (global){ 825 | 'use strict'; 826 | 827 | function bindArguments(args, ownerInfo) { 828 | for (var i = args.length - 1; i >= 0; i--) { 829 | if (typeof args[i] === 'function') { 830 | args[i] = global.zone.bind(args[i], false, ownerInfo); 831 | } 832 | } 833 | return args; 834 | }; 835 | 836 | function bindArgumentsOnce(args, ownerInfo) { 837 | for (var i = args.length - 1; i >= 0; i--) { 838 | if (typeof args[i] === 'function') { 839 | args[i] = global.zone.bindOnce(args[i], ownerInfo); 840 | } 841 | } 842 | return args; 843 | }; 844 | 845 | function patchPrototype(obj, fnNames, kind) { 846 | fnNames.forEach(function (name) { 847 | var delegate = obj[name]; 848 | if (delegate) { 849 | obj[name] = function () { 850 | var ownerInfo = {type: kind + "." + name}; 851 | return delegate.apply(this, bindArguments(arguments, ownerInfo)); 852 | }; 853 | } 854 | }); 855 | }; 856 | 857 | function patchProperty(obj, prop) { 858 | var desc = Object.getOwnPropertyDescriptor(obj, prop) || { 859 | enumerable: true, 860 | configurable: true 861 | }; 862 | 863 | // A property descriptor cannot have getter/setter and be writable 864 | // deleting the writable and value properties avoids this error: 865 | // 866 | // TypeError: property descriptors must not specify a value or be writable when a 867 | // getter or setter has been specified 868 | delete desc.writable; 869 | delete desc.value; 870 | 871 | // substr(2) cuz 'onclick' -> 'click', etc 872 | var eventName = prop.substr(2); 873 | var _prop = '_' + prop; 874 | 875 | desc.set = function (fn) { 876 | if (this[_prop]) { 877 | this.removeEventListener(eventName, this[_prop]); 878 | } 879 | 880 | if (typeof fn === 'function') { 881 | this[_prop] = fn; 882 | this.addEventListener(eventName, fn, false); 883 | } else { 884 | this[_prop] = null; 885 | } 886 | }; 887 | 888 | desc.get = function () { 889 | return this[_prop]; 890 | }; 891 | 892 | Object.defineProperty(obj, prop, desc); 893 | }; 894 | 895 | function patchProperties(obj, properties) { 896 | 897 | (properties || (function () { 898 | var props = []; 899 | for (var prop in obj) { 900 | props.push(prop); 901 | } 902 | return props; 903 | }()). 904 | filter(function (propertyName) { 905 | return propertyName.substr(0,2) === 'on'; 906 | })). 907 | forEach(function (eventName) { 908 | patchProperty(obj, eventName); 909 | }); 910 | }; 911 | 912 | function patchEventTargetMethods(obj, thing) { 913 | var addDelegate = obj.addEventListener; 914 | obj.addEventListener = function (eventName, fn) { 915 | 916 | var elementName; 917 | if(this === window) { 918 | elementName = 'window'; 919 | } else if(this) { 920 | elementName = this.localName || this.nodeName; 921 | } else { 922 | elementName = "no element"; 923 | } 924 | 925 | var ownerInfo = { 926 | type: thing + ".addEventListener", 927 | event: eventName, 928 | name: elementName, 929 | }; 930 | 931 | // `this` can be undefined 932 | if(this && this.attributes) { 933 | ownerInfo.attributes = {}; 934 | var attributesArray = Array.prototype.slice.call(this.attributes); 935 | attributesArray.forEach(function (attr) { 936 | ownerInfo.attributes[attr.name] = attr.value; 937 | }); 938 | } 939 | 940 | fn._bound = fn._bound || {}; 941 | arguments[1] = fn._bound[eventName] = zone.bind(fn, false, ownerInfo); 942 | return addDelegate.apply(this, arguments); 943 | }; 944 | 945 | var removeDelegate = obj.removeEventListener; 946 | obj.removeEventListener = function (eventName, fn) { 947 | if(arguments[1]._bound && arguments[1]._bound[eventName]) { 948 | var _bound = arguments[1]._bound; 949 | arguments[1] = _bound[eventName]; 950 | delete _bound[eventName]; 951 | } 952 | var result = removeDelegate.apply(this, arguments); 953 | global.zone.dequeueTask(fn); 954 | return result; 955 | }; 956 | }; 957 | 958 | // wrap some native API on `window` 959 | function patchClass(className) { 960 | var OriginalClass = global[className]; 961 | if (!OriginalClass) return; 962 | 963 | global[className] = function () { 964 | var ownerInfo = {type: className}; 965 | var a = bindArguments(arguments, ownerInfo); 966 | switch (a.length) { 967 | case 0: this._o = new OriginalClass(); break; 968 | case 1: this._o = new OriginalClass(a[0]); break; 969 | case 2: this._o = new OriginalClass(a[0], a[1]); break; 970 | case 3: this._o = new OriginalClass(a[0], a[1], a[2]); break; 971 | case 4: this._o = new OriginalClass(a[0], a[1], a[2], a[3]); break; 972 | default: throw new Error('what are you even doing?'); 973 | } 974 | }; 975 | 976 | var instance = new OriginalClass(); 977 | 978 | var prop; 979 | for (prop in instance) { 980 | (function (prop) { 981 | if (typeof instance[prop] === 'function') { 982 | global[className].prototype[prop] = function () { 983 | return this._o[prop].apply(this._o, arguments); 984 | }; 985 | } else { 986 | Object.defineProperty(global[className].prototype, prop, { 987 | set: function (fn) { 988 | if (typeof fn === 'function') { 989 | this._o[prop] = global.zone.bind(fn); 990 | } else { 991 | this._o[prop] = fn; 992 | } 993 | }, 994 | get: function () { 995 | return this._o[prop]; 996 | } 997 | }); 998 | } 999 | }(prop)); 1000 | } 1001 | 1002 | for (prop in OriginalClass) { 1003 | if (prop !== 'prototype' && OriginalClass.hasOwnProperty(prop)) { 1004 | global[className][prop] = OriginalClass[prop]; 1005 | } 1006 | } 1007 | }; 1008 | 1009 | module.exports = { 1010 | bindArguments: bindArguments, 1011 | bindArgumentsOnce: bindArgumentsOnce, 1012 | patchPrototype: patchPrototype, 1013 | patchProperty: patchProperty, 1014 | patchProperties: patchProperties, 1015 | patchEventTargetMethods: patchEventTargetMethods, 1016 | patchClass: patchClass 1017 | }; 1018 | 1019 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 1020 | },{}]},{},[1]); 1021 | -------------------------------------------------------------------------------- /client/hijack.js: -------------------------------------------------------------------------------- 1 | // Hijack only if zone is available 2 | if(!window.Zone || !window.Zone.inited) { 3 | return; 4 | } 5 | 6 | // some EnvironmentVariables to optimize tracking 7 | // see /assests/utils.js 8 | Zone.fromCall = new Meteor.EnvironmentVariable(); 9 | Zone.fromObserve = new Meteor.EnvironmentVariable(); 10 | Zone.notFromForEach = new Meteor.EnvironmentVariable(); 11 | 12 | var ConnectionProto = getConnectionProto(); 13 | 14 | /* 15 | * Hijack method calls 16 | */ 17 | ConnectionProto.apply = hijackConnection( 18 | ConnectionProto.apply, 19 | 'Connection.apply' 20 | ); 21 | 22 | /** 23 | * For better stackTraces 24 | */ 25 | Meteor.call = hijackConnection(Meteor.call, 'Meteor.call'); 26 | 27 | /* 28 | * Hijack DDP subscribe method 29 | * Used when connecting to external DDP servers 30 | */ 31 | ConnectionProto.subscribe = hijackSubscribe( 32 | ConnectionProto.subscribe, 33 | 'Connection.subscribe' 34 | ); 35 | 36 | /** 37 | * Hijack Meteor.subscribe because Meteor.subscribe binds to 38 | * Connection.subscribe before the hijack 39 | */ 40 | Meteor.subscribe = hijackSubscribe(Meteor.subscribe, 'Meteor.subscribe'); 41 | 42 | hijackCursor(LocalCollection.Cursor.prototype); 43 | 44 | /** 45 | * Hijack Template.prototype.events() to add useful owner info to zone object 46 | * Use UI.Component.events for older versions of Meteor 47 | * e.g. {type: 'templateEvent', event: 'click .selector', template: 'home'} 48 | */ 49 | if(Template.prototype) { 50 | Template.prototype.events = hijackComponentEvents(Template.prototype.events); 51 | } else if (UI.Component) { 52 | UI.Component.events = hijackComponentEvents(UI.Component.events); 53 | } 54 | 55 | /** 56 | * Hijack global template helpers using `UI.registerHelper` 57 | */ 58 | hijackGlobalHelpers(UI._globalHelpers); 59 | UI.registerHelper = hijackNewGlobalHelpers(UI.registerHelper); 60 | 61 | /** 62 | * Hijack each templates rendered handler to add template name to owner info 63 | */ 64 | var CoreTemplates = ['prototype', '__body__', '__dynamic', '__dynamicWithDataContext', '__IronDefaultLayout__']; 65 | Meteor.startup(function () { 66 | _(Template).each(function (template, name) { 67 | if(typeof template === 'object') { 68 | // hijack template helpers including 'rendered' 69 | if(_.indexOf(CoreTemplates, name) === -1) { 70 | hijackTemplateHelpers(template, name); 71 | template.helpers = hijackNewTemplateHelpers(template.helpers, name); 72 | } 73 | } 74 | }); 75 | }); 76 | 77 | /** 78 | * Hijack Session.set to add events 79 | */ 80 | Session.set = hijackSessionSet(Session.set, 'Session.set'); 81 | 82 | /** 83 | * Hijack Deps.autorun to set correct zone owner type 84 | * Otherwise these will be setTimeout 85 | */ 86 | Deps.flush = hijackDepsFlush(Deps.flush, 'Deps.flush'); 87 | 88 | //--------------------------------------------------------------------------\\ 89 | 90 | function getConnectionProto() { 91 | var con = DDP.connect(getCurrentUrlOrigin()); 92 | con.disconnect(); 93 | var proto = con.constructor.prototype; 94 | return proto; 95 | } 96 | 97 | function getCurrentUrlOrigin() { 98 | // Internet Explorer doesn't have window.location.origin 99 | return window.location.origin || window.location.protocol 100 | + window.location.hostname 101 | + window.location.port; 102 | } 103 | 104 | // we've a better error handling support with zones 105 | // Meteor._debug will prevent it (specially inside deps) 106 | // So we are killing Meteor._debug 107 | var originalMeteorDebug = Meteor._debug; 108 | Meteor._debug = function(message, stack) { 109 | var err = new Error(message); 110 | err.stack = (stack instanceof Error)? stack.stack: stack; 111 | if(zone) { 112 | zone.onError(err); 113 | } else { 114 | originalMeteorDebug(message, stack); 115 | } 116 | }; 117 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | var fs = Npm.require('fs'); 2 | var path = Npm.require('path'); 3 | 4 | Package.describe({ 5 | name: 'meteorhacks:zones', 6 | summary: 'Zone.Js integration for meteor', 7 | version: "1.6.0", 8 | git: "https://github.com/meteorhacks/zones.git" 9 | }); 10 | 11 | Package.on_use(function (api) { 12 | addPackageFiles(api); 13 | api.export('Zones', 'server'); 14 | }); 15 | 16 | Package.on_test(function (api) { 17 | addPackageFiles(api); 18 | 19 | api.use([ 20 | 'tinytest', 21 | 'test-helpers', 22 | ], 'client'); 23 | 24 | api.add_files([ 25 | 'tests/_both.js' 26 | ], ['client', 'server']); 27 | 28 | api.add_files([ 29 | 'tests/_server.js' 30 | ], 'server'); 31 | 32 | api.add_files([ 33 | 'tests/loader.js', 34 | 'tests/reporters.js', 35 | 'tests/hijacks/methods.js', 36 | 'tests/hijacks/subscriptions.js', 37 | 'tests/hijacks/collections.js', 38 | ], 'client'); 39 | }); 40 | 41 | function addPackageFiles(api) { 42 | if(api.versionsFrom) { 43 | api.versionsFrom('METEOR@0.9.2.1'); 44 | api.use('meteorhacks:inject-initial@1.0.0', ['server']); 45 | } else { 46 | api.use('inject-initial'); 47 | } 48 | 49 | api.add_files([ 50 | 'assets/utils.js', 51 | 'assets/before.js', 52 | 'assets/zone.js', 53 | 'assets/after.js', 54 | 'assets/reporters.js', 55 | 'assets/tracer.js', 56 | ], 'client', {isAsset: true}); 57 | 58 | api.add_files(['server/inject.js'], 'server'); 59 | 60 | api.add_files([ 61 | 'client/hijack.js' 62 | ], 'client'); 63 | 64 | api.use('underscore', 'client'); 65 | api.use('ui', 'client'); 66 | api.use('templating', 'client'); 67 | api.use('deps', 'client'); 68 | api.use('session', 'client'); 69 | api.use('livedata', 'client'); 70 | api.use('minimongo', 'client'); 71 | } 72 | 73 | //--------------------------------------------------------------------------\\ 74 | 75 | function meteorRoot() { 76 | var currentDir = process.cwd(); 77 | while (currentDir) { 78 | var newDir = path.dirname(currentDir); 79 | if (isAppDir(currentDir)) { 80 | break; 81 | } else if (newDir === currentDir) { 82 | return null; 83 | } else { 84 | currentDir = newDir; 85 | } 86 | } 87 | return currentDir; 88 | } 89 | 90 | function isAppDir(filepath) { 91 | try { 92 | return fs.statSync(path.join(filepath, '.meteor', 'packages')).isFile(); 93 | } catch (e) { 94 | return false; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /server/inject.js: -------------------------------------------------------------------------------- 1 | var format = Npm.require('util').format; 2 | 3 | // only Meteor < 0.9 has this tyoe of naming for packages 4 | if(Package['inject-initial']) { 5 | Inject = Package['inject-initial'].Inject; 6 | var packageName = 'zones'; 7 | } else { 8 | // for Meteor 0.9 + 9 | Inject = Package['meteorhacks:inject-initial'].Inject; 10 | 11 | // this is a trick to idnentify the test environment 12 | // need to set this env var before running tests 13 | if(process.env['METEOR_ENV'] == 'test') { 14 | var packageName = 'local-test_meteorhacks_zones'; 15 | } else { 16 | var packageName = 'meteorhacks_zones'; 17 | } 18 | } 19 | 20 | var fileList = [ 21 | 'utils.js', 'before.js', 'zone.js', 'tracer.js', 22 | 'after.js', 'reporters.js' 23 | ]; 24 | 25 | var cacheAvoider = (new Date).getTime(); 26 | var finalHtml = ''; 27 | fileList.forEach(function(file) { 28 | var template = '\n'; 29 | finalHtml += format(template, packageName, file, cacheAvoider); 30 | }); 31 | 32 | Zones = { 33 | html: finalHtml, 34 | enabled: true, 35 | }; 36 | 37 | Zones.enable = function () { 38 | Zones.enabled = true; 39 | }; 40 | 41 | Zones.disable = function () { 42 | Zones.enabled = false; 43 | }; 44 | 45 | Inject.rawHead('zones', function () { 46 | return Zones.enabled ? Zones.html : ''; 47 | }); 48 | -------------------------------------------------------------------------------- /smart.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zones", 3 | "description": "Zone.Js integration for meteor", 4 | "homepage": "https://github.com/meteorhacks/zones", 5 | "author": "MeteorHacks", 6 | "version": "1.4.0", 7 | "git": "https://github.com/meteorhacks/zones.git", 8 | "packages": { 9 | "inject-initial": {} 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/_both.js: -------------------------------------------------------------------------------- 1 | 2 | TestCollection = new Meteor.Collection('test-collection'); 3 | 4 | TestCollection.allow({ 5 | insert: function (userId, doc) { 6 | return true; 7 | }, 8 | update: function (userId, doc, fields, modifier) { 9 | return true; 10 | }, 11 | remove: function (userId, doc) { 12 | return true; 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /tests/_server.js: -------------------------------------------------------------------------------- 1 | 2 | Meteor.methods({ 3 | 'zone-on': function () { 4 | Zones.enable(); 5 | }, 6 | 7 | 'zone-off': function () { 8 | Zones.disable(); 9 | }, 10 | 11 | 'test-method': function () { 12 | return; 13 | } 14 | }); 15 | 16 | Meteor.publish('test-ready', function () { 17 | this.ready(); 18 | }); 19 | 20 | Meteor.publish('test-error', function () { 21 | throw new Meteor.Error(); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/hijacks/collections.js: -------------------------------------------------------------------------------- 1 | Tinytest.addAsync( 2 | 'Hijacks - Collections - insert', 3 | function (test, next) { 4 | Zone.Reporters.removeAll(); 5 | Zone.Reporters.add('test-reporter', function (zone) { 6 | 7 | // test whether zone has correct owner info 8 | var owner = zone.owner; 9 | var expected = { 10 | args: [ 11 | [{_id: 'foo', bar: 'baz'}], 12 | {returnStubValue: true} 13 | ], 14 | name: '/test-collection/insert', 15 | type: 'Connection.apply', 16 | // zoneId: 123 17 | }; 18 | 19 | test.equal('object', typeof owner); 20 | test.equal('number', typeof owner.time); 21 | delete owner.time; 22 | test.equal('number', typeof owner.zoneId); 23 | delete owner.zoneId; 24 | test.equal(expected, owner); 25 | 26 | // test whether zone has correct info 27 | // the parent zone contains method info 28 | var info = zone.infoMap[zone.parent.id]; 29 | var expectedInfo = { 30 | type: 'Connection.apply', 31 | name: '/test-collection/insert', 32 | // time: 123, 33 | args: [ 34 | [{_id: 'foo', bar: 'baz'}], 35 | {returnStubValue: true} 36 | ], 37 | }; 38 | 39 | test.equal('object', typeof info); 40 | test.equal('number', typeof info['Connection.apply'].time); 41 | delete info['Connection.apply'].time; 42 | test.equal(expectedInfo, info['Connection.apply']); 43 | 44 | // reset zone for other tests and continue 45 | Zone.Reporters.add(Zone.longStackTrace); 46 | Zone.Reporters.remove('test-reporter'); 47 | next(); 48 | }); 49 | 50 | TestCollection.remove({_id: 'foo'}, function () { 51 | TestCollection.insert({_id: 'foo', bar: 'baz'}, function () { 52 | throw new Error('test-error'); 53 | }); 54 | }); 55 | } 56 | ); 57 | 58 | Tinytest.addAsync( 59 | 'Hijacks - Collections - update', 60 | function (test, next) { 61 | Zone.Reporters.removeAll(); 62 | Zone.Reporters.add('test-reporter', function (zone) { 63 | 64 | // test whether zone has correct owner info 65 | var owner = zone.owner; 66 | var expected = { 67 | args: [ 68 | [{_id: 'foo'}, {$set: {bar: 'bat'}}, {}], 69 | {returnStubValue: true} 70 | ], 71 | name: '/test-collection/update', 72 | type: 'Connection.apply', 73 | // zoneId: 123 74 | }; 75 | 76 | test.equal('object', typeof owner); 77 | test.equal('number', typeof owner.time); 78 | delete owner.time; 79 | test.equal('number', typeof owner.zoneId); 80 | delete owner.zoneId; 81 | test.equal(expected, owner); 82 | 83 | // test whether zone has correct info 84 | // the parent zone contains method info 85 | var info = zone.infoMap[zone.parent.id]; 86 | var expectedInfo = { 87 | type: 'Connection.apply', 88 | name: '/test-collection/update', 89 | // time: 123, 90 | args: [ 91 | [{_id: 'foo'}, {$set: {bar: 'bat'}}, {}], 92 | {returnStubValue: true} 93 | ], 94 | }; 95 | 96 | test.equal('object', typeof info); 97 | test.equal('number', typeof info['Connection.apply'].time); 98 | delete info['Connection.apply'].time; 99 | test.equal(expectedInfo, info['Connection.apply']); 100 | 101 | // reset zone for other tests and continue 102 | Zone.Reporters.add(Zone.longStackTrace); 103 | Zone.Reporters.remove('test-reporter'); 104 | next(); 105 | }); 106 | 107 | TestCollection.remove({_id: 'foo'}, function () { 108 | TestCollection.insert({_id: 'foo', bar: 'baz'}, function () { 109 | TestCollection.update({_id: 'foo'}, {$set: {bar: 'bat'}}, function () { 110 | throw new Error('test-error'); 111 | }); 112 | }); 113 | }); 114 | } 115 | ); 116 | 117 | Tinytest.addAsync( 118 | 'Hijacks - Collections - upsert (new)', 119 | function (test, next) { 120 | Zone.Reporters.removeAll(); 121 | Zone.Reporters.add('test-reporter', function (zone) { 122 | 123 | // test whether zone has correct owner info 124 | var owner = zone.owner; 125 | var expected = { 126 | args: [ 127 | [ 128 | {_id: 'foo'}, 129 | {$set: {bar: 'bat'}}, 130 | { 131 | _returnObject: true, 132 | upsert: true, 133 | // insertedId: 'asd' 134 | } 135 | ], 136 | {returnStubValue: true} 137 | ], 138 | name: '/test-collection/update', 139 | type: 'Connection.apply', 140 | // zoneId: 123 141 | }; 142 | 143 | test.equal('object', typeof owner); 144 | test.equal('number', typeof owner.time); 145 | delete owner.time; 146 | test.equal('number', typeof owner.zoneId); 147 | delete owner.zoneId; 148 | if(owner.args[0][2].insertedId) { 149 | test.equal('string', typeof owner.args[0][2].insertedId); 150 | } 151 | delete owner.args[0][2].insertedId; 152 | test.equal(expected, owner); 153 | 154 | // test whether zone has correct info 155 | // the parent zone contains method info 156 | var info = zone.infoMap[zone.parent.id]; 157 | var expectedInfo = { 158 | type: 'Connection.apply', 159 | name: '/test-collection/update', 160 | // time: 123, 161 | args: [ 162 | [ 163 | {_id: 'foo'}, 164 | {$set: {bar: 'bat'}}, 165 | { 166 | _returnObject: true, 167 | upsert: true, 168 | // insertedId: 'asd' 169 | } 170 | ], 171 | {returnStubValue: true} 172 | ], 173 | }; 174 | 175 | test.equal('object', typeof info); 176 | test.equal('number', typeof info['Connection.apply'].time); 177 | delete info['Connection.apply'].time; 178 | var insertedId = info['Connection.apply'].args[0][2].insertedId; 179 | if(insertedId) { 180 | test.equal('string', typeof insertedId); 181 | delete info['Connection.apply'].args[0][2].insertedId; 182 | } 183 | test.equal(expectedInfo, info['Connection.apply']); 184 | 185 | // reset zone for other tests and continue 186 | Zone.Reporters.add(Zone.longStackTrace); 187 | Zone.Reporters.remove('test-reporter'); 188 | next(); 189 | }); 190 | 191 | TestCollection.remove({_id: 'foo'}, function () { 192 | TestCollection.upsert({_id: 'foo'}, {$set: {bar: 'bat'}}, function () { 193 | throw new Error('test-error'); 194 | }); 195 | }); 196 | } 197 | ); 198 | 199 | Tinytest.addAsync( 200 | 'Hijacks - Collections - upsert', 201 | function (test, next) { 202 | Zone.Reporters.removeAll(); 203 | Zone.Reporters.add('test-reporter', function (zone) { 204 | 205 | // test whether zone has correct owner info 206 | var owner = zone.owner; 207 | var expected = { 208 | args: [ 209 | [ 210 | {_id: 'foo'}, 211 | {$set: {bar: 'bat'}}, 212 | { 213 | _returnObject: true, 214 | upsert: true, 215 | // insertedId: 'asd' 216 | } 217 | ], 218 | {returnStubValue: true} 219 | ], 220 | name: '/test-collection/update', 221 | type: 'Connection.apply', 222 | // zoneId: 123 223 | }; 224 | 225 | test.equal('object', typeof owner); 226 | test.equal('number', typeof owner.time); 227 | delete owner.time; 228 | test.equal('number', typeof owner.zoneId); 229 | delete owner.zoneId; 230 | if(owner.args[0][2].insertedId) { 231 | test.equal('string', typeof owner.args[0][2].insertedId); 232 | } 233 | delete owner.args[0][2].insertedId; 234 | test.equal(expected, owner); 235 | 236 | // test whether zone has correct info 237 | // the parent zone contains method info 238 | var info = zone.infoMap[zone.parent.id]; 239 | var expectedInfo = { 240 | type: 'Connection.apply', 241 | name: '/test-collection/update', 242 | // time: 123, 243 | args: [ 244 | [ 245 | {_id: 'foo'}, 246 | {$set: {bar: 'bat'}}, 247 | { 248 | _returnObject: true, 249 | upsert: true, 250 | // insertedId: 'asd' 251 | } 252 | ], 253 | {returnStubValue: true} 254 | ], 255 | }; 256 | 257 | test.equal('object', typeof info); 258 | test.equal('number', typeof info['Connection.apply'].time); 259 | delete info['Connection.apply'].time; 260 | var insertedId = info['Connection.apply'].args[0][2].insertedId; 261 | if(insertedId) { 262 | test.equal('string', typeof insertedId); 263 | delete info['Connection.apply'].args[0][2].insertedId; 264 | } 265 | test.equal(expectedInfo, info['Connection.apply']); 266 | 267 | // reset zone for other tests and continue 268 | Zone.Reporters.add(Zone.longStackTrace); 269 | Zone.Reporters.remove('test-reporter'); 270 | next(); 271 | }); 272 | 273 | TestCollection.remove({_id: 'foo'}, function () { 274 | TestCollection.insert({_id: 'foo', bar: 'baz'}, function () { 275 | TestCollection.upsert({_id: 'foo'}, {$set: {bar: 'bat'}}, function () { 276 | throw new Error('test-error'); 277 | }); 278 | }); 279 | }); 280 | } 281 | ); 282 | 283 | Tinytest.addAsync( 284 | 'Hijacks - Collections - remove', 285 | function (test, next) { 286 | Zone.Reporters.removeAll(); 287 | Zone.Reporters.add('test-reporter', function (zone) { 288 | 289 | // test whether zone has correct owner info 290 | var owner = zone.owner; 291 | var expected = { 292 | args: [ 293 | [{_id: 'foo'}], 294 | {returnStubValue: true} 295 | ], 296 | name: '/test-collection/remove', 297 | type: 'Connection.apply', 298 | // zoneId: 123 299 | }; 300 | 301 | test.equal('object', typeof owner); 302 | test.equal('number', typeof owner.time); 303 | delete owner.time; 304 | test.equal('number', typeof owner.zoneId); 305 | delete owner.zoneId; 306 | test.equal(expected, owner); 307 | 308 | // test whether zone has correct info 309 | // the parent zone contains method info 310 | var info = zone.infoMap[zone.parent.id]; 311 | var expectedInfo = { 312 | type: 'Connection.apply', 313 | name: '/test-collection/remove', 314 | // time: 123, 315 | args: [ 316 | [{_id: 'foo'}], 317 | {returnStubValue: true} 318 | ], 319 | }; 320 | 321 | test.equal('object', typeof info); 322 | test.equal('number', typeof info['Connection.apply'].time); 323 | delete info['Connection.apply'].time; 324 | test.equal(expectedInfo, info['Connection.apply']); 325 | 326 | // reset zone for other tests and continue 327 | Zone.Reporters.add(Zone.longStackTrace); 328 | Zone.Reporters.remove('test-reporter'); 329 | next(); 330 | }); 331 | 332 | TestCollection.remove({_id: 'foo'}, function () { 333 | TestCollection.insert({_id: 'foo', bar: 'baz'}, function () { 334 | TestCollection.remove({_id: 'foo'}, function () { 335 | throw new Error('test-error'); 336 | }); 337 | }); 338 | }); 339 | } 340 | ); 341 | -------------------------------------------------------------------------------- /tests/hijacks/methods.js: -------------------------------------------------------------------------------- 1 | 2 | Tinytest.addAsync( 3 | 'Hijacks - Methods - default', 4 | function (test, next) { 5 | Zone.Reporters.removeAll(); 6 | Zone.Reporters.add('test-reporter', function (zone) { 7 | 8 | // test whether zone has correct owner info 9 | var owner = zone.owner; 10 | var expectedOwner = { 11 | args: ['arg1', 'arg2'], 12 | name: 'test', 13 | // time: 123, 14 | type: 'Meteor.call', 15 | // zoneId: 123 16 | }; 17 | 18 | test.equal('object', typeof owner); 19 | test.equal('number', typeof owner.time); 20 | delete owner.time; 21 | test.equal('number', typeof owner.zoneId); 22 | delete owner.zoneId; 23 | test.equal(expectedOwner, owner); 24 | 25 | // test whether zone has correct info 26 | // the parent zone contains method info 27 | var info = zone.infoMap[zone.parent.id]; 28 | var expectedInfo = { 29 | 'Meteor.call': { 30 | type: 'Meteor.call', 31 | name: 'test', 32 | // time: 123, 33 | args: ['arg1', 'arg2'], 34 | } 35 | }; 36 | 37 | test.equal('object', typeof info); 38 | test.equal('number', typeof info['Meteor.call'].time); 39 | delete info['Meteor.call'].time; 40 | test.equal(expectedInfo, info); 41 | 42 | // reset zone for other tests and continue 43 | Zone.Reporters.add(Zone.longStackTrace); 44 | Zone.Reporters.remove('test-reporter'); 45 | next(); 46 | }); 47 | 48 | // remove info from previous tests (if any) 49 | zone._info = {}; 50 | 51 | Meteor.call('test', 'arg1', 'arg2', function () { 52 | throw new Error('test-error'); 53 | }); 54 | } 55 | ); 56 | -------------------------------------------------------------------------------- /tests/hijacks/subscriptions.js: -------------------------------------------------------------------------------- 1 | 2 | Tinytest.addAsync( 3 | 'Hijacks - Subscriptions - default', 4 | function (test, next) { 5 | Zone.Reporters.removeAll(); 6 | Zone.Reporters.add('test-reporter', function (zone) { 7 | 8 | // test whether zone has correct owner info 9 | var owner = zone.owner; 10 | var expected = { 11 | args: ['arg1', 'arg2'], 12 | name: 'test-ready', 13 | type: 'Meteor.subscribe', 14 | // zoneId: 123 15 | }; 16 | 17 | test.equal('object', typeof owner); 18 | test.equal('number', typeof owner.time); 19 | delete owner.time; 20 | test.equal('number', typeof owner.zoneId); 21 | delete owner.zoneId; 22 | test.equal(expected, owner); 23 | 24 | // test whether zone has correct info 25 | // the parent zone contains method info 26 | var info = zone.infoMap[zone.parent.id]; 27 | var expectedInfo = { 28 | 'Meteor.subscribe': { 29 | type: 'Meteor.subscribe', 30 | name: 'test-ready', 31 | // time: 123, 32 | args: ['arg1', 'arg2'], 33 | } 34 | }; 35 | 36 | test.equal('object', typeof info); 37 | test.equal('number', typeof info['Meteor.subscribe'].time); 38 | delete info['Meteor.subscribe'].time; 39 | test.equal(expectedInfo, info); 40 | 41 | // reset zone for other tests and continue 42 | Zone.Reporters.add(Zone.longStackTrace); 43 | Zone.Reporters.remove('test-reporter'); 44 | next(); 45 | }); 46 | 47 | // remove info from previous tests (if any) 48 | zone._info = {}; 49 | 50 | Meteor.subscribe('test-ready', 'arg1', 'arg2', function () { 51 | throw new Error('test-error'); 52 | }); 53 | } 54 | ); 55 | 56 | Tinytest.addAsync( 57 | 'Hijacks - Subscriptions - onReady', 58 | function (test, next) { 59 | Zone.Reporters.removeAll(); 60 | Zone.Reporters.add('test-reporter', function (zone) { 61 | 62 | // test whether zone has correct owner info 63 | var owner = zone.owner; 64 | var expected = { 65 | args: ['arg1', 'arg2'], 66 | callbackType: 'onReady', 67 | name: 'test-ready', 68 | type: 'Meteor.subscribe', 69 | // zoneId: 123 70 | }; 71 | 72 | test.equal('object', typeof owner); 73 | test.equal('number', typeof owner.time); 74 | delete owner.time; 75 | test.equal('number', typeof owner.zoneId); 76 | delete owner.zoneId; 77 | test.equal(expected, owner); 78 | 79 | // test whether zone has correct info 80 | // the parent zone contains method info 81 | var info = zone.infoMap[zone.parent.id]; 82 | var expectedInfo = { 83 | 'Meteor.subscribe': { 84 | type: 'Meteor.subscribe', 85 | name: 'test-ready', 86 | // time: 123, 87 | args: ['arg1', 'arg2'], 88 | callbackType: 'onReady' 89 | } 90 | }; 91 | 92 | test.equal('object', typeof info); 93 | test.equal('number', typeof info['Meteor.subscribe'].time); 94 | delete info['Meteor.subscribe'].time; 95 | test.equal(expectedInfo, info); 96 | 97 | // reset zone for other tests and continue 98 | Zone.Reporters.add(Zone.longStackTrace); 99 | Zone.Reporters.remove('test-reporter'); 100 | next(); 101 | }); 102 | 103 | // remove info from previous tests (if any) 104 | zone._info = {}; 105 | 106 | Meteor.subscribe('test-ready', 'arg1', 'arg2', { 107 | onReady: function () { 108 | throw new Error('test-error'); 109 | } 110 | }); 111 | } 112 | ); 113 | 114 | Tinytest.addAsync( 115 | 'Hijacks - Subscriptions - onError', 116 | function (test, next) { 117 | Zone.Reporters.removeAll(); 118 | Zone.Reporters.add('test-reporter', function (zone) { 119 | 120 | // test whether zone has correct owner info 121 | var owner = zone.owner; 122 | var expected = { 123 | args: ['arg1', 'arg2'], 124 | callbackType: 'onError', 125 | name: 'test-error', 126 | type: 'Meteor.subscribe', 127 | // zoneId: 123 128 | }; 129 | 130 | test.equal('object', typeof owner); 131 | test.equal('number', typeof owner.time); 132 | delete owner.time; 133 | test.equal('number', typeof owner.zoneId); 134 | delete owner.zoneId; 135 | test.equal(expected, owner); 136 | 137 | // test whether zone has correct info 138 | // the parent zone contains method info 139 | var info = zone.infoMap[zone.parent.id]; 140 | var expectedInfo = { 141 | 'Meteor.subscribe': { 142 | type: 'Meteor.subscribe', 143 | name: 'test-error', 144 | // time: 123, 145 | args: ['arg1', 'arg2'], 146 | callbackType: 'onError' 147 | } 148 | }; 149 | 150 | test.equal('object', typeof info); 151 | test.equal('number', typeof info['Meteor.subscribe'].time); 152 | delete info['Meteor.subscribe'].time; 153 | test.equal(expectedInfo, info); 154 | 155 | // reset zone for other tests and continue 156 | Zone.Reporters.add(Zone.longStackTrace); 157 | Zone.Reporters.remove('test-reporter'); 158 | next(); 159 | }); 160 | 161 | // remove info from previous tests (if any) 162 | zone._info = {}; 163 | 164 | Meteor.subscribe('test-error', 'arg1', 'arg2', { 165 | onError: function () { 166 | throw new Error('test-error'); 167 | } 168 | }); 169 | } 170 | ); 171 | -------------------------------------------------------------------------------- /tests/loader.js: -------------------------------------------------------------------------------- 1 | 2 | var SCRIPTS = [ 3 | '/packages/*/assets/utils.js', 4 | '/packages/*/assets/before.js', 5 | '/packages/*/assets/zone.js', 6 | '/packages/*/assets/tracer.js', 7 | '/packages/*/assets/after.js', 8 | '/packages/*/assets/reporters.js', 9 | ]; 10 | 11 | Tinytest.add( 12 | 'Loader - Load all critical files first', 13 | function (test) { 14 | var scripts = document.getElementsByTagName('script'); 15 | scripts = Array.prototype.slice.call(scripts); 16 | scripts = scripts.slice(0, 6).map(getSrc).map(replacePackageName); 17 | test.equal(SCRIPTS, scripts); 18 | } 19 | ); 20 | 21 | Tinytest.add( 22 | 'Loader - do not override some functions', 23 | function (test) { 24 | if(requestAnimationFrame) { 25 | var nativeCodeRegEx = /\[native code\]/gm; 26 | var fnString = requestAnimationFrame.toString(); 27 | test.isTrue(nativeCodeRegEx.exec(fnString)); 28 | } 29 | } 30 | ); 31 | 32 | Tinytest.addAsync( 33 | 'Loader - do not load if Zone is disabled', 34 | function (test, next) { 35 | // disable it and see whether zones don't get loaded 36 | Meteor.call('zone-off', function () { 37 | $.get(location.href, function (html, status) { 38 | // append the html to a temporary container to search 39 | var scripts = $('
').append(html).find('script'); 40 | scripts = Array.prototype.slice.call(scripts); 41 | scripts = scripts.map(getSrc); 42 | scripts.forEach(function (path) { 43 | test.equal(SCRIPTS.indexOf(path), -1); 44 | }); 45 | 46 | // enable it and test whether zones scripts are loaded 47 | Meteor.call('zone-on', function () { 48 | $.get(location.href, function (html, status) { 49 | // append the html to a temporary container to search 50 | var scripts = $('
').append(html).find('script'); 51 | scripts = Array.prototype.slice.call(scripts); 52 | scripts = scripts.slice(0, 6).map(getSrc).map(replacePackageName); 53 | test.equal(SCRIPTS, scripts); 54 | 55 | // end the test 56 | next(); 57 | }); 58 | }); 59 | 60 | }); 61 | }); 62 | } 63 | ); 64 | 65 | function getSrc(el) { 66 | return el.src.replace(location.origin, ''); 67 | } 68 | 69 | function replacePackageName (path) { 70 | var withNoPackageName = path.replace(/(.*)\/packages\/(.*)\/assets/, '/packages/*/assets'); 71 | var withNoCacheAvoider = withNoPackageName.replace(/\?.*/, ''); 72 | return withNoCacheAvoider; 73 | } -------------------------------------------------------------------------------- /tests/reporters.js: -------------------------------------------------------------------------------- 1 | 2 | Tinytest.add( 3 | 'Reporters - get()', 4 | function (test) { 5 | var reporters = Zone.Reporters.get(); 6 | test.equal('object', typeof reporters) 7 | test.equal('function', typeof reporters.longStackTrace) 8 | } 9 | ); 10 | 11 | Tinytest.add( 12 | 'Reporters - get(longStackTrace)', 13 | function (test) { 14 | var reporter = Zone.Reporters.get('longStackTrace'); 15 | test.equal('function', typeof reporter) 16 | } 17 | ); 18 | 19 | Tinytest.add( 20 | 'Reporters - add(name, reporter)', 21 | function (test) { 22 | Zone.Reporters.add('test-reporter', Function()); 23 | var reporter = Zone.Reporters.get('test-reporter'); 24 | test.equal('function', typeof reporter); 25 | Zone.Reporters.remove('test-reporter'); 26 | } 27 | ); 28 | 29 | Tinytest.add( 30 | 'Reporters - remove(name)', 31 | function (test) { 32 | Zone.Reporters.add('test-reporter', Function()); 33 | Zone.Reporters.remove('test-reporter'); 34 | var reporter = Zone.Reporters.get('test-reporter'); 35 | test.equal('undefined', typeof reporter); 36 | } 37 | ); 38 | 39 | Tinytest.add( 40 | 'Reporters - removeAll()', 41 | function (test) { 42 | Zone.Reporters.add('test-reporter', Function()); 43 | Zone.Reporters.removeAll(); 44 | Zone.Reporters.add(Zone.Reporters.longStackTrace); 45 | var reporter = Zone.Reporters.get('test-reporter'); 46 | test.equal('undefined', typeof reporter); 47 | } 48 | ); 49 | 50 | Tinytest.add( 51 | 'Reporters - run(zone)', 52 | function (test) { 53 | var zone = {foo: 'bar'}; 54 | Zone.Reporters.removeAll(); 55 | Zone.Reporters.add('test-reporter', toBaz); 56 | Zone.Reporters.run(zone); 57 | test.equal('baz', zone.foo); 58 | Zone.Reporters.remove('test-reporter'); 59 | Zone.Reporters.add(Zone.Reporters.longStackTrace); 60 | 61 | function toBaz(zone) { 62 | zone.foo = 'baz'; 63 | } 64 | } 65 | ); 66 | 67 | Tinytest.add( 68 | 'Reporters - getErrorMessage - an error object', 69 | function (test) { 70 | var message = "hello"; 71 | var err = new Error(message); 72 | test.equal(Zone.Reporters.getErrorMessage(err), message); 73 | } 74 | ); 75 | 76 | Tinytest.add( 77 | 'Reporters - getErrorMessage - string', 78 | function (test) { 79 | var message = "hello"; 80 | test.equal(Zone.Reporters.getErrorMessage(message), message); 81 | } 82 | ); 83 | 84 | Tinytest.add( 85 | 'Reporters - getErrorMessage - object', 86 | function (test) { 87 | var message = "hello"; 88 | var err = {message: message}; 89 | test.equal(Zone.Reporters.getErrorMessage(err), message); 90 | } 91 | ); 92 | 93 | Tinytest.add( 94 | 'Reporters - getErrorMessage - number', 95 | function (test) { 96 | var number = 2; 97 | test.equal(Zone.Reporters.getErrorMessage(number), number.toString()); 98 | } 99 | ); 100 | 101 | Tinytest.add( 102 | 'Reporters - getErrorMessage - nothing', 103 | function (test) { 104 | test.equal(/Oops/.test(Zone.Reporters.getErrorMessage()), true); 105 | } 106 | ); 107 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | [ 4 | "application-configuration", 5 | "1.0.3" 6 | ], 7 | [ 8 | "base64", 9 | "1.0.1" 10 | ], 11 | [ 12 | "binary-heap", 13 | "1.0.1" 14 | ], 15 | [ 16 | "blaze", 17 | "2.0.3" 18 | ], 19 | [ 20 | "blaze-tools", 21 | "1.0.1" 22 | ], 23 | [ 24 | "boilerplate-generator", 25 | "1.0.1" 26 | ], 27 | [ 28 | "callback-hook", 29 | "1.0.1" 30 | ], 31 | [ 32 | "check", 33 | "1.0.2" 34 | ], 35 | [ 36 | "ddp", 37 | "1.0.11" 38 | ], 39 | [ 40 | "deps", 41 | "1.0.5" 42 | ], 43 | [ 44 | "ejson", 45 | "1.0.4" 46 | ], 47 | [ 48 | "follower-livedata", 49 | "1.0.2" 50 | ], 51 | [ 52 | "geojson-utils", 53 | "1.0.1" 54 | ], 55 | [ 56 | "html-tools", 57 | "1.0.2" 58 | ], 59 | [ 60 | "htmljs", 61 | "1.0.2" 62 | ], 63 | [ 64 | "id-map", 65 | "1.0.1" 66 | ], 67 | [ 68 | "jquery", 69 | "1.0.1" 70 | ], 71 | [ 72 | "json", 73 | "1.0.1" 74 | ], 75 | [ 76 | "livedata", 77 | "1.0.11" 78 | ], 79 | [ 80 | "logging", 81 | "1.0.5" 82 | ], 83 | [ 84 | "meteor", 85 | "1.1.3" 86 | ], 87 | [ 88 | "meteorhacks:inject-initial", 89 | "1.0.0" 90 | ], 91 | [ 92 | "minifiers", 93 | "1.1.2" 94 | ], 95 | [ 96 | "minimongo", 97 | "1.0.5" 98 | ], 99 | [ 100 | "mongo", 101 | "1.0.8" 102 | ], 103 | [ 104 | "observe-sequence", 105 | "1.0.3" 106 | ], 107 | [ 108 | "ordered-dict", 109 | "1.0.1" 110 | ], 111 | [ 112 | "random", 113 | "1.0.1" 114 | ], 115 | [ 116 | "reactive-dict", 117 | "1.0.4" 118 | ], 119 | [ 120 | "reactive-var", 121 | "1.0.3" 122 | ], 123 | [ 124 | "retry", 125 | "1.0.1" 126 | ], 127 | [ 128 | "routepolicy", 129 | "1.0.2" 130 | ], 131 | [ 132 | "session", 133 | "1.0.4" 134 | ], 135 | [ 136 | "spacebars", 137 | "1.0.3" 138 | ], 139 | [ 140 | "spacebars-compiler", 141 | "1.0.3" 142 | ], 143 | [ 144 | "templating", 145 | "1.0.9" 146 | ], 147 | [ 148 | "tracker", 149 | "1.0.3" 150 | ], 151 | [ 152 | "ui", 153 | "1.0.4" 154 | ], 155 | [ 156 | "underscore", 157 | "1.0.1" 158 | ], 159 | [ 160 | "webapp", 161 | "1.1.4" 162 | ], 163 | [ 164 | "webapp-hashing", 165 | "1.0.1" 166 | ] 167 | ], 168 | "pluginDependencies": [], 169 | "toolVersion": "meteor-tool@1.0.35", 170 | "format": "1.0" 171 | } --------------------------------------------------------------------------------