You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
354 lines
8.7 KiB
JavaScript
354 lines
8.7 KiB
JavaScript
// content-scripts/inject.js
|
|
// this file is run from the content script context (separate JS VM from the app, but same DOM)
|
|
// but injects an 'instrumentation' script tag into the app context
|
|
// confusing, right?
|
|
|
|
var instument = function instument (window) {
|
|
// Helper to determine if the root 'ng' module has been loaded
|
|
// window.angular may be available if the app is bootstrapped asynchronously, but 'ng' might
|
|
// finish loading later.
|
|
var ngLoaded = function () {
|
|
if (!window.angular) {
|
|
return false;
|
|
}
|
|
try {
|
|
window.angular.module('ng');
|
|
}
|
|
catch (e) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
if (!ngLoaded()) {
|
|
// TODO: var name
|
|
function areWeThereYet (ev) {
|
|
if (ev.srcElement.tagName === 'SCRIPT') {
|
|
var oldOnload = ev.srcElement.onload;
|
|
ev.srcElement.onload = function () {
|
|
if (ngLoaded()) {
|
|
document.removeEventListener('DOMNodeInserted', areWeThereYet);
|
|
instument(window);
|
|
}
|
|
if (oldOnload) {
|
|
oldOnload.apply(this, arguments);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
document.addEventListener('DOMNodeInserted', areWeThereYet);
|
|
return;
|
|
}
|
|
|
|
// do not patch twice
|
|
if (window.__ngDebug) {
|
|
return;
|
|
}
|
|
|
|
|
|
// Helpers
|
|
// =======
|
|
|
|
var throttle = require('./lib/throttle.js');
|
|
var summarizeObject = require('./lib/summarizeObject.js');
|
|
var niceNames = require('./lib/niceNames.js');
|
|
|
|
// helper to extract dependencies from function arguments
|
|
// not all versions of AngularJS expose annotate
|
|
var annotate = angular.injector().annotate || require('./lib/annotate.js');
|
|
|
|
// polyfill for performance.now on older webkit
|
|
if (!performance.now) {
|
|
performance.now = performance.webkitNow;
|
|
}
|
|
|
|
// Send notifications from app context to devtools context
|
|
// in order to do this, we need to create a DOM element across which
|
|
// the app and content script contexts can communicate
|
|
var eventProxyElement = document.createElement('div');
|
|
eventProxyElement.id = '__ngDebugElement';
|
|
eventProxyElement.style.display = 'none';
|
|
document.body.appendChild(eventProxyElement);
|
|
|
|
var customEvent = document.createEvent('Event');
|
|
customEvent.initEvent('myCustomEvent', true, true);
|
|
|
|
var fireCustomEvent = function (data) {
|
|
data.appId = instrumentedAppId;
|
|
eventProxyElement.innerText = JSON.stringify(data);
|
|
eventProxyElement.dispatchEvent(customEvent);
|
|
};
|
|
|
|
|
|
// given a scope object, return an object with deep clones
|
|
// of the models exposed on that scope
|
|
var getScopeLocals = function (scope) {
|
|
var scopeLocals = {}, prop;
|
|
for (prop in scope) {
|
|
if (scope.hasOwnProperty(prop) && prop !== 'this' && prop[0] !== '$') {
|
|
scopeLocals[prop] = decycle(scope[prop]);
|
|
}
|
|
}
|
|
return scopeLocals;
|
|
};
|
|
|
|
|
|
|
|
// Private state
|
|
// =============
|
|
|
|
//var bootstrap = window.angular.bootstrap;
|
|
var debug = {
|
|
// map of scopes --> watcher function name strings
|
|
watchers: {},
|
|
|
|
// maps of watch/apply exp/fns to perf data
|
|
watchPerf: {},
|
|
applyPerf: {},
|
|
|
|
// map of scope.$ids --> $scope objects
|
|
scopes: {},
|
|
|
|
// whether or not to emit profile data
|
|
profiling: false,
|
|
|
|
// map of $ids --> [] array of things being watched
|
|
modelWatchers: {},
|
|
// map of $id + watcher --> value
|
|
modelWatchersState: {},
|
|
|
|
// map of $ids --> refs to $rootScope objects
|
|
rootScopes: {},
|
|
|
|
deps: []
|
|
};
|
|
|
|
var instrumentedAppId = window.location.host + '~' + Date.now() + '~' Math.random();
|
|
|
|
|
|
// Utils
|
|
// =====
|
|
|
|
var getScopeTree = function (id) {
|
|
|
|
var names = niceNames();
|
|
|
|
var traverse = function (sc) {
|
|
var tree = {
|
|
id: sc.$id,
|
|
name: names[sc.$id],
|
|
watchers: debug.watchers[sc.$id],
|
|
children: []
|
|
};
|
|
|
|
var child = sc.$$childHead;
|
|
if (child) {
|
|
do {
|
|
tree.children.push(traverse(child));
|
|
} while (child !== sc.$$childTail && (child = child.$$nextSibling));
|
|
}
|
|
|
|
return tree;
|
|
};
|
|
|
|
var root = debug.rootScopes[id];
|
|
var tree = traverse(root);
|
|
|
|
return tree;
|
|
};
|
|
|
|
var getWatchPerf = function () {
|
|
var changes = [];
|
|
angular.forEach(debug.watchPerf, function (info, name) {
|
|
if (info.time > 0) {
|
|
changes.push({
|
|
name: name,
|
|
time: info.time
|
|
});
|
|
info.time = 0;
|
|
}
|
|
});
|
|
return changes;
|
|
};
|
|
|
|
// Emit stuff
|
|
// ==========
|
|
|
|
var emit = {
|
|
modelChange: throttle(function (id, watchers) {
|
|
var scope = debug.scopes[id];
|
|
var changes = {};
|
|
|
|
watchers = watchers || debug.modelWatchers[id];
|
|
|
|
if (scope && debug.modelWatchers[id]) {
|
|
|
|
Object.keys(debug.modelWatchers[id]).
|
|
forEach(function (watcher) {
|
|
var newValue = api.getModel(id, watcher),
|
|
newString = JSON.stringify(newValue),
|
|
prop = id + '~' + watcher;
|
|
|
|
if (debug.modelWatchersState[prop] !== newString) {
|
|
changes[watcher] = newValue;
|
|
debug.modelWatchersState[prop] = newString;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (Object.keys(changes).length > 0) {
|
|
fireCustomEvent({
|
|
action: 'modelChange',
|
|
id: id,
|
|
changes: changes
|
|
});
|
|
}
|
|
}, 50),
|
|
|
|
scopeChange: throttle(function (id) {
|
|
fireCustomEvent({
|
|
action: 'scopeChange',
|
|
id: id,
|
|
scope: getScopeTree(id)
|
|
});
|
|
}, 50),
|
|
|
|
scopeDeleted: function (id) {
|
|
fireCustomEvent({
|
|
action: 'scopeDeleted',
|
|
id: id
|
|
});
|
|
},
|
|
|
|
watcherChange: throttle(function (id) {
|
|
if (debug.modelWatchers[id]) {
|
|
fireCustomEvent({
|
|
action: 'watcherChange',
|
|
id: id,
|
|
watchers: debug.watchers[id]
|
|
});
|
|
}
|
|
}, 50),
|
|
|
|
watchPerfChange: throttle(function (str) {
|
|
if (debug.profiling) {
|
|
fireCustomEvent({
|
|
action: 'watchPerfChange',
|
|
watcher: str,
|
|
value: debug.watchPerf[str]
|
|
});
|
|
}
|
|
}, 50),
|
|
|
|
applyPerfChange: throttle(function (str) {
|
|
if (debug.profiling) {
|
|
fireCustomEvent({
|
|
action: 'applyPerfChange',
|
|
watcher: str,
|
|
value: debug.applyPerf[str]
|
|
});
|
|
}
|
|
}, 50),
|
|
|
|
// might be worth limiting
|
|
watchPerf: function () {
|
|
throw new Error('Implement me :c');
|
|
}
|
|
};
|
|
|
|
|
|
// Public API
|
|
// ==========
|
|
|
|
var api = window.__ngDebug = {
|
|
|
|
profiling: function (setting) {
|
|
debug.profiling = setting;
|
|
},
|
|
|
|
getDeps: function () {
|
|
return debug.deps;
|
|
},
|
|
|
|
getRootScopeIds: function () {
|
|
return Object.keys(debug.rootScopes);
|
|
},
|
|
|
|
getAppId: function () {
|
|
return instrumentedAppId;
|
|
},
|
|
|
|
fireCustomEvent : fireCustomEvent,
|
|
niceNames : niceNames,
|
|
getModel : summarizeObject,
|
|
|
|
setSomeModel: function (id, path, value) {
|
|
debug.scope[id].$apply(path + '=' + JSON.stringify(value));
|
|
},
|
|
|
|
watchModel: function (id, path) {
|
|
debug.modelWatchers[id] = debug.modelWatchers[id] || {};
|
|
debug.modelWatchers[id][path || ''] = true;
|
|
if (!path || path === '') {
|
|
debug.modelWatchersState = {};
|
|
}
|
|
emit.modelChange(id);
|
|
emit.watcherChange(id);
|
|
},
|
|
|
|
// unwatches all children of the given path
|
|
// Ex:
|
|
// if watching 'foo.bar.baz', 'foo.bar', and 'foo'
|
|
// unwatchModel('001', 'foo.bar')
|
|
// unwatches 'foo.bar.baz' and 'foo.bar'
|
|
unwatchModel: function (id, path) {
|
|
if (!debug.modelWatchers[id]) {
|
|
return;
|
|
}
|
|
if (path === undefined) {
|
|
path = '';
|
|
}
|
|
Object.keys(modelWatchers[id]).forEach(function (key) {
|
|
if (key.substr(0, path.length) === path) {
|
|
delete debug.modelWatchers[id][key];
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
var recordDependencies = function (providerName, dependencies) {
|
|
debug.deps.push({
|
|
name: providerName,
|
|
imports: dependencies
|
|
});
|
|
};
|
|
|
|
require('./lib/decorate.js');
|
|
|
|
};
|
|
|
|
// inject into the application context from the content script context
|
|
|
|
var inject = function () {
|
|
var script = window.document.createElement('script');
|
|
script.innerHTML = '(' + instument.toString() + '(window))';
|
|
document.head.appendChild(script);
|
|
|
|
// handle forwarding the events sent from the app context to the
|
|
// background page context
|
|
var eventProxyElement = document.getElementById('__ngDebugElement');
|
|
|
|
if (eventProxyElement) {
|
|
eventProxyElement.addEventListener('myCustomEvent', function () {
|
|
var eventData = JSON.parse(eventProxyElement.innerText);
|
|
chrome.extension.sendMessage(eventData);
|
|
});
|
|
document.removeEventListener('DOMContentLoaded', inject);
|
|
}
|
|
};
|
|
|
|
// only inject if cookie is set
|
|
if (document.cookie.indexOf('__ngDebug=true') != -1) {
|
|
document.addEventListener('DOMContentLoaded', inject);
|
|
}
|