├── src ├── frontend │ ├── app.css │ ├── components │ │ ├── node-item │ │ │ ├── node-attributes.css │ │ │ ├── node-open-tag.css │ │ │ ├── node-open-tag.html │ │ │ ├── node-item.css │ │ │ ├── node-attributes.ts │ │ │ ├── node-attributes.html │ │ │ ├── node-open-tag.ts │ │ │ ├── node-item.html │ │ │ └── node-item.ts │ │ ├── ng-module-info │ │ │ ├── ng-module-info.css │ │ │ ├── ng-module-info.ts │ │ │ └── ng-module-info.html │ │ ├── router-tree │ │ │ ├── router-tree.css │ │ │ └── router-tree.html │ │ ├── report-error │ │ │ ├── report-error.css │ │ │ ├── report-error.html │ │ │ └── report-error.ts │ │ ├── property-value │ │ │ ├── property-value.html │ │ │ └── property-value.ts │ │ ├── state-values │ │ │ ├── state-values.css │ │ │ ├── state-values.html │ │ │ └── state-values.ts │ │ ├── component-tree │ │ │ ├── component-tree.css │ │ │ └── component-tree.html │ │ ├── ng-module-config-view │ │ │ ├── ng-module-config-view.css │ │ │ ├── ng-module-config-view.ts │ │ │ └── ng-module-config-view.html │ │ ├── search │ │ │ ├── search.css │ │ │ └── search.html │ │ ├── accordion │ │ │ ├── accordion.html │ │ │ └── accordion.ts │ │ ├── components-tab-menu │ │ │ ├── components-tab-menu.html │ │ │ └── components-tab-menu.ts │ │ ├── property-editor │ │ │ ├── property-editor.css │ │ │ └── property-editor.html │ │ ├── feedback-form │ │ │ ├── overall-exp │ │ │ │ ├── overall-exp.css │ │ │ │ └── overall-exp.ts │ │ │ ├── google-service │ │ │ │ └── google-service.ts │ │ │ ├── feedback-form.css │ │ │ ├── feedback-form.html │ │ │ └── feedback-form.ts │ │ ├── analytics-popup │ │ │ ├── analytics-popup.html │ │ │ └── analytics-popup.ts │ │ ├── tree-view │ │ │ ├── tree-view.html │ │ │ └── tree-view.ts │ │ ├── router-info │ │ │ ├── router-info.ts │ │ │ └── router-info.html │ │ ├── render-error │ │ │ ├── render-error.ts │ │ │ └── render-error.html │ │ ├── info-panel │ │ │ ├── info-panel.html │ │ │ └── info-panel.ts │ │ ├── split-pane │ │ │ └── split-pane.html │ │ ├── render-state │ │ │ ├── render-state.css │ │ │ └── render-state.html │ │ ├── tab-menu │ │ │ ├── tab-menu.ts │ │ │ └── tab-menu.html │ │ ├── dependency-info │ │ │ ├── dependency-info.html │ │ │ └── dependency-info.ts │ │ ├── highlightable.ts │ │ ├── injector-tree │ │ │ └── injector-tree.html │ │ └── component-info │ │ │ ├── component-info.html │ │ │ └── component-info.ts │ ├── state │ │ ├── expand-state.ts │ │ ├── index.ts │ │ ├── tab.ts │ │ ├── component-property-state.ts │ │ ├── options.ts │ │ ├── component-instance-state.ts │ │ └── component-view-state.ts │ ├── channel │ │ ├── index.ts │ │ └── direct-connection.ts │ ├── utils │ │ ├── index.ts │ │ ├── uncaught-error-handler.ts │ │ ├── object-types.ts │ │ ├── match.ts │ │ ├── graph-utils.ts │ │ ├── parse-data.ts │ │ ├── highlightable.ts │ │ └── parse-utils.ts │ ├── store │ │ ├── reducers.ts │ │ └── model.ts │ ├── epics │ │ ├── index.ts │ │ └── gtm.ts │ ├── middleware │ │ └── send-analytics.ts │ ├── reducers │ │ └── main-reducer.ts │ ├── app.html │ └── actions │ │ └── main-actions.ts ├── styles │ ├── utils │ │ ├── highlight.css │ │ ├── utils.css │ │ ├── animations.css │ │ └── white-space.css │ ├── components │ │ ├── split-pane.css │ │ ├── node-items.css │ │ ├── components.css │ │ ├── injector-tree.css │ │ ├── header.css │ │ ├── router-tree.css │ │ ├── info-panel.css │ │ ├── accordian.css │ │ └── tab-menu.css │ ├── app.css │ ├── properties.css │ └── base.css ├── structures │ ├── index.ts │ ├── stack.ts │ └── message-queue.ts ├── communication │ ├── hash.ts │ ├── index.ts │ ├── application-error.ts │ ├── message.ts │ ├── message-type.ts │ └── message-dispatch.ts ├── utils │ ├── configuration.ts │ ├── serialize-binary.ts │ ├── index.ts │ ├── scalar.ts │ ├── error-handling.ts │ ├── property-path.test.ts │ ├── function-name.ts │ ├── property-path.ts │ ├── circular-recurse.ts │ ├── patch.test.ts │ ├── ng-validate.ts │ └── serialize.test.ts ├── tree │ ├── index.ts │ ├── change.ts │ ├── path.ts │ ├── node.ts │ ├── mutable-tree-factory.ts │ ├── decorators.ts │ └── mutable-tree.ts ├── backend │ ├── utils │ │ ├── index.ts │ │ ├── parse-ng-version.ts │ │ ├── highlighter.raw │ │ ├── app-check.ts │ │ ├── find-element.ts │ │ ├── highlighter.test.ts │ │ ├── highlighter.ts │ │ ├── parse-router.ts │ │ └── node-traversal.ts │ ├── indirect-connection.ts │ └── connection.ts ├── devtools │ └── devtools.ts ├── gtm-connection │ └── gtm-connection.ts ├── sentry-connection │ └── sentry-connection.ts ├── options.ts ├── channel │ └── channel.ts └── content-script.ts ├── images ├── augury.png ├── icon128.png ├── icon16.png ├── icon48.png ├── search.png ├── augury-popup-icon.png ├── toolbarButtonGlyphs_2x.png ├── Triangle.svg ├── augury-logo.svg ├── close.svg ├── augury-bw.svg ├── github.svg ├── guide.svg ├── feedback.svg ├── feedback-light.svg ├── report-issue.svg └── report-issue-light.svg ├── assets ├── screenloop.gif ├── augury_logo-1400x560.png ├── augury_logo-440x280.png └── augury_logo-920x680.png ├── docs ├── angular_flow.png └── anugury-architecture.png ├── .editorconfig ├── webpack.vendor.ts ├── webpack.test.bootstrap.ts ├── frontend.html ├── circle.yml ├── index.html ├── .github └── ISSUE_TEMPLATE.md ├── tsconfig.json ├── .mention-bot ├── changelog-template.md ├── .gitignore ├── LICENSE ├── manifest.json ├── popup.js ├── tslint.json ├── webpack.test.config.js ├── s3-upload.sh ├── crxmake.sh ├── release-process.md ├── CODE_OF_CONDUCT.md └── package.json /src/frontend/app.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/utils/highlight.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/components/node-item/node-attributes.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/components/node-item/node-open-tag.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/augury.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/augury/dev/images/augury.png -------------------------------------------------------------------------------- /images/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/augury/dev/images/icon128.png -------------------------------------------------------------------------------- /images/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/augury/dev/images/icon16.png -------------------------------------------------------------------------------- /images/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/augury/dev/images/icon48.png -------------------------------------------------------------------------------- /images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/augury/dev/images/search.png -------------------------------------------------------------------------------- /assets/screenloop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/augury/dev/assets/screenloop.gif -------------------------------------------------------------------------------- /docs/angular_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/augury/dev/docs/angular_flow.png -------------------------------------------------------------------------------- /src/structures/index.ts: -------------------------------------------------------------------------------- 1 | export * from './stack'; 2 | export * from './message-queue'; 3 | -------------------------------------------------------------------------------- /src/communication/hash.ts: -------------------------------------------------------------------------------- 1 | export const getRandomHash = () => Math.random().toString(16).slice(2); 2 | -------------------------------------------------------------------------------- /src/frontend/state/expand-state.ts: -------------------------------------------------------------------------------- 1 | export enum ExpandState { 2 | Expanded, 3 | Collapsed, 4 | } 5 | -------------------------------------------------------------------------------- /docs/anugury-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/augury/dev/docs/anugury-architecture.png -------------------------------------------------------------------------------- /images/augury-popup-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/augury/dev/images/augury-popup-icon.png -------------------------------------------------------------------------------- /src/frontend/channel/index.ts: -------------------------------------------------------------------------------- 1 | export * from './connection'; 2 | export * from './direct-connection'; 3 | -------------------------------------------------------------------------------- /assets/augury_logo-1400x560.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/augury/dev/assets/augury_logo-1400x560.png -------------------------------------------------------------------------------- /assets/augury_logo-440x280.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/augury/dev/assets/augury_logo-440x280.png -------------------------------------------------------------------------------- /assets/augury_logo-920x680.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/augury/dev/assets/augury_logo-920x680.png -------------------------------------------------------------------------------- /images/toolbarButtonGlyphs_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/augury/dev/images/toolbarButtonGlyphs_2x.png -------------------------------------------------------------------------------- /src/frontend/components/ng-module-info/ng-module-info.css: -------------------------------------------------------------------------------- 1 | :host > main { 2 | } 3 | :host > main > span.module-title { 4 | } 5 | -------------------------------------------------------------------------------- /images/Triangle.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/frontend/components/router-tree/router-tree.css: -------------------------------------------------------------------------------- 1 | pre { 2 | margin: 15px; 3 | } 4 | 5 | .no-routes { 6 | color: #b0b0b0; 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/components/split-pane.css: -------------------------------------------------------------------------------- 1 | 2 | /* Split Pane */ 3 | 4 | [split-pane-secondary-content] { 5 | border-left: 1px solid; 6 | } -------------------------------------------------------------------------------- /src/styles/utils/utils.css: -------------------------------------------------------------------------------- 1 | 2 | /* Utilities */ 3 | 4 | @import './animations.css'; 5 | @import './white-space.css'; 6 | @import './colors.css'; 7 | -------------------------------------------------------------------------------- /src/utils/configuration.ts: -------------------------------------------------------------------------------- 1 | /// The amount of time we highlight nodes that have been changed or updated 2 | export const highlightTime = 1000; 3 | 4 | -------------------------------------------------------------------------------- /src/tree/index.ts: -------------------------------------------------------------------------------- 1 | export * from './change'; 2 | export * from './metadata'; 3 | export * from './node'; 4 | export * from './path'; 5 | export * from './mutable-tree'; 6 | 7 | -------------------------------------------------------------------------------- /src/frontend/components/report-error/report-error.css: -------------------------------------------------------------------------------- 1 | .report-error-heading { 2 | background: #F4CE42; 3 | } 4 | 5 | .stack-trace { 6 | overflow: auto; 7 | color: #FF2525; 8 | } -------------------------------------------------------------------------------- /src/frontend/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './graph-utils'; 2 | export * from './parse-data'; 3 | export * from './parse-utils'; 4 | export * from './object-types'; 5 | export * from './match'; 6 | -------------------------------------------------------------------------------- /src/backend/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './description'; 2 | export * from './highlighter'; 3 | export * from './parse-router'; 4 | export * from './parse-modules'; 5 | export * from './node-traversal'; 6 | -------------------------------------------------------------------------------- /src/frontend/components/node-item/node-open-tag.html: -------------------------------------------------------------------------------- 1 |

2 | {{node.name}} 3 |

4 | -------------------------------------------------------------------------------- /src/communication/index.ts: -------------------------------------------------------------------------------- 1 | export * from './application-error'; 2 | export * from './hash'; 3 | export * from './message'; 4 | export * from './message-dispatch'; 5 | export * from './message-factory'; 6 | export * from './message-type'; 7 | -------------------------------------------------------------------------------- /src/frontend/components/property-value/property-value.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{key}}: 4 | 5 | 6 | {{value}} 7 | 8 |
9 | -------------------------------------------------------------------------------- /src/frontend/components/state-values/state-values.css: -------------------------------------------------------------------------------- 1 | .property { 2 | width: calc(100% - 10px); 3 | margin-top: 2px; 4 | margin-bottom: 2px; 5 | } 6 | 7 | .property > span { 8 | float: left; 9 | margin-right: 5px; 10 | } -------------------------------------------------------------------------------- /src/frontend/store/reducers.ts: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | import {mainReducer} from '../reducers/main-reducer'; 3 | import {IAppState} from './model'; 4 | 5 | export const rootReducer = combineReducers({ 6 | main: mainReducer, 7 | }); 8 | -------------------------------------------------------------------------------- /src/frontend/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './component-instance-state'; 2 | export * from './component-property-state'; 3 | export * from './component-view-state'; 4 | export * from './expand-state'; 5 | export * from './options'; 6 | export * from './tab'; 7 | -------------------------------------------------------------------------------- /src/utils/serialize-binary.ts: -------------------------------------------------------------------------------- 1 | const msgpack = require('msgpack-lite'); 2 | 3 | export const serializeBinary = (object: T): Buffer => 4 | msgpack.encode(object); 5 | 6 | export const deserializeBinary = (buffer: Buffer) => 7 | msgpack.decode(buffer); 8 | -------------------------------------------------------------------------------- /src/devtools/devtools.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create an Augury panel from the Component Tree View 3 | * on Chrome Development Tools window. 4 | */ 5 | chrome.devtools.panels.create( 6 | 'Augury', 7 | '../images/augury.png', 8 | '../frontend.html' 9 | ); 10 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './circular-recurse'; 2 | export * from './configuration'; 3 | export * from './function-name'; 4 | export * from './ng-validate'; 5 | export * from './scalar'; 6 | export * from './serialize'; 7 | export * from './serialize-binary'; 8 | -------------------------------------------------------------------------------- /src/frontend/components/state-values/state-values.html: -------------------------------------------------------------------------------- 1 |
2 | 6 | 7 |
8 | -------------------------------------------------------------------------------- /src/utils/scalar.ts: -------------------------------------------------------------------------------- 1 | export const isScalar = value => { 2 | switch (typeof value) { 3 | case 'string': 4 | case 'number': 5 | case 'boolean': 6 | case 'function': 7 | case 'undefined': 8 | return true; 9 | default: 10 | return false; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/frontend/store/model.ts: -------------------------------------------------------------------------------- 1 | import {StateTab, Tab} from '../state/tab'; 2 | 3 | export interface IAppState { 4 | main: IAuguryState; 5 | } 6 | 7 | export interface IAuguryState { 8 | selectedTab: Tab; 9 | selectedComponentsSubTab: StateTab; 10 | DOMSelectionActive: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/components/node-items.css: -------------------------------------------------------------------------------- 1 | 2 | /* Node Items */ 3 | 4 | .node-item { 5 | cursor: pointer; 6 | } 7 | 8 | .node-item-name { 9 | display: inline; 10 | } 11 | 12 | .node-item-property { 13 | display: inline; 14 | } 15 | 16 | .node-item-value { 17 | display: inline; 18 | } 19 | -------------------------------------------------------------------------------- /src/frontend/components/component-tree/component-tree.css: -------------------------------------------------------------------------------- 1 | :host > main { 2 | min-height: min-content; 3 | display: block; 4 | } 5 | 6 | :host > main > bt-node-item { 7 | min-width: 100%; 8 | display: table; 9 | white-space: nowrap; 10 | } 11 | 12 | :host { 13 | width: 100%; 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | max_line_length = 120 11 | indent_brace_style = 1TBS 12 | spaces_around_operators = true 13 | quote_type = auto 14 | -------------------------------------------------------------------------------- /src/backend/utils/parse-ng-version.ts: -------------------------------------------------------------------------------- 1 | declare const getAllAngularRootElements: () => Element[]; 2 | 3 | export const parseNgVersion = () => { 4 | const rootElements = getAllAngularRootElements(); 5 | if (rootElements && rootElements[0]) { 6 | return rootElements[0].getAttribute('ng-version'); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /webpack.vendor.ts: -------------------------------------------------------------------------------- 1 | // Polyfills 2 | import 'reflect-metadata'; 3 | import 'core-js'; 4 | import 'zone.js/dist/zone'; 5 | 6 | // Angular 7 | import '@angular/platform-browser-dynamic'; 8 | import '@angular/core'; 9 | import '@angular/common'; 10 | import '@angular/http'; 11 | 12 | // RxJS 13 | import 'rxjs'; 14 | -------------------------------------------------------------------------------- /src/styles/components/components.css: -------------------------------------------------------------------------------- 1 | 2 | /* Components */ 3 | 4 | @import './accordian.css'; 5 | @import './header.css'; 6 | @import './info-panel.css'; 7 | @import './injector-tree.css'; 8 | @import './node-items.css'; 9 | @import './router-tree.css'; 10 | @import './split-pane.css'; 11 | @import './tab-menu.css'; 12 | -------------------------------------------------------------------------------- /webpack.test.bootstrap.ts: -------------------------------------------------------------------------------- 1 | // This is equivalent of saying find all the test files, and call require on each dynamically so as to make sure they are all within the bundle generated by tests. 2 | 3 | let testContext = (<{ context?: Function }>require).context('./', true, /\.test\.ts/); 4 | testContext.keys().forEach(testContext); -------------------------------------------------------------------------------- /src/backend/utils/highlighter.raw: -------------------------------------------------------------------------------- 1 | padding: 5px; 2 | font-size: 11px; 3 | line-height: 11px; 4 | position: absolute; 5 | text-align: right; 6 | z-index: 9999999999999 !important; 7 | pointer-events: none; 8 | min-height: 5px; 9 | background: rgba(126, 183, 253, 0.3); 10 | border: 1px solid rgba(126, 183, 253, 0.7) !important; 11 | color: #6da9d7 !important; -------------------------------------------------------------------------------- /src/frontend/state/tab.ts: -------------------------------------------------------------------------------- 1 | export enum Tab { 2 | /// A tree representation of application components 3 | ComponentTree, 4 | 5 | /// A tree of router paths 6 | RouterTree, 7 | 8 | /// A list of loaded NgModules 9 | NgModules, 10 | } 11 | 12 | export enum StateTab { 13 | /// Properties panel 14 | Properties, 15 | 16 | /// Injector graph 17 | InjectorGraph 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/frontend/components/ng-module-config-view/ng-module-config-view.css: -------------------------------------------------------------------------------- 1 | :host th.config-label { 2 | text-transform: capitalize; 3 | } 4 | :host table { 5 | table-layout: fixed; 6 | width: 100%; 7 | } 8 | :host table td div.label-wrapper { 9 | text-overflow: ellipsis; 10 | overflow: hidden; 11 | } 12 | :host table td div.list-container { 13 | max-height: 18em; 14 | overflow-y: auto; 15 | } 16 | -------------------------------------------------------------------------------- /frontend.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Augury 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/tree/change.ts: -------------------------------------------------------------------------------- 1 | export type Operation = 'add' 2 | | 'copy' 3 | | 'replace' 4 | | 'move' 5 | | 'remove' 6 | | 'test'; 7 | 8 | export interface Change { 9 | /// The operation that this change represents (add, remove, etc) 10 | op: Operation; 11 | 12 | /// The path to the element in the document being changed 13 | path: string; 14 | 15 | /// Right operand (value) 16 | value; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/frontend/components/node-item/node-item.css: -------------------------------------------------------------------------------- 1 | :host .node-item-name { 2 | margin-left: 0; 3 | white-space: nowrap; 4 | } 5 | 6 | :host .node-item.self { 7 | white-space: nowrap; 8 | } 9 | 10 | :host .node-item-value { 11 | margin-left: 2px; 12 | } 13 | 14 | :host .node-item-close-tag { 15 | margin-left: 15px; 16 | } 17 | 18 | :host .parenthesis, 19 | :host .punctuation { 20 | color: darkcyan; 21 | } 22 | -------------------------------------------------------------------------------- /src/frontend/components/ng-module-info/ng-module-info.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | } from '@angular/core'; 5 | 6 | @Component({ 7 | selector: 'ng-module-info', 8 | template: require('./ng-module-info.html'), 9 | styles: [require('to-string!./ng-module-info.css')], 10 | }) 11 | export class NgModuleInfo { 12 | @Input() private ngModules: {[key: string]: any}; 13 | 14 | constructor() {} 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/frontend/components/node-item/node-attributes.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | 3 | import {Property} from '../../../backend/utils'; 4 | 5 | @Component({ 6 | selector: 'node-attributes', 7 | template: require('./node-attributes.html'), 8 | styles: [require('to-string!./node-attributes.css')], 9 | }) 10 | export class NodeAttributes { 11 | @Input() private attributes: Array; 12 | } 13 | -------------------------------------------------------------------------------- /src/frontend/components/node-item/node-attributes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | () 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/structures/stack.ts: -------------------------------------------------------------------------------- 1 | export class Stack { 2 | private elements: Array = []; 3 | 4 | get size(): number { 5 | return this.elements.length; 6 | } 7 | 8 | clear() { 9 | this.elements = []; 10 | } 11 | 12 | push(element: T) { 13 | this.elements.push(element); 14 | } 15 | 16 | pop(): T { 17 | if (this.elements.length === 0) { 18 | return null; 19 | } 20 | return this.elements.pop(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/styles/utils/animations.css: -------------------------------------------------------------------------------- 1 | .rotate90 { 2 | -webkit-transform: rotate(-90deg); 3 | -moz-transform: rotate(-90deg); 4 | -o-transform: rotate(-90deg); 5 | -ms-transform: rotate(-90deg); 6 | transform: rotate(-90deg); 7 | } 8 | 9 | .rotate180 { 10 | -webkit-transform: rotate(-180deg); 11 | -moz-transform: rotate(-180deg); 12 | -o-transform: rotate(-180deg); 13 | -ms-transform: rotate(-180deg); 14 | transform: rotate(-180deg); 15 | } 16 | -------------------------------------------------------------------------------- /src/frontend/components/node-item/node-open-tag.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | } from '@angular/core'; 5 | 6 | import {NodeAttributes} from './node-attributes'; 7 | 8 | @Component({ 9 | selector: 'node-open-tag', 10 | template: require('./node-open-tag.html'), 11 | styles: [require('to-string!./node-open-tag.css')], 12 | }) 13 | export class NodeOpenTag { 14 | @Input() private node; 15 | @Input() private hasChildren: boolean; 16 | } 17 | -------------------------------------------------------------------------------- /src/frontend/components/search/search.css: -------------------------------------------------------------------------------- 1 | :host .icon { 2 | display: inline-block; 3 | width: 20px; 4 | height: 20px; 5 | background: transparent url('/images/search.png') center center; 6 | background-size: contain; 7 | -webkit-mask-image: initial; 8 | margin-left: 3px; 9 | padding: 0; 10 | opacity: 0.7; 11 | } 12 | 13 | :host input { 14 | background-color: transparent; 15 | font-size: 1em; 16 | padding: 4px 5px 4px 0; 17 | outline: none !important; 18 | } -------------------------------------------------------------------------------- /src/backend/indirect-connection.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Message, 3 | MessageFactory, 4 | messageJumpContext, 5 | browserSubscribeResponse, 6 | } from '../communication'; 7 | 8 | export const send = (message: Message): Promise => { 9 | return new Promise((resolve, reject) => { 10 | browserSubscribeResponse(message.messageId, response => resolve(response)); 11 | messageJumpContext(MessageFactory.dispatchWrapper(message)); 12 | }); 13 | }; 14 | 15 | -------------------------------------------------------------------------------- /src/styles/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Index 3 | */ 4 | 5 | /* Basscss Modules */ 6 | @import 'basscss'; 7 | @import 'basscss-border-colors'; 8 | @import 'basscss-layout'; 9 | @import 'basscss-type-scale'; 10 | @import 'basscss-typography'; 11 | 12 | /* CSS Folder Imports */ 13 | @import './base.css'; 14 | @import './components/components.css'; 15 | @import './properties.css'; 16 | @import './utils/utils.css'; 17 | 18 | .transparent { 19 | opacity: 0; 20 | pointer-events: none; 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/error-handling.ts: -------------------------------------------------------------------------------- 1 | import {ApplicationError, ApplicationErrorType} from '../communication/application-error'; 2 | import { 3 | MessageFactory, 4 | } from '../communication'; 5 | 6 | export const reportUncaughtError = (err: SerializeableError, ngVersion: string) => { 7 | chrome.runtime.sendMessage(MessageFactory.sendUncaughtError(err, ngVersion)); 8 | }; 9 | 10 | export interface SerializeableError { 11 | name: string; 12 | message: string; 13 | stack: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/frontend/epics/index.ts: -------------------------------------------------------------------------------- 1 | import {combineEpics} from 'redux-observable'; 2 | 3 | import { 4 | domSelectionGtmEpic, 5 | tabChangeGtmEpic, 6 | subTabChangeGtmEpic, 7 | emitValueGtmEpic, 8 | updatePropertyGtmEpic, 9 | initializeAuguryGtmEpic 10 | } from './gtm'; 11 | 12 | export const rootEpic = combineEpics( 13 | domSelectionGtmEpic, 14 | tabChangeGtmEpic, 15 | subTabChangeGtmEpic, 16 | emitValueGtmEpic, 17 | updatePropertyGtmEpic, 18 | initializeAuguryGtmEpic 19 | ); 20 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 4.2.3 4 | post: 5 | - npm install -g npm@3.x.x 6 | 7 | dependencies: 8 | pre: 9 | - rm -rf node_modules typings 10 | post: 11 | - if [ ! $SENTRY_KEY ]; then export SENTRY_KEY=DUMMY_SENTRY_KEY; fi; npm run prod-build; ./crxmake.sh 12 | 13 | test: 14 | pre: 15 | - git grep --color TODO | cat 16 | 17 | #deployment: 18 | # release: 19 | # branch: master 20 | # commands: 21 | # - npm run pack 22 | # - ./s3-upload.sh 23 | -------------------------------------------------------------------------------- /src/structures/message-queue.ts: -------------------------------------------------------------------------------- 1 | export class MessageQueue { 2 | private queue = new Array(); 3 | 4 | /// Empty the queue 5 | clear() { 6 | this.queue = []; 7 | } 8 | 9 | /// Add a new message to the queue 10 | enqueue(element: T) { 11 | this.queue.push(element); 12 | } 13 | 14 | /// Read all the messages in the queue and remove them in one operation 15 | dequeue(): Array { 16 | const q = this.queue; 17 | this.queue = []; 18 | return q; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Angular 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/frontend/components/accordion/accordion.html: -------------------------------------------------------------------------------- 1 |
3 |
5 |
6 |
7 | {{sectionTitle}}
8 |
9 |
11 | 12 |
13 | -------------------------------------------------------------------------------- /src/frontend/components/component-tree/component-tree.html: -------------------------------------------------------------------------------- 1 |
2 | 9 | 10 |
11 | -------------------------------------------------------------------------------- /images/augury-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/components/components-tab-menu/components-tab-menu.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
6 | {{t.title}} 7 |
8 |
9 | 10 |
11 | -------------------------------------------------------------------------------- /src/styles/components/injector-tree.css: -------------------------------------------------------------------------------- 1 | bt-injector-tree { 2 | & .link { 3 | stroke-width: 1.5px; 4 | } 5 | 6 | & .node-circle { 7 | stroke: none; 8 | cursor: pointer; 9 | } 10 | 11 | & .arrow { 12 | marker-end: url(#suit); 13 | } 14 | 15 | & .dashed5 { 16 | stroke-dasharray: 5px 5px; 17 | } 18 | 19 | & #suit { 20 | stroke-width: 1.414px; 21 | } 22 | 23 | & .graph-panel { 24 | overflow-y: auto; 25 | } 26 | 27 | & .focused-node-info-panel { 28 | overflow-y: auto; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/frontend/utils/uncaught-error-handler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ErrorHandler, 3 | } from '@angular/core'; 4 | 5 | export class UncaughtErrorHandler implements ErrorHandler { 6 | listeners: Array<(err: Error) => void> = []; 7 | 8 | addListener(listener: (err: Error) => void): () => void { 9 | this.listeners.push(listener); 10 | return () => { 11 | this.listeners = this.listeners.filter(l => l !== listener); 12 | }; 13 | } 14 | 15 | handleError(err: Error): void { 16 | this.listeners.forEach(fn => fn(err)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/frontend/components/ng-module-info/ng-module-info.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

{{moduleName}}

5 |
6 |
7 | 9 | 10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /src/frontend/components/report-error/report-error.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | An uncaught exception prevents Augury from continuing. A stack trace is reproduced below. 6 | 7 |

8 |
9 |
10 |
11 |
12 |
13 |         {{error.error.stack}}
14 |       
15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /src/frontend/utils/object-types.ts: -------------------------------------------------------------------------------- 1 | const observableProperties = ['isUnsubscribed', 'isStopped']; 2 | 3 | export const isObservable = object => { 4 | if (object == null) { 5 | return false; 6 | } 7 | 8 | return observableProperties.every(k => object.hasOwnProperty(k)); 9 | }; 10 | 11 | export const isSubject = object => { 12 | return isObservable(object) && object.hasOwnProperty('hasError'); 13 | }; 14 | 15 | export const isLargeArray = object => { 16 | if (Array.isArray(object) === false) { 17 | return false; 18 | } 19 | return object.length > 100; 20 | }; 21 | -------------------------------------------------------------------------------- /src/frontend/components/ng-module-config-view/ng-module-config-view.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | } from '@angular/core'; 5 | 6 | @Component({ 7 | selector: 'ng-module-config-view', 8 | template: require('./ng-module-config-view.html'), 9 | styles: [require('to-string!./ng-module-config-view.css')], 10 | }) 11 | export class NgModuleConfigView { 12 | @Input() private config: {[key: string]: Array}; 13 | private keys: Array = ['imports', 'exports', 'providers', 'declarations', 'providersInDeclarations']; 14 | 15 | constructor() {} 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/frontend/components/property-editor/property-editor.css: -------------------------------------------------------------------------------- 1 | .property-editor { 2 | display: flex; 3 | padding-bottom: 2px; 4 | } 5 | 6 | .info-key { 7 | margin-right: 0.5em; 8 | line-height: 2em; 9 | } 10 | 11 | .property-editor input { 12 | width: 100%; 13 | border-radius: 1px; 14 | box-shadow: none; 15 | transition: all 0.2s ease-in-out; 16 | height: 2em; 17 | padding: 0 5px; 18 | } 19 | 20 | .state-container { 21 | display: flex; 22 | flex-grow: 2; 23 | line-height: 2em; 24 | height: 2em; 25 | } 26 | 27 | .state-container > span { 28 | width: 100%; 29 | } 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Augury version (required): 2 | Angular version (required): 3 | Date: 4 | OS: 5 | 6 | -- Please make sure you're using the latest version of Augury before reporting an issue. 7 | 8 | Demo test application: 9 | -- Git repository for demo app showing the issue (optional but very helpful for difficult issues). 10 | -- If a code snippet will completely show the issue, please include it. 11 | 12 | Description of issue: 13 | -- Include (clipped) screenshot images if possible. 14 | 15 | 16 | Steps to reproduce: 17 | 18 | 1. 19 | 2. 20 | 3. 21 | 22 | Additional details: 23 | 24 | -------------------------------------------------------------------------------- /src/frontend/components/report-error/report-error.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EventEmitter, 3 | Component, 4 | Input, 5 | Output, 6 | } from '@angular/core'; 7 | 8 | import { 9 | ApplicationErrorType, 10 | ApplicationError, 11 | } from '../../../communication'; 12 | 13 | @Component({ 14 | selector: 'report-error', 15 | template: require('./report-error.html'), 16 | styles: [require('to-string!./report-error.css')], 17 | }) 18 | export class ReportError { 19 | @Input() private error: ApplicationError; 20 | @Output() private reportError: EventEmitter = new EventEmitter(); 21 | } 22 | -------------------------------------------------------------------------------- /src/styles/components/header.css: -------------------------------------------------------------------------------- 1 | 2 | /* Header */ 3 | 4 | .setting { 5 | -webkit-mask-position: -200px -29px; 6 | height: 16px; 7 | margin: auto; 8 | width: 16px; 9 | } 10 | 11 | .setting-dropdown { 12 | -webkit-user-select: none; 13 | z-index: 200; 14 | 15 | & input { 16 | vertical-align: text-bottom; 17 | } 18 | 19 | & li { 20 | padding-left: 1rem; 21 | } 22 | 23 | & .descriptive-text { 24 | max-width: 25em; 25 | } 26 | } 27 | 28 | .logo { 29 | margin-top: 4px; 30 | margin-left: 5px; 31 | margin-right: 10px; 32 | width: 20px; 33 | height: 22px; 34 | } 35 | -------------------------------------------------------------------------------- /src/frontend/components/feedback-form/overall-exp/overall-exp.css: -------------------------------------------------------------------------------- 1 | .overall-exp a { 2 | padding: 2px; 3 | text-decoration: none; 4 | cursor: pointer; 5 | display: inline-block; 6 | } 7 | 8 | .overall-exp a:active, .overall-exp a:focus { 9 | outline: none; 10 | } 11 | 12 | .overall-exp .overall-exp-img { 13 | height: 32px; 14 | width: 32px; 15 | fill: #333333; 16 | } 17 | 18 | :host.dark .overall-exp .overall-exp-img { 19 | fill: #f1f1f1; 20 | } 21 | 22 | a.selected { 23 | background: rgba(0,0,0,0.2); 24 | } 25 | 26 | :host.dark a.selected { 27 | background: rgba(255,255,255,0.2); 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.6.2", 3 | "compileOnSave": false, 4 | "buildOnSave": false, 5 | "compilerOptions": { 6 | "lib": ["es6", "dom"], 7 | "target": "es5", 8 | "module": "commonjs", 9 | "declaration": false, 10 | "noImplicitAny": false, 11 | "removeComments": true, 12 | "noLib": false, 13 | "emitDecoratorMetadata": true, 14 | "experimentalDecorators": true, 15 | "sourceMap": true, 16 | "listFiles": false 17 | }, 18 | "exclude": [ 19 | "node_modules", 20 | "example-apps", 21 | "build", 22 | "typings" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/tree/path.ts: -------------------------------------------------------------------------------- 1 | 2 | export type Path = Array; 3 | 4 | export const serializePath = (path: Path): string => { 5 | return path.join(' '); 6 | }; 7 | 8 | const numberOrString = (segment: string): string | number => { 9 | const v = parseInt(segment, 10); 10 | if (isNaN(v)) { 11 | return segment; 12 | } 13 | return v; 14 | }; 15 | 16 | export const deserializePath = (path: string): Path => { 17 | return path.split(/ /).map(numberOrString); 18 | }; 19 | 20 | export const deserializeChangePath = (path: string): Path => { 21 | return path.split(/\/| /).map(numberOrString); 22 | }; 23 | -------------------------------------------------------------------------------- /src/styles/components/router-tree.css: -------------------------------------------------------------------------------- 1 | 2 | /* Router Tree */ 3 | 4 | bt-router-tree { 5 | & .link { 6 | stroke-width: 1px; 7 | fill: none; 8 | } 9 | 10 | & .node { 11 | cursor: pointer; 12 | } 13 | } 14 | 15 | .node-route { 16 | stroke-width: 1px; 17 | } 18 | 19 | .node-aux-route { 20 | stroke-width: 1px; 21 | } 22 | 23 | /* Router Info Modal */ 24 | 25 | bt-router-info { 26 | background: white; 27 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.10), 0 3px 6px rgba(0, 0, 0, 0.10); 28 | min-width: 200px; 29 | position: absolute; 30 | right: 20px; 31 | top: 80px; 32 | z-index: 100; 33 | } 34 | -------------------------------------------------------------------------------- /.mention-bot: -------------------------------------------------------------------------------- 1 | { 2 | "maxReviewers": 2, 3 | "numFilesToCheck": 10, 4 | "message": "@pullRequester, thanks! @reviewers, please review this.", 5 | "alwaysNotifyForPaths": [ 6 | { 7 | "name": "ghuser", 8 | "files": ["src/js/**/*.js"] 9 | } 10 | ], 11 | "fallbackNotifyForPaths": [ 12 | { 13 | "name": "ghuser", 14 | "files": ["src/js/**/*.js"] 15 | } 16 | ], 17 | "findPotentialReviewers": true, 18 | "fileBlacklist": ["*.md"], 19 | "userBlacklist": ["winkerVSbecks"], 20 | "userBlacklistForPR": ["winkerVSbecks"], 21 | "requiredOrgs": [], 22 | "actions": ["opened"] 23 | } 24 | -------------------------------------------------------------------------------- /src/styles/components/info-panel.css: -------------------------------------------------------------------------------- 1 | 2 | /* Info Panel */ 3 | 4 | .editable { 5 | border-bottom: dashed 1px !important; 6 | border: 0; 7 | display: inline-block; 8 | padding: 0 5px; 9 | 10 | &:focus { 11 | border-bottom: 0 !important; 12 | } 13 | } 14 | 15 | bt-component-info a, 16 | bt-component-info a:link, 17 | bt-component-info a:visited, 18 | bt-component-info a:active { 19 | color: var(--bt-link-color); 20 | text-decoration: none; 21 | } 22 | 23 | bt-component-info a:hover { 24 | text-decoration: underline; 25 | } 26 | 27 | bt-dependency ul li:last-child { 28 | border-bottom: none; 29 | } 30 | -------------------------------------------------------------------------------- /src/styles/components/accordian.css: -------------------------------------------------------------------------------- 1 | 2 | /* Accordian */ 3 | 4 | .expand-arrow { 5 | -webkit-mask-position: -18px -96px; 6 | margin: auto 5px; 7 | pointer-events: none; 8 | } 9 | 10 | .icon { 11 | -webkit-mask-image: url(../../images/toolbarButtonGlyphs_2x.png); 12 | -webkit-mask-size: 352px 168px; 13 | display: inline-block; 14 | height: 12px; 15 | width: 12px; 16 | } 17 | 18 | .double-arrow { 19 | -webkit-mask-position: -74px 16px; 20 | background-color: var(--bt-link-color); 21 | } 22 | 23 | .no-highlight { 24 | -webkit-user-select: none; 25 | -moz-user-select: none; 26 | -ms-user-select: none; 27 | } 28 | -------------------------------------------------------------------------------- /src/frontend/components/accordion/accordion.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'accordion', 5 | template: require('./accordion.html'), 6 | }) 7 | export class Accordion { 8 | @Input() private sectionTitle: string; 9 | @Input() private defaultExpanded: boolean; 10 | 11 | private expansionState: boolean = null; 12 | 13 | private get expanded(): boolean { 14 | if (this.expansionState == null) { 15 | return this.defaultExpanded; 16 | } 17 | return this.expansionState; 18 | } 19 | 20 | private set expanded(v: boolean) { 21 | this.expansionState = v; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /changelog-template.md: -------------------------------------------------------------------------------- 1 | 2 | # Release Number (Release Date) 3 | 4 | ### Sections 5 | * Bug Fixes 6 | * Features 7 | * Performance Improvements 8 | * BREAKING CHANGES 9 | 10 | ### Description of Issues 11 | > **Name:** Description about the issue ([issue number](issue link)) 12 | 13 | * **Issue number**: Github Issue number eg. `8db97b0` 14 | * **Name**: Short one word description of the issue 15 | * **Description**: One Line description of the issue usually from realated git issue 16 | * **Issue Link**: Github Issue Link 17 | 18 | >* **stickers:** Get Augury stickers ([21](https://github.com/rangle/augury/issues/21)) 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/frontend/components/ng-module-config-view/ng-module-config-view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 17 | 18 | 19 |
5 | {{key}} 6 |
12 |
13 |
{{item}}
14 | None 15 |
16 |
20 | -------------------------------------------------------------------------------- /src/frontend/components/property-value/property-value.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectorRef, 3 | Component, 4 | Inject, 5 | Input, 6 | } from '@angular/core'; 7 | 8 | import {Highlightable} from '../../utils/highlightable'; 9 | 10 | @Component({ 11 | selector: 'bt-property-value', 12 | template: require('./property-value.html'), 13 | }) 14 | export class PropertyValue extends Highlightable { 15 | @Input() private key: string; 16 | @Input() private value: string; 17 | 18 | constructor(@Inject(ChangeDetectorRef) changeDetectorRef: ChangeDetectorRef) { 19 | super(changeDetectorRef, changes => changes && changes.hasOwnProperty('value')); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/frontend/components/analytics-popup/analytics-popup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | We would like to gather some data to help improve Augury but first we need your consent. Do you agree to let us collect your usage information? 4 |
5 |
6 | 10 | 14 | 15 |
16 | -------------------------------------------------------------------------------- /src/utils/property-path.test.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape'; 2 | 3 | import { 4 | pathExists, 5 | getAtPath, 6 | } from './property-path'; 7 | 8 | test('utils/property-path: pathExists', t => { 9 | t.plan(2); 10 | 11 | const testObject = { 12 | testProp: 'a_value' 13 | }; 14 | 15 | t.true(pathExists(testObject, 'testProp'), 'exists'); 16 | t.false(pathExists(testObject, 'nonExistentTestProp'), 'does not exist'); 17 | 18 | }); 19 | 20 | test('utils/property-path: getAtPath', t => { 21 | t.plan(1); 22 | 23 | const testObject = { 24 | testProp: 'a_value' 25 | }; 26 | 27 | t.equal(getAtPath(testObject, 'testProp').value, 'a_value'); 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /src/utils/function-name.ts: -------------------------------------------------------------------------------- 1 | /// Extract the name of a function (the `name' property does not appear to be set 2 | /// in some cases). A variant of this appeared in older Augury code and it appears 3 | /// to cover the cases where name is not available as a property. 4 | export const functionName = (fn: Function): string => { 5 | const extract = (value: string) => value.match(/^function ([^\(]*)\(/); 6 | 7 | let name: string = (fn).name; 8 | if (name == null || name.length === 0) { 9 | const match = extract(fn.toString()); 10 | if (match != null && match.length > 1) { 11 | return match[1]; 12 | } 13 | return fn.toString(); 14 | } 15 | return name; 16 | }; 17 | -------------------------------------------------------------------------------- /src/utils/property-path.ts: -------------------------------------------------------------------------------- 1 | // Checks to see if the property path exists. Used mostly in the transformer functions for 2 | // checking the existence of certain nested properties in the Angular debug object, which 3 | // may change in the future. 4 | export const pathExists = (object: any, ...args: any[]): boolean => { 5 | return getAtPath(object, ...args).exists; 6 | }; 7 | 8 | export const getAtPath = (obj: any, ...args: any[]): any => { 9 | for (let i = 0; i < args.length; i++) { 10 | if (!obj || !(args[i] in obj)) { 11 | return { exists: false, value: void 0 }; 12 | } 13 | 14 | obj = obj[args[i]]; 15 | } 16 | return { 17 | exists: true, 18 | value: obj, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /images/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/frontend/middleware/send-analytics.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {MainActions} from '../actions/main-actions'; 3 | import {Options} from '../state'; 4 | import {AnalyticsConsent} from '../../options'; 5 | import {MessageFactory} from '../../communication'; 6 | 7 | @Injectable() 8 | export class SendAnalytics { 9 | constructor(private options: Options) {} 10 | 11 | middleware = store => next => action => { 12 | if (action.type === MainActions.SEND_ANALYTICS && 13 | this.options.analyticsConsent === AnalyticsConsent.Yes) { 14 | chrome.runtime.sendMessage(MessageFactory.analyticsEvent( 15 | action.payload.event, 16 | action.payload.desc)); 17 | } 18 | return next(action); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/frontend/state/component-property-state.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | 3 | import {Path, serializePath} from '../../tree'; 4 | 5 | import {ExpandState} from './expand-state'; 6 | 7 | @Injectable() 8 | export class ComponentPropertyState { 9 | private expanded = new Set(); 10 | 11 | toggleExpand(path: Path) { 12 | const serializedPath = serializePath(path); 13 | 14 | if (this.expanded.has(serializedPath)) { 15 | this.expanded.delete(serializedPath); 16 | } 17 | else { 18 | this.expanded.add(serializedPath); 19 | } 20 | } 21 | 22 | expansionState(path: Path): ExpandState { 23 | return this.expanded.has(serializePath(path)) 24 | ? ExpandState.Expanded 25 | : ExpandState.Collapsed; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | 4 | # Runtime data 5 | pids 6 | *.pid 7 | *.seed 8 | 9 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 10 | .grunt 11 | 12 | # Sass cache folder 13 | .sass-cache 14 | 15 | # Users Environment Variables 16 | .lock-wscript 17 | .idea/ 18 | *.iml 19 | npm-debug* 20 | *~ 21 | \#*# 22 | .#* 23 | *.keystore 24 | *.sw* 25 | .DS_Store 26 | ._* 27 | Thumbs.db 28 | .cache 29 | *.sublime-project 30 | *.sublime-workspace 31 | *swp 32 | *swo 33 | *swn 34 | build 35 | dist 36 | temp 37 | tmp 38 | node_modules 39 | bower_components 40 | 41 | keys.md 42 | .env 43 | 44 | # Project reference 45 | ref 46 | NOTES.md 47 | 48 | typings 49 | 50 | key.pem 51 | .vscode/ 52 | 53 | # build output 54 | *.crx 55 | augury.zip 56 | download.html 57 | -------------------------------------------------------------------------------- /src/frontend/components/analytics-popup/analytics-popup.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | Input, 5 | Output, 6 | SimpleChanges, 7 | ChangeDetectionStrategy, 8 | } from '@angular/core'; 9 | 10 | import { 11 | Options, 12 | AnalyticsConsent, 13 | } from '../../state'; 14 | 15 | @Component({ 16 | selector: 'bt-analytics-popup', 17 | template: require('./analytics-popup.html'), 18 | }) 19 | export class AnalyticsPopup { 20 | private AnalyticsConsent = AnalyticsConsent; 21 | @Input() private options: Options; 22 | 23 | @Output() private hideComponent = new EventEmitter(); 24 | 25 | private onAnalyticsConsentChange = (analyticsConsent: AnalyticsConsent) => { 26 | this.options.analyticsConsent = analyticsConsent; 27 | this.hideComponent.emit(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/backend/utils/app-check.ts: -------------------------------------------------------------------------------- 1 | declare const getAllAngularTestabilities: Function; 2 | declare const getAllAngularRootElements: Function; 3 | declare const ng: any; 4 | 5 | export const isAngular = () => { 6 | return typeof getAllAngularTestabilities === 'function' 7 | && typeof getAllAngularRootElements === 'function'; 8 | }; 9 | 10 | export const isDebugMode = () => { 11 | if (typeof getAllAngularRootElements === 'function' 12 | && typeof ng !== 'undefined') { 13 | 14 | const rootElements = getAllAngularRootElements(); 15 | const firstRootDebugElement = rootElements && rootElements.length ? 16 | ng.probe(rootElements[0]) : null; 17 | 18 | return firstRootDebugElement !== null 19 | && firstRootDebugElement !== void 0 20 | && firstRootDebugElement.injector; 21 | } 22 | return false; 23 | }; 24 | -------------------------------------------------------------------------------- /src/frontend/components/tree-view/tree-view.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 11 | 12 |
13 |
14 |
15 | 19 | 20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /src/gtm-connection/gtm-connection.ts: -------------------------------------------------------------------------------- 1 | import { MessageType } from '../communication'; 2 | 3 | const initializeGTM = (w, d, s, l, i) => { 4 | w[l] = w[l] || []; 5 | w[l].push({ 6 | 'gtm.start': new Date().getTime(), 7 | 'event': 'gtm.js' 8 | }); 9 | 10 | let f = d.getElementsByTagName(s)[0], 11 | j = d.createElement(s), 12 | dl = l !== 'dataLayer' ? '&l=' + l : ''; 13 | 14 | j.async = true; 15 | j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; 16 | f.parentNode.insertBefore(j, f); 17 | 18 | chrome.runtime.onMessage.addListener((message) => { 19 | if (message && message.messageType === MessageType.GoogleTagManagerSend) { 20 | pushTag(message.content); 21 | } 22 | }); 23 | }; 24 | 25 | const pushTag = (tag) => (window as any).dataLayer.push(tag); 26 | 27 | initializeGTM(window, document, 'script', 'dataLayer', 'GTM-NTK59FH'); 28 | -------------------------------------------------------------------------------- /src/sentry-connection/sentry-connection.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Message, 3 | MessageType, 4 | deserializeMessage, 5 | } from '../communication'; 6 | 7 | import * as Raven from 'raven-js'; 8 | 9 | declare const SENTRY_KEY: string; 10 | if (SENTRY_KEY && SENTRY_KEY.length > 0) { 11 | Raven 12 | .config(SENTRY_KEY, { release: chrome.runtime.getManifest().version }) 13 | .install(); 14 | 15 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 16 | if (message && message.messageType === MessageType.SendUncaughtError) { 17 | deserializeMessage(message); 18 | reportError(message.content); 19 | } 20 | }); 21 | } 22 | 23 | const reportError = (errMsg) => { 24 | const e = new Error(errMsg.error.message); 25 | e.name = errMsg.error.name; 26 | e.stack = errMsg.error.stack; 27 | Raven.captureException(e, { 28 | tags: { 29 | ngVersion: errMsg.ngVersion, 30 | }, 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/backend/utils/find-element.ts: -------------------------------------------------------------------------------- 1 | import { 2 | highlight, 3 | clear as clearHighlights 4 | } from './highlighter'; 5 | 6 | import { 7 | MessageFactory 8 | } from '../../communication'; 9 | 10 | import {send} from '../indirect-connection'; 11 | 12 | // Find a mutable tree node based on its DOM target 13 | export function onFindElement(e, tree) { 14 | let foundNode = null; 15 | 16 | const findNode = (node) => { 17 | if (node.nativeElement() === e.target) { 18 | foundNode = node; 19 | } 20 | }; 21 | 22 | // recurse the tree 23 | tree.recurseAll(findNode); 24 | 25 | return foundNode; 26 | } 27 | 28 | export function onElementFound(node, highlights, buffer) { 29 | if (node) { 30 | buffer.enqueue(MessageFactory.foundDOMElement(node)); 31 | send(MessageFactory.push()); 32 | } 33 | 34 | // if there are highlights, clear them 35 | if (highlights) { 36 | clearHighlights(highlights.map); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/frontend/components/property-editor/property-editor.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | undefined 6 | null 7 | "{{value}}" 8 | "" 9 | {{value}} 10 | 11 | 12 | 13 | 21 | 22 | 23 |
24 | -------------------------------------------------------------------------------- /src/frontend/components/router-info/router-info.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | 3 | import {Route} from '../../../backend/utils'; 4 | 5 | @Component({ 6 | selector: 'bt-router-info', 7 | template: require('./router-info.html'), 8 | }) 9 | 10 | export class RouterInfo { 11 | @Input() private selectedRoute: Route | any; 12 | 13 | private hasSelection() { 14 | return this.selectedRoute.data && 15 | this.selectedRoute.data.length > 0; 16 | } 17 | 18 | private ngOnChanges() { 19 | if (!this.selectedRoute || !this.selectedRoute.data) { 20 | return; 21 | } 22 | 23 | this.selectedRoute._data = []; 24 | 25 | for (let key in this.selectedRoute.data.data) { 26 | if (this.selectedRoute.data.data.hasOwnProperty(key)) { 27 | this.selectedRoute._data.push({ 28 | key: key, 29 | value: this.selectedRoute.data.data[key] 30 | }); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/frontend/components/render-error/render-error.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EventEmitter, 3 | Component, 4 | Input, 5 | Output, 6 | } from '@angular/core'; 7 | 8 | import {Options, Theme} from '../../state'; 9 | 10 | import { 11 | ApplicationErrorType, 12 | ApplicationError, 13 | } from '../../../communication'; 14 | 15 | @Component({ 16 | selector: 'render-error', 17 | template: require('./render-error.html'), 18 | host: { 19 | '[class.dark]': 'isDevtoolsDarkTheme' 20 | } 21 | }) 22 | export class RenderError { 23 | @Input() private error: ApplicationError; 24 | @Output() private reportError: EventEmitter = new EventEmitter(); 25 | 26 | private isDevtoolsDarkTheme = this.setIsDarkTheme(); 27 | private ApplicationErrorType = ApplicationErrorType; 28 | 29 | constructor(private options: Options) { 30 | } 31 | 32 | setIsDarkTheme() { 33 | return (chrome.devtools.panels).themeName === 'dark' && 34 | this.options.theme === Theme.Dark; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/frontend/reducers/main-reducer.ts: -------------------------------------------------------------------------------- 1 | import {MainActions} from '../actions/main-actions'; 2 | import {StateTab, Tab} from '../state/tab'; 3 | import {IAppState, IAuguryState} from '../store/model'; 4 | import * as R from 'ramda'; 5 | 6 | const INITIAL_STATE: IAuguryState = { 7 | selectedTab: Tab.ComponentTree, 8 | selectedComponentsSubTab: StateTab.Properties, 9 | DOMSelectionActive: false, 10 | }; 11 | 12 | export function mainReducer(state: IAuguryState = INITIAL_STATE, 13 | action): IAuguryState { 14 | 15 | switch (action.type) { 16 | case MainActions.SELECT_TAB: 17 | return R.assoc('selectedTab', action.payload, state); 18 | } 19 | 20 | switch (action.type) { 21 | case MainActions.SELECT_COMPONENTS_SUB_TAB: 22 | return R.assoc('selectedComponentsSubTab', action.payload, state); 23 | } 24 | 25 | switch (action.type) { 26 | case MainActions.DOM_SELECTION_ACTIVE_CHANGE: 27 | return R.assoc('DOMSelectionActive', action.payload, state); 28 | } 29 | 30 | return state; 31 | } 32 | -------------------------------------------------------------------------------- /images/augury-bw.svg: -------------------------------------------------------------------------------- 1 | augury-logo -------------------------------------------------------------------------------- /src/frontend/utils/match.ts: -------------------------------------------------------------------------------- 1 | import {Node} from '../../tree'; 2 | import {Route} from '../../backend/utils/parse-router'; 3 | 4 | export const matchString = (query: string, value: string): boolean => { 5 | const llhs = (query || '').toLocaleLowerCase(); 6 | const lrhs = (value || '').toLocaleLowerCase(); 7 | 8 | return lrhs.indexOf(llhs) >= 0; 9 | }; 10 | 11 | export const matchValue = (query: string, value: T): boolean => { 12 | if (value == null) { 13 | return false; 14 | } 15 | return matchString(query, value.toString()); 16 | }; 17 | 18 | export const matchNode = (node: Node, query: string): boolean => { 19 | if (matchString(query, node.name)) { 20 | return true; 21 | } 22 | 23 | if (node.description) { 24 | const matches = node.description 25 | .map(d => matchValue(query, d.value)).filter(v => v === true); 26 | 27 | return matches.length > 0; 28 | } 29 | 30 | return false; 31 | }; 32 | 33 | export const matchRoute = (route: Route, query: string): boolean => { 34 | throw new Error('Not implemented'); 35 | }; 36 | -------------------------------------------------------------------------------- /src/utils/circular-recurse.ts: -------------------------------------------------------------------------------- 1 | import {Path} from '../tree'; 2 | 3 | import {isScalar} from './scalar'; 4 | 5 | export type Apply = (value) => void; 6 | 7 | // Recursive traversal of an object tree, but will not traverse circular references or DOM elements 8 | export const recurse = (object, apply: Apply) => { 9 | const visited = new Set(); 10 | 11 | const visit = value => { 12 | if (value == null || 13 | isScalar(value) || 14 | /Element/.test(Object.prototype.toString.call(value)) || 15 | value.top === window) { 16 | return; 17 | } 18 | 19 | if (visited.has(value)) { // circular loop 20 | return; 21 | } 22 | 23 | visited.add(value); 24 | 25 | apply(value); 26 | 27 | if (Array.isArray(value) || value instanceof Set) { 28 | (value).forEach((v, k) => visit(v)); 29 | } 30 | else if (value instanceof Map) { 31 | value.forEach((v, k) => visit(v)); 32 | } 33 | else { 34 | Object.keys(value).forEach(k => visit(value[k])); 35 | } 36 | }; 37 | 38 | visit(object); 39 | }; 40 | -------------------------------------------------------------------------------- /src/frontend/components/info-panel/info-panel.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 19 | 20 | 21 | 29 | 30 | -------------------------------------------------------------------------------- /images/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/components/split-pane/split-pane.html: -------------------------------------------------------------------------------- 1 |
5 |
8 | 9 |
10 |
13 | 14 |
15 | 16 |
20 |
21 | 22 |
25 |
26 | -------------------------------------------------------------------------------- /src/frontend/components/feedback-form/google-service/google-service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Http} from '@angular/http'; 3 | 4 | 5 | const GOOGLE_FORM_URL = 6 | 'https://docs.google.com/forms/d/18VuNfbKUzFXNkPv7-QhSDmgKHhfOeWJBKugD2o37Wu8/formResponse'; 7 | 8 | export interface FeedbackFormData { 9 | overallExp: string; 10 | feedback: string; 11 | } 12 | 13 | /** 14 | * Service for sending feedback to google forms 15 | * to be saved into a google spreadsheet. 16 | */ 17 | @Injectable() 18 | export class GoogleService { 19 | constructor(private http: Http) { 20 | } 21 | 22 | public sendFeedback(data: FeedbackFormData) { 23 | return this.http.get(this._getFinalUrl(data)).toPromise(); 24 | } 25 | 26 | private _getFinalUrl(feedbackData: FeedbackFormData) { 27 | // Entry ids are found by inspected the source of the live google form. 28 | // Searching for `name="entry.` in order to find each ID. 29 | const params = `?entry.1709491946=${feedbackData.overallExp}&entry.593240098=${feedbackData.feedback}`; 30 | return GOOGLE_FORM_URL + params; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Rangle.io 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. -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Augury", 3 | "short_name": "Augury", 4 | "version": "1.14.0", 5 | "description": "Extends the Developer Tools, adding tools for debugging and profiling Angular applications.", 6 | "permissions": [ 7 | "storage" 8 | ], 9 | "browser_action": { 10 | "default_icon": "images/augury.png", 11 | "default_popup": "popup.html" 12 | }, 13 | "devtools_page": "index.html", 14 | "background": { 15 | "scripts": [ 16 | "build/background.js" 17 | ], 18 | "persistent": false 19 | }, 20 | "content_scripts": [{ 21 | "matches": [ 22 | "" 23 | ], 24 | "js": [ 25 | "build/content-script.js" 26 | ], 27 | "run_at": "document_end" 28 | }], 29 | "web_accessible_resources": [ 30 | "node_modules/*", 31 | "build/*" 32 | ], 33 | "manifest_version": 2, 34 | "content_security_policy": "script-src 'self' https://www.google-analytics.com https://www.googletagmanager.com 'unsafe-eval'; object-src 'self'", 35 | "icons": { 36 | "16": "images/icon16.png", 37 | "48": "images/icon48.png", 38 | "128": "images/icon128.png" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/frontend/utils/graph-utils.ts: -------------------------------------------------------------------------------- 1 | export class GraphUtils { 2 | addText(svg: any, x: number, y: number, text: string, maxChars: number = 0) { 3 | const fittedText = maxChars > 0 && text && text.length > maxChars ? `${text.slice(0, maxChars - 3)}...` : text; 4 | svg 5 | .append('text') 6 | .attr('x', x) 7 | .attr('y', y) 8 | .text(fittedText); 9 | } 10 | 11 | addCircle(svg: any, x: number, y: number, r: number, clazz: string, 12 | mouseOverFn?: () => void, mouseOutFn?: () => void) { 13 | svg 14 | .append('circle') 15 | .attr('cx', x) 16 | .attr('cy', y) 17 | .attr('r', r) 18 | .attr('stroke-width', 1) 19 | .attr('class', clazz) 20 | .on('mouseover', mouseOverFn ? mouseOverFn : () => null) 21 | .on('mouseout', mouseOutFn ? mouseOutFn : () => null); 22 | } 23 | 24 | addLine(svg: any, x1: number, y1: number, x2: number, y2: number, clazz: string) { 25 | svg 26 | .append('line') 27 | .attr('x1', x1) 28 | .attr('y1', y1) 29 | .attr('x2', x2) 30 | .attr('y2', y2) 31 | .attr('class', 'link ' + (clazz || '')); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/tree/node.ts: -------------------------------------------------------------------------------- 1 | import {Property, Dependency} from '../backend/utils/description'; 2 | 3 | export interface EventListener { 4 | name: string; 5 | callback: Function; 6 | } 7 | 8 | export interface InputProperty { 9 | propertyKey: string; 10 | bindingPropertyName?: string; 11 | } 12 | 13 | export interface OutputProperty extends InputProperty {} // outputs can be aliased too 14 | 15 | export interface Node { 16 | id: string; 17 | augury_token_id: string; 18 | name: string; 19 | isComponent: boolean; 20 | changeDetection: number; 21 | description: Array; 22 | nativeElement: () => HTMLElement; // null on frontend 23 | listeners: Array; 24 | dependencies: Array; 25 | directives: Array; 26 | providers: Array; 27 | input: Array; 28 | output: Array; 29 | source: string; 30 | children: Array; 31 | properties: { 32 | [key: string]: any; 33 | }; 34 | attributes: { 35 | [key: string]: string; 36 | }; 37 | classes: { 38 | [key: string]: boolean; 39 | }; 40 | styles: { 41 | [key: string]: string; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/frontend/components/components-tab-menu/components-tab-menu.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | Input, 5 | Output, 6 | } from '@angular/core'; 7 | import { 8 | InstanceWithMetadata, 9 | Metadata, 10 | Node, 11 | ObjectType, 12 | Path, 13 | } from '../../../tree'; 14 | 15 | import {StateTab} from '../../state'; 16 | 17 | export interface StateTabDescription { 18 | title: string; 19 | tab; 20 | } 21 | 22 | import {UserActions} from '../../actions/user-actions/user-actions'; 23 | 24 | @Component({ 25 | selector: 'bt-components-tab-menu', 26 | template: require('./components-tab-menu.html'), 27 | }) 28 | export class ComponentsTabMenu { 29 | @Input() selectedStateTab; 30 | @Output() tabChange: EventEmitter = new EventEmitter(); 31 | 32 | private tabs: Array = [{ 33 | title: 'Properties', 34 | tab: StateTab.Properties, 35 | }, { 36 | title: 'Injector Graph', 37 | tab: StateTab.InjectorGraph, 38 | }]; 39 | 40 | constructor(private userActions: UserActions) { 41 | } 42 | 43 | private onSelect(tab: StateTabDescription) { 44 | this.tabChange.emit(tab.tab); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/frontend/components/feedback-form/overall-exp/overall-exp.ts: -------------------------------------------------------------------------------- 1 | import {Input, Component} from '@angular/core'; 2 | 3 | import {Options, Theme} from '../../../state'; 4 | 5 | export enum Experience { 6 | Good, 7 | Bad, 8 | Unspecified, 9 | } 10 | 11 | 12 | @Component({ 13 | selector: 'bt-overall-exp-control', 14 | template: require('./overall-exp.html'), 15 | styles: [require('to-string!./overall-exp.css')], 16 | host: { 17 | '[class.dark]': 'isDarkTheme()' 18 | } 19 | }) 20 | 21 | export class OverallExpControl { 22 | @Input() private isDarkTheme; 23 | 24 | private _overallExperience: Experience = Experience.Unspecified; 25 | 26 | constructor(private options: Options) { 27 | } 28 | 29 | setExperience(wasGoodExperience: boolean, chosen, other) { 30 | this._overallExperience = wasGoodExperience ? 31 | Experience.Good : 32 | Experience.Bad; 33 | chosen.setAttribute('class', 'selected'); 34 | other.setAttribute('class', ''); 35 | } 36 | 37 | get rating() { 38 | return this._overallExperience; 39 | } 40 | 41 | public resetRating() { 42 | this._overallExperience = Experience.Unspecified; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/frontend/utils/parse-data.ts: -------------------------------------------------------------------------------- 1 | export default class ParseData { 2 | 3 | static BOOLEAN_CONSTANTS = { 4 | 'true': true, 5 | 'false': false 6 | }; 7 | 8 | public static parseNumber(data: any): number { 9 | return +data; 10 | } 11 | 12 | public static parseBoolean(data: any): boolean { 13 | return this.BOOLEAN_CONSTANTS[data.toString().toLowerCase()]; 14 | } 15 | 16 | public static convertToNumber(data: any, oldValue: number): number { 17 | const newValue: number = isNaN(+data) ? oldValue : +data; 18 | return newValue; 19 | } 20 | 21 | public static convertToBoolean(data: any, oldValue: boolean): boolean { 22 | let newValue: boolean = 23 | this.BOOLEAN_CONSTANTS[data.toLowerCase()] === undefined ? 24 | oldValue : this.BOOLEAN_CONSTANTS[data.toLowerCase()]; 25 | return newValue; 26 | } 27 | 28 | public static getType(state: any, key: string): string { 29 | return typeof state[key]; 30 | } 31 | 32 | public static checkType(state: any, key: string, value: any): boolean { 33 | return (typeof state[key]) === (typeof value); 34 | } 35 | 36 | public static getTypeByValue(value: any): string { 37 | return typeof value; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/frontend/components/node-item/node-item.html: -------------------------------------------------------------------------------- 1 |
2 |
8 |
15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 | 29 | 30 |
31 |
32 | -------------------------------------------------------------------------------- /src/frontend/components/render-state/render-state.css: -------------------------------------------------------------------------------- 1 | .emitter { 2 | position: relative; 3 | display: flex; 4 | width: 100%; 5 | border-radius: 2px; 6 | box-shadow: none; 7 | -webkit-transition: all 0.4s linear; 8 | transition: all 0.4s linear; 9 | } 10 | 11 | .emitter input { 12 | display: inline-block; 13 | width: 100%; 14 | vertical-align: top; 15 | } 16 | 17 | .emitter button { 18 | margin-left: 5px; 19 | } 20 | 21 | .property-container { 22 | display: inline-flex; 23 | white-space: nowrap; 24 | position: relative; 25 | } 26 | 27 | .classification { 28 | width: 100%; 29 | display: flex; 30 | } 31 | 32 | .set-state-item-undefined { 33 | position: absolute; 34 | top: -5px; left: 0; 35 | font-size: 16px; 36 | cursor: pointer; 37 | margin-right: 10px; 38 | text-decoration: none; 39 | } 40 | 41 | .info-key { 42 | display: inline-block; 43 | margin-right: 0.5em; 44 | } 45 | 46 | .info-key.output { 47 | margin-top: 0.5em; 48 | } 49 | 50 | .emit-state { 51 | position: absolute; 52 | display: inline-block; 53 | margin-top: 2px; 54 | right: 5em; 55 | font-size: 1.2em; 56 | } 57 | 58 | .emitted { 59 | color: darkgreen; 60 | } 61 | 62 | .failed { 63 | color: red; 64 | } 65 | -------------------------------------------------------------------------------- /src/frontend/components/feedback-form/feedback-form.css: -------------------------------------------------------------------------------- 1 | :host { 2 | font-size: 16px; 3 | color: #222; 4 | } 5 | :host.dark { 6 | color: #f1f1f1; 7 | } 8 | 9 | section.feedback-form-container { 10 | padding: 10px 0; 11 | } 12 | 13 | #fbf-feedback { 14 | display: block; 15 | width: 100%; 16 | height: auto; 17 | font-size: 14px; 18 | } 19 | 20 | .form-group { 21 | margin-bottom: 20px; 22 | } 23 | 24 | .form-group .btn-group { 25 | display: inline-block; 26 | } 27 | 28 | .fbf-button { 29 | padding: 10px 20px; 30 | background: #343434; 31 | border-radius: 0; 32 | color: white; 33 | font-weight: bold; 34 | border: none; 35 | } 36 | 37 | :host.dark .fbf-button { 38 | background: #00cdff; 39 | } 40 | 41 | .fbf-button:active, .fbf-button:focus { 42 | outline: none; 43 | } 44 | 45 | .fbf-button { 46 | background: #008aff; 47 | color: #f1f1f1; 48 | transition: background .3s ease; 49 | } 50 | 51 | .fbf-button:active { 52 | background: #0059bb; 53 | } 54 | 55 | .fbf-button:hover { 56 | background: #00cdff; 57 | cursor: pointer; 58 | } 59 | 60 | .feedback-close-form-button { 61 | display: block; 62 | text-decoration: none; 63 | top: 0; right: 15px; 64 | position: absolute; 65 | font-size: 32px; 66 | cursor: pointer; 67 | } 68 | -------------------------------------------------------------------------------- /src/communication/application-error.ts: -------------------------------------------------------------------------------- 1 | import {SerializeableError} from '../utils/error-handling'; 2 | 3 | export enum ApplicationErrorType { 4 | None, 5 | 6 | // The application being debugged is running in production mode and therefore 7 | // is incompatible with Augury and cannot be debugged. 8 | ProductionMode, 9 | 10 | // The application being debugged in not an Angular App. 11 | NotNgApp, 12 | 13 | // An uncaught exception prevents the application from being debugged 14 | UncaughtException, 15 | } 16 | 17 | export interface ApplicationError { 18 | 19 | /// The class of error being represented 20 | errorType: ApplicationErrorType; 21 | 22 | /// The class of error being represented 23 | error?: SerializeableError; 24 | 25 | /// Additional details about the error 26 | details: string; 27 | 28 | /// Stack trace information 29 | stackTrace: string; 30 | } 31 | 32 | export class ApplicationError implements ApplicationError { 33 | constructor(errorType: ApplicationErrorType, error?: SerializeableError, details?: string, stack?: string) { 34 | this.errorType = errorType; 35 | this.error = error; 36 | this.details = details || error ? error.message : null; 37 | this.stackTrace = stack || error ? error.stack : new Error().stack; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/styles/utils/white-space.css: -------------------------------------------------------------------------------- 1 | 2 | /* White Space */ 3 | 4 | .pt0 { 5 | padding-top: 0; 6 | } 7 | 8 | .pr0 { 9 | padding-right: 0; 10 | } 11 | 12 | .pb0 { 13 | padding-bottom: 0; 14 | } 15 | 16 | .pl0 { 17 | padding-left: 0; 18 | } 19 | 20 | .pt1 { 21 | padding-top: var(--space-1); 22 | } 23 | 24 | .pr1 { 25 | padding-right: var(--space-1); 26 | } 27 | 28 | .pb1 { 29 | padding-bottom: var(--space-1); 30 | } 31 | 32 | .pl1 { 33 | padding-left: var(--space-1); 34 | } 35 | 36 | .pt2 { 37 | padding-top: var(--space-2); 38 | } 39 | 40 | .pr2 { 41 | padding-right: var(--space-2); 42 | } 43 | 44 | .pb2 { 45 | padding-bottom: var(--space-2); 46 | } 47 | 48 | .pl2 { 49 | padding-left: var(--space-2); 50 | } 51 | 52 | .pt3 { 53 | padding-top: var(--space-3); 54 | } 55 | 56 | .pr3 { 57 | padding-right: var(--space-3); 58 | } 59 | 60 | .pb3 { 61 | padding-bottom: var(--space-3); 62 | } 63 | 64 | .pl3 { 65 | padding-left: var(--space-3); 66 | } 67 | 68 | .pt4 { 69 | padding-top: var(--space-4); 70 | } 71 | 72 | .pr4 { 73 | padding-right: var(--space-4); 74 | } 75 | 76 | .pb4 { 77 | padding-bottom: var(--space-4); 78 | } 79 | 80 | .pl4 { 81 | padding-left: var(--space-4); 82 | } 83 | 84 | .myn2{ 85 | margin-top: -var(--space-2); 86 | margin-bottom: -var(--space-2); 87 | } 88 | -------------------------------------------------------------------------------- /src/frontend/components/tab-menu/tab-menu.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | Input, 5 | Output, 6 | } from '@angular/core'; 7 | import { 8 | InstanceWithMetadata, 9 | Metadata, 10 | Node, 11 | ObjectType, 12 | Path, 13 | } from '../../../tree'; 14 | 15 | export interface TabDescription { 16 | title: string; 17 | tab; 18 | } 19 | 20 | import {UserActions} from '../../actions/user-actions/user-actions'; 21 | 22 | @Component({ 23 | selector: 'bt-tab-menu', 24 | template: require('./tab-menu.html'), 25 | }) 26 | export class TabMenu { 27 | @Input() tabs: Array; 28 | @Input() selectedTab; 29 | @Input() domSelectionActive; 30 | 31 | @Output() tabChange: EventEmitter = new EventEmitter(); 32 | @Output() domSelectionActiveChange: EventEmitter = new EventEmitter(); 33 | 34 | constructor(private userActions: UserActions) { 35 | } 36 | 37 | private selectElement() { 38 | if (this.domSelectionActive) { 39 | this.userActions.cancelFindElement(); 40 | this.domSelectionActiveChange.emit(false); 41 | } else { 42 | this.userActions.findElement(); 43 | this.domSelectionActiveChange.emit(true); 44 | } 45 | } 46 | 47 | private onSelect(tab: TabDescription) { 48 | this.tabChange.emit(tab.tab); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | function initialize() { 2 | subVersionNumbers(); 3 | document.getElementById("create_issue").addEventListener("click", generateGithubIssuesBodyText); 4 | } 5 | 6 | function subVersionNumbers() { 7 | var versionInstances = document.getElementsByClassName("version_number"); 8 | var version = chrome.runtime.getManifest().version || "1.1.0"; 9 | 10 | for(var i = 0; i < versionInstances.length; i++){ 11 | versionInstances[i].innerHTML = "(" + version + ")"; 12 | } 13 | } 14 | 15 | function generateGithubIssuesBodyText() { 16 | const date = (new Date()).toUTCString(); 17 | const body = `Augury: ${chrome.runtime.getManifest().version} 18 | Date: ${date} 19 | OS: ${navigator.platform} 20 | 21 | Demo test application: 22 | -- Git repository for demo app showing the issue (optional but very helpful for difficult issues). 23 | -- If a code snippet will completely show the issue, please include it. 24 | 25 | Description of issue: 26 | -- Include (clipped) screenshot images if possible. 27 | 28 | Angular version (required): ??? 29 | 30 | Steps to reproduce: 31 | 32 | 1. 33 | 2. 34 | 3. 35 | 36 | Additional details: 37 | 38 | `; 39 | 40 | window.open(`https://github.com/rangle/augury/issues/new?body=${window.encodeURI(body)}`); 41 | } 42 | 43 | document.addEventListener("DOMContentLoaded", initialize); 44 | 45 | -------------------------------------------------------------------------------- /src/frontend/components/render-error/render-error.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 7 | 8 |
9 |
10 |
11 |

12 | This application is not an Angular application. 13 |

14 |

15 | This application cannot be inspected using Augury. 16 |

17 |
18 |
19 |
20 |
21 |

22 | This application is running in production mode 23 | and therefore cannot be inspected using Augury. 24 |

25 |

26 | If this is an Angular application, please rebuild your application in debug mode or remove the 27 | call to enableProdMode(). 28 |

29 |
30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /src/frontend/components/router-tree/router-tree.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 |
7 |

8 | There are no routes to display. 9 |

10 |

If you have routes defined and not seeing anything, for version of Angular older than 2.3.0, please see the README.

11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 |
19 |
23 |
24 |
25 |
26 | 30 | 31 |
32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /src/tree/mutable-tree-factory.ts: -------------------------------------------------------------------------------- 1 | import {MutableTree} from './mutable-tree'; 2 | import {transform} from './transformer'; 3 | import {Node} from './node'; 4 | import {SimpleOptions} from '../options'; 5 | 6 | export const transformToTree = 7 | (root, index: number, options: SimpleOptions, increment: (n: number) => void) => { 8 | const map = new Map(); 9 | try { 10 | return transform([index], root, options, map, increment); 11 | } 12 | finally { 13 | map.clear(); // release references 14 | } 15 | }; 16 | 17 | export const createTree = (roots: Array) => { 18 | const tree = new MutableTree(); 19 | tree.roots = roots; 20 | return tree; 21 | }; 22 | 23 | export interface ElementTransformResult { 24 | /// The tree containing a root for each application on the page 25 | tree: MutableTree; 26 | 27 | /// The total number of nodes transformed 28 | count: number; 29 | } 30 | 31 | export const createTreeFromElements = 32 | (roots: Array, options: SimpleOptions): ElementTransformResult => { 33 | const tree = new MutableTree(); 34 | 35 | /// Keep track of the number of nodes that we process as part of this transformation 36 | let count = 0; 37 | 38 | tree.roots = roots.map((r, index) => transformToTree(r, index, options, n => count += n)); 39 | 40 | return {tree, count}; 41 | }; 42 | -------------------------------------------------------------------------------- /src/styles/properties.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom Properties 3 | */ 4 | 5 | :root { 6 | --bt-link-color: #2828AB; 7 | 8 | 9 | /** 10 | * Light Mode (lmode) and Dark Mode (dmode) of Injector Graph (igraph) Colours 11 | */ 12 | --lmode-igraph-component-primary: #2828AB; 13 | --lmode-igraph-component-secondary: #EBF2FC; 14 | --lmode-igraph-dependency-primary: #F05057; 15 | --lmode-igraph-dependency-secondary: #FFF0F0; 16 | --lmode-igraph-dependency-legend-origin: #C6E3FF; 17 | --lmode-igraph-dependency-legend-provided: #ffa4a4; 18 | 19 | --dmode-igraph-component-primary: #23a9ab; 20 | --dmode-igraph-component-secondary: #EBF2FC; 21 | --dmode-igraph-dependency-primary: #F05057; 22 | --dmode-igraph-dependency-secondary: #FFF0F0; 23 | 24 | 25 | /** 26 | * Light Mode (lmode) and Dark Mode (dmode) of Router Tree (rtree) Colours 27 | */ 28 | --lmode-rtree-node-primary: #F05057; 29 | --lmode-rtree-node-secondary: #FFF0F0; 30 | --lmode-rtree-aux-node-primary: #2828AB; 31 | --lmode-rtree-aux-node-secondary: #EBF2FC; 32 | 33 | --dmode-rtree-node-primary: #F05057; 34 | --dmode-rtree-node-secondary: #FFF0F0; 35 | --dmode-rtree-aux-node-primary: #23a9ab; 36 | --dmode-rtree-aux-node-secondary: #EBF2FC; 37 | 38 | 39 | --space-1: 0.1818rem; 40 | --space-2: 0.3636rem; 41 | --space-3: 0.7273rem; 42 | --space-4: 1.455rem; 43 | } 44 | -------------------------------------------------------------------------------- /src/frontend/components/feedback-form/feedback-form.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 7 | 8 | 41 | 42 |
43 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [true, "check-space"], 5 | "curly": true, 6 | "eofline": true, 7 | "forin": true, 8 | "indent": [true, "spaces"], 9 | "label-position": true, 10 | "max-line-length": [true, 120], 11 | "no-arg": true, 12 | "no-bitwise": false, 13 | "no-console": [true, 14 | "debug", 15 | "info", 16 | "time", 17 | "timeEnd", 18 | "trace" 19 | ], 20 | "no-construct": true, 21 | "no-debugger": true, 22 | "no-duplicate-variable": true, 23 | "no-empty": false, 24 | "no-eval": true, 25 | "no-shadowed-variable": true, 26 | "no-string-literal": true, 27 | "no-switch-case-fall-through": false, 28 | "trailing-comma": "never", 29 | "no-trailing-whitespace": true, 30 | "no-unused-expression": true, 31 | "no-use-before-declare": false, 32 | "no-var-keyword": true, 33 | "one-line": [true, 34 | "check-open-brace", 35 | "check-whitespace" 36 | ], 37 | "quotemark": [true, "single"], 38 | "radix": true, 39 | "semicolon": true, 40 | "triple-equals": [true, "allow-null-check"], 41 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"], 42 | "whitespace": [true, 43 | "check-branch", 44 | "check-decl", 45 | "check-operator", 46 | "check-separator", 47 | "check-type" 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/frontend/app.html: -------------------------------------------------------------------------------- 1 |
4 |
5 | 8 |
9 | 31 | 32 |
33 | 36 | -------------------------------------------------------------------------------- /images/guide.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | guide 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/frontend/components/dependency-info/dependency-info.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 13 |
14 | 15 | 16 |
17 |
18 | 21 | 22 | 23 | {{selectedDependency}} 24 | 25 |
26 | 27 | 46 |
47 | -------------------------------------------------------------------------------- /webpack.test.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | 4 | module.exports = { 5 | entry: { 6 | 'test': [ 7 | path.join(__dirname, 'webpack.vendor.ts'), 8 | path.join(__dirname, 'webpack.test.bootstrap.ts') 9 | ] 10 | }, 11 | 12 | output: { 13 | path: path.join(__dirname, './build'), 14 | filename: '[name].js' 15 | }, 16 | stats: { 17 | colors: true, 18 | reasons: true 19 | }, 20 | module: { 21 | loaders: [{ 22 | // Support for .ts files. 23 | test: /\.ts$/, 24 | loader: 'ts', 25 | exclude: /node_modules/, 26 | query: { 27 | 'ignoreDiagnostics': [] 28 | }, 29 | exclude: [ 30 | /node_modules/ 31 | ] 32 | }, { 33 | test: /\.css$/, 34 | loader: 'css!postcss' 35 | }, { 36 | test: /\.png$/, 37 | loader: "url-loader?mimetype=image/png" 38 | }, { 39 | test: /\.html$/, 40 | loader: 'raw' 41 | }], 42 | noParse: [ 43 | /rtts_assert\/src\/rtts_assert/, 44 | /reflect-metadata/, 45 | /.+zone\.js\/dist\/.+/, 46 | /.+angular2\/bundles\/.+/ 47 | ] 48 | }, 49 | resolve: { 50 | extensions: ['', '.ts', '.js', '.jsx'], 51 | modulesDirectories: ['src', 'node_modules'] 52 | }, 53 | 54 | node: { 55 | 'fs': 'empty' 56 | }, 57 | 58 | plugins: [ 59 | new webpack.DefinePlugin({ 60 | chrome: '{runtime: {connect: function() {}}}' 61 | }) 62 | ] 63 | }; 64 | -------------------------------------------------------------------------------- /src/frontend/components/search/search.html: -------------------------------------------------------------------------------- 1 |
3 |
4 | 11 |
12 | 13 | {{ current + 1 }} of {{ total }} 14 | 15 |
16 |
17 |
18 | 23 |
24 |
25 | 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /src/frontend/components/feedback-form/feedback-form.ts: -------------------------------------------------------------------------------- 1 | import {Output, EventEmitter, Component, ViewChild} from '@angular/core'; 2 | import {FormControl} from '@angular/forms'; 3 | 4 | import {OverallExpControl, Experience} from './overall-exp/overall-exp'; 5 | import {GoogleService, FeedbackFormData} from './google-service/google-service'; 6 | import {Options, Theme} from '../../state'; 7 | 8 | 9 | @Component({ 10 | selector: 'bt-feedback-form', 11 | template: require('./feedback-form.html'), 12 | styles: [require('to-string!./feedback-form.css')], 13 | host: { 14 | '[class.dark]': 'isDarkTheme()' 15 | } 16 | }) 17 | 18 | export class FeedbackForm { 19 | private isDarkTheme = () => this.options.theme === Theme.Dark; 20 | private feedbackControl: FormControl = new FormControl(''); 21 | private isSubmitting = false; 22 | 23 | @Output() onCloseForm: EventEmitter = new EventEmitter(); 24 | 25 | @ViewChild('experienceControl') 26 | private expControl: OverallExpControl; 27 | 28 | constructor(private options: Options, private googleForms: GoogleService) { } 29 | 30 | onSubmit() { 31 | this.isSubmitting = true; 32 | const feedback: FeedbackFormData = { 33 | overallExp: Experience[this.expControl.rating], 34 | feedback: this.feedbackControl.value, 35 | }; 36 | 37 | this.googleForms.sendFeedback(feedback) 38 | .then(() => { 39 | this.isSubmitting = false; 40 | this.expControl.resetRating(); 41 | this.feedbackControl.reset(); 42 | this.onCloseForm.emit(); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/frontend/components/tree-view/tree-view.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | Input, 5 | Output, 6 | ViewChild, 7 | } from '@angular/core'; 8 | 9 | import { 10 | MutableTree, 11 | Node, 12 | } from '../../../tree'; 13 | 14 | import {UserActions} from '../../actions/user-actions/user-actions'; 15 | import {Search} from '../search/search'; 16 | 17 | @Component({ 18 | selector: 'bt-tree-view', 19 | template: require('./tree-view.html'), 20 | }) 21 | export class TreeView { 22 | @Input() private selectedNode: Node; 23 | @Input() private tree: MutableTree; 24 | 25 | @Output() private collapseChildren = new EventEmitter(); 26 | @Output() private expandChildren = new EventEmitter(); 27 | @Output() private inspectElement = new EventEmitter(); 28 | @Output() private selectNode = new EventEmitter(); 29 | 30 | @ViewChild(Search) private search: Search; 31 | 32 | private searchNode: Node; 33 | 34 | constructor(private userActions: UserActions) {} 35 | 36 | ngOnChanges(changes) { 37 | if (this.search === null) { 38 | return; 39 | } 40 | 41 | if (changes.hasOwnProperty('selectedNode') && this.selectedNode !== this.searchNode) { 42 | this.searchNode = null; 43 | this.search.reset(); 44 | } 45 | } 46 | 47 | private onRetrieveSearchResults = (query: string): Promise> => { 48 | return this.userActions.searchComponents(this.tree, query); 49 | } 50 | 51 | private onSelectedSearchResultChanged(node: Node) { 52 | this.searchNode = node; 53 | this.selectNode.emit(node); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/backend/utils/highlighter.test.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape'; 2 | 3 | import {highlight} from './highlighter'; 4 | 5 | test('utils/highlighter: passing undefined', t => { 6 | t.plan(1); 7 | const hls = highlight([]); 8 | t.deepEqual(hls, undefined, 'get undefined highlight'); 9 | t.end(); 10 | }); 11 | 12 | test('utils/highlighter: test highlight', t => { 13 | t.plan(2); 14 | document.body.innerHTML = ''; 15 | 16 | const div = document.createElement('div'); 17 | div.setAttribute('value', 'value'); 18 | 19 | const node = document.createTextNode('innerText'); 20 | div.appendChild(node); 21 | 22 | document.body.appendChild(div); 23 | 24 | const hls = highlight( 25 | [{id: '0', nativeElement: () => div, name: 'highlight div'}]); 26 | 27 | const all = document.querySelectorAll('div'); 28 | const h: any = all[all.length - 1]; 29 | 30 | t.deepEqual(h.style.padding, '5px', 'get highlighted padding'); 31 | t.deepEqual(h.style.position, 'absolute', 'get highlighted position'); 32 | 33 | t.end(); 34 | }); 35 | 36 | test('utils/highlighter: test highlight', t => { 37 | t.plan(1); 38 | document.clear(); 39 | 40 | const div = document.createElement('div'); 41 | div.setAttribute('value', 'value'); 42 | 43 | const node = document.createTextNode('innerText'); 44 | div.appendChild(node); 45 | 46 | document.body.appendChild(div); 47 | 48 | const hls = highlight( 49 | [{id: '1', nativeElement: () => div, name: 'foo'}]); 50 | 51 | highlight([]); 52 | t.deepEqual(document.getElementsByTagName('div').length, 3, 53 | 'remove all highlight'); 54 | t.end(); 55 | }); 56 | -------------------------------------------------------------------------------- /s3-upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # 3 | # Push the latest build to batarangle s3 bucket 4 | 5 | echo "Start" 6 | 7 | # bucket name 8 | bucket=batarangle.io 9 | dateValue=$(date -u +'%Y%m%dT%H%M%SZ') 10 | 11 | # filename generated using circleci 12 | file="augury-$CIRCLE_BUILD_NUM.crx" 13 | resource="/${bucket}/${file}" 14 | contentType="application/x-compressed-tar" 15 | 16 | # create signed token 17 | stringToSign="PUT\n\n${contentType}\n${dateValue}\n${resource}" 18 | 19 | # fetch aws credentials from env variables 20 | s3Key=$AWS_ACCESS_KEY_ID 21 | s3Secret=$AWS_SECRET_ACCESS_KEY 22 | 23 | # create hmac signature 24 | signature=`echo -en ${stringToSign} | openssl sha1 -hmac ${s3Secret} -binary | base64` 25 | 26 | # curl request to put the file 27 | curl -X PUT -T "${file}" \ 28 | -H "Host: ${bucket}.s3.amazonaws.com" \ 29 | -H "Date: ${dateValue}" \ 30 | -H "Content-Type: ${contentType}" \ 31 | -H "Authorization: AWS ${s3Key}:${signature}" \ 32 | http://${bucket}.s3.amazonaws.com/${file} 33 | 34 | 35 | file="download.html" 36 | resource="/${bucket}/${file}" 37 | contentType="text/html" 38 | 39 | # create signed token 40 | stringToSign="PUT\n\n${contentType}\n${dateValue}\n${resource}" 41 | 42 | # create hmac signature 43 | signature=`echo -en ${stringToSign} | openssl sha1 -hmac ${s3Secret} -binary | base64` 44 | 45 | # curl request to put the file 46 | curl -X PUT -T "${file}" \ 47 | -H "Host: ${bucket}.s3.amazonaws.com" \ 48 | -H "Date: ${dateValue}" \ 49 | -H "Content-Type: ${contentType}" \ 50 | -H "Authorization: AWS ${s3Key}:${signature}" \ 51 | http://${bucket}.s3.amazonaws.com/${file} 52 | 53 | echo "Finish" -------------------------------------------------------------------------------- /src/styles/base.css: -------------------------------------------------------------------------------- 1 | 2 | /* Base */ 3 | 4 | html, 5 | body { 6 | /* Override basscss default white background to prevent a white flash during load. */ 7 | background-color: inherit !important; 8 | 9 | font-size: 11px; 10 | font-family: '.SFNSDisplay-Regular', 'Helvetica Neue', 'Lucida Grande', sans-serif; 11 | } 12 | 13 | bt-tab-menu > header { 14 | padding-left: 5px; 15 | } 16 | 17 | split-pane-secondary-content { 18 | border-left: 1px solid rgb(92, 92, 92); 19 | } 20 | 21 | .monospace { 22 | font-family: 'Menlo', monospace; 23 | } 24 | 25 | .vh-100 { 26 | height: 100vh; 27 | } 28 | 29 | .maxheight-100pct { 30 | max-height: 100%; 31 | } 32 | 33 | .minheight-100pct { 34 | min-height: 100%; 35 | } 36 | 37 | .minwidth-100pct { 38 | min-width: 100%; 39 | } 40 | 41 | .pointer { 42 | cursor: pointer; 43 | } 44 | 45 | .border-transparent { 46 | border-left: 1px solid transparent; 47 | border-right: 1px solid transparent; 48 | border-top: 1px solid transparent; 49 | } 50 | 51 | .outline { 52 | outline: 0; 53 | } 54 | 55 | .border-none { 56 | border: 0; 57 | } 58 | 59 | .disabled-color { 60 | opacity: 0.6; 61 | } 62 | 63 | .expander { 64 | display: inline-block; 65 | width: 0.8rem; 66 | height: 0.7rem; 67 | background-color: transparent; 68 | background: transparent url(/images/Triangle.svg) center center; 69 | -webkit-transition: transform 250ms ease-in-out; 70 | -moz-transition: transform 250ms ease-in-out; 71 | transition: transform 250ms ease-in-out; 72 | } 73 | 74 | .expander.transparent { 75 | margin-right: 1px; 76 | } 77 | 78 | .decorator { 79 | display: inline-block; 80 | margin-right: -2px; 81 | } 82 | 83 | .message-gray { 84 | color: #b0b0b0; 85 | } 86 | -------------------------------------------------------------------------------- /src/frontend/components/router-info/router-info.html: -------------------------------------------------------------------------------- 1 |
2 | 9 | 55 |
56 | -------------------------------------------------------------------------------- /src/frontend/utils/highlightable.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectorRef, 3 | SimpleChanges, 4 | NgZone, 5 | } from '@angular/core'; 6 | 7 | import {highlightTime} from '../../utils/configuration'; 8 | 9 | const initialTimespan = highlightTime; 10 | 11 | export abstract class Highlightable { 12 | private isUpdated = false; 13 | 14 | private timespan = initialTimespan; // scales down 15 | 16 | private resetUpdateState; 17 | 18 | constructor( 19 | private elementChangeDetector: ChangeDetectorRef, 20 | private elementIsUpdated?: (changes?: SimpleChanges) => boolean 21 | ) {} 22 | 23 | protected ngOnChanges(changes: SimpleChanges) { 24 | if (typeof this.elementIsUpdated === 'function') { 25 | if (this.elementIsUpdated(changes)) { 26 | this.changed(); 27 | } 28 | } 29 | else { 30 | this.changed(); 31 | } 32 | } 33 | 34 | protected ngOnDestroy() { 35 | this.elementChangeDetector = null; 36 | 37 | this.clear(); 38 | } 39 | 40 | protected clear() { 41 | clearTimeout(this.resetUpdateState); 42 | 43 | this.resetUpdateState = null; 44 | 45 | this.isUpdated = false; 46 | 47 | if (this.elementChangeDetector) { 48 | this.elementChangeDetector.detectChanges(); 49 | } 50 | } 51 | 52 | protected changed() { 53 | this.isUpdated = true; 54 | 55 | if (this.resetUpdateState != null) { 56 | clearTimeout(this.resetUpdateState); 57 | 58 | this.timespan = initialTimespan * 0.1; 59 | } 60 | else { 61 | this.timespan = initialTimespan; 62 | } 63 | 64 | this.resetUpdateState = setTimeout(() => this.clear(), highlightTime); 65 | 66 | if (this.elementChangeDetector) { 67 | this.elementChangeDetector.detectChanges(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/styles/components/tab-menu.css: -------------------------------------------------------------------------------- 1 | .tab-menu-title { 2 | -webkit-user-select: none; 3 | cursor: pointer; 4 | font-size: 11.5px; 5 | white-space: nowrap; 6 | } 7 | 8 | .select-element { 9 | width: 16px; 10 | height: 16px; 11 | margin-top: 4px; 12 | margin-right: 10px; 13 | padding: 3px; 14 | } 15 | 16 | .ngVersion { 17 | line-height: 31px; 18 | font-weight: bold; 19 | color: #5128a5; 20 | padding-right: 5px; 21 | } 22 | 23 | .dark .ngVersion { 24 | color: #A28AD0; 25 | } 26 | 27 | /* FEEDBACK FORM */ 28 | 29 | .feedback-form { 30 | position: absolute; 31 | top: 35px; right: 35px; 32 | box-shadow: 5px 5px 5px rgba(0,0,0,0.2); 33 | border: 1px solid rgba(0,0,0,0.4); 34 | padding: 20px; 35 | width: 50%; 36 | } 37 | 38 | .feedback-form::before { 39 | content: ''; 40 | position: absolute; 41 | top: -11px; right: 4px; 42 | width: 0; 43 | height: 0; 44 | border-left: 11px solid transparent; 45 | border-right: 11px solid transparent; 46 | border-bottom: 11px solid rgba(0,0,0,0.4); 47 | } 48 | 49 | .feedback-form::after { 50 | content: ''; 51 | position: absolute; 52 | top: -10px; right: 5px; 53 | width: 0; 54 | height: 0; 55 | border-left: 10px solid transparent; 56 | border-right: 10px solid transparent; 57 | border-bottom: 10px solid white; 58 | } 59 | 60 | .dark .feedback-form::after, .dark .feedback-from::before { 61 | border-bottom-color: #242424; 62 | } 63 | 64 | .feedback-button { 65 | align-items: center; 66 | cursor: pointer; 67 | display: flex; 68 | font-size: 11.5px; 69 | padding-right: 10px; 70 | } 71 | 72 | .feedback-button:active { 73 | background: rgba(0,0,0,0.05) 74 | } 75 | 76 | .feedback-button .feedback-icon { 77 | height: 20px; 78 | margin: auto 5px; 79 | width: 20px; 80 | } 81 | -------------------------------------------------------------------------------- /src/frontend/components/highlightable.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectorRef, 3 | SimpleChanges, 4 | NgZone, 5 | } from '@angular/core'; 6 | 7 | import {highlightTime} from '../../utils/configuration'; 8 | 9 | const initialTimespan = highlightTime; 10 | 11 | export abstract class Highlightable { 12 | private isUpdated = false; 13 | 14 | private timespan = initialTimespan; // scales down 15 | 16 | private resetUpdateState; 17 | 18 | constructor( 19 | private elementChangeDetector: ChangeDetectorRef, 20 | private elementIsUpdated?: (changes?: SimpleChanges) => boolean 21 | ) {} 22 | 23 | protected ngOnChanges(changes: SimpleChanges) { 24 | if (typeof this.elementIsUpdated === 'function') { 25 | if (this.elementIsUpdated(changes)) { 26 | this.changed(); 27 | } 28 | } 29 | else { 30 | this.changed(); 31 | } 32 | } 33 | 34 | protected ngOnDestroy() { 35 | this.elementChangeDetector = null; 36 | 37 | this.clear(); 38 | } 39 | 40 | protected clear() { 41 | clearTimeout(this.resetUpdateState); 42 | 43 | this.resetUpdateState = null; 44 | 45 | this.isUpdated = false; 46 | 47 | if (this.elementChangeDetector) { 48 | this.elementChangeDetector.detectChanges(); 49 | } 50 | } 51 | 52 | protected changed() { 53 | this.isUpdated = true; 54 | 55 | if (this.resetUpdateState != null) { 56 | clearTimeout(this.resetUpdateState); 57 | 58 | this.timespan = initialTimespan * 0.1; 59 | } 60 | else { 61 | this.timespan = initialTimespan; 62 | } 63 | 64 | this.resetUpdateState = setTimeout(() => this.clear(), highlightTime); 65 | 66 | if (this.elementChangeDetector) { 67 | this.elementChangeDetector.detectChanges(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | export enum Theme { 2 | Light, 3 | Dark, 4 | } 5 | 6 | export enum ComponentView { 7 | Hybrid, // show all elements with Angular properties set 8 | All, // show all components and elements 9 | Components, // show components only 10 | } 11 | 12 | export enum AnalyticsConsent { 13 | NotSet, 14 | Yes, 15 | No, 16 | } 17 | 18 | export interface SimpleOptions { 19 | theme?: Theme; 20 | componentView?: ComponentView; 21 | analyticsConsent?: AnalyticsConsent; 22 | } 23 | 24 | export const defaultOptions = (): SimpleOptions => { 25 | return { 26 | theme: Theme.Light, 27 | componentView: ComponentView.Hybrid, 28 | analyticsConsent: AnalyticsConsent.NotSet, 29 | }; 30 | }; 31 | 32 | export const loadOptions = (): Promise => { 33 | return loadFromStorage() 34 | .then(result => { 35 | const combined = Object.assign({}, defaultOptions(), result); 36 | 37 | // for backward compatibility previous installs that saved as a string: 38 | switch (combined.theme) { 39 | case 'light': 40 | combined.theme = Theme.Light; 41 | break; 42 | case 'dark': 43 | combined.theme = Theme.Dark; 44 | break; 45 | } 46 | 47 | return combined; 48 | }); 49 | }; 50 | 51 | const loadFromStorage = (): Promise => { 52 | return new Promise(resolve => { 53 | const keys = ['componentView', 'theme', 'analyticsConsent']; 54 | 55 | chrome.storage.sync.get(keys, (result: SimpleOptions) => { 56 | resolve(result); 57 | }); 58 | }); 59 | }; 60 | 61 | export const saveOptions = (options: SimpleOptions) => { 62 | for (const key of Object.keys(options)) { 63 | chrome.storage.sync.set({ 64 | [key]: options[key] 65 | }); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /images/feedback.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Desktop HD 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/frontend/components/tab-menu/tab-menu.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
6 | 7 | 8 | 9 | 10 | 13 | 16 | 17 | 18 | 19 | 20 |
21 |
24 | {{t.title}} 25 |
26 |
27 | 28 |
29 | -------------------------------------------------------------------------------- /images/feedback-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Desktop HD 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/frontend/state/options.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | 3 | import { Observable } from 'rxjs/Observable'; 4 | import { Subject } from 'rxjs/Subject'; 5 | 6 | import { 7 | ComponentView, 8 | SimpleOptions, 9 | Theme, 10 | defaultOptions, 11 | loadOptions, 12 | saveOptions, 13 | AnalyticsConsent, 14 | } from '../../options'; 15 | 16 | export {ComponentView}; 17 | export {SimpleOptions}; 18 | export {Theme}; 19 | export {AnalyticsConsent}; 20 | 21 | @Injectable() 22 | export class Options { 23 | private cachedOptions = defaultOptions(); 24 | 25 | private subject = new Subject(); 26 | 27 | get changes(): Observable { 28 | return this.subject.asObservable(); 29 | } 30 | 31 | load() { 32 | return loadOptions().then(options => { 33 | Object.assign(this.cachedOptions, options); 34 | 35 | this.publish(); 36 | 37 | return options; 38 | }); 39 | } 40 | 41 | get theme(): Theme { 42 | return this.cachedOptions.theme; 43 | } 44 | 45 | set theme(theme: Theme) { 46 | this.cachedOptions.theme = theme; 47 | this.publish(); 48 | } 49 | 50 | get analyticsConsent(): AnalyticsConsent { 51 | return this.cachedOptions.analyticsConsent; 52 | } 53 | 54 | set analyticsConsent(analyticsConsent: AnalyticsConsent) { 55 | this.cachedOptions.analyticsConsent = analyticsConsent; 56 | this.publish(); 57 | } 58 | 59 | get componentView(): ComponentView { 60 | return this.cachedOptions.componentView; 61 | } 62 | 63 | set componentView(componentView: ComponentView) { 64 | this.cachedOptions.componentView = componentView; 65 | this.publish(); 66 | } 67 | 68 | simpleOptions(): SimpleOptions { 69 | return this.cachedOptions; 70 | } 71 | 72 | private publish() { 73 | saveOptions(this.cachedOptions); 74 | this.subject.next(this); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/frontend/actions/main-actions.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {dispatch} from '@angular-redux/store'; 3 | import {Tab, StateTab} from '../state'; 4 | import {IAppState} from '../store/model'; 5 | import {Path} from '../../tree'; 6 | 7 | @Injectable() 8 | export class MainActions { 9 | static readonly SELECT_TAB = 'SELECT_TAB'; 10 | static readonly SELECT_COMPONENTS_SUB_TAB = 'SELECT_COMPONENTS_SUB_TAB'; 11 | static readonly DOM_SELECTION_ACTIVE_CHANGE = 'DOM_SELECTION_ACTIVE_CHANGE'; 12 | static readonly SEND_ANALYTICS = 'SEND_ANALYTICS'; 13 | static readonly EMIT_VALUE = 'EMIT_VALUE'; 14 | static readonly UPDATE_PROPERTY = 'UPDATE_PROPERTY'; 15 | static readonly INITIALIZE_AUGURY = 'INITIALIZE_AUGURY'; 16 | 17 | @dispatch() 18 | selectTab = (tab: Tab) => ({ 19 | type: MainActions.SELECT_TAB, 20 | payload: tab, 21 | }) 22 | 23 | @dispatch() 24 | selectComponentsSubTab = (tab: StateTab) => ({ 25 | type: MainActions.SELECT_COMPONENTS_SUB_TAB, 26 | payload: tab, 27 | }) 28 | 29 | @dispatch() 30 | setDOMSelectionActive = (state: boolean) => ({ 31 | type: MainActions.DOM_SELECTION_ACTIVE_CHANGE, 32 | payload: state, 33 | }) 34 | 35 | @dispatch() 36 | emitValue = (path: Path, data: any) => ({ 37 | type: MainActions.EMIT_VALUE, 38 | payload: { 39 | path, 40 | data 41 | } 42 | }) 43 | 44 | @dispatch() 45 | updateProperty = (path: Path, data: any) => ({ 46 | type: MainActions.UPDATE_PROPERTY, 47 | payload: { 48 | path, 49 | data 50 | } 51 | }) 52 | 53 | @dispatch() 54 | initializeAugury = () => ({ 55 | type: MainActions.INITIALIZE_AUGURY, 56 | }) 57 | 58 | @dispatch() 59 | sendAnalytics = (event: string, desc: string) => ({ 60 | type: MainActions.SEND_ANALYTICS, 61 | payload: { 62 | event, 63 | desc 64 | } 65 | }) 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/frontend/components/info-panel/info-panel.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | Input, 5 | Output, 6 | } from '@angular/core'; 7 | 8 | import {ComponentLoadState, StateTab} from '../../state'; 9 | import {UserActions} from '../../actions/user-actions/user-actions'; 10 | import { 11 | ComponentMetadata, 12 | InstanceWithMetadata, 13 | Metadata, 14 | Node, 15 | ObjectType, 16 | Path, 17 | } from '../../../tree'; 18 | 19 | @Component({ 20 | selector: 'bt-info-panel', 21 | template: require('./info-panel.html'), 22 | }) 23 | export class InfoPanel { 24 | @Input() tree; 25 | @Input() ngModules: {[key: string]: any}; 26 | @Input() node; 27 | @Input() instanceValue: InstanceWithMetadata; 28 | @Input() loadingState: ComponentLoadState; 29 | @Input() selectedStateTab: StateTab; 30 | 31 | @Output() private selectNode: EventEmitter = new EventEmitter(); 32 | @Output() private componentsSubTabMenuChange: EventEmitter = new EventEmitter(); 33 | @Output() private emitValue = new EventEmitter<{path: Path, data: any}>(); 34 | @Output() private updateProperty = new EventEmitter<{path: Path, newValue: any}>(); 35 | 36 | private StateTab = StateTab; 37 | 38 | constructor(private userActions: UserActions) {} 39 | 40 | private get state() { 41 | if (this.instanceValue) { 42 | return this.instanceValue.instance; 43 | } 44 | return null; 45 | } 46 | 47 | private get metadata(): Metadata { 48 | return this.instanceValue 49 | ? this.instanceValue.metadata 50 | : new Map(); 51 | } 52 | 53 | private get providers(): {[token: string]: any} { 54 | return this.instanceValue 55 | ? this.instanceValue.providers 56 | : {}; 57 | } 58 | 59 | private get componentMetadata(): ComponentMetadata { 60 | return this.instanceValue 61 | ? this.instanceValue.componentMetadata 62 | : new Map(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/utils/patch.test.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape'; 2 | 3 | import {compare} from './patch'; 4 | 5 | test('utils/patch: JSON patch should deal gracefully with undefined values', t => { 6 | t.plan(3); 7 | 8 | const obj1 = {a: 'foo', b: undefined}; 9 | const obj2 = {a: 'foo', b: undefined}; 10 | 11 | const changes = compare(obj1, obj2); 12 | 13 | t.ok(changes, 'changes is not null'); 14 | t.ok(Array.isArray(changes), 'changes is an array'); 15 | t.notOk(changes.length, 'objects should compare as identical'); 16 | 17 | t.end(); 18 | }); 19 | 20 | test('utils/patch: JSON patch can generate a changeset for simple objects', t => { 21 | t.plan(5); 22 | 23 | const obj1 = {a: 'foo', b: 'bar'}; 24 | const obj2 = {a: 'foo', b: 'foo'}; 25 | 26 | const changes = compare(obj1, obj2); 27 | 28 | t.ok(changes, 'changes is not null'); 29 | t.ok(Array.isArray(changes), 'changes is an array'); 30 | t.equals(1, changes.length, 'changes should contain one change'); 31 | t.equals('replace', changes[0].op, 'operation should be a "replace" op'); 32 | t.equals('/b', changes[0].path, 'paths should point to "b" property'); 33 | 34 | t.end(); 35 | }); 36 | 37 | test('utils/patch: JSON patch can generate a changeset for nested objects', t => { 38 | t.plan(8); 39 | 40 | const obj1 = {a: 'foo', b: {fizz: 'bar'}}; 41 | const obj2 = {a: 'foo', b: {fozz: 'bar'}}; 42 | 43 | const changes = compare(obj1, obj2); 44 | 45 | t.ok(changes, 'changes is not null'); 46 | t.ok(Array.isArray(changes), 'changes is an array'); 47 | t.equals(2, changes.length, 'changes should contain two changes'); 48 | t.equals('remove', changes[0].op, 'first operation should be a "remove" op'); 49 | t.equals('/b/fizz', changes[0].path, 'first operation should delete "/b/fizz"'); 50 | t.equals('add', changes[1].op, 'second operation should be an "add" op'); 51 | t.equals('/b/fozz', changes[1].path, 'second operation should add "/b/fozz"'); 52 | t.equals('bar', changes[1].value, 'second operation should have a value of "bar"'); 53 | 54 | t.end(); 55 | }); 56 | 57 | -------------------------------------------------------------------------------- /src/utils/ng-validate.ts: -------------------------------------------------------------------------------- 1 | import {messageJumpContext, browserSubscribeOnce} from '../communication/message-dispatch'; 2 | import {MessageFactory} from '../communication/message-factory'; 3 | import {MessageType} from '../communication/message-type'; 4 | import {Message} from '../communication/message'; 5 | import {send} from '../backend/indirect-connection'; 6 | 7 | import {isAngular, isDebugMode} from '../backend/utils/app-check'; 8 | import {ApplicationError, ApplicationErrorType} from '../communication'; 9 | 10 | declare const getAllAngularTestabilities: Function; 11 | declare const getAllAngularRootElements: Function; 12 | declare const ng: any; 13 | 14 | let unsubscribe: () => void; 15 | 16 | let errorToSend: Message; 17 | 18 | const sendError = () => { 19 | if (errorToSend) { 20 | send(errorToSend); 21 | } 22 | }; 23 | 24 | const handler = () => { 25 | if (isAngular()) { 26 | if (isDebugMode()) { 27 | messageJumpContext(MessageFactory.frameworkLoaded()); 28 | if (unsubscribe) { 29 | unsubscribe(); 30 | } 31 | errorToSend = null; 32 | return true; 33 | } 34 | errorToSend = MessageFactory.applicationError( 35 | new ApplicationError(ApplicationErrorType.ProductionMode)); 36 | } else { 37 | errorToSend = MessageFactory.notNgApp(); 38 | } 39 | 40 | browserSubscribeOnce(MessageType.Initialize, sendError); 41 | 42 | sendError(); 43 | 44 | return false; 45 | }; 46 | 47 | if (!handler()) { 48 | const subscribe = () => { 49 | if (MutationObserver) { 50 | const observer = new MutationObserver(mutations => handler()); 51 | observer.observe(document, { childList: true, subtree: true }); 52 | 53 | return () => observer.disconnect(); 54 | } 55 | 56 | const eventKeys = ['DOMNodeInserted', 'DOMNodeRemoved']; 57 | 58 | eventKeys.forEach(k => document.addEventListener(k, handler, false)); 59 | 60 | return () => eventKeys.forEach(k => document.removeEventListener(k, handler, false)); 61 | }; 62 | 63 | unsubscribe = subscribe(); 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/backend/utils/highlighter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MutableTree, 3 | Node, 4 | } from '../../tree'; 5 | 6 | export interface Offsets { 7 | x: number; 8 | y: number; 9 | w: number; 10 | h: number; 11 | } 12 | 13 | const styles = require('to-string!raw!./highlighter.raw'); 14 | 15 | let highlights = new Map(); 16 | 17 | const offsets = (node): Offsets => { 18 | const vals = { 19 | x: node.offsetLeft, 20 | y: node.offsetTop, 21 | w: node.offsetWidth, 22 | h: node.offsetHeight 23 | }; 24 | 25 | while (node = node.offsetParent) { 26 | vals.x += node.offsetLeft; 27 | vals.y += node.offsetTop; 28 | } 29 | return vals; 30 | }; 31 | 32 | const highlightNode = (node, label: string): HTMLElement => { 33 | if (node == null) { 34 | return; 35 | } 36 | 37 | const overlay = document.createElement('div'); 38 | overlay.setAttribute('style', styles); 39 | if (label) { 40 | overlay.textContent = label; 41 | } 42 | 43 | const pos = offsets(node); 44 | overlay.style.left = `${pos.x}px`; 45 | overlay.style.top = `${pos.y}px`; 46 | overlay.style.width = `${pos.w}px`; 47 | overlay.style.height = `${pos.h}px`; 48 | 49 | document.body.appendChild(overlay); 50 | 51 | return overlay; 52 | }; 53 | 54 | export const clear = (map) => { 55 | if (!map) { 56 | return; 57 | } 58 | 59 | map.forEach( 60 | (value, key) => { 61 | try { 62 | value.remove(); 63 | } 64 | catch (e) { 65 | } 66 | }); 67 | }; 68 | 69 | export const highlight = (nodes: Array) => { 70 | if (nodes == null || nodes.length === 0) { 71 | clear(highlights); 72 | return; 73 | } 74 | 75 | const elements = new Array(); 76 | const map = new Map(); 77 | 78 | for (const node of nodes.filter(n => n != null)) { 79 | const element = highlightNode(node.nativeElement(), node.name); 80 | elements.push(element); 81 | 82 | map.set(node.id, element); 83 | } 84 | 85 | highlights = map; 86 | 87 | return { 88 | elements, 89 | map 90 | }; 91 | }; 92 | -------------------------------------------------------------------------------- /src/communication/message.ts: -------------------------------------------------------------------------------- 1 | import {MessageType} from './message-type'; 2 | 3 | import { 4 | deserialize, 5 | deserializeBinary, 6 | serializeBinary, 7 | } from '../utils'; 8 | 9 | export enum Serialize { 10 | None, 11 | Binary, 12 | Recreator, 13 | } 14 | 15 | export interface Message { 16 | messageId: string; 17 | messageSource: string; 18 | messageType: MessageType; 19 | serialize?: Serialize; 20 | content?: T; 21 | } 22 | 23 | export interface MessageResponse extends Message { 24 | messageResponseId: string; 25 | error?: Error; 26 | } 27 | 28 | export interface MessageHandler { 29 | (message: Message, sendResponse: (response: MessageResponse) => void): any; 30 | } 31 | 32 | export interface Subscription { 33 | unsubscribe(): void; 34 | } 35 | 36 | export const messageSource = 'AUGURY_INSPECTED_APPLICATION'; 37 | 38 | export const checkSource = 39 | (message: Message) => message.messageSource === messageSource; 40 | 41 | export const testResponse = 42 | (request: Message, response: MessageResponse) => { 43 | return checkSource(response) 44 | && response.messageResponseId === request.messageId 45 | && response.messageType === MessageType.Response; 46 | }; 47 | 48 | export const deserializeMessage = (message: Message) => { 49 | switch (message.serialize) { 50 | case Serialize.Binary: 51 | message.content = deserializeBinary( message.content); 52 | break; 53 | case Serialize.Recreator: 54 | message.content = deserialize(message.content); 55 | break; 56 | case Serialize.None: 57 | break; 58 | default: 59 | throw new Error(`Unknown serialization type: ${message.serialize}`); 60 | } 61 | 62 | message.serialize = Serialize.None; 63 | }; 64 | 65 | export const serializeMessage = (message: Message) => { 66 | switch (message.serialize) { 67 | case Serialize.None: 68 | message.content = serializeBinary(message.content); 69 | message.serialize = Serialize.Binary; 70 | default: 71 | break; 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /src/frontend/components/state-values/state-values.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectorRef, 3 | Component, 4 | Input, 5 | Output, 6 | EventEmitter, 7 | } from '@angular/core'; 8 | 9 | import {UserActions} from '../../actions/user-actions/user-actions'; 10 | import {Highlightable} from '../../utils/highlightable'; 11 | import {functionName} from '../../../utils'; 12 | import {propertyIndex} from '../../../backend/utils'; 13 | import { 14 | Path, 15 | ObjectType, 16 | Metadata, 17 | } from '../../../tree'; 18 | 19 | @Component({ 20 | selector: 'bt-state-values', 21 | template: require('./state-values.html'), 22 | styles: [require('to-string!./state-values.css')], 23 | }) 24 | export class StateValues extends Highlightable { 25 | @Input() path: Path; 26 | @Input() metadata: ObjectType; 27 | @Input() value; 28 | 29 | @Output() updateValue = new EventEmitter<{path: Path, propertyKey: Path, newValue}>(); 30 | 31 | private editable: boolean = false; 32 | 33 | constructor( 34 | private changeDetector: ChangeDetectorRef, 35 | private userActions: UserActions 36 | ) { 37 | super(changeDetector, changes => this.hasChanged(changes)); 38 | } 39 | 40 | private hasChanged(changes) { 41 | if (changes == null || !changes.hasOwnProperty('value')) { 42 | return false; 43 | } 44 | 45 | const oldValue = changes.value.previousValue; 46 | const newValue = changes.value.currentValue; 47 | 48 | if (oldValue && oldValue.toString() === 'CD_INIT_VALUE') { 49 | return false; 50 | } 51 | 52 | if (typeof oldValue === 'function' && typeof newValue === 'function') { 53 | return functionName(oldValue) !== functionName(newValue); 54 | } 55 | 56 | return oldValue !== newValue; 57 | } 58 | 59 | private get key(): string | number { 60 | return this.path[this.path.length - 1]; 61 | } 62 | 63 | private onValueChanged(newValue) { 64 | if (newValue !== this.value) { 65 | const index = propertyIndex(this.path); 66 | 67 | const path = this.path.slice(0, index); 68 | 69 | const propertyKey = this.path.slice(index); 70 | 71 | this.updateValue.emit({path, propertyKey, newValue}); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/backend/connection.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Message, 3 | MessageHandler, 4 | MessageType, 5 | deserializeMessage, 6 | } from '../communication'; 7 | 8 | const subscriptions = new Set(); 9 | 10 | chrome.runtime.onMessage.addListener( 11 | (message: Message, sender: chrome.runtime.MessageSender) => { 12 | deserializeMessage(message); 13 | 14 | const cannotRespond = () => { 15 | throw new Error('You cannot respond through MessageHandler'); 16 | }; 17 | 18 | subscriptions.forEach(handler => handler(message, cannotRespond)); 19 | 20 | return true; 21 | }); 22 | 23 | export const subscribe = (handler: MessageHandler) => { 24 | subscriptions.add(handler); 25 | 26 | return { 27 | unsubscribe: () => subscriptions.delete(handler) 28 | }; 29 | }; 30 | 31 | export const send = (message: Message) => { 32 | if (message.messageType === MessageType.CompleteTree || 33 | message.messageType === MessageType.TreeDiff || 34 | message.messageType === MessageType.DispatchWrapper) { 35 | /// These types of messages should never be sent through this mechanism. A DispatchWrapper 36 | /// message is for communication between content-script and the backend and has no business 37 | /// being sent to the frontend. Similarly, a message containing tree data should be sent 38 | /// through the {@link MessageBuffer} mechanism in backend.ts instead of through this port. 39 | /// Sending a message with the {@link send} function will cause that message to take a very 40 | /// circuitous route and will be serialized and deserialized repeatedly. Therefore large 41 | /// messages must be sent using the {@link MessageBuffer} mechanism in order to avoid major 42 | /// performance bottlenecks and UI latency. 43 | const description = MessageType[message.messageType]; 44 | throw new Error(`A ${description} message should never be posted through the communication port`); 45 | } 46 | 47 | return new Promise((resolve, reject) => { 48 | chrome.runtime.sendMessage(message, 49 | response => { 50 | if (response) { 51 | resolve(response); 52 | } 53 | else { 54 | reject(chrome.runtime.lastError); 55 | } 56 | }); 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /src/utils/serialize.test.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape'; 2 | import {serialize, deserialize} from './serialize'; 3 | 4 | test('utils/serialize: Serialize Array', t => { 5 | t.plan(1); 6 | 7 | const arrayObject = [1, 2, 3, 4]; 8 | 9 | t.deepEqual(arrayObject, deserialize(serialize(arrayObject)), 'Serialize/deserialize simple array'); 10 | 11 | }); 12 | 13 | test('utils/serialize: Serialize Complex Array', t => { 14 | t.plan(1); 15 | 16 | const arrayObject = [{a_key: 'a_value'}, 2, 3, 4]; 17 | 18 | t.deepEqual(arrayObject, deserialize(serialize(arrayObject)), 'Serialize/deserialize complex array'); 19 | 20 | }); 21 | 22 | test('utils/serialize: Serialize Simple Object', t => { 23 | t.plan(1); 24 | 25 | const simpleObj = {a_key: 'a_value'}; 26 | 27 | t.deepEqual(simpleObj, deserialize(serialize(simpleObj)), 'Serialize/deserialize simple object'); 28 | 29 | }); 30 | 31 | test('utils/serialize: Serialize Map Object (string key & value)', t => { 32 | t.plan(2); 33 | 34 | const map = new Map(); 35 | 36 | const mapKey = 'str_key'; 37 | const mapVal = 'str_val'; 38 | 39 | map.set(mapKey, mapVal); 40 | 41 | const o = deserialize(serialize(map)); 42 | o.forEach((v, k) => { 43 | t.deepEqual(mapKey, k, 'Serialize/deserialize key'); 44 | t.deepEqual(mapVal, v, 'Serialize/deserialize value'); 45 | }); 46 | 47 | }); 48 | 49 | test('utils/serialize: Serialize Map Object (object key & value)', t => { 50 | t.plan(2); 51 | 52 | const map = new Map(); 53 | 54 | const mapKey = {'test': 'test_val'}; 55 | const mapVal = {'str_key': 'str_value'}; 56 | 57 | map.set(mapKey, mapVal); 58 | 59 | const o = deserialize(serialize(map)); 60 | 61 | o.forEach((v, k) => { 62 | t.deepEqual(mapKey, k, 'Serialize/deserialize key'); 63 | t.deepEqual(mapVal, v, 'Serialize/deserialize value'); 64 | }); 65 | 66 | }); 67 | 68 | test('utils/serialize: Serialize Map Object (number key & value)', t => { 69 | t.plan(2); 70 | const map = new Map(); 71 | 72 | const mapKey = 100; 73 | const mapVal = 200; 74 | 75 | map.set(mapKey, mapVal); 76 | 77 | const o = deserialize(serialize(map)); 78 | o.forEach((v, k) => { 79 | t.deepEqual(mapKey, k, 'Serialize/deserialize key'); 80 | t.deepEqual(mapVal, v, 'Serialize/deserialize value'); 81 | }); 82 | 83 | }); 84 | -------------------------------------------------------------------------------- /crxmake.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # 3 | # Package Augury into crx format Chrome extension 4 | # (This will not be needed for official distribution) 5 | # Based on https://developer.chrome.com/extensions/crx#scripts 6 | 7 | node -v 8 | npm -v 9 | 10 | dir="temp" 11 | key="key.pem" 12 | name="augury" 13 | files="manifest.json build images index.html frontend.html popup.html popup.js" 14 | 15 | crx="$name.crx" 16 | pub="$name.pub" 17 | sig="$name.sig" 18 | zip="$name.zip" 19 | 20 | # Ensure environment variables exist 21 | sentry_key=${SENTRY_KEY:?"The environment variable 'SENTRY_KEY' must be set and non-empty"} 22 | 23 | # assign build name to zip and crx file in circleci env 24 | if [ $CIRCLE_BUILD_NUM ] || [ $CIRCLE_ARTIFACTS ]; then 25 | crx="$name-$CIRCLE_BUILD_NUM.crx" 26 | zip="$name-$CIRCLE_BUILD_NUM.zip" 27 | fi 28 | 29 | trap 'rm -f "$pub" "$sig"' EXIT 30 | 31 | # copy all the files we need 32 | rm -rf $dir 33 | mkdir $dir 34 | cp -R $files $dir/ 35 | rm $dir/build/*.map 36 | 37 | # generate private key key.pem if it doesn't exist already 38 | if [ ! -f $key ]; then 39 | echo "$key doesn't exist." 40 | openssl genrsa -out key.pem 1024 41 | fi 42 | 43 | # zip up the crx dir 44 | cwd=$(pwd -P) 45 | (cd "$dir" && zip -qr -9 -X "$cwd/$zip" .) 46 | 47 | # signature 48 | openssl sha1 -sha1 -binary -sign "$key" < "$zip" > "$sig" 49 | 50 | # public key 51 | openssl rsa -pubout -outform DER < "$key" > "$pub" 2>/dev/null 52 | 53 | byte_swap () { 54 | # Take "abcdefgh" and return it as "ghefcdab" 55 | echo "${1:6:2}${1:4:2}${1:2:2}${1:0:2}" 56 | } 57 | 58 | crmagic_hex="4372 3234" # Cr24 59 | version_hex="0200 0000" # 2 60 | pub_len_hex=$(byte_swap $(printf '%08x\n' $(ls -l "$pub" | awk '{print $5}'))) 61 | sig_len_hex=$(byte_swap $(printf '%08x\n' $(ls -l "$sig" | awk '{print $5}'))) 62 | ( 63 | echo "$crmagic_hex $version_hex $pub_len_hex $sig_len_hex" | xxd -r -p 64 | cat "$pub" "$sig" "$zip" 65 | ) > "$crx" 66 | 67 | echo "Wrote $crx" 68 | 69 | # move crx to artifacts folder in circleci 70 | if [ $CIRCLE_ARTIFACTS ]; then 71 | mv $crx $CIRCLE_ARTIFACTS 72 | mv $zip $CIRCLE_ARTIFACTS 73 | fi 74 | 75 | 76 | echo "" > download.html 77 | echo "Wrote file" 78 | 79 | # clean up 80 | rm -rf $dir 81 | echo "Fin." 82 | -------------------------------------------------------------------------------- /src/backend/utils/parse-router.ts: -------------------------------------------------------------------------------- 1 | export interface Route { 2 | name: string; 3 | hash: string; 4 | path: string; 5 | specificity: string; 6 | handler: string; 7 | data: any; 8 | children?: Array; 9 | isAux: boolean; 10 | } 11 | 12 | // *** Component Router *** 13 | export function parseRoutes(router: any): Route { 14 | const rootName = router.rootComponentType ? router.rootComponentType.name : 'no-name'; 15 | const rootChildren: [any] = router.config; 16 | 17 | const root: Route = { 18 | handler: rootName, 19 | name: rootName, 20 | path: '/', 21 | children: rootChildren ? assignChildrenToParent(null, rootChildren) : [], 22 | isAux: false, 23 | specificity: null, 24 | data: null, 25 | hash: null, 26 | }; 27 | 28 | return root; 29 | } 30 | 31 | function assignChildrenToParent(parentPath, children): [any] { 32 | return children.map((child) => { 33 | const childName = childRouteName(child); 34 | const childDescendents: [any] = child._loadedConfig ? child._loadedConfig.routes : child.children; 35 | 36 | // only found in aux routes, otherwise property will be undefined 37 | const isAuxRoute = !!child.outlet; 38 | 39 | const pathFragment = child.outlet ? `(${child.outlet}:${child.path})` : child.path; 40 | 41 | const routeConfig: Route = { 42 | handler: childName, 43 | data: [], 44 | hash: null, 45 | specificity: null, 46 | name: childName, 47 | path: `${parentPath ? parentPath : ''}/${pathFragment}`.split('//').join('/'), 48 | isAux: isAuxRoute, 49 | children: [], 50 | }; 51 | 52 | if (childDescendents) { 53 | routeConfig.children = assignChildrenToParent(routeConfig.path, childDescendents); 54 | } 55 | 56 | if (child.data) { 57 | for (const el in child.data) { 58 | if (child.data.hasOwnProperty(el)) { 59 | routeConfig.data.push({ 60 | key: el, 61 | value: child.data[el], 62 | }); 63 | } 64 | } 65 | } 66 | 67 | return routeConfig; 68 | }); 69 | } 70 | 71 | function childRouteName(child): string { 72 | if (child.component) { 73 | return child.component.name; 74 | } 75 | else if (child.loadChildren) { 76 | return `${child.path} [Lazy]`; 77 | } 78 | else { 79 | return 'no-name-route'; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/communication/message-type.ts: -------------------------------------------------------------------------------- 1 | export enum MessageType { 2 | // Begin the process of loading the extension 3 | Initialize, 4 | 5 | /// Angular framework has finished loading 6 | FrameworkLoaded, 7 | 8 | /// Check to see if the other side (frontend or backend) is open and responsive 9 | Ping, 10 | 11 | NotNgApp, 12 | 13 | /// Response to a previous message 14 | Response, 15 | 16 | /// An error has occurred in the backend and is being transmitted to the frontend 17 | ApplicationError, 18 | 19 | /// User signals "report this error". 20 | SendUncaughtError, 21 | 22 | /// Post a message to the browser event queue so that it can be unwrapped and 23 | /// posted to the extension from the content-script. There is no pipe that is 24 | /// direct from the backend to the frontend, so this allows us to bounce the 25 | /// message through {@link window.postMessage} so that the content script can 26 | /// receive it and send it through the multi-hop port. 27 | DispatchWrapper, 28 | 29 | /// This is an unusual message -- it contains no data itself, it just tells the 30 | /// frontend that there are messages waiting for it that it can read direct from 31 | /// the message queue instead of passing the messages through the four-hop pipe 32 | /// of backend -> content script -> channel -> frontend. 33 | Push, 34 | 35 | // Send the inspected application ng version 36 | NgVersion, 37 | 38 | /// Transmit a complete component tree 39 | CompleteTree, 40 | 41 | /// Transmit the delta of two trees 42 | TreeDiff, 43 | 44 | /// Send the list of NgModules 45 | NgModules, 46 | 47 | /// Send the complete router tree (TODO(cbond: support diff)) 48 | RouterTree, 49 | 50 | /// Select a component in the tree view 51 | SelectComponent, 52 | 53 | /// Update the value of a property inside the component tree 54 | UpdateProperty, 55 | 56 | /// Update a property on a provider reference 57 | UpdateProviderProperty, 58 | 59 | /// Emit a new value through an EventEmitter 60 | EmitValue, 61 | 62 | /// Emit an event on a particular node (ex: click, change, etc..) 63 | EmitEvent, 64 | 65 | /// Set the nodes that should be highlighted on the page 66 | Highlight, 67 | 68 | /// Find a corresponding mutable tree node based on a DOM node 69 | FindElement, 70 | 71 | GoogleTagManagerSend, 72 | } 73 | -------------------------------------------------------------------------------- /src/frontend/state/component-instance-state.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectorRef} from '@angular/core'; 2 | 3 | import { 4 | InstanceWithMetadata, 5 | Metadata, 6 | ObjectType, 7 | Node, 8 | } from '../../tree'; 9 | 10 | export enum ComponentLoadState { 11 | Idle, 12 | Received, 13 | Failed 14 | } 15 | 16 | class CachedValue { 17 | constructor( 18 | public state: ComponentLoadState, 19 | public value: InstanceWithMetadata 20 | ) {} 21 | } 22 | 23 | class LookupError { 24 | constructor(public error: Error) {} 25 | } 26 | 27 | export class ComponentInstanceState { 28 | constructor(private changeDetector: ChangeDetectorRef) {} 29 | 30 | private map = new Map(); 31 | 32 | has(node: Node): boolean { 33 | return this.map.has(node.id); 34 | } 35 | 36 | loadingState(node: Node): ComponentLoadState { 37 | if (node == null) { 38 | return null; 39 | } 40 | 41 | const cache = this.map.get(node.id); 42 | 43 | if (cache == null || cache instanceof LookupError) { 44 | return ComponentLoadState.Failed; 45 | } 46 | 47 | return ( cache).state; 48 | } 49 | 50 | componentInstance(node: Node): InstanceWithMetadata { 51 | if (node == null) { 52 | return null; 53 | } 54 | 55 | const cache = this.map.get(node.id); 56 | 57 | if (cache == null || cache instanceof LookupError) { 58 | return null; 59 | } 60 | 61 | const existing = cache; 62 | 63 | switch (existing.state) { 64 | case ComponentLoadState.Failed: 65 | return null; 66 | case ComponentLoadState.Received: 67 | return existing.value; 68 | default: 69 | throw new Error(`Unknown state: ${existing.state}`); 70 | } 71 | } 72 | 73 | wait(node: Node, promise: Promise) { 74 | promise.then(response => { 75 | this.map.set(node.id, new CachedValue(ComponentLoadState.Received, response)); 76 | 77 | this.changeDetector.detectChanges(); 78 | }) 79 | .catch(error => { 80 | this.map.set(node.id, new LookupError(error)); 81 | 82 | this.changeDetector.detectChanges(); 83 | }); 84 | } 85 | 86 | reset(identifiers?: Array) { 87 | if (identifiers == null || identifiers.length === 0) { 88 | this.map.clear(); 89 | } 90 | else { 91 | for (const id of identifiers) { 92 | this.map.delete(id); 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/channel/channel.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Message, 3 | MessageType, 4 | } from '../communication'; 5 | 6 | const connections = new Map(); 7 | 8 | /// A queue of messages that were not able to be delivered and will be 9 | /// retried when the connection to the content script or extension is 10 | /// re-established 11 | const messageBuffer = new Map>(); 12 | 13 | const drainQueue = (port: chrome.runtime.Port, buffer: Array) => { 14 | if (buffer == null || buffer.length === 0) { 15 | return; 16 | } 17 | 18 | let removed = 0; 19 | 20 | const send = (m: Message, index: number) => { 21 | port.postMessage(m); 22 | ++removed; 23 | }; 24 | 25 | try { 26 | buffer.forEach(send); 27 | } catch (error) { 28 | // port disconnected, re-try on connect. 29 | } 30 | 31 | buffer.splice(0, removed); 32 | }; 33 | 34 | chrome.runtime.onMessage.addListener( 35 | (message, sender, sendResponse) => { 36 | if (message.messageType === MessageType.Initialize) { 37 | sendResponse({ // note that this is separate from our message response system 38 | extensionId: chrome.runtime.id 39 | }); 40 | } 41 | 42 | if (sender.tab) { 43 | let sent = false; 44 | 45 | const connection = connections.get(sender.tab.id); 46 | if (connection) { 47 | try { 48 | connection.postMessage(message); 49 | sent = true; 50 | } 51 | catch (err) {} 52 | } 53 | 54 | if (sent === false) { 55 | let queue = messageBuffer.get(sender.tab.id); 56 | if (queue == null) { 57 | queue = new Array(); 58 | messageBuffer.set(sender.tab.id, queue); 59 | } 60 | 61 | queue.push(message); 62 | } 63 | } 64 | return true; 65 | }); 66 | 67 | chrome.runtime.onConnect.addListener(port => { 68 | const listener = (message, sender) => { 69 | if (connections.has(message.tabId) === false) { 70 | connections.set(message.tabId, port); 71 | } 72 | 73 | drainQueue(message.tabId, messageBuffer.get(message.tabId)); 74 | 75 | chrome.tabs.sendMessage(message.tabId, message); 76 | }; 77 | 78 | port.onMessage.addListener(listener); 79 | 80 | port.onDisconnect.addListener(() => { 81 | port.onMessage.removeListener( listener); 82 | 83 | connections.forEach((value, key, map) => { 84 | if (value === port) { 85 | map.delete(key); 86 | } 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /release-process.md: -------------------------------------------------------------------------------- 1 | # Augury Release Process 2 | Document contains detailed description of Augury release process from merging to master, creating tags and publishing to chrome store. 3 | 4 | ## Types of branches 5 | - **master**: Always contains the code for the latest main release. Code from `dev` branch must be merged into master before release. 6 | - **dev**: Contains the code for the latest dev build. All development must occur in this branch and pushed to `master` before release. 7 | 8 | ## All HEADs must pass Continuous Integration (CI) 9 | 10 | * Both dev and master must always build and pass tests. Along with Pull Request against them. 11 | 12 | ## Release Types 13 | 14 | * **Main** - a release (typically from master) with version tag `vX.Y.Z` where Z is zero (e.g. `v2.1.0`) 15 | * **Hotfix** - a bugfix release (typically from a branch forked from main) with version tag `vX.Y.Z` where Z is non-zero (e.g `v2.1.1`) Once done hotfix should be merged to main and dev both. 16 | 17 | ## Release Steps 18 | 19 | #### Update CHANGELOG.md 20 | 21 | * Checkout the `dev` branch from which you wish to release 22 | * Choose a version tag (see above) henceforth referred to as `$TAG`. 23 | * Add a changelog entry for the new tag at the top of `CHANGELOG.md`. 24 | The first line must be a markdown header of the form `## Release ${TAG#v}`. 25 | * [Changelog Template](changelog-template.md) 26 | 27 | #### Update manifest.json, package.json, and popup.html 28 | 29 | * Checkout the `dev` branch from which you wish to release 30 | * Choose a version tag (see above) henceforth referred to as `$TAG`. 31 | * Update the version number in `manifest.json`, `package.json`, and `popup.html` 32 | 33 | #### Commit & Push the updates: 34 | 35 | git commit -m "Add release $TAG" CHANGELOG.md manifest.json package.json 36 | git push 37 | 38 | #### Create Version Tag 39 | 40 | Next you must tag the changelog commit with `$TAG` 41 | 42 | git tag -a -m "Release $TAG" $TAG 43 | 44 | #### Merge Dev 45 | 46 | Next merge the `dev` branch to `master` branch so `master` has the code for release 47 | 48 | #### Create Build 49 | 50 | Create the release build using build script 51 | 52 | bash crxmake.sh 53 | 54 | #### Push to Chrome Store 55 | 56 | Upload the latest build file `batarangle.crx` to Chrome Store and update version numbers on the extension. 57 | 58 | 59 | ## Post Release 60 | 61 | * Download and install latest build from chrome store. 62 | * Perform sanity checks on the build by checking it against any Angular application 63 | -------------------------------------------------------------------------------- /src/frontend/components/dependency-info/dependency-info.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | Input, 5 | Output, 6 | } from '@angular/core'; 7 | 8 | import {UserActions} from '../../actions/user-actions/user-actions'; 9 | 10 | import { 11 | MutableTree, 12 | Node, 13 | } from '../../../tree'; 14 | 15 | import {Dependency} from '../../../backend/utils/description'; 16 | 17 | import {Stack} from '../../../structures'; 18 | 19 | @Component({ 20 | selector: 'bt-dependency-info', 21 | template: require('./dependency-info.html'), 22 | }) 23 | export class DependencyInfo { 24 | @Input() selectedNode: Node; 25 | @Input() tree: MutableTree; 26 | 27 | @Output() private selectNode = new EventEmitter(); 28 | 29 | private selectedDependency: Dependency; 30 | 31 | private dependentComponents: Array = []; 32 | 33 | private navigationStack = new Stack(); 34 | 35 | constructor(private userActions: UserActions) {} 36 | 37 | private get dependencies(): Array<{[key: string]: any}> { 38 | if (this.selectedNode == null) { 39 | return []; 40 | } 41 | return this.selectedNode.dependencies; 42 | } 43 | 44 | private get hasDependencies() { 45 | return this.dependentComponents && 46 | this.dependentComponents.length > 0; 47 | } 48 | 49 | private select(dependency: Dependency) { 50 | this.selectedDependency = dependency; 51 | 52 | this.dependentComponents = this.getDependencies(dependency); 53 | } 54 | 55 | private onDependencySelected(dependency: Dependency) { 56 | if (this.selectedDependency) { 57 | this.navigationStack.push(this.selectedDependency); 58 | } 59 | 60 | this.select(dependency); 61 | } 62 | 63 | private onBack() { 64 | if (this.navigationStack.size === 0) { 65 | this.reset(); 66 | } 67 | else { 68 | this.select(this.navigationStack.pop()); 69 | } 70 | } 71 | 72 | private reset() { 73 | this.navigationStack.clear(); 74 | 75 | this.selectedDependency = null; 76 | 77 | this.dependentComponents = []; 78 | } 79 | 80 | private getDependencies(dependency: Dependency): Array { 81 | if (this.tree == null) { 82 | return []; 83 | } 84 | 85 | const dependents = new Array(); 86 | 87 | this.tree.recurseAll(node => { 88 | this.dependencies.map((dep: any) => { 89 | if (dep && dependency && dep.id === dependency.id) { 90 | dependents.push(node); 91 | } 92 | }); 93 | }); 94 | 95 | return dependents; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/frontend/epics/gtm.ts: -------------------------------------------------------------------------------- 1 | import {MainActions} from '../actions/main-actions'; 2 | 3 | const DOM_SELECTION_GTM = 'auguryDOMSelection'; 4 | const TAB_CHANGE_GTM = 'auguryTabChange'; 5 | const SUB_TAB_CHANGE_GTM = 'augurySubTabChange'; 6 | const EMIT_VALUE_GTM = 'auguryEmitValue'; 7 | const UPDATE_PROPERTY_GTM = 'auguryUpdateProperty'; 8 | const INITIALIZE_AUGURY_GTM = 'auguryInitialize'; 9 | 10 | const TAB_TYPES = [ 11 | 'Component Tree', 12 | 'Router Tree', 13 | 'NgModules', 14 | ]; 15 | 16 | const SUB_TAB_TYPES = [ 17 | 'Properties', 18 | 'Injector Graph', 19 | ]; 20 | 21 | 22 | export const domSelectionGtmEpic = action$ => 23 | action$.ofType(MainActions.DOM_SELECTION_ACTIVE_CHANGE) 24 | .filter(action => action.payload) 25 | .mapTo({ 26 | type: MainActions.SEND_ANALYTICS, 27 | payload: { 28 | event: DOM_SELECTION_GTM, 29 | desc: 'DOM Selection Active' 30 | } 31 | }); 32 | 33 | export const tabChangeGtmEpic = actions$ => 34 | actions$.ofType(MainActions.SELECT_TAB) 35 | .map(action => { 36 | return { 37 | type: MainActions.SEND_ANALYTICS, 38 | payload: { 39 | event: TAB_CHANGE_GTM, 40 | desc: TAB_TYPES[action.payload] 41 | } 42 | }; 43 | }); 44 | 45 | export const subTabChangeGtmEpic = actions$ => 46 | actions$.ofType(MainActions.SELECT_COMPONENTS_SUB_TAB) 47 | .map(action => { 48 | return { 49 | type: MainActions.SEND_ANALYTICS, 50 | payload: { 51 | event: SUB_TAB_CHANGE_GTM, 52 | desc: SUB_TAB_TYPES[action.payload] 53 | } 54 | }; 55 | }); 56 | 57 | export const emitValueGtmEpic = actions$ => 58 | actions$.ofType(MainActions.EMIT_VALUE) 59 | .mapTo({ 60 | type: MainActions.SEND_ANALYTICS, 61 | payload: { 62 | event: EMIT_VALUE_GTM, 63 | desc: 'Emit Clicked' 64 | } 65 | }); 66 | 67 | export const updatePropertyGtmEpic = actions$ => 68 | actions$.ofType(MainActions.UPDATE_PROPERTY) 69 | .mapTo({ 70 | type: MainActions.SEND_ANALYTICS, 71 | payload: { 72 | event: UPDATE_PROPERTY_GTM, 73 | desc: 'Update property' 74 | } 75 | }); 76 | 77 | export const initializeAuguryGtmEpic = actions$ => 78 | actions$.ofType(MainActions.INITIALIZE_AUGURY) 79 | .mapTo({ 80 | type: MainActions.SEND_ANALYTICS, 81 | payload: { 82 | event: INITIALIZE_AUGURY_GTM, 83 | desc: 'Initialize Augury' 84 | } 85 | }); 86 | -------------------------------------------------------------------------------- /src/frontend/utils/parse-utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MutableTree, 3 | Node, 4 | Path, 5 | deserializePath, 6 | serializePath, 7 | } from '../../tree'; 8 | 9 | import {Dependency} from '../../backend/utils/description'; 10 | 11 | export class ParseUtils { 12 | getParentNodeIds(nodeId: string) { 13 | const path = deserializePath(nodeId); 14 | 15 | const result = new Array(); 16 | 17 | for (let i = 1; i < path.length; ++i) { 18 | result.push(serializePath(path.slice(0, i))); 19 | } 20 | 21 | return result; 22 | } 23 | 24 | getNodeDependency(node: Node, dependencyId: string) { 25 | return node.dependencies.reduce((prev, curr, idx, p) => 26 | prev ? prev : p[idx].id === dependencyId ? p[idx] : null, null); 27 | } 28 | 29 | checkNodeProvidesDependency(node: Node, dependency: Dependency) { 30 | return node.providers.reduce((prev, curr, idx, p) => 31 | prev ? prev : p[idx].id === dependency.id, false); 32 | } 33 | 34 | getDependencyProvider(tree: MutableTree, nodeId: string, dependency: Dependency) { 35 | if (tree == null) { 36 | return null; 37 | } 38 | 39 | const node = tree.lookup(nodeId); 40 | if (this.checkNodeProvidesDependency(node, dependency) 41 | && dependency.decorators.indexOf('@SkipSelf') < 0) { 42 | return node; 43 | } 44 | 45 | const nodeIds = this.getParentNodeIds(nodeId); 46 | 47 | for (const id of nodeIds) { 48 | const matchingNode = tree.lookup(id); 49 | if (this.checkNodeProvidesDependency(matchingNode, dependency)) { 50 | return matchingNode; 51 | } 52 | } 53 | 54 | return null; 55 | } 56 | 57 | getParentHierarchy(tree: MutableTree, node: Node, filter?: (n: Node) => boolean): Array { 58 | if (tree == null) { 59 | return []; 60 | } 61 | 62 | const nodeIds = this.getParentNodeIds(node.id); 63 | 64 | const hierarchy = nodeIds.reduce( 65 | (array, id) => { 66 | const matchingNode = tree.lookup(id); 67 | if (matchingNode) { 68 | array.push(matchingNode); 69 | } 70 | return array; 71 | }, 72 | []); 73 | 74 | if (typeof filter === 'function') { 75 | return hierarchy.filter(n => filter(n)); 76 | } 77 | 78 | return hierarchy; 79 | } 80 | 81 | flatten(list: Array): Array { 82 | return list.reduce((a, b) => 83 | a.concat(Array.isArray(b.children) ? 84 | [this.copyParent(b), ...this.flatten(b.children)] : b), 85 | []); 86 | } 87 | 88 | copyParent(p: Object) { 89 | return Object.assign({}, p, { children: undefined }); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/tree/decorators.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InputProperty, 3 | OutputProperty, 4 | } from './node'; 5 | 6 | import {functionName} from '../utils'; 7 | 8 | export const classDecorators = (token): Array => 9 | Reflect.getOwnMetadata('annotations', token) || []; 10 | 11 | export const propertyDecorators = (instance): Array => 12 | Reflect.getOwnMetadata('propMetadata', instance.constructor) || []; 13 | 14 | export const parameterTypes = (instance): Array => 15 | Reflect.getOwnMetadata('design:paramtypes', instance.constructor) || []; 16 | 17 | export const injectedParameterDecorators = (instance): Array => 18 | Reflect.getOwnMetadata('parameters', instance.constructor) || []; 19 | 20 | export const iteratePropertyDecorators = (instance, fn: (key: string, decorator) => void) => { 21 | if (instance == null) { 22 | return; 23 | } 24 | 25 | const decorators = propertyDecorators(instance); 26 | 27 | for (const key of Object.keys(decorators)) { 28 | for (const meta of decorators[key]) { 29 | fn(key, meta); 30 | } 31 | } 32 | }; 33 | 34 | export const componentMetadata = (token) => { 35 | if (!token) { 36 | return null; 37 | } 38 | 39 | return classDecorators(token).find(d => d.toString() === '@Component'); 40 | }; 41 | 42 | export const componentInputs = (metadata, instance): Array => { 43 | const inputs: Array = 44 | ((metadata && metadata.inputs) || []).map(p => ({propertyKey: p})); 45 | 46 | iteratePropertyDecorators(instance, 47 | (key: string, meta) => { 48 | if (inputs.find(i => i.propertyKey === key) == null) { 49 | if (meta.toString() === '@Input') { 50 | inputs.push({propertyKey: key, bindingPropertyName: meta.bindingPropertyName}); 51 | } 52 | } 53 | }); 54 | 55 | return inputs; 56 | }; 57 | 58 | export const componentOutputs = (metadata, instance): Array => { 59 | const outputs: Array = 60 | ((metadata && metadata.outputs) || []).map(p => ({propertyKey: p})); 61 | 62 | iteratePropertyDecorators(instance, 63 | (key: string, meta) => { 64 | if (meta.toString() === '@Output') { 65 | outputs.push({propertyKey: key, bindingPropertyName: meta.bindingPropertyName}); 66 | } 67 | }); 68 | 69 | return Array.from(outputs); 70 | }; 71 | 72 | export interface Query { 73 | propertyKey: string; 74 | selector: string; 75 | } 76 | 77 | export const componentQueryChildren = (type: string, metadata, instance): Array => { 78 | const queries = new Array(); 79 | 80 | iteratePropertyDecorators(instance, 81 | (key: string, meta) => { 82 | if (meta.toString() === type) { 83 | queries.push({propertyKey: key, selector: functionName(meta.selector)}); 84 | } 85 | }); 86 | 87 | return queries; 88 | }; 89 | -------------------------------------------------------------------------------- /src/backend/utils/node-traversal.ts: -------------------------------------------------------------------------------- 1 | import {DebugElement} from '@angular/core'; 2 | 3 | import { 4 | MutableTree, 5 | Node, 6 | Path, 7 | tokenName, 8 | } from '../../tree'; 9 | 10 | // The path we get is a series of numbers followed by the names of properties 11 | // (in the case of emit or updateProperty). So we want to just pull the node 12 | // path and omit the property names (although they are used later). 13 | export const getNodeFromPartialPath = (tree: MutableTree, path: Path): Node => { 14 | const pindex = propertyIndex(path); 15 | 16 | return tree.traverse(path.slice(0, pindex)); 17 | }; 18 | 19 | // When we are emitting values or updating properties for a component, the path 20 | // we get really contains two paths. The first is a path to the node itself, 21 | // which is composed of indexes into the tree. Following that is a path to a 22 | // property inside the componentInstance. The second path describes the piece 23 | // of state that we wish to change or emit. 24 | export const getPropertyPath = (path: Path): Path => { 25 | const index = propertyIndex(path); 26 | 27 | if (index === path.length) { // not found 28 | return []; 29 | } 30 | 31 | return path.slice(index); 32 | }; 33 | 34 | // Get the value of an instance variable from a combination of a node path and 35 | // a property path. (See the comment for {@link getPropertyPath} for details) 36 | export const getInstanceFromPath = (instance, path: Path) => { 37 | if (instance == null) { 38 | return null; 39 | } 40 | 41 | const propertyPath = path.slice(0); 42 | 43 | while (propertyPath.length > 0) { 44 | instance = instance[propertyPath.shift()]; 45 | if (instance == null) { 46 | return null; 47 | } 48 | } 49 | 50 | return instance; 51 | }; 52 | 53 | export const getNodeProvider = (element: DebugElement, providerToken: string, propertyPath: Path) => { 54 | const token = element.providerTokens.find(t => tokenName(t) === providerToken); 55 | if (token == null) { 56 | return null; 57 | } 58 | 59 | const path = getPropertyPath(propertyPath.slice(0, propertyPath.length - 1)); 60 | 61 | return getInstanceFromPath(element.injector.get(token), path); 62 | }; 63 | 64 | // We want to retrieve the parent of the object described in {@param path} 65 | export const getNodeInstanceParent = (element: DebugElement, path: Path) => { 66 | if (path.length === 0) { 67 | return null; 68 | } 69 | 70 | const propertyPath = getPropertyPath(path.slice(0, path.length - 1)); 71 | if (propertyPath.length > 0) { 72 | return getInstanceFromPath(element.componentInstance, propertyPath); 73 | } 74 | else { 75 | return element.componentInstance; 76 | } 77 | }; 78 | 79 | export const propertyIndex = (path: Path): number => { 80 | let index = 0; 81 | while (index < path.length) { 82 | if (typeof path[index] !== 'number') { 83 | break; 84 | } 85 | ++index; 86 | } 87 | return index; 88 | }; 89 | -------------------------------------------------------------------------------- /src/content-script.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Message, 3 | MessageFactory, 4 | MessageType, 5 | messageJumpContext, 6 | browserSubscribeDispatch, 7 | browserSubscribeOnce, 8 | } from './communication'; 9 | 10 | import { 11 | send, 12 | subscribe, 13 | } from './backend/connection'; 14 | 15 | import {loadOptions, SimpleOptions} from './options'; 16 | 17 | const scriptInjection = new Set(); 18 | 19 | const inject = (fn: (element: HTMLScriptElement) => void) => { 20 | const script = document.createElement('script'); 21 | fn(script); 22 | document.documentElement.appendChild(script); 23 | script.parentNode.removeChild(script); 24 | }; 25 | 26 | const injectScript = (path: string) => { 27 | if (scriptInjection.has(path)) { 28 | return; 29 | } 30 | 31 | inject(script => { 32 | script.src = chrome.extension.getURL(path); 33 | }); 34 | 35 | scriptInjection.add(path); 36 | }; 37 | 38 | export const injectSettings = (options: SimpleOptions) => { 39 | inject(script => { 40 | const serialized = JSON.stringify(options); 41 | 42 | script.textContent = `this.treeRenderOptions = ${serialized};`; 43 | }); 44 | }; 45 | 46 | browserSubscribeOnce(MessageType.FrameworkLoaded, 47 | () => { 48 | loadOptions().then(options => { 49 | // We want to load the tree rendering options that the UI has saved 50 | // because that allows us to send the correct tree immediately upon 51 | // startup and send it to the message queue, allowing Augury to render 52 | // instantly as soon as the application is loaded. Without this bit 53 | // of code we would have to wait for the frontend to start and load its 54 | // options and then request the tree, which would add a lot of latency 55 | // to startup. 56 | injectSettings(options); 57 | 58 | injectScript('build/backend.js'); 59 | }); 60 | 61 | return true; 62 | }); 63 | 64 | browserSubscribeDispatch(message => { 65 | if (message.messageType === MessageType.DispatchWrapper) { 66 | send(message.content) 67 | .then(response => { 68 | messageJumpContext(MessageFactory.response(message, response, true)); 69 | }) 70 | .catch(error => { 71 | messageJumpContext(MessageFactory.response(message, error, false)); 72 | }); 73 | } 74 | }); 75 | 76 | subscribe((message: Message) => messageJumpContext(message)); 77 | 78 | send(MessageFactory.initialize()) 79 | .then((response: {extensionId: string}) => { 80 | injectScript('build/ng-validate.js'); 81 | }) 82 | .catch(error => { 83 | console.error('Augury initialization has failed', error); 84 | }); 85 | 86 | const propertyKey = '$$el'; 87 | const warningText = `$$el will only be set in the 'top' execution context, \ 88 | which you can select via the dropdown in the console pane \ 89 | (https://developers.google.com/web/tools/chrome-devtools/console/\ 90 | #execution-context).`; 91 | 92 | Object.defineProperty(window, propertyKey, { value: warningText }); 93 | -------------------------------------------------------------------------------- /src/frontend/state/component-view-state.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | 3 | import { Observable } from 'rxjs/Observable'; 4 | import { Subject } from 'rxjs/Subject'; 5 | 6 | import { 7 | MutableTree, 8 | Node, 9 | deserializePath, 10 | } from '../../tree'; 11 | 12 | import {highlightTime} from '../../utils'; 13 | 14 | import {ParseUtils} from '../utils/parse-utils'; 15 | 16 | import {ExpandState} from './expand-state'; 17 | 18 | const checkReferenceId = (node: Node) => { 19 | if (node == null) { 20 | throw new Error('Node has no associated ID'); 21 | } 22 | }; 23 | 24 | @Injectable() 25 | export class ComponentViewState { 26 | private subject = new Subject(); 27 | 28 | private expansion = new Map(); 29 | 30 | private changed = new Set(); 31 | 32 | private selected: string; // node path ID 33 | 34 | get changes(): Observable { 35 | return this.subject.asObservable(); 36 | } 37 | 38 | nodeIsChanged(node: Node) { 39 | return this.changed.has(node.id); 40 | } 41 | 42 | nodesChanged(identifiers: Array) { 43 | for (const id of identifiers) { 44 | this.changed.add(id); 45 | } 46 | 47 | const remove = () => { 48 | for (const id of identifiers) { 49 | this.changed.delete(id); 50 | } 51 | 52 | this.subject.next(void 0); 53 | }; 54 | 55 | setTimeout(() => remove(), highlightTime); 56 | } 57 | 58 | expandState(node: Node, expandState?: ExpandState) { 59 | checkReferenceId(node); 60 | 61 | if (expandState != null) { 62 | this.expansion.set(node.id, expandState); 63 | this.publish(); 64 | } 65 | else { 66 | return this.expansion.get(node.id); 67 | } 68 | } 69 | 70 | selectionState(node: Node): boolean { 71 | return this.selected === node.id; 72 | } 73 | 74 | selectedTreeNode(tree: MutableTree): Node { 75 | if (this.selected == null) { 76 | return null; 77 | } 78 | 79 | const path = deserializePath(this.selected); 80 | 81 | return tree.traverse(path); 82 | } 83 | 84 | select(node: Node) { 85 | checkReferenceId(node); 86 | 87 | if (node) { 88 | const parseUtils = new ParseUtils(); 89 | 90 | const path = deserializePath(node.id); 91 | 92 | // If this node is not even visible, we must expand its parents 93 | for (const parentId of parseUtils.getParentNodeIds(node.id)) { 94 | const collapsed = this.expansion.get(parentId) !== ExpandState.Expanded; 95 | if (collapsed) { 96 | this.expansion.set(parentId, ExpandState.Expanded); 97 | } 98 | } 99 | } 100 | 101 | this.selected = node.id; 102 | 103 | this.publish(); 104 | } 105 | 106 | unselect() { 107 | this.selected = null; 108 | this.publish(); 109 | } 110 | 111 | private publish() { 112 | this.subject.next(void 0); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/frontend/components/injector-tree/injector-tree.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | No component selected 5 |

6 |
7 |
10 |
11 |

12 | Component Hierarchy 13 |

14 |
15 | 16 | 19 | {{node.name}} 20 | 21 | 24 | » 25 | 26 | 27 |
28 |
29 |
30 |

31 | Injector Graph 32 |

33 |
34 |
35 |
36 |
37 |
38 |
40 | 41 | {{parentHierarchy[focusedComponent].name}} 42 | 43 | 44 |
    45 |
  • 46 | NgModule: 47 | 48 | {{ngModules.tokenIdMap[parentHierarchy[focusedComponent].augury_token_id].module}} 49 | 50 |
  • 51 |
52 |
53 | 54 |
56 | 57 | {{parentHierarchy[focusedComponent].dependencies[focusedDependency].name}} 58 | 59 | 60 |
    61 |
  • 62 | Injected By: 63 | {{parentHierarchy[focusedComponent].name}} 64 |
  • 65 |
  • 66 | NgModule: 67 | 68 | {{ngModules.tokenIdMap[parentHierarchy[focusedComponent].dependencies[focusedDependency].id].module}} 69 | 70 |
  • 71 |
72 |
73 |
74 | -------------------------------------------------------------------------------- /images/report-issue.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | Github Report Issue 19 | 40 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | Github Report Issue 51 | 52 | 53 | Rajinder Yadav 54 | 55 | 56 | 58 | 59 | 60 | 61 | 67 | 71 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /images/report-issue-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | Github Report Issue 19 | 40 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | Github Report Issue 51 | 52 | 53 | Rajinder Yadav 54 | 55 | 56 | 58 | 59 | 60 | 61 | 67 | 71 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/frontend/components/render-state/render-state.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | No state to show 4 | 5 | 6 | 7 | 8 | 9 |
14 | 15 | 16 | @Input('{{getAlias(k)}}') 17 | 18 | 19 | @Output('{{getAlias(k)}}') 20 | 21 | 22 | @ViewChild({{getSelector(k)}}) 23 | 24 | 25 | @ViewChildren({{getSelector(k)}}) 26 | 27 | 28 | @ContentChild({{getSelector(k)}}) 29 | 30 | 31 | @ContentChildren({{getSelector(k)}}) 32 | 33 | 34 | {{k}}: 35 | × 39 |
40 | 41 | 42 |
43 | 44 | 45 |
46 | 50 |
51 | 52 | {{displayType(k)}} 53 | 54 | 61 | 62 |
63 |
64 | 73 | 74 |
75 |
76 | -------------------------------------------------------------------------------- /src/frontend/components/node-item/node-item.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectorRef, 3 | Component, 4 | EventEmitter, 5 | Input, 6 | Output, 7 | } from '@angular/core'; 8 | 9 | import {Node} from '../../../tree/node'; 10 | import {UserActions} from '../../actions/user-actions/user-actions'; 11 | 12 | import { 13 | ExpandState, 14 | ComponentViewState, 15 | } from '../../state'; 16 | 17 | /// The number of levels of tree nodes that we expand by default 18 | export const defaultExpansionDepth = 3; 19 | 20 | @Component({ 21 | selector: 'bt-node-item', 22 | template: require('./node-item.html'), 23 | styles: [require('to-string!./node-item.css')], 24 | }) 25 | export class NodeItem { 26 | @Input() node; 27 | 28 | // The depth of this node in the tree 29 | @Input() level: number; 30 | 31 | // Emitted when this node is selected 32 | @Output() private selectNode = new EventEmitter(); 33 | 34 | // Emitted when this node is selected for element inspection 35 | @Output() private inspectElement = new EventEmitter(); 36 | 37 | // Expand this node and all its children 38 | @Output() private expandChildren = new EventEmitter(); 39 | 40 | // Collapse this node and all its children 41 | @Output() private collapseChildren = new EventEmitter(); 42 | 43 | constructor( 44 | private changeDetector: ChangeDetectorRef, 45 | private viewState: ComponentViewState, 46 | private userActions: UserActions 47 | ) {} 48 | 49 | private get selected(): boolean { 50 | return this.viewState.selectionState(this.node); 51 | } 52 | 53 | private get expanded(): boolean { 54 | const state = this.viewState.expandState(this.node); 55 | if (state == null) { // user has not expanded or collapsed explicitly 56 | return this.defaultExpanded; 57 | } 58 | return state === ExpandState.Expanded; 59 | } 60 | 61 | private get defaultExpanded(): boolean { 62 | return this.level < defaultExpansionDepth; 63 | } 64 | 65 | private get hasChildren(): boolean { 66 | return this.node.children.length > 0; 67 | } 68 | 69 | /// Select the element in inspect window on double click 70 | onDblClick(event: MouseEvent) { 71 | this.inspectElement.emit(this.node); 72 | } 73 | 74 | onClick(event: MouseEvent) { 75 | if (event.ctrlKey || event.metaKey) { 76 | this.expandChildren.emit(this.node); 77 | } 78 | else if (event.altKey) { 79 | this.collapseChildren.emit(this.node); 80 | } 81 | 82 | this.selectNode.emit(this.node); 83 | } 84 | 85 | onMouseOut(event: MouseEvent) { 86 | this.userActions.clearHighlight(); 87 | } 88 | 89 | onMouseOver($event) { 90 | this.userActions.highlight(this.node); 91 | } 92 | 93 | onToggleExpand($event) { 94 | const defaultState = 95 | this.defaultExpanded 96 | ? ExpandState.Expanded 97 | : ExpandState.Collapsed; 98 | 99 | this.userActions.toggle(this.node, defaultState); 100 | 101 | this.changeDetector.detectChanges(); 102 | } 103 | 104 | trackById = (index: number, node: Node) => node.id; 105 | } 106 | -------------------------------------------------------------------------------- /src/tree/mutable-tree.ts: -------------------------------------------------------------------------------- 1 | import {Change} from './change'; 2 | import {Node} from './node'; 3 | import {Path, deserializePath} from './path'; 4 | import {apply, compare} from '../utils/patch'; 5 | 6 | export class MutableTree { 7 | public roots: Array; 8 | 9 | /// Compare this tree to another tree and generate a delta 10 | diff(nextTree: MutableTree): Array { 11 | const changes = compare(this, nextTree); 12 | 13 | const exclude = /nativeElement$/; 14 | 15 | return changes.filter(c => exclude.test(c.path) === false); 16 | } 17 | 18 | /// Apply a set of changes to this tree, mutating it 19 | patch(changes: Array) { 20 | apply(this, changes); 21 | } 22 | 23 | /// Look up a node in the tree based on its ID. Recall that an ID is a 24 | /// tree traversal path that has been serialized into a string. So we 25 | /// deserialize the path and then traverse the tree using that information 26 | /// instead of doing an actual search, so that the look up is much faster 27 | /// because we do not have to do any comparisons. There is no searching 28 | /// involved, so this is a very fast operation. 29 | lookup(id: string) { 30 | return this.traverse(deserializePath(id)); 31 | } 32 | 33 | /// Retreive a node matching {@link path} (fast) 34 | traverse(path: Path): Node { 35 | path = path.slice(0); 36 | 37 | const root = this.roots[path.shift()]; 38 | if (root == null) { 39 | return null; 40 | } 41 | 42 | if (path.length === 0) { 43 | return root; 44 | } 45 | 46 | let iterator = root; 47 | 48 | for (const index of path) { 49 | if (iterator == null) { 50 | return null; 51 | } 52 | 53 | switch (typeof index) { 54 | case 'number': 55 | if (iterator.children.length <= index) { 56 | return null; // not found 57 | } 58 | iterator = iterator.children[index]; 59 | break; 60 | case 'string': 61 | iterator = iterator[index]; 62 | break; 63 | } 64 | } 65 | 66 | return iterator; 67 | } 68 | 69 | /// Apply a function to all nodes in the specified tree index 70 | recurse(rootIndex: number, fn: (node: Node) => boolean | void) { 71 | const applyfn = (node: Node) => { 72 | fn(node); 73 | 74 | for (const child of node.children || []) { 75 | if (applyfn(child) === false) { 76 | return false; 77 | } 78 | } 79 | }; 80 | 81 | return applyfn(this.roots[rootIndex]); 82 | } 83 | 84 | /// Apply a function recursively to all nodes in all roots 85 | recurseAll(fn: (node: Node) => boolean | void) { 86 | for (let index = 0; index < this.roots.length; ++index) { 87 | if (this.recurse(index, fn) === false) { 88 | return false; 89 | } 90 | } 91 | } 92 | 93 | filter(fn: (node: Node) => boolean): Array { 94 | const results = new Array(); 95 | 96 | this.recurseAll(node => { 97 | if (fn(node)) { 98 | results.push(node); 99 | } 100 | }); 101 | 102 | return results; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/frontend/channel/direct-connection.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | 3 | import { 4 | Message, 5 | MessageResponse, 6 | } from '../../communication'; 7 | 8 | import { 9 | deserialize 10 | } from '../../utils'; 11 | 12 | /// For large messages, we use a strategy of pulling the data directly from the 13 | /// backend code using inspectedWindow instead of sending the data through the 14 | /// multiple ports that messages are typically sent through. This is a performance 15 | /// optimization. To send a normal message from the backend, to the content script, 16 | /// to the background channel, and finally to the frontend, requires four 17 | /// serialize / deserialize operations to happen in sequence and introduces a large 18 | /// amount of latency into the application. 19 | @Injectable() 20 | export class DirectConnection { 21 | handleImmediate(message: Message): Promise { 22 | return this.remoteExecute(`inspectedApplication.handleImmediate(${JSON.stringify(message)})`) 23 | .then(response => deserialize(response)); 24 | } 25 | 26 | readQueue(processor: (message: Message, respond: (response: MessageResponse) => void) => void) { 27 | /// We are being told that there are messages waiting for us in the backend 28 | /// message buffer. For large amounts of data, we do not send them through 29 | /// the normal pipe because it involves four separate serialize + deserialize 30 | /// operations and causes dramatic latency. Instead, when a large amount of 31 | /// data is being sent from the backend, it just adds it to a message queue 32 | /// and sends us a small {@link MessageType.Push} message to indicate that the 33 | /// buffer has messages waiting in it and we need to read and process them. 34 | /// These messages are subject only to one serialize + deserialize sequence 35 | /// (which inspectedWindow.eval() uses internally). 36 | return this.remoteExecute('inspectedApplication.readMessageQueue()') 37 | .then(result => { 38 | const encode = value => JSON.stringify(value); 39 | 40 | for (const message of result) { 41 | const respond = (response: MessageResponse) => { 42 | this.remoteExecute(`inspectedApplication.response(${encode(response)})`); 43 | }; 44 | 45 | processor(message, respond); 46 | } 47 | }) 48 | .catch(error => { 49 | throw new Error(`Failed to read message queue: ${error.stack || error.message}`); 50 | }); 51 | } 52 | 53 | private remoteExecute(code: string): Promise { 54 | return new Promise((resolve, reject) => { 55 | type ExceptionInfo = chrome.devtools.inspectedWindow.EvaluationExceptionInfo; 56 | 57 | const handler = (result, exceptionInfo: ExceptionInfo) => { 58 | if (exceptionInfo && 59 | (exceptionInfo.isError || 60 | exceptionInfo.isException)) { 61 | const e = new Error('Code evaluation failed'); 62 | if (exceptionInfo.isException) { 63 | e.stack = exceptionInfo.value; 64 | } 65 | reject(e); 66 | } 67 | else { 68 | resolve(result); 69 | } 70 | }; 71 | 72 | chrome.devtools.inspectedWindow.eval(code, handler); 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [augury@rangle.io](mailto:augury@rangle.io). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /src/communication/message-dispatch.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Message, 3 | MessageResponse, 4 | Subscription, 5 | checkSource, 6 | deserializeMessage, 7 | } from './message'; 8 | 9 | import {MessageFactory} from './message-factory'; 10 | import {MessageType} from './message-type'; 11 | 12 | import { 13 | deserialize, 14 | serialize, 15 | } from '../utils/serialize'; 16 | 17 | export interface DispatchHandler { 18 | (message: Message): Response; 19 | } 20 | 21 | const subscriptions = new Set(); 22 | 23 | const dispatchers = new Set(); 24 | 25 | export const browserSubscribeDispatch = (handler: DispatchHandler): Subscription => { 26 | dispatchers.add(handler); 27 | 28 | return { 29 | unsubscribe: () => dispatchers.delete(handler) 30 | }; 31 | }; 32 | 33 | export const browserSubscribe = (handler: DispatchHandler): Subscription => { 34 | subscriptions.add(handler); 35 | 36 | return { 37 | unsubscribe: () => subscriptions.delete(handler) 38 | }; 39 | }; 40 | 41 | export const browserSubscribeOnce = (messageType: MessageType, handler: DispatchHandler) => { 42 | const messageHandler = (message: Message) => { 43 | if (message.messageType === messageType) { 44 | try { 45 | deserializeMessage(message); 46 | 47 | return handler(message); 48 | } 49 | finally { 50 | subscription.unsubscribe(); 51 | } 52 | } 53 | }; 54 | 55 | const subscription = browserSubscribe(messageHandler); 56 | }; 57 | 58 | export const browserSubscribeResponse = (messageId: string, handler: DispatchHandler) => { 59 | const messageHandler = (response: MessageResponse) => { 60 | if (response.messageType === MessageType.Response && 61 | response.messageResponseId === messageId) { 62 | try { 63 | deserializeMessage(response); 64 | 65 | return handler(response); 66 | } 67 | finally { 68 | subscription.unsubscribe(); 69 | } 70 | } 71 | }; 72 | 73 | const subscription = browserSubscribe(messageHandler); 74 | }; 75 | 76 | export const browserUnsubscribe = (handler: DispatchHandler) => 77 | subscriptions.delete(handler); 78 | 79 | export const messageJumpContext = (message: Message) => { 80 | window.postMessage(message, '*'); 81 | }; 82 | 83 | export const browserDispatch = (message: Message) => { 84 | if (checkSource(message) === false) { 85 | return; 86 | } 87 | 88 | if (message.messageType === MessageType.DispatchWrapper) { 89 | dispatchers.forEach(handler => handler(message)); 90 | } 91 | else if (message.messageType !== MessageType.Response) { 92 | let dispatchResult; 93 | subscriptions.forEach(handler => { 94 | if (dispatchResult == null) { 95 | dispatchResult = handler(message); 96 | } 97 | else { 98 | handler(message); 99 | } 100 | }); 101 | 102 | if (dispatchResult !== undefined) { 103 | const response = 104 | MessageFactory.dispatchWrapper( 105 | MessageFactory.response(message, dispatchResult, false)); 106 | messageJumpContext(response); 107 | } 108 | } 109 | else { 110 | subscriptions.forEach(handler => handler(message)); 111 | } 112 | }; 113 | 114 | window.addEventListener('message', 115 | (event: MessageEvent) => { 116 | if (event.source === window) { 117 | browserDispatch(event.data); 118 | } 119 | }); 120 | 121 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "augury", 3 | "version": "1.14.0", 4 | "description": "Chrome Developer Tools Extension for inspecting Angular 2.0 applications", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/rangle/augury.git" 8 | }, 9 | "keywords": [ 10 | "angular", 11 | "angularjs", 12 | "chrome", 13 | "extension" 14 | ], 15 | "engines": { 16 | "node": ">= 4.2.3 < 6", 17 | "npm": ">= 3.5.3" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/rangle/augury/issues" 21 | }, 22 | "homepage": "https://github.com/rangle/augury", 23 | "scripts": { 24 | "prod-build": "webpack --optimize-dedupe", 25 | "build": "webpack --colors --display-error-details --display-cached", 26 | "dev": "webpack --colors --display-error-details --display-cached --watch", 27 | "dev-build": "cross-env NODE_ENV=development npm run build", 28 | "webpack": "webpack", 29 | "clean": "rimraf build", 30 | "start": "rimraf build && cross-env NODE_ENV=development webpack --watch", 31 | "test": "npm run lint && webpack --config webpack.test.config.js && cat build/test.js | tape-run | tap-spec", 32 | "prepack": "npm run clean && npm run build", 33 | "pack": "./crxmake.sh", 34 | "lint": "tslint 'src/**/*.ts'" 35 | }, 36 | "dependencies": { 37 | "@angular-redux/store": "6.4.1", 38 | "@angular/common": "4.1.3", 39 | "@angular/compiler": "4.1.3", 40 | "@angular/core": "4.1.3", 41 | "@angular/forms": "4.1.3", 42 | "@angular/http": "4.1.3", 43 | "@angular/platform-browser": "4.1.3", 44 | "@angular/platform-browser-dynamic": "4.1.3", 45 | "@angular/router": "4.1.3", 46 | "@types/redux-logger": "3.0.0", 47 | "basscss": "7.1.1", 48 | "basscss-border-colors": "2.1.0", 49 | "basscss-type-scale": "1.0.5", 50 | "basscss-typography": "3.0.3", 51 | "core-js": "2.2.2", 52 | "cross-env": "3.1.4", 53 | "crypto": "0.0.3", 54 | "d3": "4.5.0", 55 | "expose-loader": "0.7.1", 56 | "immutable": "3.7.6", 57 | "jsonschema": "^1.1.1", 58 | "ramda": "0.24.1", 59 | "redux": "3.6.0", 60 | "redux-logger": "3.0.6", 61 | "redux-observable": "0.14.1", 62 | "rxjs": "5.3.0", 63 | "zone.js": "0.8.4" 64 | }, 65 | "devDependencies": { 66 | "@types/chrome": "0.0.38", 67 | "@types/clone": "0.1.30", 68 | "@types/d3": "4.4.1", 69 | "@types/d3-hierarchy": "1.0.4", 70 | "@types/d3-selection": "1.0.9", 71 | "@types/d3-shape": "1.0.7", 72 | "@types/node": "7.0.5", 73 | "@types/tape": "4.2.28", 74 | "autoprefixer": "6.3.6", 75 | "basscss-layout": "3.1.0", 76 | "clone": "2.1.0", 77 | "css-loader": "0.26.1", 78 | "d3-hierarchy": "1.1.1", 79 | "d3-selection": "1.0.3", 80 | "d3-shape": "1.0.4", 81 | "es6-promise": "4.0.5", 82 | "es6-shim": "0.35.0", 83 | "file-loader": "0.10.0", 84 | "msgpack-lite": "0.1.20", 85 | "object-assign": "4.1.1", 86 | "postcss-cssnext": "2.5.2", 87 | "postcss-import": "9.1.0", 88 | "postcss-loader": "1.0.0", 89 | "raven-js": "^3.16.0", 90 | "raw-loader": "0.5.1", 91 | "reflect-metadata": "0.1.9", 92 | "rimraf": "2.5.4", 93 | "style-loader": "0.13.1", 94 | "tap-spec": "4.1.1", 95 | "tape": "4.2.2", 96 | "tape-run": "2.1.3", 97 | "to-string-loader": "1.1.4", 98 | "ts-loader": "2.1.0", 99 | "tslint": "5.4.3", 100 | "tslint-loader": "3.5.3", 101 | "typescript": "2.3.4", 102 | "url-loader": "0.5.7", 103 | "webpack": "1.14.0", 104 | "webpack-dev-server": "1.16.2" 105 | }, 106 | "license": "MIT" 107 | } 108 | -------------------------------------------------------------------------------- /src/frontend/components/component-info/component-info.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ node && node.name || 'No component selected' }} 5 |   6 | 10 | (View Source) 11 | 12 |

13 | 15 | ($$el in Console) 16 | 17 |
18 | 19 |
22 | Change Detection: {{changeDetectionStrategies[node.changeDetection]}} 23 |
24 | 25 |
26 | 27 |
28 | 36 | 37 |
38 |
39 | 40 | 42 |
43 |
    44 |
  • 45 | ({{listener.name}}) 46 | 48 |
  • 49 |
50 |
51 |
52 | 53 | 54 |
55 |
    56 |
  • 57 | 58 | {{directive}} 59 | 60 |
  • 61 |
62 |
63 |
64 | 65 | 66 |
67 |
68 | 79 | 82 |
83 |
84 |
85 | 86 | 87 |
88 | 92 | 93 |
94 |
95 |
96 |
97 | -------------------------------------------------------------------------------- /src/frontend/components/component-info/component-info.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | Input, 5 | Output, 6 | SimpleChanges, 7 | ChangeDetectionStrategy, 8 | } from '@angular/core'; 9 | 10 | import {ComponentLoadState} from '../../state'; 11 | 12 | import { 13 | ComponentMetadata, 14 | Metadata, 15 | MutableTree, 16 | Node, 17 | Path, 18 | deserializePath, 19 | } from '../../../tree'; 20 | 21 | import {functionName} from '../../../utils'; 22 | 23 | import {UserActions} from '../../actions/user-actions/user-actions'; 24 | 25 | @Component({ 26 | selector: 'bt-component-info', 27 | template: require('./component-info.html'), 28 | }) 29 | export class ComponentInfo { 30 | @Input() private node: Node; 31 | @Input() private tree: MutableTree; 32 | @Input() private state; 33 | @Input() private providers: Array; 34 | @Input() private metadata: Metadata; 35 | @Input() private componentMetadata: ComponentMetadata; 36 | @Input() private loadingState: ComponentLoadState; 37 | 38 | @Output() private selectNode = new EventEmitter(); 39 | @Output() private emitValue = new EventEmitter<{path: Path, data: any}>(); 40 | @Output() private updateProperty = new EventEmitter<{path: Path, newValue: any}>(); 41 | 42 | private changeDetectionStrategies = ChangeDetectionStrategy; 43 | private ComponentLoadState = ComponentLoadState; 44 | private path: Path; 45 | 46 | constructor(private actions: UserActions) {} 47 | 48 | ngOnChanges() { 49 | if (this.node) { 50 | this.path = deserializePath(this.node.id); 51 | } 52 | } 53 | 54 | private get hasState() { 55 | if (this.node == null || this.state == null) { 56 | return false; 57 | } 58 | 59 | return Object.keys(this.state).length > 0; 60 | } 61 | 62 | private onTriggerTemplateEvent(listener) { 63 | this.actions.triggerEvent(this.node, listener); 64 | } 65 | 66 | private get hasDirectives() { 67 | return this.node && 68 | this.node.directives && 69 | this.node.directives.length > 0; 70 | } 71 | 72 | private get hasTemplateEventListeners() { 73 | return this.node && 74 | this.node.listeners && 75 | this.node.listeners.length; 76 | } 77 | 78 | private get hasDependencies() { 79 | return this.node && 80 | this.node.dependencies && 81 | this.node.dependencies.length > 0; 82 | } 83 | 84 | private get hasInstanceProviders() { 85 | return this.providers && this.providers.length > 0; 86 | } 87 | 88 | private get instanceProvidersObject() { 89 | if (this.hasInstanceProviders === false) { 90 | return {}; 91 | } 92 | return this.providers.reduce((p, c) => Object.assign(p, {[c[0]]: c[1]}), {}); 93 | } 94 | 95 | private onViewComponentSource() { 96 | chrome.devtools.inspectedWindow.eval(` 97 | var root = ng.probe(inspectedApplication.nodeFromPath('${this.node.id}')); 98 | if (root) { 99 | if (root.componentInstance) { 100 | inspect(root.componentInstance.constructor); 101 | } 102 | else { 103 | throw new Error('This component has no instance and therefore no constructor'); 104 | } 105 | }`); 106 | } 107 | 108 | private onUpdateProperty(event: {path: Path, propertyKey: Path, newValue}) { 109 | this.actions.updateProperty(event.path.concat(event.propertyKey), event.newValue); 110 | this.updateProperty.emit({ path: event.path.concat(event.propertyKey), newValue: event.newValue }); 111 | if (this.node) { 112 | this.selectNode.emit(this.node); 113 | } 114 | } 115 | 116 | private onUpdateProvider(event: {path: Path, propertyKey: Path, newValue}) { 117 | this.actions.updateProvider(event.path, event.propertyKey, event.newValue); 118 | 119 | if (this.node) { 120 | this.selectNode.emit(this.node); 121 | } 122 | } 123 | } 124 | --------------------------------------------------------------------------------