├── 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 | 0">
2 |
3 | ({{d.key}}
=" {{d.value}}
" , )
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 |
10 |
12 |
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 |
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 | Send Report
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 |
5 | {{key}}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
{{item}}
14 |
None
15 |
16 |
17 |
18 |
19 |
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 |
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 |
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 |
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 |
34 | No components were found on the page
35 |
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 |
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 |
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 |
10 |
11 |
12 |
13 | path:
14 |
15 |
16 | {{selectedRoute.path}}
17 |
18 |
19 |
20 |
21 |
22 | specificity:
23 |
24 |
25 | {{selectedRoute.specificity}}
26 |
27 |
28 |
29 |
30 | handler:
31 |
32 |
33 | {{selectedRoute.handler}}
34 |
35 |
36 |
37 |
38 | data:
39 |
40 |
52 |
53 |
54 |
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 |
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 |
0" class="flex flex-none flex-column border-bottom pl4 pb3">
11 |
12 | Component Hierarchy
13 |
14 |
28 |
29 |
30 |
31 | Injector Graph
32 |
33 |
36 |
37 |
38 |
0 && focusedDependency < 0"
39 | class="bg-panel border-bottom pl4 py3 px3 m0 flex flex-column focused-node-info-panel">
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 |
= 0"
55 | class="bg-panel border-bottom pl4 py3 px3 m0 flex flex-column focused-node-info-panel">
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 |
48 | Emit
49 |
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 | Trigger Event
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | {{directive}}
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
77 |
78 |
79 |
80 | Failed to load component state
81 |
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 |
--------------------------------------------------------------------------------