From ec23cd386cd7e63ab737d6d289b5dd34a9220b94 Mon Sep 17 00:00:00 2001 From: semen Date: Sat, 28 Oct 2023 11:22:21 +0300 Subject: [PATCH 01/28] adjusted material icons --- public/styles.css | 14 ++++++++++- src/devtools/components/icon/styles.css | 20 ++++++++++++---- .../EventList/panels/event-list-header.ts | 23 +++++++++++++------ 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/public/styles.css b/public/styles.css index 0d36063..f46caac 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1,5 +1,13 @@ /* #region root styles */ +@font-face { + font-family: 'Material Symbols Rounded'; + font-style: normal; + font-weight: 100 700; + src: url(https://fonts.gstatic.com/s/materialsymbolsrounded/v146/sykg-zNym6YjUruM-QrEh7-nyTnjDwKNJ_190Fjzag.woff2) format('woff2'); +} + + *, ::before, ::after { @@ -29,7 +37,7 @@ :root { --bg-color: #282828; - --text-color: #e9e9e9; + --text-color: #c7c7c7; --success-color: #84ff84; --warning-color: #ffcb7d; @@ -38,6 +46,10 @@ --border-color: #474747; --bg-color-highlight: #3e3e3e; + + --icon-color: #c7c7c7; + --icon-color-hover: #e8eaed; + --icon-color-active: #e8eaed; } html { diff --git a/src/devtools/components/icon/styles.css b/src/devtools/components/icon/styles.css index ec82e07..948aa14 100644 --- a/src/devtools/components/icon/styles.css +++ b/src/devtools/components/icon/styles.css @@ -1,8 +1,12 @@ +:host { + width: 20px; + height: 20px; +} + .material-symbols-outlined { - font-family: 'Material Symbols Outlined'; + font-family: 'Material Symbols Rounded'; font-weight: normal; font-style: normal; - font-size: 24px; line-height: 1; letter-spacing: normal; text-transform: none; @@ -10,8 +14,16 @@ white-space: nowrap; word-wrap: normal; direction: ltr; - -moz-font-feature-settings: 'liga'; - -moz-osx-font-smoothing: grayscale; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; + font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' -27, 'opsz' 20; user-select: none; + font-size: 20px; + + color: var(--icon-color); +} + +.material-symbols-outlined:hover { + color: var(--icon-color-hover); } diff --git a/src/devtools/pages/EventList/panels/event-list-header.ts b/src/devtools/pages/EventList/panels/event-list-header.ts index 57d2c27..b23b3f8 100644 --- a/src/devtools/pages/EventList/panels/event-list-header.ts +++ b/src/devtools/pages/EventList/panels/event-list-header.ts @@ -11,20 +11,29 @@ export class EventListHeaderElement extends LitElement { static styles = css` .header { - height: 2rem; + display: flex; + align-items: center; + height: 1.75rem; + } + + .separator { + width: 1px; + height: 1rem; + + background: var(--border-color); + margin: 0 6px; } ` protected render() { return html`
- - + + +
+
select
+
` } - - _clear() { - store.dispatch(clearEvents()) - } } From 7c01529716d4e3b0d024cb7beaa7c34faee03b99 Mon Sep 17 00:00:00 2001 From: semen Date: Sun, 12 Nov 2023 19:43:40 +0300 Subject: [PATCH 02/28] Added pause buttons, implemented custom events --- package.json | 3 +- src/background.js | 2 + .../components/action-marker/styles.css | 7 ++- .../components/controls/button-group/index.ts | 20 +++++++ .../controls/button-icon/ButtonClickEvent.ts | 9 +++ .../components/controls/button-icon/index.ts | 28 ++++++++++ .../controls/button-icon/styles.css | 3 + .../button-toggle/ButtonToggleEvent.ts | 13 +++++ .../controls/button-toggle/index.ts | 52 ++++++++++++++++++ src/devtools/components/icon/index.ts | 8 ++- src/devtools/components/icon/styles.css | 7 ++- src/devtools/components/index.ts | 5 +- .../inputs/search/SearchChangeEvent.ts | 14 +++++ .../components/inputs/search/index.ts | 22 ++++++++ .../components/inputs/search/styles.css | 0 .../components/profiler-event/index.ts | 7 +-- .../{core => lib}/StatefulLItElement.ts | 0 .../EventList/panels/event-list-header.ts | 55 ++++++++++++++----- .../pages/EventList/panels/event-list.ts | 2 +- .../pages/EventList/panels/event-viewer.ts | 2 +- src/devtools/store/EventList/slice.ts | 29 +++++++++- src/devtools/styles/reset-button.css | 3 + src/types/declarations.d.ts | 2 +- webpack.config.js | 5 +- 24 files changed, 260 insertions(+), 38 deletions(-) create mode 100644 src/devtools/components/controls/button-group/index.ts create mode 100644 src/devtools/components/controls/button-icon/ButtonClickEvent.ts create mode 100644 src/devtools/components/controls/button-icon/index.ts create mode 100644 src/devtools/components/controls/button-icon/styles.css create mode 100644 src/devtools/components/controls/button-toggle/ButtonToggleEvent.ts create mode 100644 src/devtools/components/controls/button-toggle/index.ts create mode 100644 src/devtools/components/inputs/search/SearchChangeEvent.ts create mode 100644 src/devtools/components/inputs/search/index.ts create mode 100644 src/devtools/components/inputs/search/styles.css rename src/devtools/{core => lib}/StatefulLItElement.ts (100%) create mode 100644 src/devtools/styles/reset-button.css diff --git a/package.json b/package.json index 7b02319..27be53e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "profiler devtool for chrome for debugging incoding.framework.js", "main": "index.js", "scripts": { - "build": "npx webpack", + "build": "npx webpack build --mode production -d source-map", + "dev": "npx webpack watch --mode production", "lint": "eslint src/", "lintfix": "eslint src/ --fix" }, diff --git a/src/background.js b/src/background.js index 609de88..e16c25f 100644 --- a/src/background.js +++ b/src/background.js @@ -102,6 +102,8 @@ function establishBidirectionalConnection(tabId, one, two) { function shutdown() { + console.warn('devtools connection shutdown') + one.onMessage.removeListener(listenerOne); two.onMessage.removeListener(listenerTwo); one.disconnect(); diff --git a/src/devtools/components/action-marker/styles.css b/src/devtools/components/action-marker/styles.css index ba64659..0b774b1 100644 --- a/src/devtools/components/action-marker/styles.css +++ b/src/devtools/components/action-marker/styles.css @@ -1,6 +1,6 @@ .marker { - width: 0.75rem; - height: 0.75rem; + width: 0.5rem; + height: 0.5rem; border-radius: 1rem; @@ -13,6 +13,9 @@ } .action { + display: inline-block; + height: 100%; + margin-left: 0.25rem; vertical-align: middle; } diff --git a/src/devtools/components/controls/button-group/index.ts b/src/devtools/components/controls/button-group/index.ts new file mode 100644 index 0000000..df8c8c4 --- /dev/null +++ b/src/devtools/components/controls/button-group/index.ts @@ -0,0 +1,20 @@ +import { LitElement, css, html } from "lit"; +import { customElement } from "lit/decorators.js"; + +@customElement('btn-group') +export class ButtonGroupElement extends LitElement { + + static styles = css` + :host { + display: flex; + + gap: 0.25rem; + } + ` + + protected render() { + return html` + + ` + } +} diff --git a/src/devtools/components/controls/button-icon/ButtonClickEvent.ts b/src/devtools/components/controls/button-icon/ButtonClickEvent.ts new file mode 100644 index 0000000..f535a41 --- /dev/null +++ b/src/devtools/components/controls/button-icon/ButtonClickEvent.ts @@ -0,0 +1,9 @@ +export default class ButtonClickEvent extends Event { + + constructor() { + super('click', { + bubbles: false, + cancelable: true + }); + } +} diff --git a/src/devtools/components/controls/button-icon/index.ts b/src/devtools/components/controls/button-icon/index.ts new file mode 100644 index 0000000..1e7b78c --- /dev/null +++ b/src/devtools/components/controls/button-icon/index.ts @@ -0,0 +1,28 @@ +import { LitElement, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +import styles from "./styles.css" +import resetButtonStyles from "../../../styles/reset-button.css" +import ButtonClickEvent from "./ButtonClickEvent"; + +@customElement('btn-icon') +export class IconButtonElement extends LitElement { + + static styles = [styles, resetButtonStyles] + + @property() icon: string + + @property() color?: string + + protected render() { + return html` + + ` + } + + private dispatchChange() { + this.dispatchEvent(new ButtonClickEvent()) + } +} diff --git a/src/devtools/components/controls/button-icon/styles.css b/src/devtools/components/controls/button-icon/styles.css new file mode 100644 index 0000000..2e02341 --- /dev/null +++ b/src/devtools/components/controls/button-icon/styles.css @@ -0,0 +1,3 @@ +:host { + height: 1.25rem; +} diff --git a/src/devtools/components/controls/button-toggle/ButtonToggleEvent.ts b/src/devtools/components/controls/button-toggle/ButtonToggleEvent.ts new file mode 100644 index 0000000..48183a0 --- /dev/null +++ b/src/devtools/components/controls/button-toggle/ButtonToggleEvent.ts @@ -0,0 +1,13 @@ +export default class ButtonToggleEvent extends Event { + + enabled: boolean + + constructor(value: boolean) { + super('toggle', { + bubbles: false, + composed: true, + }) + + this.enabled = value + } +} diff --git a/src/devtools/components/controls/button-toggle/index.ts b/src/devtools/components/controls/button-toggle/index.ts new file mode 100644 index 0000000..580c0c2 --- /dev/null +++ b/src/devtools/components/controls/button-toggle/index.ts @@ -0,0 +1,52 @@ +import { LitElement, html } from "lit"; +import { customElement, queryAssignedElements, state } from "lit/decorators.js"; +import ButtonToggleEvent from "./ButtonToggleEvent"; + +@customElement('btn-toggle') +export class ButtonToggleElement extends LitElement { + + @state() enabled: boolean = true + + @queryAssignedElements({ slot: 'enabled' }) enabledButton: Array + + @queryAssignedElements({ slot: 'disabled' }) disabledButton: HTMLElement[] + + protected render() { + return html` + + + ` + } + + connectedCallback(): void { + super.connectedCallback() + + this.addEventListener('click', this.toggleEnabled) + } + + disconnectedCallback(): void { + super.disconnectedCallback() + + this.removeEventListener('click', this.toggleEnabled) + } + + private initSlot() { + this.enabledButton.forEach(el => el.style.display = '') + this.disabledButton.forEach(el => el.style.display = 'none') + } + + private toggleEnabled() { + this.enabled = !this.enabled + + if (this.enabled) { + this.enabledButton.forEach(el => el.style.display = 'block') + this.disabledButton.forEach(el => el.style.display = 'none') + } + else { + this.enabledButton.forEach(el => el.style.display = 'none') + this.disabledButton.forEach(el => el.style.display = 'block') + } + + this.dispatchEvent(new ButtonToggleEvent(this.enabled)) + } +} diff --git a/src/devtools/components/icon/index.ts b/src/devtools/components/icon/index.ts index a7d4fc8..b113f5d 100644 --- a/src/devtools/components/icon/index.ts +++ b/src/devtools/components/icon/index.ts @@ -3,16 +3,20 @@ import { customElement, property } from "lit/decorators.js"; import styles from "./styles.css" -@customElement('m-icon') +@customElement('material-icon') export class IconElement extends LitElement { static styles = styles @property() icon: string = '' + @property() color?: string + protected render() { + const colorStyle = this.color ? `color: ${this.color};` : `` + return html` - + ${this.icon} ` diff --git a/src/devtools/components/icon/styles.css b/src/devtools/components/icon/styles.css index 948aa14..100627d 100644 --- a/src/devtools/components/icon/styles.css +++ b/src/devtools/components/icon/styles.css @@ -1,6 +1,7 @@ :host { - width: 20px; - height: 20px; + display: block; + width: 1.25rem; + height: 1.25rem; } .material-symbols-outlined { @@ -19,7 +20,7 @@ font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' -27, 'opsz' 20; user-select: none; - font-size: 20px; + font-size: 1.25rem; color: var(--icon-color); } diff --git a/src/devtools/components/index.ts b/src/devtools/components/index.ts index 2f9a938f..9c344c5 100644 --- a/src/devtools/components/index.ts +++ b/src/devtools/components/index.ts @@ -2,4 +2,7 @@ import "./action-marker" import "./time-marker" import "./icon" import "./profiler-event" - +import "./controls/button-icon" +import "./controls/button-group" +import "./controls/button-toggle" +import "./inputs/search" diff --git a/src/devtools/components/inputs/search/SearchChangeEvent.ts b/src/devtools/components/inputs/search/SearchChangeEvent.ts new file mode 100644 index 0000000..043a33b --- /dev/null +++ b/src/devtools/components/inputs/search/SearchChangeEvent.ts @@ -0,0 +1,14 @@ + +export default class SearchEvent extends Event { + + public value: string + + constructor(value: string) { + super('search', { + bubbles: false, + composed: true + }) + + this.value = value + } +} diff --git a/src/devtools/components/inputs/search/index.ts b/src/devtools/components/inputs/search/index.ts new file mode 100644 index 0000000..10f42c7 --- /dev/null +++ b/src/devtools/components/inputs/search/index.ts @@ -0,0 +1,22 @@ +import { LitElement, html } from "lit"; +import { customElement, property, query } from "lit/decorators.js"; +import SearchEvent from "./SearchChangeEvent"; + +@customElement('input-search') +export class InputSearchElement extends LitElement { + + @property() placeholder: string = '' + + @query('input') searchInput: HTMLInputElement + + protected render() { + return html` + + ` + } + + private onsearch(ev: KeyboardEvent) { + const value = this.searchInput.value + this.dispatchEvent(new SearchEvent(value)) + } +} diff --git a/src/devtools/components/inputs/search/styles.css b/src/devtools/components/inputs/search/styles.css new file mode 100644 index 0000000..e69de29 diff --git a/src/devtools/components/profiler-event/index.ts b/src/devtools/components/profiler-event/index.ts index 3b34260..c04af0e 100644 --- a/src/devtools/components/profiler-event/index.ts +++ b/src/devtools/components/profiler-event/index.ts @@ -19,13 +19,8 @@ export class ProfilerEventElement extends LitElement { return html`
- +
${this.data.eventName}
-
- - - -
` } diff --git a/src/devtools/core/StatefulLItElement.ts b/src/devtools/lib/StatefulLItElement.ts similarity index 100% rename from src/devtools/core/StatefulLItElement.ts rename to src/devtools/lib/StatefulLItElement.ts diff --git a/src/devtools/pages/EventList/panels/event-list-header.ts b/src/devtools/pages/EventList/panels/event-list-header.ts index b23b3f8..785c464 100644 --- a/src/devtools/pages/EventList/panels/event-list-header.ts +++ b/src/devtools/pages/EventList/panels/event-list-header.ts @@ -1,19 +1,21 @@ -import '../../../components/icon' - import { LitElement, css, html } from "lit"; import { customElement } from "lit/decorators.js" -import { clearEvents } from "../../../store/EventList/slice"; -import store from "../../../store"; +import SearchEvent from '../../../components/inputs/search/SearchChangeEvent'; +import store from '../../../store'; +import { clearEvents, pauseEvents, resumeEvents } from '../../../store/EventList/slice'; +import ButtonToggleEvent from '../../../components/controls/button-toggle/ButtonToggleEvent'; @customElement('event-list-header') export class EventListHeaderElement extends LitElement { static styles = css` - .header { + :host { display: flex; align-items: center; - height: 1.75rem; + padding: 0 6px; + + height: 26px; } .separator { @@ -27,13 +29,40 @@ export class EventListHeaderElement extends LitElement { protected render() { return html` -
- - -
-
select
-
-
+ + + + + + + + + +
+ +
select
+ +
+ + ` } + + private clearEventList() { + store.dispatch(clearEvents()) + } + + private togglePauseEvents(ev: ButtonToggleEvent) { + console.log(ev.enabled); + + if (ev.enabled) { + store.dispatch(resumeEvents()) + } else { + store.dispatch(pauseEvents()) + } + } + + private searchEvents(ev: SearchEvent) { + console.log(ev.value) + } } diff --git a/src/devtools/pages/EventList/panels/event-list.ts b/src/devtools/pages/EventList/panels/event-list.ts index 0228da3..c67a31c 100644 --- a/src/devtools/pages/EventList/panels/event-list.ts +++ b/src/devtools/pages/EventList/panels/event-list.ts @@ -8,7 +8,7 @@ import IncodingEvent from "../../../models/incodingEvent"; import { RootState } from "../../../store"; import scrollStyles from "../../../styles/scroll.css" -import StatefulLitElement from "../../../core/StatefulLItElement"; +import StatefulLitElement from "../../../lib/StatefulLItElement"; import { selectEvents } from "../../../store/EventList/selectors"; diff --git a/src/devtools/pages/EventList/panels/event-viewer.ts b/src/devtools/pages/EventList/panels/event-viewer.ts index ab8d24f..0942baf 100644 --- a/src/devtools/pages/EventList/panels/event-viewer.ts +++ b/src/devtools/pages/EventList/panels/event-viewer.ts @@ -4,7 +4,7 @@ import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; import JsonData from "../../../models/jsonData"; import { RootState } from '../../../store'; -import StatefulLitElement from '../../../core/StatefulLItElement'; +import StatefulLitElement from '../../../lib/StatefulLItElement'; @customElement('event-viewer') diff --git a/src/devtools/store/EventList/slice.ts b/src/devtools/store/EventList/slice.ts index cc39e21..8cbaebc 100644 --- a/src/devtools/store/EventList/slice.ts +++ b/src/devtools/store/EventList/slice.ts @@ -3,11 +3,13 @@ import IncodingEvent from "../../models/incodingEvent"; import { IncodingEventExecutedMessage, IncodingEventMessage } from "../../../messages/messages-list"; interface EventListState { - events: IncodingEvent[] + events: IncodingEvent[], + eventsPaused: boolean } const initialState: EventListState = { - events: [] + events: [], + eventsPaused: false } export const eventListSlice = createSlice({ @@ -15,9 +17,17 @@ export const eventListSlice = createSlice({ initialState, reducers: { addEvent: (state, action: PayloadAction) => { + if (state.eventsPaused) { + return + } + state.events = [...state.events, action.payload] }, updateEvent: (state, action: PayloadAction) => { + if (state.eventsPaused) { + return + } + const { uuid, jsonData, executionTimeMs } = action.payload const finishedEvent = state.events.find(item => item.uuid === uuid)! @@ -28,10 +38,23 @@ export const eventListSlice = createSlice({ }, clearEvents: state => { state.events = [] + }, + + pauseEvents: state => { + state.eventsPaused = true + }, + resumeEvents: state => { + state.eventsPaused = false } } }) -export const { addEvent, updateEvent, clearEvents } = eventListSlice.actions +export const { + addEvent, + updateEvent, + clearEvents, + pauseEvents, + resumeEvents +} = eventListSlice.actions export default eventListSlice.reducer diff --git a/src/devtools/styles/reset-button.css b/src/devtools/styles/reset-button.css new file mode 100644 index 0000000..6dcc87b --- /dev/null +++ b/src/devtools/styles/reset-button.css @@ -0,0 +1,3 @@ +button { + all: unset; +} diff --git a/src/types/declarations.d.ts b/src/types/declarations.d.ts index 108edfe..e984360 100644 --- a/src/types/declarations.d.ts +++ b/src/types/declarations.d.ts @@ -13,7 +13,7 @@ declare global { "profiler-event": ProfilerEventElement; "action-marker": ActionMarkerElement; "time-marker": TimeMarkerElement; - "m-icon": IconElement; + "material-icon": IconElement; "event-list": EventListElement; "event-list-page": EventListPage; diff --git a/webpack.config.js b/webpack.config.js index 9f5e98e..a043782 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -30,8 +30,5 @@ module.exports = { } } ], - }, - mode: 'production', - devtool: 'source-map' - + } } From 0d776d80a1bfa1bb7f49ab2726917419d3142b23 Mon Sep 17 00:00:00 2001 From: semen Date: Sun, 12 Nov 2023 20:06:32 +0300 Subject: [PATCH 03/28] Remove build/ directory --- build/incoding.profiler.zip | Bin 105697 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 build/incoding.profiler.zip diff --git a/build/incoding.profiler.zip b/build/incoding.profiler.zip deleted file mode 100644 index 5cf2060451ffb608c21bd525aef684eddefe07fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105697 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
)Qvo!RZ#Cy=gd>Ub(Bw~nOog(Xzn|Btz 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 n6 z0e(Z=Y7i>G1n`u|J(xkps-j%-J3)VK^skPR6+m~o*;o%d?$yOQS&maV5o(64?)2&k z11x`PcnLOL$+r*2zX)1G#LjCn?@h> z3<0*M;C4N~8WXhbvO_KJYwNF5aab(WC`KEZ?oTlDGA?EQ+MOrR)eH66k{0;5@XZDK zip>^rh$Sjd;$VG^doq)$t#$e4Y!P)i;Av!Ds}R02+ZU(m5m!H*)>N|^70j#6UbEzy z&nH^gFoMjb)osKUXYM*YZOJLFyXu~KE~&f1>J%uoplY{J^4GW^)`G58=x&DsXP8kk z_!^GESq1DzN6z- zBkwXOnerUg)9N5TPY!@rjOersf7pZFb#=2dj;B?k689&QIYl0%-ReHddug0kx`R=z zGioFqGPgQ$DRR9AQAg-hIN@C~o{w~OvQ0)2x0wgYjJ)#OH0mHrM~PDAFi?J`TjXHk z<|eqGIZD>~EJ=P%NG0%`dtgF_)I)N}lB)Aah7XTqFxV&%o)^;y_1|GD#4{$oIN0Ho z7uoz1bSNLkv#~CtVpZdd8V%8|NZ+MFk6E1N>Va3~$cU#Ng<`8mok^Of5)D#C#OFS= z<|_*9pCiYAJF+eo>&RL3r%qYSXQSj5ETquT_wQZ~XE5rYHmjn@uF|TXl#~816eoK< zT2CipN*N>AS#(gx38LQLsN^r@15p7*^G%6)Qsi8A2Kx)m$0GeapW+$F*0nM+Zaecij`%NSkw1^C7$XK_^bjb7g7T{p zMq%jPwiW;c6Duu&Jg9y?!>)RO!Scr4v`Qw}4*c84T1k+{hn@q`g4LHz1EBJ;F~8e^ zq1&bHmC#>@Zeq=Qg3wB)zokQK0Tb&Fh3j%?^i~m2WR7cO)=DE|6SFuFS!!^C8N2xe z%|gDfKlw^x-FSf_ifMHd@aHtSPG)VV;77{6>Llcrw4Z{q-#K@$L(8)L%6*Ha<&?8gNBy>}-7$ZtLKXe|$eu&G%ehAAFcNH5YewRCT zGL=~Be)7<6XEwej@=?>~M7(krBy zcAGJuEq>VF`W`;|IbgpwPG*~DMFr>r2-8m>+h!oY0R440H^t{skj3n-f2OuQf_4y&6u?jUe4k&h4t~# z!-vyPZb^fr>dO5kP^^D82b|=a*psPQ5J15OVlzw5@tSMcJZvu?JQ|*d{qGt5s-9EL zb~9jZFo$4gl&}mWXcx(af~N;R!sZh;OQkVAl%xV{b2>t+1F~pEdPn?pye$`J*gB5! z5P;c3d0Pxlu~U1=|6!bdrZ^$#SivCiuHXc zO5#;CB5CpD2b)oZfSF^9&-?kSQn!PkIRY}a>Iaj}gup^4pI9)kqpv||Tm0|fy`I51 za2?R%LidP2NbcN~HZopWI*ZJr=_57OaFAe|Lk&a-%Ke z#Q0*@P%~Nmimj!nyD=5zHkUP|$HLcxZ@vLI-*oj7fW%GY#!^06N)uZT?@kjm9Jl*}Y%wgNbXW|Zprj2_Zt?lEmWbDeLLM&e-#4XS zogU@G+aC{Ky##tX`1$XHtwmG~yG4`%Vel*hkl9myTpJF%TuUKR21TEU2a$TOH)#Bd zfTEAs(pxg@ZcE!4lv$I2huv)01myE)uT_Jrf7x3^V}!h{otnsd(Oe^~J{wGx!oWn) z=X0iIG#hpSMdM;byP>{tv@RyZK0d6&1|u3FBGV}Ei5^$mJdFVdNdG&14mSF_7$?wr zRY&@iK0h0zlxsUZ@2YQk?^Y;2h9CQUUievZj=NA04x(`n1}~Vtoe$A?o^{ixii)V{ zOYevU?qvi_z&uslN5_DJ^ejX5Zpw+T09`5IwZP`gd$%YEW!rMhD$evD=I~UXKLc9g zEbHb`(Tf-g(5f0DN_l(#K8!eZ_T8 z3;338_f!`Ov-R5!G73adG0YZ=g(SX3bWZe+a|oFsHD$jG?>%gGIlNs&G36MR@SQ&Y z2!uRAy5P5HY7n(1WBT~kqS()$Ge$`ftv!m?V1>dCrGg#=dF1?R4FmTT2Ow8{EdW_& zmQ>uW1p^Kc7mMhUnnl3gb$g3#!5Hz{$If9g0TCGOAgD7Zo_I-S3e@f)n~Wy-3QQhp zI<2Z?%v$@oNLEv17uSbqM79LNHI@Fzwgo(g0W0R%t}Yr2TZDuml2cB8J4>NPWZDTQ z96NpiVKBaX@W2$M+I?fKhfag?kXoWUFJ#*MQB-B%3C*{dCuY zyx6^r^9yQjBG&_VK@fQrJZeVt+UM+nQfKt%l`5amNxE;|y5Nwq$Ha$H2Lc zjs?efNSziM3k*z`^s=M$csP4H>z2T~25D5_dqrbQ5cxvA1GAtR)JJi0#a zP2fMnqKjptDJDb!A-Xo8Ttacmy@R=TIQJFihJ*;Qont498qhj zGQ0Sd-?S^)G%QZgs7qA0*-}NBH$aLv!+G~(bRF&VqEGO68T}Gn^?K1w|1vJAl|4kA zdonloZ?OrAa*QGX5I$}TdQCuz0S>b8DEk~C z17}&l?n(argdfP&w?8ejj;5#?HHrXF06zhnK);Vpz*Jj0v#OnH1wW zfTsgJqzcC?h{p-6^mt-v-0!>vIU1M@FPNPhn9Cg&+CbbWP(YwavIumeGmS@A@dcWi z={3&B^kiB*s&WG()C!XZ+cAs=S+f)J_B#hj(z&dvX*qa+AN%9vzwS*t|~8t{{6k!UNZ5 zu`&VpMv6Z@{_Eqj?;Z(d<#jr)E*+iyWrAejBnlJBEFPzL+1n$sB#?GLQ2Qq9Vr3(g zxgam{q^VeSIiH+0G(G+|K3U};b|IwD{ z_QJAUXoE3g-Z8%$hKV|3ond8TOF6z%2B!j+t;sXue4ZJQ>#ZQrtDKw@l%J@rxkg#M zAIM~wZg_QV^r-IE`ye^zV!5PwGS+jSr}~PVGVtDS+;d^8cG>y5W^FF)h3z3~D)oU3 zTOlf(uK`zGQ}zFcE5RSG1f8$yNlqY8AhMTOn{e z*`=1Fe4qsGCO*jj*W6HMqebC>mpMN-CO_ajedw)OGQc_A^3b8=fy%e9Enm$}47hlJ zC}SJg+d4A98aAKgmREvYXdgX%_`g{n(AD5PE3P+hEYthKGo|lIrzqEq?tPGpM*ur| z5m7oEpDUtqNbwtw8HU{>*(Ohev{O~FIJ(RJ{w8B?HsVJXEohUzvnUAnz1zB$xoc8i zIh2V!M#I$tWjEeipu{chf+|Gr!(u7|HG7|aqyZ1Q4l>WUyOV;d3LO~6L7N>JU~Xj6wl(pMo*WBB#vqw~` z*dtQg9#OWmN3_|K|JxVZy3yX{DDF$wDS>3DZKb}idQqj-_sS;%4des}?cl?zkAB{t zKPv-t3Q5AsYZ~uTX(LXBT-JNCg$!R9ysQQ^>2l2hAMaX)CUGsK_=+TlL}?SUjLUOMTG|B?R`ss zq&7c|j!L1)pGAdbZl<}OR>FTHt%l+Rwl?CX5>@x_Gw01!rmp&C*4+njBdxj*K71%2 z=w?>KCulRr;poB7Fm4bM%3gU?9pAsdrUq#?6t$T{G?PO#BRw>Lx_@7^oDRrV$_`|@ zjg-BG()sqK5`_i{=hwCbAP+HPTW`5lKs1Rox+1NKoZ>JnRKEyyMOpCI*05LGsYbMD zwn2E2*TGqsM|P)QeT4rYR`RZ3Ha%}G2BSVd9xM1yE# z2y0~Iq}A1Monhf#l#=>d;y4i>YeD;@&g+TvvOdvRVq|7^Ez+CKg@S75nspO$jF|B} z$fI=V?M|~{`orU=)j$l*?CET{bPpO8y}=Bpl-^kmhxWeGcFb|Vs_kSi8+toT+2(Dz zi=<8DXqadN*XYUc;r0mjjNN>g9F2~97UdO%{*22=Qc|9*D= ze$Cd8%0<~i-w>h^sp9utcMG7;VkknW^m<(t380&!cbGX0z-wkVw{dqyz76mWvy&Va z(JqVT6sscY}_88OH4=3r5VH*k^PuQ{vGo%`d=cqD+brv|_z|RV!%>V|c%|eBM z$)Ztj5qE(p$=g-{%ID8V$abk0#0i|17~W4qw1$0Hr^8C?1KTv3^x#X3kMJq!8w(z- zTm7ndSzKeN*b)RET}5eBfy*C{A0k9$8S5--YY8ydwWN=Fc3kdtuDJmSfqwoxHlciT zj^Cww;?YqOAs>&9j*pMos#eJuCL$N)0O~l#HTE(+$5Ivs5Ckcp9DFyDb**5AX%+hJfsx@Q4WfjC5Tl&Y}uQyyO7(sLZBMKm2Ih7Vd2ouShL zx-3{U^)9*C`!s!u_~xLERd1k)E2^}EPa;-J>QOJ6A=b=<9P9hA4zkxe(vb8G09N%- zcntkeIQWNEZg!L%Bi0jKN_TgPDnN)$WWBic$CP37*%pW z2U0dS(dwg;`rjLA7qGET#E0o8HyJe_1F~^AUqn+MKYkqgu*v2@Km}pb_&(sHDsJ7q zP9PmC4}B1)W$Z*zDGj55f|h^(zG&YT7|E$yKqXIG^@Lr<{5|yo7CF{tXWwWPfLs+FgPPe%-c}L z+TK!k`u_&xCd^-hjsNF=|Gzi3si;swK7y}o48$iR5q+>WW0)EF0Ct*qWQojdeA@y& zqQSHC=&#>TKvacQbhDUO+v>vrqeOwI_1A~v9u03^|; zh+tz|7(YEIGzELQMe!MMA6T}xDd#B8M%{dRBu?GovuFsN zrIXERY=dL~3p~YiTQlz+*4j8N)kr;bS1jxWF0rasJ200QrmN`7oW1B4oxr9b z0)aeK4c7E0NME4XOfvmKRj8%W5#QA#gfsC+N$KBowOFb#vuz-0Nh3jQ!J{TvfW`NK zQT0~fuT+9zR~~Z$L3wJNuKN60;?2)0cH{tkc8C!LqgsRaf{{b}9%~pr!Z55o?8yu} zS=RNlu%O8K1iML^b3QV1)>MjFzA1q<@SaNxaAnIteKo#=)_^M_^^I_ z3Oi<@xZcmJ0#5*UZ_kp;_+tu_^22&L0YI040A8LTg_jOUp_R%=SfGdSc`=%oT_h4` zc3eO|dJ*kx*eiBWMc9-{US4|?<(3pJQbCL!@Z&=%PAl#)TP#RX1WkFtO?2s@%YwGSZx*auLN%M* zfEqr=#pw^gPf^i1)asEZ_ijy9Cf#8^#jUm8G$co}W7y(UPhrPbE&BIvXN%LpEE)~x z-CQXy05~i^0=QhNKdg6ba>#%jI{nLZ3>;gZNM*M_*$>z)(Q#A|i*pGO+6D!s@#@}f z+P#G&Y=j}}tc{|e7_eqkqPSg#+|E9wJ9>&FF{tCwcsPdeHCqfiKRm$Z$>Xq$ay(en z%_GS6!-Fh+a*E^k!vk26PZvf87?B~JS5mu9-0}-Fu2qt8*{!W$(^xz$o*K2OQyVnK z1LKP^^aH1PZ(00gH(5W?pcE7PLTEKcK&1c%jIH!7>N9p>!L0_>@NqPWKfR=F>7W=s z`hEfPrl;nRt960BwM~a~5{d!`bRXj4Kpy!cTqtk(I=C*XbpRm9keZwaq6et9b?BOR z$WK44-*y(Huej~Nf{xGI3`1&t3=oMfb<{bgQb$zkSe23&A&O$BNJ;Dl#ccN>@Qwv0 zF{CG?FX=0d$%pmRZQ;r#D;E43m1$9l{`ICx8<|`K2BV(6WFjM!5U1D=mvbaN z77`aX!lz+)Gy%TbJ(_h95{SUS9tW2Z9R_FGUi6Nm8^Z{yBphM=#L{`!%S8CqxugYx zOaT907CDFLsI@`pECC~g{%D#7`VXu^%7$RuBAToX%S;QF0k;&2bD&NPBp`xGp@32A zVQb_wd-aEB-XETMUoXhd-(u)Ud{xdL802Qf&bfmZUQ748IzP9KWo~X80Ut7+$w4nN z`_pnivZa1m!oyC)Qwu~J6l_K2a2&Jjf-rVgf{Ha5cGfZ?5r&I>HRmjYAe4ub)dci( zGBSm|ms-00SYu>6(a7ooJ~Zy2xAdYBR9;to?~iC|e=ypw%xJ$_WVB_x)!>W!z^7b> zXr?tD$GEd6Z92mE>79*@RxYxJcQXFa97^$kH_MyJS&@~fi$@L2*ky*M6{NHAN)+oH zaIwxCQEX#7Q0!W`E$4X9DGTLbrFK&Wv`BQezMFn>zM3Y9&Lj6@g7&vuCs4D6RhDUaD6tnRrrq?vY@cquJne%aTQv50mPe}~Z z1ge70p|2O6V!Mc4%u|7){3Hxgq+lZ?T)-Z;9Dy8Sd647JG<&#c5M*_F=KyK6Uz6Lf zTCxj<1rzHaQo85HOB=NnRzek1i&rCKe%h-bR%Wk&_Vix5^i4Jq#D-VRAFJITtKC=M zR)o&`5cz$z6|waZA3IvLcMzu`PVlmWKA~eQKPK_GW*g#vUO6hHyINinICp83tIw7` z{Q6Zmrg>U}YYs<_6lZ6J9$UwngmG9rp+8Nr4w+AN@IQDe1hb#S=oSaN4gR)rBt!tw z7zxoJk**sBv36uv@YvsEY`Kp67DUT^|8A$K`>^yR*`_!!B)S_sqU9d>qVvpl@xE@p z1SO7I|AhdxhK*^pN6DyS4^gs0Dt0j|t_Q#dIcdc>xDNaP;76+A^30k#u@=CJxqo!f z@>rq8^ap6u!X;<4|5%y-Sed_ORBs|^8K8s?6xDzB^ns!BlD*Zf?Z3gYUK+=0p-CK! z0(f9ZGl;DAZG4gVt1+ei_l9ORxl4F8^#VB)&g;V!d*cG@rk}R7uvMyMOkx9sPznBE z(NX(_+gf?W;4NElP4sL|p$sqsd%;CO_Bu=Cd;tQW%EPo|(Q@z_6dg?7lOZ>@!;$HA zV0vL&ge@@g4H-ccCa&nqvzz^KcLTp~V0-ai`bmNjT<{VMbQCJ5;wmM&KQ;LnXWflG zI?alX2G20m5}pm%h&o-qu{pxqW%1^39RLIQmbhmg?{0OO zS_7N_)GzW^Nm&9``oDlUI#&=pfvW!J;EC@rbT zn1!p=CA}KzbBXvT$I&_>q+A~yt)HA+CubO-=>)GS1{wC`1o+1V{!^Dp!%M|~UWg&U z*lh5RFF1yW1LdA3UT$^|4%V?6v>o$TczrHL93GYS2c97>1rIp5OcMNtHL<1>LBPL% zKo?%oEoO%57Fz}XPVT|N$N^IC!7CkA#f)W%zJ>_R1YN*C7ImB zrBww_HDslf1uv>Ws0{ht99Bq7$E-78)5#bQ6=4#>O(xUo=75AlD$G8zg1+At*$rMi zcfbsUDpM|#@b-*dl;Md*Ab22`9r%l$6VxH$I|0wuAs={uO+L_wUp(6be`D-0{xvcQ zw5u*EX9sF6iI&)txD+-*c)@>j_=g-(bOc+1`f?hBJwy`^QCd=*y+quf$B;N_%y~2Z zgYaNh?ekSr>8}KeuBL=%?83Z6Y!+u_GW(c}RW}jMV#g>1!<5p5zaImi@}G~`Jz_6X zh9m^WgonJ?8Nt$462U+t4dI@t!x!}t6_!X!D5Lwmxr>ZWx;_cl8f*M zo=W8D@aCK+hwhfW>v&BQ*G(060gV_A#$n)7C6w|(xPl=e5Y3-wF_SCM0+G%M zq3R4CqZN6CIPFM1#NNs1ij-sLC1PP5$4PJDbt^toLUiY%#)uClo+EH|iCc8?iB39cD2sZWHXPATcWh zKD;AuNr7v|%omXMa9&`MDK!UALBazrOIXP@^BjWL^gU-GP6I ztX)<6;Veo15ocFK{BPmk&q#Q2S?weJ<6Hz$gD9@#wGRZF60anM7hF5gnU2{6g5)@D zuNi$KMW8eql0KPm6xm`%{DOrL5FY;(0OZ+PrY(zIUl{X2axmPMik4m2(}j+J$~nEx z!V1>Qc>WRo;V}IQ|B#@n_~JEZKq-le3DKSKy<6~~Ok!W(eZbWChW{M$ALLB0`0;1{ z^P2xW=RdC)i5hJf&R-4>5ZA$|APn#hN#uDa5G5kn!`uJjlKcGUNB*!oolKL|E)k82WdKi-USQ?47KH7QoUU~2=6$8wuom*ND{ za+({%A=YlZbd`k;Ljt)j84cXv_`A=%Bx*e z;}Tkxa)R0-)mAk7W-LZy3)fw_#1fp|DbdmZ#uqZbqC2pR7Rg-(g|2cw3KcwlT1P2` zF^IU6cW*HRoy3(AhxwJLIY+dT;6_}RK&133LuY2Xh$RMg$mz?bd?>5x`fxX?vTpf-Xhvx77%<1yt)jD zs6Hq_HF8^2A!-;FZ7?G@3uWT zNJMkXVtV?$O`-P1Hci(2>|HT_fxEi`Cd%ZI?iUhi!11Q>i8kNT@bg0wXX#rVm9JbG zm4Ol8hLO_tGc36QTu)j+5TgC4zR0!4X7 zwCHD|DQ!xdRi`X3BAI({U@_SFK*oiRkzyAh^Q=1T&S(|QWzuA<-D1&f$=8z{EQ2l= zTFJ-d2-3k-Z!KFfeWBK<NEyHYM*8V`e{B z&Z}7w5zWUyVyKibw`}p0jMUw6VbBXH^g%jU#_CzUNLJF7@!XYkPPbv_V;e*=@I9@l zdlphp_10|2C%2@_S6`<=pEj_neB!45_D>ygO>rR=bZhN+BHUcL1zb$ETE|o)PxSc4 z>}`_KGFwIxarwnfUR_7+V%q#_5~WF8t)Sd?qC)19(e`;N-qvNCbc2GZ`?!Am>EhxY z>$f^HA-XK=ygT>_xOhQYasH*WL?q%l&PmkWltRXLBqjRn4FvFyB_bG^^Sh-SD6!SK za+TL7R6bAz8mwS0&VFXZVRq=iF4&&w0NSwq%*S;}q}Pk(Zx;GdARgsFAZVGP z*H))Om&etvZHlVVd5>Y+gR-cN5g7_}Ol%4(;HP1J;9J^)8f0td%n)dzS)dw~&Nvi3c3UNflf=57L^za}PYwDO6tM zkF5dn9S?Pfx0`Ig-b=`PpHjq=H$+Hf)(a%ccj(CkyIT4>+^Rg_#Sz6b34K2ICIPP; zi_)_8BYx3kX0P_AWquZ-kovyxjc=5DO5Du= zPn0}Rl3r(4e0V#>yM|^qg@yr{1?5~R5GjFVm?(wC+6BhOwFrIFxD zq9VFdai(M6Flk7-R||J5Kno%BrDENEuST`;$1XB!}X|2 zM}FIb9MMg;oB*-;L5tlnYDS$8^`Oob;gBJ=8#LooUDtWTM1L86S{hGQTV<38=Z2|t zzF9P~AT_@4XqV@8I_YqO!zQo(8}%0~WJLTfTWPni=3@mag&CfXTk8^W1Gu4el)$ag#RibYZCQ*?l-w4QtfVY!tt3KKo#o)V1 zXIGHqpg05HM~V$6WKLGAnm z9TBcBb^7BI#vVg_JY_~|gCEt6)dqzOK%fDFnmB5N_YMl>LjCAGPniS9f<{yW?zN3_ ziA{+UCDEXcjZ1b-gYQ!r8pIve#-?q%9I{jTSOETrCB=v(y1C7ziwyt--~s@Y{}JnC zXN$1%fVU4T~^HkOcjxs={>tuDh(XY4I z3XWc~VXuYc<$7)A1m3;;I{z~r0_>s_THbrSNMI`>ON%@h{BrXhAlS8WNr6Xq?pTG$ z=d(lYY>QFGHsJJ=h$sxk%;XjX9VA@E!F}6+9WNY*9tc+a>c7a=JtLgw|D^wI?mA4c zB`=-DBb`zul!SR{EkaI2+xQjAjQ)Yt6YZ5|?p2OFo%rVdIBCC{iaY^tJh#%gEYEDl z4fXVq*QT*9<*+I98(m^6~KUdKY6V{gA4s+`>O4rYfMUn&*aLeutBf zfwF}w)Wqb*S@b1PLKK@3XZFQIiA*0CbQFS!FgvVnFTxoU@ev|aDE~Pj}_)gOLFXF znM~%`6}~ccH>Z_wi{@rHDS9UAh0bK-jQS@`igqgQcn9KMuJhwYh&QHXy=D6$JI zGPX*$--L}O;{`P++2d-R$T2{=R0@p1t5=`iF}ha~2a^bSoAx|v=*9d3aH%w$o0B!0 ziJNudCUaXttE&Z&&lr7pb#ZzjGq!dEelEYmOJ?q7bV2vG`CSk>hz}9~0I8T4f%ad` zPteucBf-qm>$3*M6)*RdxFh4tC{kCZ-CsT1V8-AjwKGj`jE5rct<08BGwn}K8e1|n z1)omJq|EK?e7`tu3=Hgunu$enXncKm@aj>HikGGH_RqgH2*lVXI^w`x-?(>{)zq;&aCf+ZqzXkq2Bym^m01WV`-H% zx7l`lDd}U={GGF?NBb;OuEMX161&7&wwi_H9+$8%HUuHOzs=&HhvYmq8Xg-ExLVv? zI$k;y#Pn=sJl653-1=hFE8V$Y@K=^AsblAfG6;p%Q{&ci!{zknzrwebZ zk5hEP?>0jaX~4Y}y|+HJ^utac}#7Q-(*q2=uSMtWT z-S=nfkSsHhIVpv`(K|1`!bbNQjOXM(d>xSk5%nF?+}ZFZA-)Jq1ds~CipXt)zw4p%)U;s(L0QaJA+ABVf`DUqa47J|c5{sT;<4o%skf|#*Tj1cEgK#dxMC`0iYRan& z;y?{1fOlj;;5$lrDEi`iX+_TH`Xz^MUXB>A+b)W7^*!0~sJP#bwZ(V&wVPgeh)}Dm z3vH2Ak7pA+dpj)oG>0i)mG&K?j9lN2uqnSVw@qSgGTK8HfWA62S}C%1piEhM$iX#w z_4le#@QeO-(@LDDm$YCdb3f>;0llKa1D@zc7PB>OYlQA`n>@K*!}oLB+B@JrQ%A7_ zK~TQJ=&$=cI0eE?ZARZMN4I~?Ax?;$kr&4sL-@@^HPS~q+klw@wOT}X(OFQ{{J#63 z$6?#FqsktLLD>+2D0h)2d;W}(L{LR~^##2&rTNBKWLEszy`x!VnyNa#KfiAeC?VZI z+-P<|V)i%#8&#cp`MX%;47YrO2$tu{_ z!^W#f1KTE_=fJaG)j;l*yeWi+dP%0hiC$_ejrOY)OE|Jh>U0`Uai{B5i^O{o$9@v~ zWZ705OCJ}}&fa~up0AGIxu6Y_r#GA{9^c#FS%${ySeU+I-AWhPx6kL_Q?6#?@Vv6`aJS-YNZ%^03S5GOFcK>{~;J} zRGpL+SLH}sXn9kYwgmPw#8U{SXxg{O9(NAk>PMjjHCSU@h?Z42H(2vHQ;nMW?l^K1{MsSnqubqwrz3S|03 zEx%|rb#F5cuUXI*bzQ=7oF27I7QXfAN$8Atnrp{p|H`0rABFEN5MS?92`F^|cbm!9 z>7gh$uF}=pyI#P{=E zo`T0$6-{(g@k!3hMWSO->d<}$tHKOS*{F;4!)PeBaP~X+or*idYhcL=8d`ED0%M0~ z@{EETn0_rU3p_=l9DRD96TTbr2Nu0v&hfZ)9}5Eat8VB#V2?-dYhq$v~1sL zS6s4RllHQ)^6ezU22Hj(^)bgopNl<%B5*7F;UM6A^s1;;$dBAA=}ILI)k}-)04a_b z^38y8r-wEm>mCWIvx0{AtIb7KPWw{$8cM7<9IE=<#iPCu*M7r|pg7^kKxrK}&0l3e zMMDyZkGiO)l}$hdoG!{MPbRGf$981pB=AzwE(P?gdt28loi*iFdpvC`XSUPsBjU3} zxPj>XJe;$<(3RIe-uTI*?oEq<+UVO~zQ|0B+n+0owQ?8jDD&7ad>a2^ztSmWd68F3 zLa^1={<*ZU0r{Aytay2eedU9c`?nOr4;)=C%2ruP8yf>vMAr^}IK0988Ci`4*3|Ul z>(J2Ns?1S8CV#A4DLYacA&YC61N|%-GHv&Tm-Q$NxjZ*jfNd6WPzjh!o*DL@wcTri zQ>q=~W%Ax|p0`q*v=0uQR=iMkDd(dI213qWSc!b_c1d&JvLzXL;> zS9!TOVZ7SqhUbJ~Xqi`R1L|N*`@Vise`B6uSV%^*C`D%b%|rUIDBx55^}PFF(GEPB zR_UjL5sm}*WBiNAV#TVg)5$XYZg9sb3+LQ~x^Vc!Qch%m#&-5UCz zLnm{b5;3+2DPXtVrWA99z&1`d0b#(6$BJroZm+a8W{x;6=Q&6}+C@r^byA$p8+(Pt zb}B>T^V(VnVOsmSWt-WeIXUx7RvnX?K6O^UMfpZ@Q6o~}W7e2Iyp7FAyPIvHSWmX@ z@ZFd6lK`0o|GlVtXMCpsEC3)4GsynAsNY8su?&Kn7;2-<5fX@FI2=1%Q&5^Ho0==w*TV&N$0l$UK`0{m1h$%zo3y$pK?QY zE9A6{?&uYZcYK4=_4sJKU&D=nfZK6&^9|6np)WUXjyTr-urU*uuJ7>lboMx%f(*wt zGs!+{7!Q=>l72`z2D7*GLSWJccQ@Ch znrEHDq>(2I;8X@NJh>d-LIm&|o$9DVb%w^q#!Ipro21k7e zmtSqP>LWazX_TyxJZ{u5uBKYG1c>t{sKK0KM7kt&N$17>kp|#nR{Ew0R zPS@-(-D1u#JNL!x{CB$MVeMw;@}G1qK~G7iOPHedgr=%m`)k=ZEMrg1J6EG93Q*JB87e0NgB?? z%+EzM$FSL4dRaqNif$~M*{Xf@qo8A;L$6S5m{RrQxV!zncA)9IEWrM*L7&D4{3)xC z^Si%lb(`7rr4_V&zi&EYnK_Y6unR*3^dMmau^ae|HZYlFFf>t@Kh!nG8gvWo!Ol7a zWfgG|j5HCF+EAI^$_~c*TSe_P_&r4!_4wfYw~D+F{w{W2g0@~>-iF#hRF($yeu9mqi+=*q3=7%-azA{P2&|sS8y1E6Fya z`qFVdZ5Dhk5+=I@fU^_PaF?j2uxs z0D$KIWCH*%LwYdr=KtaUwt_0X%JngQ0KoDH03i8m3P(}^0BYxhaCdj{`WM;r_TfxW z4DbQ7bh^JlsO4Xvpq9EaSjP}7i10&P-$2lJHzgk)EWoNZE`a>6NiMEUa!q;vR#W<4 z{$)sEG}b>VOxJ~8FADwP=wg0z*Goj#`RiHzA3iKqru8Y1N zy#HXlssF&POZ=^^>%r^~YK%Dw{1w{%DfQo?!5>sN9qS*l;W~HSMgHO78CZZn4pUDP W5AzBEFcv@#NW!>N*({7r0saf5lYEH) From 936f39cd12a89238d4c35599e6d5ff7805c7a6d3 Mon Sep 17 00:00:00 2001 From: semen Date: Sun, 19 Nov 2023 16:41:40 +0300 Subject: [PATCH 04/28] Add search for events, improve styles New `search` field was added inside EventListState to handle search, search logic is implemented inside `selectEvents` selector inside event list slice selectors. This selector is memoized with redux/toolkit and will not be re-filtered unless search or array is not changed --- package.json | 4 +-- public/styles.css | 1 - .../components/controls/button-icon/index.ts | 6 ++++- .../controls/button-toggle/index.ts | 4 +-- src/devtools/components/index.ts | 1 + .../components/inputs/search/index.ts | 5 ++++ src/devtools/components/no-content/index.ts | 25 +++++++++++++++++++ src/devtools/pages/EventList/index.css | 3 ++- .../EventList/panels/event-list-header.ts | 16 +++++++----- .../pages/EventList/panels/event-list.ts | 24 ++++++++++++++---- src/devtools/store/EventList/selectors.ts | 20 ++++++++++++++- src/devtools/store/EventList/slice.ts | 17 ++++++++++--- src/messages/messages.ts | 6 +++-- src/utils.js | 3 ++- 14 files changed, 110 insertions(+), 25 deletions(-) create mode 100644 src/devtools/components/no-content/index.ts diff --git a/package.json b/package.json index 27be53e..ff11920 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "profiler devtool for chrome for debugging incoding.framework.js", "main": "index.js", "scripts": { - "build": "npx webpack build --mode production -d source-map", - "dev": "npx webpack watch --mode production", + "build": "npx webpack build --mode production", + "dev": "npx webpack watch --mode production -d source-map", "lint": "eslint src/", "lintfix": "eslint src/ --fix" }, diff --git a/public/styles.css b/public/styles.css index f46caac..40ea347 100644 --- a/public/styles.css +++ b/public/styles.css @@ -55,7 +55,6 @@ html { background-color: var(--bg-color); color: var(--text-color); - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } diff --git a/src/devtools/components/controls/button-icon/index.ts b/src/devtools/components/controls/button-icon/index.ts index 1e7b78c..55ec970 100644 --- a/src/devtools/components/controls/button-icon/index.ts +++ b/src/devtools/components/controls/button-icon/index.ts @@ -14,9 +14,13 @@ export class IconButtonElement extends LitElement { @property() color?: string + constructor() { + super() + } + protected render() { return html` - ` diff --git a/src/devtools/components/controls/button-toggle/index.ts b/src/devtools/components/controls/button-toggle/index.ts index 580c0c2..d2eed0b 100644 --- a/src/devtools/components/controls/button-toggle/index.ts +++ b/src/devtools/components/controls/button-toggle/index.ts @@ -7,7 +7,7 @@ export class ButtonToggleElement extends LitElement { @state() enabled: boolean = true - @queryAssignedElements({ slot: 'enabled' }) enabledButton: Array + @queryAssignedElements({ slot: 'enabled' }) enabledButton: HTMLElement[] @queryAssignedElements({ slot: 'disabled' }) disabledButton: HTMLElement[] @@ -31,7 +31,7 @@ export class ButtonToggleElement extends LitElement { } private initSlot() { - this.enabledButton.forEach(el => el.style.display = '') + this.enabledButton.forEach(el => el.style.display = 'block') this.disabledButton.forEach(el => el.style.display = 'none') } diff --git a/src/devtools/components/index.ts b/src/devtools/components/index.ts index 9c344c5..8a3e02c 100644 --- a/src/devtools/components/index.ts +++ b/src/devtools/components/index.ts @@ -6,3 +6,4 @@ import "./controls/button-icon" import "./controls/button-group" import "./controls/button-toggle" import "./inputs/search" +import "./no-content" diff --git a/src/devtools/components/inputs/search/index.ts b/src/devtools/components/inputs/search/index.ts index 10f42c7..c577dde 100644 --- a/src/devtools/components/inputs/search/index.ts +++ b/src/devtools/components/inputs/search/index.ts @@ -2,9 +2,13 @@ import { LitElement, html } from "lit"; import { customElement, property, query } from "lit/decorators.js"; import SearchEvent from "./SearchChangeEvent"; +import styles from "./styles.css" + @customElement('input-search') export class InputSearchElement extends LitElement { + static styles = [styles] + @property() placeholder: string = '' @query('input') searchInput: HTMLInputElement @@ -17,6 +21,7 @@ export class InputSearchElement extends LitElement { private onsearch(ev: KeyboardEvent) { const value = this.searchInput.value + this.dispatchEvent(new SearchEvent(value)) } } diff --git a/src/devtools/components/no-content/index.ts b/src/devtools/components/no-content/index.ts new file mode 100644 index 0000000..6e1c2ab --- /dev/null +++ b/src/devtools/components/no-content/index.ts @@ -0,0 +1,25 @@ +import { LitElement, css, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +@customElement('no-content') +export class NoContentPlaceholderElement extends LitElement { + + static styles = css` + :host { + display: flex; + justify-content: center; + align-items: center; + + height: 100%; + width: 100%; + } + ` + + @property() text: string = '' + + protected render() { + return html` + ${this.text} + ` + } +} diff --git a/src/devtools/pages/EventList/index.css b/src/devtools/pages/EventList/index.css index 0f83905..202f34d 100644 --- a/src/devtools/pages/EventList/index.css +++ b/src/devtools/pages/EventList/index.css @@ -13,7 +13,8 @@ } event-list-header { - flex-grow: 0; + flex-shrink: 0; + border-bottom: 1px solid var(--border-color); } diff --git a/src/devtools/pages/EventList/panels/event-list-header.ts b/src/devtools/pages/EventList/panels/event-list-header.ts index 785c464..d523f7f 100644 --- a/src/devtools/pages/EventList/panels/event-list-header.ts +++ b/src/devtools/pages/EventList/panels/event-list-header.ts @@ -2,7 +2,7 @@ import { LitElement, css, html } from "lit"; import { customElement } from "lit/decorators.js" import SearchEvent from '../../../components/inputs/search/SearchChangeEvent'; import store from '../../../store'; -import { clearEvents, pauseEvents, resumeEvents } from '../../../store/EventList/slice'; +import { clearEvents, pauseEvents, resetSearch, resumeEvents, searchEvents } from '../../../store/EventList/slice'; import ButtonToggleEvent from '../../../components/controls/button-toggle/ButtonToggleEvent'; @@ -44,7 +44,7 @@ export class EventListHeaderElement extends LitElement {
- + ` } @@ -53,8 +53,6 @@ export class EventListHeaderElement extends LitElement { } private togglePauseEvents(ev: ButtonToggleEvent) { - console.log(ev.enabled); - if (ev.enabled) { store.dispatch(resumeEvents()) } else { @@ -62,7 +60,13 @@ export class EventListHeaderElement extends LitElement { } } - private searchEvents(ev: SearchEvent) { - console.log(ev.value) + private onSearchEvents(ev: SearchEvent) { + const searchValue = ev.value + + if (searchValue !== '') { + store.dispatch(searchEvents(searchValue)) + } else { + store.dispatch(resetSearch()) + } } } diff --git a/src/devtools/pages/EventList/panels/event-list.ts b/src/devtools/pages/EventList/panels/event-list.ts index c67a31c..86a6f74 100644 --- a/src/devtools/pages/EventList/panels/event-list.ts +++ b/src/devtools/pages/EventList/panels/event-list.ts @@ -43,16 +43,30 @@ export class EventListElement extends StatefulLitElement { } protected render() { + const hasEvents = this.events.length != 0 + return html` -
- ${repeat(this.events, event => event.uuid + event.executionTimeMs, (event) => html` - - `)} +
+ ${hasEvents ? this.renderList() : this.renderEmpty()}
` } - _onMouseWheel() { + private renderList() { + return html` + ${repeat(this.events, event => event.uuid + event.executionTimeMs, (event) => html` + + `)} + ` + } + + private renderEmpty() { + return html` + + ` + } + + private onMouseWheel() { const containerScroll = this.container.scrollHeight - this.container.clientHeight this.scrollAttached = this.container.scrollTop === containerScroll; diff --git a/src/devtools/store/EventList/selectors.ts b/src/devtools/store/EventList/selectors.ts index 04feca7..b413eea 100644 --- a/src/devtools/store/EventList/selectors.ts +++ b/src/devtools/store/EventList/selectors.ts @@ -1,5 +1,23 @@ +import { createSelector } from "@reduxjs/toolkit"; import { RootState } from ".."; +import IncodingEvent from "../../models/incodingEvent"; + +const eventsSelector = (state: RootState) => state.eventList.events +const searchSelector = (state: RootState) => state.eventList.search + +const selectEvents = createSelector([eventsSelector, searchSelector], + (events, search) => { + if (search === null) { + return events + } + + const predicate = (event: IncodingEvent) => { + return event.action.includes(search) || + event.eventName.includes(search) + } + + return events.filter(predicate) + }) -const selectEvents = (state: RootState) => state.eventList.events export { selectEvents } diff --git a/src/devtools/store/EventList/slice.ts b/src/devtools/store/EventList/slice.ts index 8cbaebc..4b9686b 100644 --- a/src/devtools/store/EventList/slice.ts +++ b/src/devtools/store/EventList/slice.ts @@ -4,12 +4,14 @@ import { IncodingEventExecutedMessage, IncodingEventMessage } from "../../../mes interface EventListState { events: IncodingEvent[], - eventsPaused: boolean + eventsPaused: boolean, + search: string | null } const initialState: EventListState = { events: [], - eventsPaused: false + eventsPaused: false, + search: null } export const eventListSlice = createSlice({ @@ -45,6 +47,13 @@ export const eventListSlice = createSlice({ }, resumeEvents: state => { state.eventsPaused = false + }, + + resetSearch: state => { + state.search = null + }, + searchEvents: (state, action: PayloadAction) => { + state.search = action.payload } } }) @@ -54,7 +63,9 @@ export const { updateEvent, clearEvents, pauseEvents, - resumeEvents + resumeEvents, + searchEvents, + resetSearch } = eventListSlice.actions export default eventListSlice.reducer diff --git a/src/messages/messages.ts b/src/messages/messages.ts index 55c858d..2784ee1 100644 --- a/src/messages/messages.ts +++ b/src/messages/messages.ts @@ -1,6 +1,6 @@ import { IncodingEventExecutedMessage, - IncodingEventMessage + IncodingEventMessage, } from "./messages-list" interface Messages { @@ -13,7 +13,7 @@ export interface ProfilerMessage { data: Messages[keyof Messages] } -export default function sendMessage(source: Window | chrome.runtime.Port, name: TKey, data: Messages[TKey]) { +function sendMessage(source: Window | chrome.runtime.Port, name: TKey, data: Messages[TKey]) { const message: ProfilerMessage = { data: data, name: name @@ -21,3 +21,5 @@ export default function sendMessage(source: Window source.postMessage(message) } + +export default sendMessage diff --git a/src/utils.js b/src/utils.js index 8ea8537..3879be4 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,6 @@ /** - * Creating Guid + * Custom implementation of GUIDs due to crypto.randomUUID inaccessibility + * in devtools * @returns string * @see http://guid.us/GUID/JavaScript */ From ed52b4a92a649a3dddc691ecbc65a01665b55c9f Mon Sep 17 00:00:00 2001 From: semen Date: Wed, 22 Nov 2023 00:34:57 +0300 Subject: [PATCH 05/28] Fix #15 for background.js goes inactive Preventing background.js from going inactive by periodically performing actions to reset service_worker inactive timeout --- public/manifest.json | 2 +- src/background.js | 93 +++++++++++++++++++++++++++++--------------- 2 files changed, 63 insertions(+), 32 deletions(-) diff --git a/public/manifest.json b/public/manifest.json index 1c73ccf..9c6201b 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -11,7 +11,7 @@ }, "permissions": [ - "activeTab", "scripting" + "activeTab", "scripting", "storage" ], "host_permissions": [ diff --git a/src/background.js b/src/background.js index e16c25f..91093b8 100644 --- a/src/background.js +++ b/src/background.js @@ -6,8 +6,42 @@ 'use strict' +let keepingAliveInterval + const connectionPorts = {} +keepBackgroundAlive() +dynamiclyInjectContentScript() + +chrome.runtime.onConnect.addListener(function onConnect(port) { + let name = null + let tab = null + + if (Number.isInteger(+port.name)) { + name = 'devtools' + tab = +port.name + + installContentScript(tab) + } + else { + name = 'contentScript' + tab = port.sender.tab.id + } + + if (connectionPorts[tab] == undefined) { + connectionPorts[tab] = { + debtools: null, + contentScript: null + } + } + + connectionPorts[tab][name] = port + + if (connectionPorts[tab].devtools != null && connectionPorts[tab].contentScript != null) { + establishBidirectionalConnection(tab, connectionPorts[tab].devtools, connectionPorts[tab].contentScript) + } +}) + async function dynamiclyInjectContentScript() { const scriptsToInject = [ @@ -47,37 +81,6 @@ async function installContentScript(tab) { } } -dynamiclyInjectContentScript() - -chrome.runtime.onConnect.addListener(function onConnect(port) { - let name = null - let tab = null - - if (Number.isInteger(+port.name)) { - name = 'devtools' - tab = +port.name - - installContentScript(tab) - } - else { - name = 'contentScript' - tab = port.sender.tab.id - } - - if (connectionPorts[tab] == undefined) { - connectionPorts[tab] = { - debtools: null, - contentScript: null - } - } - - connectionPorts[tab][name] = port - - if (connectionPorts[tab].devtools != null && connectionPorts[tab].contentScript != null) { - establishBidirectionalConnection(tab, connectionPorts[tab].devtools, connectionPorts[tab].contentScript) - } -}) - function establishBidirectionalConnection(tabId, one, two) { const listen = (anotherPort) => function (message) { @@ -110,5 +113,33 @@ function establishBidirectionalConnection(tabId, one, two) { two.disconnect(); connectionPorts[tabId] = null; + + killBackground() } } + + +/** + * background.js tries to go inactive if no actions is perfomed by service_worker, + * so by that, we'll try to keep him alive by constantly running some actions + * @see https://developer.chrome.com/docs/extensions/migrating/to-service-workers + */ +async function keepBackgroundAlive() { + keepingAliveInterval = setInterval(tick, 5 * 1000) +} + + +/** + * some low-cost action + */ +async function tick() { + await chrome.storage.local.set({ '_': Date.now() }) +} + + +/** + * allowing background.js to kill himself + */ +function killBackground() { + clearInterval(keepingAliveInterval) +} From bfd554ed676de1c13c794a6a723b5216ba32de78 Mon Sep 17 00:00:00 2001 From: semen Date: Sun, 26 Nov 2023 11:49:02 +0300 Subject: [PATCH 06/28] Add path alias to `tsconfing.json` --- .../components/controls/button-icon/index.ts | 2 +- src/devtools/components/profiler-event/index.ts | 6 +++--- src/devtools/{lib => core}/StatefulLItElement.ts | 2 +- src/devtools/models/incodingEvent.ts | 2 +- .../pages/EventList/panels/event-list-header.ts | 14 ++++++++++---- src/devtools/pages/EventList/panels/event-list.ts | 14 +++++--------- .../pages/EventList/panels/event-viewer.ts | 7 ++++--- src/devtools/store/EventList/slice.ts | 2 +- src/devtools/store/EventViewer/slice.ts | 2 +- src/devtools/utils/actionColors.ts | 2 +- tsconfig.json | 5 +++++ webpack.config.js | 3 +++ 12 files changed, 36 insertions(+), 25 deletions(-) rename src/devtools/{lib => core}/StatefulLItElement.ts (92%) diff --git a/src/devtools/components/controls/button-icon/index.ts b/src/devtools/components/controls/button-icon/index.ts index 55ec970..d6348ba 100644 --- a/src/devtools/components/controls/button-icon/index.ts +++ b/src/devtools/components/controls/button-icon/index.ts @@ -2,7 +2,7 @@ import { LitElement, html } from "lit"; import { customElement, property } from "lit/decorators.js"; import styles from "./styles.css" -import resetButtonStyles from "../../../styles/reset-button.css" +import resetButtonStyles from "@devtools/styles/reset-button.css" import ButtonClickEvent from "./ButtonClickEvent"; @customElement('btn-icon') diff --git a/src/devtools/components/profiler-event/index.ts b/src/devtools/components/profiler-event/index.ts index c04af0e..d562f21 100644 --- a/src/devtools/components/profiler-event/index.ts +++ b/src/devtools/components/profiler-event/index.ts @@ -1,8 +1,8 @@ import { LitElement, html } from "lit"; import { customElement, property, query } from "lit/decorators.js" -import IncodingEvent from '../../models/incodingEvent'; -import { select } from '../../store/EventViewer/slice'; -import store from '../../store'; +import IncodingEvent from '@devtools/models/incodingEvent'; +import { select } from '@devtools/store/EventViewer/slice'; +import store from '@devtools/store'; import styles from "./styles.css" diff --git a/src/devtools/lib/StatefulLItElement.ts b/src/devtools/core/StatefulLItElement.ts similarity index 92% rename from src/devtools/lib/StatefulLItElement.ts rename to src/devtools/core/StatefulLItElement.ts index 4503113..364196a 100644 --- a/src/devtools/lib/StatefulLItElement.ts +++ b/src/devtools/core/StatefulLItElement.ts @@ -1,5 +1,5 @@ import { LitElement } from "lit"; -import store, { RootState } from "../store"; +import store, { RootState } from "@devtools/store"; import { Unsubscribe } from "@reduxjs/toolkit"; export default class StatefulLitElement extends LitElement { diff --git a/src/devtools/models/incodingEvent.ts b/src/devtools/models/incodingEvent.ts index 9f33692..f90d96f 100644 --- a/src/devtools/models/incodingEvent.ts +++ b/src/devtools/models/incodingEvent.ts @@ -1,4 +1,4 @@ -import JsonData from "../models/jsonData" +import JsonData from "@devtools/models/jsonData" import Actions from "./actions" export default interface IncodingEvent { diff --git a/src/devtools/pages/EventList/panels/event-list-header.ts b/src/devtools/pages/EventList/panels/event-list-header.ts index d523f7f..d7688e7 100644 --- a/src/devtools/pages/EventList/panels/event-list-header.ts +++ b/src/devtools/pages/EventList/panels/event-list-header.ts @@ -1,9 +1,15 @@ import { LitElement, css, html } from "lit"; import { customElement } from "lit/decorators.js" -import SearchEvent from '../../../components/inputs/search/SearchChangeEvent'; -import store from '../../../store'; -import { clearEvents, pauseEvents, resetSearch, resumeEvents, searchEvents } from '../../../store/EventList/slice'; -import ButtonToggleEvent from '../../../components/controls/button-toggle/ButtonToggleEvent'; +import SearchEvent from '@devtools/components/inputs/search/SearchChangeEvent'; +import store from '@devtools/store'; +import { + clearEvents, + pauseEvents, + resetSearch, + resumeEvents, + searchEvents +} from '@devtools/store/EventList/slice'; +import ButtonToggleEvent from '@devtools/components/controls/button-toggle/ButtonToggleEvent'; @customElement('event-list-header') diff --git a/src/devtools/pages/EventList/panels/event-list.ts b/src/devtools/pages/EventList/panels/event-list.ts index 86a6f74..f9291cb 100644 --- a/src/devtools/pages/EventList/panels/event-list.ts +++ b/src/devtools/pages/EventList/panels/event-list.ts @@ -1,15 +1,11 @@ -import "../../../components/profiler-event" - import { css, html } from "lit"; import { customElement, state, query } from "lit/decorators.js"; import { repeat } from "lit/directives/repeat.js" - -import IncodingEvent from "../../../models/incodingEvent"; -import { RootState } from "../../../store"; - -import scrollStyles from "../../../styles/scroll.css" -import StatefulLitElement from "../../../lib/StatefulLItElement"; -import { selectEvents } from "../../../store/EventList/selectors"; +import IncodingEvent from "@devtools/models/incodingEvent"; +import { RootState } from "@devtools/store"; +import scrollStyles from "@devtools/styles/scroll.css" +import StatefulLitElement from "@devtools/core/StatefulLItElement"; +import { selectEvents } from "@devtools/store/EventList/selectors"; const styles = css` diff --git a/src/devtools/pages/EventList/panels/event-viewer.ts b/src/devtools/pages/EventList/panels/event-viewer.ts index 0942baf..9c775be 100644 --- a/src/devtools/pages/EventList/panels/event-viewer.ts +++ b/src/devtools/pages/EventList/panels/event-viewer.ts @@ -2,9 +2,10 @@ import '@alenaksu/json-viewer' import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; -import JsonData from "../../../models/jsonData"; -import { RootState } from '../../../store'; -import StatefulLitElement from '../../../lib/StatefulLItElement'; +import JsonData from "@devtools/models/jsonData" +import { RootState } from '@devtools/store'; +import StatefulLitElement from '@devtools/core/StatefulLItElement'; + @customElement('event-viewer') diff --git a/src/devtools/store/EventList/slice.ts b/src/devtools/store/EventList/slice.ts index 4b9686b..fb2f312 100644 --- a/src/devtools/store/EventList/slice.ts +++ b/src/devtools/store/EventList/slice.ts @@ -1,5 +1,5 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import IncodingEvent from "../../models/incodingEvent"; +import IncodingEvent from "@devtools/models/incodingEvent"; import { IncodingEventExecutedMessage, IncodingEventMessage } from "../../../messages/messages-list"; interface EventListState { diff --git a/src/devtools/store/EventViewer/slice.ts b/src/devtools/store/EventViewer/slice.ts index c558002..ac4a48d 100644 --- a/src/devtools/store/EventViewer/slice.ts +++ b/src/devtools/store/EventViewer/slice.ts @@ -1,5 +1,5 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import IncodingEvent from "../../models/incodingEvent"; +import IncodingEvent from "@devtools/models/incodingEvent"; interface EventViewerState { selected: IncodingEvent | null diff --git a/src/devtools/utils/actionColors.ts b/src/devtools/utils/actionColors.ts index 4f608c6..ec44f5e 100644 --- a/src/devtools/utils/actionColors.ts +++ b/src/devtools/utils/actionColors.ts @@ -1,4 +1,4 @@ -import Actions from "../models/actions"; +import Actions from "@devtools/models/actions"; const actionColors: { [key in Actions]: string } = { 'Direct': 'rgb(238, 238, 238)', diff --git a/tsconfig.json b/tsconfig.json index d28ef5a..cd5d130 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,5 +17,10 @@ "strict": true, "strictPropertyInitialization": false, + + "baseUrl": "./", + "paths": { + "@devtools/*": ["./src/devtools/*"], + } } } diff --git a/webpack.config.js b/webpack.config.js index a043782..c972cdb 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -14,6 +14,9 @@ module.exports = { }, resolve: { extensions: ['.js', '.ts'], + alias: { + '@devtools': path.resolve(__dirname, 'src/devtools/') + } }, module: { rules: [ From 2cc30ae915f0c3c438ce81949b714eb831056421 Mon Sep 17 00:00:00 2001 From: semen Date: Sun, 26 Nov 2023 21:11:40 +0300 Subject: [PATCH 07/28] Add multiple platforms build support. --- .gitignore | 5 +- README.md | 49 ++++++++++++- package-lock.json | 63 +++++++++++++++++ package.json | 11 ++- platforms/PrebuildExtensionPlugin.js | 53 ++++++++++++++ {public => platforms/chrome}/manifest.json | 0 platforms/edge/manifest.json | 20 ++++++ src/background.js | 15 ++-- .../pages/EventList/panels/event-list.ts | 4 +- .../pages/EventList/panels/event-viewer.ts | 1 - src/devtools/store/EventList/selectors.ts | 2 +- webpack.config.js | 70 ++++++++++--------- 12 files changed, 242 insertions(+), 51 deletions(-) create mode 100644 platforms/PrebuildExtensionPlugin.js rename {public => platforms/chrome}/manifest.json (100%) create mode 100644 platforms/edge/manifest.json diff --git a/.gitignore b/.gitignore index 0f151d7..6e9244d 100644 --- a/.gitignore +++ b/.gitignore @@ -130,6 +130,5 @@ dist .pnp.* # public build files -public/*.js -public/*LICENSE* -public/*.map +debug/ +release/ diff --git a/README.md b/README.md index 429e124..30630e0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,47 @@ -## Incoding profiler -Permalink to [download](https://github.com/nice-nickname/incoding.profiler/raw/main/build/incoding.profiler.zip) latest version +## incoding.profiler + +Profiling devtools for [Incoding.Framework](https://github.com/Incoding-Software/Incoding-Framework). + + +## Browser support +This extension uses [Manifest V3](https://developer.chrome.com/docs/extensions/mv3/intro/) and currently supports this platforms: + +- Chrome +- Microsoft Edge (Chrome) + + +## Installation in browser + +Download latest build archive from [latest release](https://github.com/nice-nickname/incoding.profiler/releases/latest). + +Then, simply follow these steps: +- Open browser extensions manager +- Select __Manage extensions__ +- Check __Developer mode__ +- Click __Load packed__ and select build folder + + +## Building from sources + +First, you need to download sources: + +```bash +git clone this@repo +``` + +Then, choose target browser from list of supported browsers and eval: + +```bash +npm run prod: +``` + +This will build extension in `prod/` folder. + +### Contributing & building debug version + +This project uses `webpack` for project build. To start a debug session with devserver watching changes in code, just type in: +```bash +npm run dev: +``` + +This will build extension in `debug/` folder, which you can load as unpacked extension diff --git a/package-lock.json b/package-lock.json index e101bed..767ec57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@typescript-eslint/parser": "^6.7.0", "adm-zip": "^0.5.10", "eslint": "^8.49.0", + "fs-extra": "^11.1.1", "lit-css-loader": "^2.0.1", "ts-loader": "^9.4.4", "typescript": "^5.1.6", @@ -1553,6 +1554,20 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1889,6 +1904,18 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -2741,6 +2768,15 @@ "node": ">=6.4.0" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", @@ -4097,6 +4133,17 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4350,6 +4397,16 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -4936,6 +4993,12 @@ "integrity": "sha512-J2SQ2QLjiknNGbNdScaNZsXgmMGI0kYNrXaDlr4obnPW9ni1jljb1NeEVWAiTgZ8z+EBWP2ozfT9vpy03rjlMQ==", "dev": true }, + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true + }, "update-browserslist-db": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", diff --git a/package.json b/package.json index ff11920..19eeb04 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,14 @@ "description": "profiler devtool for chrome for debugging incoding.framework.js", "main": "index.js", "scripts": { - "build": "npx webpack build --mode production", - "dev": "npx webpack watch --mode production -d source-map", + "prod:chrome": "npx webpack build --mode production --env mode=release --env platform=chrome", + "prod:edge": "npx webpack build --mode production --env mode=release --env platform=edge", + "prod:firefox": "npx webpack build --mode production --env mode=release --env platform=edge", + + "dev:chrome": "npx webpack watch --mode production -d source-map --env mode=debug --env platform=chrome", + "dev:edge": "npx webpack watch --mode production -d source-map --env mode=debug --env platform=edge", + "dev:firefox": "npx webpack watch --mode production -d source-map --env mode=debug --env platform=firefox", + "lint": "eslint src/", "lintfix": "eslint src/ --fix" }, @@ -18,6 +24,7 @@ "@typescript-eslint/parser": "^6.7.0", "adm-zip": "^0.5.10", "eslint": "^8.49.0", + "fs-extra": "^11.1.1", "lit-css-loader": "^2.0.1", "ts-loader": "^9.4.4", "typescript": "^5.1.6", diff --git a/platforms/PrebuildExtensionPlugin.js b/platforms/PrebuildExtensionPlugin.js new file mode 100644 index 0000000..e4fd7bc --- /dev/null +++ b/platforms/PrebuildExtensionPlugin.js @@ -0,0 +1,53 @@ +const path = require('path') +const fse = require('fs-extra') + +const pluginName = 'prebuild-extension-plugin' + +module.exports = class PrebuildExtensionPlugin { + + constructor(mode, platform) { + this.mode = this.validateMode(mode) + this.platform = this.validatePlatform(platform) + + console.log(`incoding.profiler started in '${this.mode}' mode, target platform '${this.platform}'`) + } + + apply(compiler) { + compiler.hooks.initialize.tap(pluginName, () => { + const publicPath = path.join(__dirname, `../public`) + const platformPath = path.join(__dirname, `../platforms/${this.platform}`) + const destination = this.getDestinationPath() + + fse.copySync(publicPath, destination) + fse.copySync(platformPath, destination) + }); + } + + getDestinationPath() { + return path.join(__dirname, `../${this.mode}/${this.platform}`) + } + + validateMode(mode) { + if (!mode) { + return 'debug' + } + + if (mode !== 'debug' && mode !== 'release') { + console.error(`Value '${mode}' is not valid for mode.`) + process.exit(1) + } + + return mode + } + + validatePlatform(platform) { + const manifest = path.join(__dirname, `../platforms/${platform}`, 'manifest.json') + + if (!fse.existsSync(manifest)) { + console.error(`Platform '${platform}' is not supported.`) + process.exit(1) + } + + return platform + } +} diff --git a/public/manifest.json b/platforms/chrome/manifest.json similarity index 100% rename from public/manifest.json rename to platforms/chrome/manifest.json diff --git a/platforms/edge/manifest.json b/platforms/edge/manifest.json new file mode 100644 index 0000000..9c6201b --- /dev/null +++ b/platforms/edge/manifest.json @@ -0,0 +1,20 @@ +{ + "manifest_version": 3, + "name": "incoding.profiler", + "description": "Devtools extension for profiling incoding.framework.js", + + "version": "1.0", + "devtools_page": "index.html", + + "background": { + "service_worker": "background.js" + }, + + "permissions": [ + "activeTab", "scripting", "storage" + ], + + "host_permissions": [ + "" + ] +} diff --git a/src/background.js b/src/background.js index 91093b8..f860301 100644 --- a/src/background.js +++ b/src/background.js @@ -119,21 +119,18 @@ function establishBidirectionalConnection(tabId, one, two) { } +async function ping() { + await chrome.storage.local.set({ '_': 'pong' + Date.now() }) +} + + /** * background.js tries to go inactive if no actions is perfomed by service_worker, * so by that, we'll try to keep him alive by constantly running some actions * @see https://developer.chrome.com/docs/extensions/migrating/to-service-workers */ async function keepBackgroundAlive() { - keepingAliveInterval = setInterval(tick, 5 * 1000) -} - - -/** - * some low-cost action - */ -async function tick() { - await chrome.storage.local.set({ '_': Date.now() }) + keepingAliveInterval = setInterval(ping, 5 * 1000) } diff --git a/src/devtools/pages/EventList/panels/event-list.ts b/src/devtools/pages/EventList/panels/event-list.ts index f9291cb..c4a0645 100644 --- a/src/devtools/pages/EventList/panels/event-list.ts +++ b/src/devtools/pages/EventList/panels/event-list.ts @@ -43,7 +43,9 @@ export class EventListElement extends StatefulLitElement { return html`
- ${hasEvents ? this.renderList() : this.renderEmpty()} + ${hasEvents + ? this.renderList() + : this.renderEmpty()}
` } diff --git a/src/devtools/pages/EventList/panels/event-viewer.ts b/src/devtools/pages/EventList/panels/event-viewer.ts index 9c775be..d1d857a 100644 --- a/src/devtools/pages/EventList/panels/event-viewer.ts +++ b/src/devtools/pages/EventList/panels/event-viewer.ts @@ -7,7 +7,6 @@ import { RootState } from '@devtools/store'; import StatefulLitElement from '@devtools/core/StatefulLItElement'; - @customElement('event-viewer') export class EventViewerElement extends StatefulLitElement { diff --git a/src/devtools/store/EventList/selectors.ts b/src/devtools/store/EventList/selectors.ts index b413eea..f212440 100644 --- a/src/devtools/store/EventList/selectors.ts +++ b/src/devtools/store/EventList/selectors.ts @@ -1,6 +1,6 @@ import { createSelector } from "@reduxjs/toolkit"; import { RootState } from ".."; -import IncodingEvent from "../../models/incodingEvent"; +import IncodingEvent from "@devtools/models/incodingEvent"; const eventsSelector = (state: RootState) => state.eventList.events const searchSelector = (state: RootState) => state.eventList.search diff --git a/webpack.config.js b/webpack.config.js index c972cdb..4a3891e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,37 +1,43 @@ const path = require('path') +const PrebuildExtensionPlugin = require('./platforms/PrebuildExtensionPlugin') -module.exports = { - entry: { - loader: './src/loader.js', - background: './src/background.js', - devtools: './src/devtools/main.ts', - inject_profiler: './src/content-scripts/inject-profiler.js', - content_script: './src/content-scripts/content-script.js' - }, - output: { - filename: '[name].js', - path: path.join(__dirname, 'public') - }, - resolve: { - extensions: ['.js', '.ts'], - alias: { - '@devtools': path.resolve(__dirname, 'src/devtools/') - } - }, - module: { - rules: [ - { - test: /\.ts?$/, - use: 'ts-loader', - exclude: /node_modules/, - }, - { - test: /\.css$/, - loader: 'lit-css-loader', - options: { - specifier: 'lit-element' - } +module.exports = (env) => { + const plugin = new PrebuildExtensionPlugin(env.mode, env.platform) + + return { + entry: { + loader: './src/loader.js', + background: './src/background.js', + devtools: './src/devtools/main.ts', + inject_profiler: './src/content-scripts/inject-profiler.js', + content_script: './src/content-scripts/content-script.js' + }, + output: { + filename: '[name].js', + path: plugin.getDestinationPath() + }, + resolve: { + extensions: ['.js', '.ts'], + alias: { + '@devtools': path.resolve(__dirname, 'src/devtools/') } - ], + }, + module: { + rules: [ + { + test: /\.ts?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + { + test: /\.css$/, + loader: 'lit-css-loader', + options: { + specifier: 'lit-element' + } + } + ], + }, + plugins: [plugin] } } From 890bfd863c9c0889980d4a31b984619f914436d8 Mon Sep 17 00:00:00 2001 From: semen Date: Sun, 26 Nov 2023 21:39:10 +0300 Subject: [PATCH 08/28] Add workflow for building to multiple platforms --- .github/workflows/build-release.yml | 24 ++++++++++++++++++++++ .github/workflows/release.yml | 32 ++++++++--------------------- 2 files changed, 32 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/build-release.yml diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000..7e15437 --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,24 @@ +name: Build and release + +on: [workflow_call] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: build devtools ($TARGET) + run: npm run prod:$TARGET + + - name: archive build + run: | + zip -q -r -9 incoding.profiler.chrome.zip release/$TARGET + + - name: upload zip asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: $UPLOAD_URL + asset_path: ./incoding.profiler.$TARGET.zip + asset_name: incoding.profiler.$TARGET.zip + asset_content_type: application/zip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6cb5ce8..0172f6c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,14 +21,6 @@ jobs: - name: install dependencies run: npm ci - - name: build devtools - run: npm run build - - - name: archive build - run: | - zip -q -r -9 incoding.profiler.zip public - tar -czf incoding.profiler.tar.gz public - - name: get version id: get_version uses: PaulHatch/semantic-version@v5.0.3 @@ -49,22 +41,14 @@ jobs: draft: false prerelease: false - - name: upload zip asset - uses: actions/upload-release-asset@v1 + - name: create chrome release + uses: ./.github/workflows/build-release.yml env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./incoding.profiler.zip - asset_name: incoding.profiler.zip - asset_content_type: application/zip + TARGET: chrome + UPLOAD_URL: ${{ steps.create_release.outputs.upload_url }} - - name: upload tar.gz asset - uses: actions/upload-release-asset@v1 + - name: create chrome release + uses: ./.github/workflows/build-release.yml env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./incoding.profiler.tar.gz - asset_name: incoding.profiler.tar.gz - asset_content_type: application/tar + TARGET: edge + UPLOAD_URL: ${{ steps.create_release.outputs.upload_url }} From c879bc099e8160115694eb57cbfa5431049f3e7e Mon Sep 17 00:00:00 2001 From: semen Date: Tue, 28 Nov 2023 15:13:47 +0300 Subject: [PATCH 09/28] Add resources json for strings --- .../pages/EventList/panels/event-list.ts | 3 ++- .../pages/EventList/panels/event-viewer.ts | 25 ++++++++++++------- src/devtools/resources/index.ts | 7 ++++++ src/devtools/resources/labels.json | 4 +++ src/devtools/store/EventViewer/selectors.ts | 5 ++++ tsconfig.json | 3 ++- 6 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 src/devtools/resources/index.ts create mode 100644 src/devtools/resources/labels.json create mode 100644 src/devtools/store/EventViewer/selectors.ts diff --git a/src/devtools/pages/EventList/panels/event-list.ts b/src/devtools/pages/EventList/panels/event-list.ts index c4a0645..fbc5a72 100644 --- a/src/devtools/pages/EventList/panels/event-list.ts +++ b/src/devtools/pages/EventList/panels/event-list.ts @@ -6,6 +6,7 @@ import { RootState } from "@devtools/store"; import scrollStyles from "@devtools/styles/scroll.css" import StatefulLitElement from "@devtools/core/StatefulLItElement"; import { selectEvents } from "@devtools/store/EventList/selectors"; +import resources from "@devtools/resources"; const styles = css` @@ -60,7 +61,7 @@ export class EventListElement extends StatefulLitElement { private renderEmpty() { return html` - + ` } diff --git a/src/devtools/pages/EventList/panels/event-viewer.ts b/src/devtools/pages/EventList/panels/event-viewer.ts index d1d857a..44bfcb3 100644 --- a/src/devtools/pages/EventList/panels/event-viewer.ts +++ b/src/devtools/pages/EventList/panels/event-viewer.ts @@ -1,28 +1,35 @@ import '@alenaksu/json-viewer' - import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; import JsonData from "@devtools/models/jsonData" import { RootState } from '@devtools/store'; import StatefulLitElement from '@devtools/core/StatefulLItElement'; +import { selectSelectedJsonData } from '@devtools/store/EventViewer/selectors'; +import resources from '@devtools/resources'; @customElement('event-viewer') export class EventViewerElement extends StatefulLitElement { - @state() private jsonData: JsonData + @state() private jsonData: JsonData | null = null protected onStateChanged(state: RootState): void { - const selectedEvent = state.eventViewer.selected - - if (selectedEvent && selectedEvent.jsonData) { - this.jsonData = selectedEvent.jsonData - } + this.jsonData = selectSelectedJsonData(state) } protected render() { return html` - - ` + ${this.jsonData !== null + ? this.renderContent() + : this.renderEmpty() + }` + } + + private renderEmpty() { + return html`` + } + + private renderContent() { + return html`` } } diff --git a/src/devtools/resources/index.ts b/src/devtools/resources/index.ts new file mode 100644 index 0000000..ed92df2 --- /dev/null +++ b/src/devtools/resources/index.ts @@ -0,0 +1,7 @@ +import labels from "./labels.json" + +const resources = { + ...labels +} + +export default resources diff --git a/src/devtools/resources/labels.json b/src/devtools/resources/labels.json new file mode 100644 index 0000000..cd5ac51 --- /dev/null +++ b/src/devtools/resources/labels.json @@ -0,0 +1,4 @@ +{ + "no_items_in_list": "No elements to see here... 😞", + "no_selected_item": "Select event to see jsonData 😗" +} diff --git a/src/devtools/store/EventViewer/selectors.ts b/src/devtools/store/EventViewer/selectors.ts new file mode 100644 index 0000000..e44d679 --- /dev/null +++ b/src/devtools/store/EventViewer/selectors.ts @@ -0,0 +1,5 @@ +import { RootState } from ".."; + +const selectSelectedJsonData = (state: RootState) => state.eventViewer.selected?.jsonData || null + +export { selectSelectedJsonData } diff --git a/tsconfig.json b/tsconfig.json index cd5d130..1b359ea 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,7 @@ "baseUrl": "./", "paths": { "@devtools/*": ["./src/devtools/*"], - } + }, + "resolveJsonModule": true } } From ab59ec2c4fa0d5bb1fa0392e1612041d41e85dae Mon Sep 17 00:00:00 2001 From: semen Date: Tue, 28 Nov 2023 15:47:35 +0300 Subject: [PATCH 10/28] Add delay to search --- package.json | 2 -- src/devtools/components/inputs/search/index.ts | 10 +++++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 19eeb04..5ffb530 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,9 @@ "prod:chrome": "npx webpack build --mode production --env mode=release --env platform=chrome", "prod:edge": "npx webpack build --mode production --env mode=release --env platform=edge", "prod:firefox": "npx webpack build --mode production --env mode=release --env platform=edge", - "dev:chrome": "npx webpack watch --mode production -d source-map --env mode=debug --env platform=chrome", "dev:edge": "npx webpack watch --mode production -d source-map --env mode=debug --env platform=edge", "dev:firefox": "npx webpack watch --mode production -d source-map --env mode=debug --env platform=firefox", - "lint": "eslint src/", "lintfix": "eslint src/ --fix" }, diff --git a/src/devtools/components/inputs/search/index.ts b/src/devtools/components/inputs/search/index.ts index c577dde..70674b6 100644 --- a/src/devtools/components/inputs/search/index.ts +++ b/src/devtools/components/inputs/search/index.ts @@ -11,8 +11,12 @@ export class InputSearchElement extends LitElement { @property() placeholder: string = '' + @property({ type: Number }) delayMs: number = 100 + @query('input') searchInput: HTMLInputElement + private delayTimeout: number + protected render() { return html` @@ -22,6 +26,10 @@ export class InputSearchElement extends LitElement { private onsearch(ev: KeyboardEvent) { const value = this.searchInput.value - this.dispatchEvent(new SearchEvent(value)) + clearTimeout(this.delayTimeout) + + this.delayTimeout = window.setTimeout(() => { + this.dispatchEvent(new SearchEvent(value)) + }, this.delayMs) } } From 4732a4b00e7970002c203397b11d41ca598cf415 Mon Sep 17 00:00:00 2001 From: semen Date: Tue, 28 Nov 2023 16:25:52 +0300 Subject: [PATCH 11/28] Improve README --- README.md | 54 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 30630e0..02cd354 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,49 @@ -## incoding.profiler +# incoding.profiler -Profiling devtools for [Incoding.Framework](https://github.com/Incoding-Software/Incoding-Framework). +> Profiling devtools for [Incoding.Framework](https://github.com/Incoding-Software/Incoding-Framework). ## Browser support -This extension uses [Manifest V3](https://developer.chrome.com/docs/extensions/mv3/intro/) and currently supports this platforms: +This extension uses [Manifest V3](https://developer.chrome.com/docs/extensions/mv3/intro/) and currently supports this browsers: -- Chrome +- Chrome - Microsoft Edge (Chrome) +## Installation -## Installation in browser - -Download latest build archive from [latest release](https://github.com/nice-nickname/incoding.profiler/releases/latest). +You can download latest build archive for all available platforms from [latest release](https://github.com/nice-nickname/incoding.profiler/releases/latest). -Then, simply follow these steps: -- Open browser extensions manager -- Select __Manage extensions__ -- Check __Developer mode__ -- Click __Load packed__ and select build folder +But, if you want to create your own build, you'll need to run following commands: +```bash +# Install dependencies +npm ci -## Building from sources +# Build extension in production mode +npm run prod: +``` -First, you need to download sources: +The above command will build the app in production mode, output files are placed in `prod/` folder. -```bash -git clone this@repo -``` +## Run development mode -Then, choose target browser from list of supported browsers and eval: +In order to build the app for development, run the following command: ```bash -npm run prod: +# Run build and watch for changes +npm run dev: ``` -This will build extension in `prod/` folder. +This will build extension in developer mode and watch for local changes. Use this option only if you want to make any changes in extension sources. Output files will be in `debug/` folder. -### Contributing & building debug version +## Installation in browser -This project uses `webpack` for project build. To start a debug session with devserver watching changes in code, just type in: -```bash -npm run dev: -``` +After you downloaded latest release and choosed desired browser (or created your own build), use output folder with source files and follow these steps: +- Open browser extensions manager +- Select **Manage extensions** +- Check **Developer mode** +- Click **Load packed** and select build folder + +### Reloading extension in development mode -This will build extension in `debug/` folder, which you can load as unpacked extension +If you're running **development** mode, and there is code changes to be applied, just re-open browser devtools, and all files will be updated. From 6515b4922f1648c2c39df93361c485a70769c59d Mon Sep 17 00:00:00 2001 From: semen Date: Thu, 7 Dec 2023 21:36:41 +0300 Subject: [PATCH 12/28] Fix rem sizing --- public/styles.css | 4 ++++ src/devtools/components/action-marker/styles.css | 4 ++-- .../components/controls/button-icon/index.ts | 10 +++++++--- .../components/controls/button-icon/styles.css | 3 --- src/devtools/components/icon/index.ts | 14 ++++++++++++-- src/devtools/components/icon/styles.css | 3 --- 6 files changed, 25 insertions(+), 13 deletions(-) delete mode 100644 src/devtools/components/controls/button-icon/styles.css diff --git a/public/styles.css b/public/styles.css index 40ea347..c62a162 100644 --- a/public/styles.css +++ b/public/styles.css @@ -58,6 +58,10 @@ html { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } +html, body { + font-size: 12px !important; +} + #root { width: 100vw; height: 100vh; diff --git a/src/devtools/components/action-marker/styles.css b/src/devtools/components/action-marker/styles.css index 0b774b1..47c99c4 100644 --- a/src/devtools/components/action-marker/styles.css +++ b/src/devtools/components/action-marker/styles.css @@ -1,6 +1,6 @@ .marker { - width: 0.5rem; - height: 0.5rem; + width: 8px; + height: 8px; border-radius: 1rem; diff --git a/src/devtools/components/controls/button-icon/index.ts b/src/devtools/components/controls/button-icon/index.ts index d6348ba..153580e 100644 --- a/src/devtools/components/controls/button-icon/index.ts +++ b/src/devtools/components/controls/button-icon/index.ts @@ -1,27 +1,31 @@ import { LitElement, html } from "lit"; import { customElement, property } from "lit/decorators.js"; -import styles from "./styles.css" import resetButtonStyles from "@devtools/styles/reset-button.css" import ButtonClickEvent from "./ButtonClickEvent"; @customElement('btn-icon') export class IconButtonElement extends LitElement { - static styles = [styles, resetButtonStyles] + static styles = [resetButtonStyles] @property() icon: string @property() color?: string + private size = '20px' + constructor() { super() + + this.style.width = this.size + this.style.height = this.size } protected render() { return html` ` } diff --git a/src/devtools/components/controls/button-icon/styles.css b/src/devtools/components/controls/button-icon/styles.css deleted file mode 100644 index 2e02341..0000000 --- a/src/devtools/components/controls/button-icon/styles.css +++ /dev/null @@ -1,3 +0,0 @@ -:host { - height: 1.25rem; -} diff --git a/src/devtools/components/icon/index.ts b/src/devtools/components/icon/index.ts index b113f5d..f78a64c 100644 --- a/src/devtools/components/icon/index.ts +++ b/src/devtools/components/icon/index.ts @@ -1,5 +1,6 @@ import { LitElement, html } from "lit"; import { customElement, property } from "lit/decorators.js"; +import { styleMap } from 'lit/directives/style-map.js'; import styles from "./styles.css" @@ -12,11 +13,20 @@ export class IconElement extends LitElement { @property() color?: string + @property() size: string = '16px' + protected render() { - const colorStyle = this.color ? `color: ${this.color};` : `` + + this.style.width = this.size + this.style.height = this.size + + const style = { + color: this.color, + fontSize: this.size + } return html` - + ${this.icon} ` diff --git a/src/devtools/components/icon/styles.css b/src/devtools/components/icon/styles.css index 100627d..ed0b6d3 100644 --- a/src/devtools/components/icon/styles.css +++ b/src/devtools/components/icon/styles.css @@ -1,7 +1,5 @@ :host { display: block; - width: 1.25rem; - height: 1.25rem; } .material-symbols-outlined { @@ -20,7 +18,6 @@ font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' -27, 'opsz' 20; user-select: none; - font-size: 1.25rem; color: var(--icon-color); } From c2739d0f208b2942112141cd3ae9f7a61dd2e4fc Mon Sep 17 00:00:00 2001 From: semen Date: Sat, 9 Dec 2023 17:26:29 +0300 Subject: [PATCH 13/28] Fix typo #36, move some files in separate directories --- src/{ => background}/background.js | 28 ++----------------- src/background/persist-connection.js | 25 +++++++++++++++++ .../controls/button-icon/ButtonClickEvent.ts | 2 +- src/devtools/components/icon/index.ts | 6 ++-- src/devtools/main.ts | 3 +- src/devtools/resources/labels.json | 3 +- src/{loader.js => index.js} | 1 + tsconfig.json | 1 + webpack.config.js | 4 +-- 9 files changed, 40 insertions(+), 33 deletions(-) rename src/{ => background}/background.js (80%) create mode 100644 src/background/persist-connection.js rename src/{loader.js => index.js} (99%) diff --git a/src/background.js b/src/background/background.js similarity index 80% rename from src/background.js rename to src/background/background.js index f860301..4bd401d 100644 --- a/src/background.js +++ b/src/background/background.js @@ -6,11 +6,12 @@ 'use strict' -let keepingAliveInterval +import { keepBackgroundAlive } from "./persist-connection" const connectionPorts = {} -keepBackgroundAlive() +const killBackground = keepBackgroundAlive() + dynamiclyInjectContentScript() chrome.runtime.onConnect.addListener(function onConnect(port) { @@ -117,26 +118,3 @@ function establishBidirectionalConnection(tabId, one, two) { killBackground() } } - - -async function ping() { - await chrome.storage.local.set({ '_': 'pong' + Date.now() }) -} - - -/** - * background.js tries to go inactive if no actions is perfomed by service_worker, - * so by that, we'll try to keep him alive by constantly running some actions - * @see https://developer.chrome.com/docs/extensions/migrating/to-service-workers - */ -async function keepBackgroundAlive() { - keepingAliveInterval = setInterval(ping, 5 * 1000) -} - - -/** - * allowing background.js to kill himself - */ -function killBackground() { - clearInterval(keepingAliveInterval) -} diff --git a/src/background/persist-connection.js b/src/background/persist-connection.js new file mode 100644 index 0000000..8f7e3bc --- /dev/null +++ b/src/background/persist-connection.js @@ -0,0 +1,25 @@ +/** + * background.js tries to go inactive if no actions is perfomed by service_worker, + * so by that, we'll try to keep him alive by constantly running some actions + * @see https://developer.chrome.com/docs/extensions/migrating/to-service-workers + */ + +let keepingAliveInterval + +function keepBackgroundAlive() { + keepingAliveInterval = setInterval(ping, 5 * 1000) + + return function killBackground() { + clearInterval(keepingAliveInterval) + } +} + + +async function ping() { + await chrome.storage.local.set({ '_': 'pong' + Date.now() }) +} + + +export { + keepBackgroundAlive +} diff --git a/src/devtools/components/controls/button-icon/ButtonClickEvent.ts b/src/devtools/components/controls/button-icon/ButtonClickEvent.ts index f535a41..1c54be9 100644 --- a/src/devtools/components/controls/button-icon/ButtonClickEvent.ts +++ b/src/devtools/components/controls/button-icon/ButtonClickEvent.ts @@ -3,7 +3,7 @@ export default class ButtonClickEvent extends Event { constructor() { super('click', { bubbles: false, - cancelable: true + composed: true }); } } diff --git a/src/devtools/components/icon/index.ts b/src/devtools/components/icon/index.ts index f78a64c..4df6b1f 100644 --- a/src/devtools/components/icon/index.ts +++ b/src/devtools/components/icon/index.ts @@ -17,14 +17,14 @@ export class IconElement extends LitElement { protected render() { - this.style.width = this.size - this.style.height = this.size - const style = { color: this.color, fontSize: this.size } + this.style.width = style.fontSize + this.style.height = style.fontSize + return html` ${this.icon} diff --git a/src/devtools/main.ts b/src/devtools/main.ts index 2f0a32b..1dee9f3 100644 --- a/src/devtools/main.ts +++ b/src/devtools/main.ts @@ -10,6 +10,7 @@ import './pages/EventList/index' import { ProfilerMessage } from "../messages/messages"; import { IncodingEventExecutedMessage, IncodingEventMessage } from '../messages/messages-list'; import { addEvent, updateEvent } from './store/EventList/slice'; +import resources from "@devtools/resources" import store from './store'; const root = document.getElementById('root')! @@ -18,7 +19,7 @@ chrome.devtools.inspectedWindow.eval( 'ExecutableBase.name', (result, error) => { if (result !== 'ExecutableBase' || error) - return root.innerHTML = 'This page does not support incoding.framework.js 😢' + return root.innerHTML = resources.no_incoding_framework_found startProfiler() }) diff --git a/src/devtools/resources/labels.json b/src/devtools/resources/labels.json index cd5ac51..ed2b57c 100644 --- a/src/devtools/resources/labels.json +++ b/src/devtools/resources/labels.json @@ -1,4 +1,5 @@ { "no_items_in_list": "No elements to see here... 😞", - "no_selected_item": "Select event to see jsonData 😗" + "no_selected_item": "Select event to see jsonData 😗", + "no_incoding_framework_found": "This page does not support incoding.framework.js 😢" } diff --git a/src/loader.js b/src/index.js similarity index 99% rename from src/loader.js rename to src/index.js index 7510e81..f5a92c8 100644 --- a/src/loader.js +++ b/src/index.js @@ -6,3 +6,4 @@ chrome.devtools.panels.create('Incoding profiler', '', 'panel.html', } ); + diff --git a/tsconfig.json b/tsconfig.json index 1b359ea..03c4488 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,7 @@ "baseUrl": "./", "paths": { "@devtools/*": ["./src/devtools/*"], + "@content-scripts/*": ["./src/content-scripts/*"] }, "resolveJsonModule": true } diff --git a/webpack.config.js b/webpack.config.js index 4a3891e..d92a6d9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,8 +6,8 @@ module.exports = (env) => { return { entry: { - loader: './src/loader.js', - background: './src/background.js', + loader: './src/index.js', + background: './src/background/background.js', devtools: './src/devtools/main.ts', inject_profiler: './src/content-scripts/inject-profiler.js', content_script: './src/content-scripts/content-script.js' From 031df1cbd0830b5b4c534fcd7487a02dcc6d7f4a Mon Sep 17 00:00:00 2001 From: semen Date: Wed, 13 Dec 2023 22:05:52 +0300 Subject: [PATCH 14/28] Upgrade lit2.8 -> lit3, remove needless packages --- package-lock.json | 166 ++++++------------ package.json | 5 +- .../pages/EventList/panels/event-viewer.ts | 3 +- 3 files changed, 53 insertions(+), 121 deletions(-) diff --git a/package-lock.json b/package-lock.json index 767ec57..76f817d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,13 @@ "version": "1.0", "license": "ISC", "dependencies": { - "@alenaksu/json-viewer": "^2.0.1", "@reduxjs/toolkit": "^1.9.5", - "lit": "^2.8.0" + "lit": "^3.1.0" }, "devDependencies": { "@types/chrome": "^0.0.243", - "@types/jquery": "^3.5.16", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", - "adm-zip": "^0.5.10", "eslint": "^8.49.0", "fs-extra": "^11.1.1", "lit-css-loader": "^2.0.1", @@ -37,14 +34,6 @@ "node": ">=0.10.0" } }, - "node_modules/@alenaksu/json-viewer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@alenaksu/json-viewer/-/json-viewer-2.0.1.tgz", - "integrity": "sha512-M6rN1bcuSGfar6ND9fFASBkez0UcWOUxMiwm2i9jlPBrpjOHOz0/utMgZhfrsgfyFPZ1H1gzfU8auJkYO1mq/g==", - "dependencies": { - "lit": "^2.3.1" - } - }, "node_modules/@babel/runtime": { "version": "7.22.11", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.11.tgz", @@ -213,16 +202,16 @@ } }, "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz", - "integrity": "sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz", + "integrity": "sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g==" }, "node_modules/@lit/reactive-element": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz", - "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.2.tgz", + "integrity": "sha512-SVOwLAWUQg3Ji1egtOt1UiFe4zdDpnWHyc5qctSceJ5XIu0Uc76YmGpIjZgx9YJ0XtdW0Jm507sDvjOu+HnB8w==", "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.0.0" + "@lit-labs/ssr-dom-shim": "^1.1.2" } }, "node_modules/@nodelib/fs.scandir": { @@ -350,15 +339,6 @@ "integrity": "sha512-T232/TneofqK30AD1LRrrf8KnjLvzrjWDp7eWST5KoiSzrBfRsLrWDPk4STQPW4NZG6v2MltnduBVmakbZOBIQ==", "dev": true }, - "node_modules/@types/jquery": { - "version": "3.5.16", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.16.tgz", - "integrity": "sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==", - "dev": true, - "dependencies": { - "@types/sizzle": "*" - } - }, "node_modules/@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", @@ -377,16 +357,10 @@ "integrity": "sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==", "dev": true }, - "node_modules/@types/sizzle": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", - "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", - "dev": true - }, "node_modules/@types/trusted-types": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz", - "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==" + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.7.0", @@ -809,15 +783,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/adm-zip": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", - "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==", - "dev": true, - "engines": { - "node": ">=6.0" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1939,13 +1904,13 @@ } }, "node_modules/lit": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz", - "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.1.0.tgz", + "integrity": "sha512-rzo/hmUqX8zmOdamDAeydfjsGXbbdtAFqMhmocnh2j9aDYqbu0fjXygjCa0T99Od9VQ/2itwaGrjZz/ZELVl7w==", "dependencies": { - "@lit/reactive-element": "^1.6.0", - "lit-element": "^3.3.0", - "lit-html": "^2.8.0" + "@lit/reactive-element": "^2.0.0", + "lit-element": "^4.0.0", + "lit-html": "^3.1.0" } }, "node_modules/lit-css-loader": { @@ -1959,19 +1924,19 @@ } }, "node_modules/lit-element": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", - "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.0.2.tgz", + "integrity": "sha512-/W6WQZUa5VEXwC7H9tbtDMdSs9aWil3Ou8hU6z2cOKWbsm/tXPAcsoaHVEtrDo0zcOIE5GF6QgU55tlGL2Nihg==", "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.1.0", - "@lit/reactive-element": "^1.3.0", - "lit-html": "^2.8.0" + "@lit-labs/ssr-dom-shim": "^1.1.2", + "@lit/reactive-element": "^2.0.0", + "lit-html": "^3.1.0" } }, "node_modules/lit-html": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", - "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.1.0.tgz", + "integrity": "sha512-FwAjq3iNsaO6SOZXEIpeROlJLUlrbyMkn4iuv4f4u1H40Jw8wkeR/OUXZUHUoiYabGk8Y4Y0F/rgq+R4MrOLmA==", "dependencies": { "@types/trusted-types": "^2.0.2" } @@ -3005,14 +2970,6 @@ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true }, - "@alenaksu/json-viewer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@alenaksu/json-viewer/-/json-viewer-2.0.1.tgz", - "integrity": "sha512-M6rN1bcuSGfar6ND9fFASBkez0UcWOUxMiwm2i9jlPBrpjOHOz0/utMgZhfrsgfyFPZ1H1gzfU8auJkYO1mq/g==", - "requires": { - "lit": "^2.3.1" - } - }, "@babel/runtime": { "version": "7.22.11", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.11.tgz", @@ -3138,16 +3095,16 @@ } }, "@lit-labs/ssr-dom-shim": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz", - "integrity": "sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz", + "integrity": "sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g==" }, "@lit/reactive-element": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz", - "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.2.tgz", + "integrity": "sha512-SVOwLAWUQg3Ji1egtOt1UiFe4zdDpnWHyc5qctSceJ5XIu0Uc76YmGpIjZgx9YJ0XtdW0Jm507sDvjOu+HnB8w==", "requires": { - "@lit-labs/ssr-dom-shim": "^1.0.0" + "@lit-labs/ssr-dom-shim": "^1.1.2" } }, "@nodelib/fs.scandir": { @@ -3254,15 +3211,6 @@ "integrity": "sha512-T232/TneofqK30AD1LRrrf8KnjLvzrjWDp7eWST5KoiSzrBfRsLrWDPk4STQPW4NZG6v2MltnduBVmakbZOBIQ==", "dev": true }, - "@types/jquery": { - "version": "3.5.16", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.16.tgz", - "integrity": "sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==", - "dev": true, - "requires": { - "@types/sizzle": "*" - } - }, "@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", @@ -3281,16 +3229,10 @@ "integrity": "sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==", "dev": true }, - "@types/sizzle": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", - "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", - "dev": true - }, "@types/trusted-types": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz", - "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==" + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, "@typescript-eslint/eslint-plugin": { "version": "6.7.0", @@ -3591,12 +3533,6 @@ "dev": true, "requires": {} }, - "adm-zip": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", - "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==", - "dev": true - }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4424,13 +4360,13 @@ } }, "lit": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz", - "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.1.0.tgz", + "integrity": "sha512-rzo/hmUqX8zmOdamDAeydfjsGXbbdtAFqMhmocnh2j9aDYqbu0fjXygjCa0T99Od9VQ/2itwaGrjZz/ZELVl7w==", "requires": { - "@lit/reactive-element": "^1.6.0", - "lit-element": "^3.3.0", - "lit-html": "^2.8.0" + "@lit/reactive-element": "^2.0.0", + "lit-element": "^4.0.0", + "lit-html": "^3.1.0" } }, "lit-css-loader": { @@ -4444,19 +4380,19 @@ } }, "lit-element": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", - "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.0.2.tgz", + "integrity": "sha512-/W6WQZUa5VEXwC7H9tbtDMdSs9aWil3Ou8hU6z2cOKWbsm/tXPAcsoaHVEtrDo0zcOIE5GF6QgU55tlGL2Nihg==", "requires": { - "@lit-labs/ssr-dom-shim": "^1.1.0", - "@lit/reactive-element": "^1.3.0", - "lit-html": "^2.8.0" + "@lit-labs/ssr-dom-shim": "^1.1.2", + "@lit/reactive-element": "^2.0.0", + "lit-html": "^3.1.0" } }, "lit-html": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", - "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.1.0.tgz", + "integrity": "sha512-FwAjq3iNsaO6SOZXEIpeROlJLUlrbyMkn4iuv4f4u1H40Jw8wkeR/OUXZUHUoiYabGk8Y4Y0F/rgq+R4MrOLmA==", "requires": { "@types/trusted-types": "^2.0.2" } diff --git a/package.json b/package.json index 5ffb530..0049a8b 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,8 @@ "license": "ISC", "devDependencies": { "@types/chrome": "^0.0.243", - "@types/jquery": "^3.5.16", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", - "adm-zip": "^0.5.10", "eslint": "^8.49.0", "fs-extra": "^11.1.1", "lit-css-loader": "^2.0.1", @@ -30,8 +28,7 @@ "webpack-cli": "^5.1.4" }, "dependencies": { - "@alenaksu/json-viewer": "^2.0.1", "@reduxjs/toolkit": "^1.9.5", - "lit": "^2.8.0" + "lit": "^3.1.0" } } diff --git a/src/devtools/pages/EventList/panels/event-viewer.ts b/src/devtools/pages/EventList/panels/event-viewer.ts index 44bfcb3..2c68184 100644 --- a/src/devtools/pages/EventList/panels/event-viewer.ts +++ b/src/devtools/pages/EventList/panels/event-viewer.ts @@ -1,4 +1,3 @@ -import '@alenaksu/json-viewer' import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; import JsonData from "@devtools/models/jsonData" @@ -30,6 +29,6 @@ export class EventViewerElement extends StatefulLitElement { } private renderContent() { - return html`` + return html`` } } From abc160bc620641d41a9a81a7c463dfcccc7fa960 Mon Sep 17 00:00:00 2001 From: semyon Date: Thu, 14 Dec 2023 10:19:25 +0300 Subject: [PATCH 15/28] Add root app component for initialization --- src/devtools/app.ts | 99 ++++++++++++++++++++++++++++++ src/devtools/index.ts | 13 ++++ src/devtools/main.ts | 53 ---------------- src/devtools/resources/labels.json | 2 + webpack.config.js | 2 +- 5 files changed, 115 insertions(+), 54 deletions(-) create mode 100644 src/devtools/app.ts create mode 100644 src/devtools/index.ts delete mode 100644 src/devtools/main.ts diff --git a/src/devtools/app.ts b/src/devtools/app.ts new file mode 100644 index 0000000..4553e19 --- /dev/null +++ b/src/devtools/app.ts @@ -0,0 +1,99 @@ +import "@devtools/components" +import '@devtools/pages/EventList/index' + +import { LitElement, html } from "lit"; +import { customElement, state } from "lit/decorators.js"; +import { choose } from "lit/directives/choose.js" +import resources from "@devtools/resources"; +import { ProfilerMessage } from "../messages/messages"; +import store from "./store"; +import { addEvent, updateEvent } from "./store/EventList/slice"; +import { + IncodingEventExecutedMessage, + IncodingEventMessage +} from "src/messages/messages-list"; + + +@customElement('incoding-profiler-devtools') +export class IncodingProfilerDevtools extends LitElement { + + @state() private status: 'loading' | 'started' | 'failed' + + private connection: chrome.runtime.Port + + override connectedCallback(): void { + super.connectedCallback() + + this.status = 'loading' + + this.checkIncodingFrameworkAndStart() + } + + override disconnectedCallback(): void { + super.disconnectedCallback() + + this.connection.onMessage.removeListener(this.onProfilerMessage) + this.connection.disconnect() + + this.status = 'loading' + } + + private checkIncodingFrameworkAndStart() { + chrome.devtools.inspectedWindow.eval( + 'ExecutableBase.name', + (result, error) => { + if (result !== 'ExecutableBase' || error) { + this.status = 'failed' + return; + } + + this.startProfiler() + }) + } + + private startProfiler() { + const tabId = String(chrome.devtools.inspectedWindow.tabId) + this.connection = chrome.runtime.connect({ name: tabId }) + + this.connection.onMessage.addListener(this.onProfilerMessage) + + this.status = 'started' + } + + private onProfilerMessage(message: ProfilerMessage) { + switch (message.name) { + case 'execute-start': + store.dispatch(addEvent(message.data)) + break; + + case 'execute-finish': + store.dispatch(updateEvent(message.data)) + break; + + default: + break; + } + } + + protected render() { + return html` + ${choose(this.status, [ + ['loading', this.renderLoading], + ['started', this.renderContent], + ['failed', this.renderFail] + ])} + ` + } + + private renderContent() { + return html`` + } + + private renderLoading() { + return html`${resources.profiler_loading}` + } + + private renderFail() { + return html`${resources.no_incoding_framework_found}` + } +} diff --git a/src/devtools/index.ts b/src/devtools/index.ts new file mode 100644 index 0000000..eaaf3a5 --- /dev/null +++ b/src/devtools/index.ts @@ -0,0 +1,13 @@ +/** + * devtools + * + * Establish background connection and handle events + */ + + +import "@devtools/app" +import { html, render } from "lit" + +const root = document.getElementById('root')! + +render(html``, root) diff --git a/src/devtools/main.ts b/src/devtools/main.ts deleted file mode 100644 index 1dee9f3..0000000 --- a/src/devtools/main.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * devtools - * - * Establish background connection and handle events - */ - -import "./components" -import './pages/EventList/index' - -import { ProfilerMessage } from "../messages/messages"; -import { IncodingEventExecutedMessage, IncodingEventMessage } from '../messages/messages-list'; -import { addEvent, updateEvent } from './store/EventList/slice'; -import resources from "@devtools/resources" -import store from './store'; - -const root = document.getElementById('root')! - -chrome.devtools.inspectedWindow.eval( - 'ExecutableBase.name', - (result, error) => { - if (result !== 'ExecutableBase' || error) - return root.innerHTML = resources.no_incoding_framework_found - - startProfiler() - }) - - -async function startProfiler() { - const tabId = String(chrome.devtools.inspectedWindow.tabId) - const connection = chrome.runtime.connect({ - name: tabId - }) - - connection.onMessage.addListener(onProfilerMessage) - - root.appendChild(document.createElement('event-list-page')) -} - - -function onProfilerMessage(message: ProfilerMessage) { - switch (message.name) { - case 'execute-start': - store.dispatch(addEvent(message.data)) - break; - - case 'execute-finish': - store.dispatch(updateEvent(message.data)) - break; - - default: - break; - } -} diff --git a/src/devtools/resources/labels.json b/src/devtools/resources/labels.json index ed2b57c..7dff5ae 100644 --- a/src/devtools/resources/labels.json +++ b/src/devtools/resources/labels.json @@ -1,4 +1,6 @@ { + "profiler_loading": "incoding.profiler is loading... ⏳", + "no_items_in_list": "No elements to see here... 😞", "no_selected_item": "Select event to see jsonData 😗", "no_incoding_framework_found": "This page does not support incoding.framework.js 😢" diff --git a/webpack.config.js b/webpack.config.js index d92a6d9..c0c9126 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -8,7 +8,7 @@ module.exports = (env) => { entry: { loader: './src/index.js', background: './src/background/background.js', - devtools: './src/devtools/main.ts', + devtools: './src/devtools/index.ts', inject_profiler: './src/content-scripts/inject-profiler.js', content_script: './src/content-scripts/content-script.js' }, From 5de003ece289abb1cabca9c6796f2aad251785a5 Mon Sep 17 00:00:00 2001 From: semen Date: Sat, 16 Dec 2023 12:18:55 +0300 Subject: [PATCH 16/28] Add prototype for generic message exchanging --- src/connection/RuntimeConnection.ts | 41 +++++++++++++++++++ src/connection/types/index.ts | 26 ++++++++++++ src/content-scripts/api/index.ts | 3 ++ .../{ => injection}/inject-profiler.js | 4 +- src/devtools/api/index.ts | 12 ++++++ webpack.config.js | 2 +- 6 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 src/connection/RuntimeConnection.ts create mode 100644 src/connection/types/index.ts create mode 100644 src/content-scripts/api/index.ts rename src/content-scripts/{ => injection}/inject-profiler.js (93%) create mode 100644 src/devtools/api/index.ts diff --git a/src/connection/RuntimeConnection.ts b/src/connection/RuntimeConnection.ts new file mode 100644 index 0000000..9bfb6dc --- /dev/null +++ b/src/connection/RuntimeConnection.ts @@ -0,0 +1,41 @@ +import { + MessageRegistry, + MessagePayload, + OnMessageHandler, + MessageTypes +} from './types' + +export class RuntimeConnection { + + private connection: chrome.runtime.Port + private listeners: Record + + connect(tab: string) { + this.connection = chrome.runtime.connect({ name: tab }) + + this.connection.onMessage.addListener(this.onMessage) + } + + disconnect() { + this.connection.onMessage.removeListener(this.onMessage) + + this.disconnect() + } + + on(type: Tkey, handler: OnMessageHandler) { + this.listeners[type] = handler + } + + post(type: TKey, payload: MessagePayload) { + this.connection.postMessage({ + type, + payload + }) + } + + private onMessage(message: MessageRegistry) { + const handler = this.listeners[message.type] + + handler?.call(this, message.payload) + } +} diff --git a/src/connection/types/index.ts b/src/connection/types/index.ts new file mode 100644 index 0000000..d5e5e6e --- /dev/null +++ b/src/connection/types/index.ts @@ -0,0 +1,26 @@ +import { + IncodingEventMessage, + IncodingEventExecutedMessage +} from "@devtools/api" + + +export interface Message { + type: TKey, + payload: TPayload +} + +export type MessageRegistry = + IncodingEventMessage | + IncodingEventExecutedMessage + ; + +export type MessageTypes = MessageRegistry['type'] + +export type MessageByKey + = Extract + +export type MessagePayload + = MessageByKey['payload'] + +export type OnMessageHandler + = (payload: MessagePayload) => void diff --git a/src/content-scripts/api/index.ts b/src/content-scripts/api/index.ts new file mode 100644 index 0000000..348a4ec --- /dev/null +++ b/src/content-scripts/api/index.ts @@ -0,0 +1,3 @@ +import { Message } from "src/connection/types"; + +export type HighlightElement = Message<'highlight-element', string> diff --git a/src/content-scripts/inject-profiler.js b/src/content-scripts/injection/inject-profiler.js similarity index 93% rename from src/content-scripts/inject-profiler.js rename to src/content-scripts/injection/inject-profiler.js index e8868fb..bbaf378 100644 --- a/src/content-scripts/inject-profiler.js +++ b/src/content-scripts/injection/inject-profiler.js @@ -4,8 +4,8 @@ * Script to intercept executing messages from incoding.framework and pass them to content-script */ -import sendMessage from "../messages/messages" -import { jqueryToSelector, uuidv4 } from "../utils" +import sendMessage from "../../messages/messages" +import { jqueryToSelector, uuidv4 } from "../../utils" /* eslint-disable */ diff --git a/src/devtools/api/index.ts b/src/devtools/api/index.ts new file mode 100644 index 0000000..9300e24 --- /dev/null +++ b/src/devtools/api/index.ts @@ -0,0 +1,12 @@ +import IncodingEvent from "@devtools/models/incodingEvent" +import { Message } from "src/connection/types" + +export type IncodingEventMessage = Message< + 'event-execution-start', + Omit +> + +export type IncodingEventExecutedMessage = Message< + 'event-execution-finish', + Pick +> diff --git a/webpack.config.js b/webpack.config.js index c0c9126..c436a74 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,7 +9,7 @@ module.exports = (env) => { loader: './src/index.js', background: './src/background/background.js', devtools: './src/devtools/index.ts', - inject_profiler: './src/content-scripts/inject-profiler.js', + inject_profiler: './src/content-scripts/injection/inject-profiler.js', content_script: './src/content-scripts/content-script.js' }, output: { From 84b2ee77c865317ad6263cf65f3490b91e5d8dc3 Mon Sep 17 00:00:00 2001 From: semen Date: Sun, 17 Dec 2023 12:02:31 +0300 Subject: [PATCH 17/28] Add runtime connection to devtools/content-script --- package-lock.json | 17 +++++++ package.json | 1 + src/connection/RuntimeConnection.ts | 48 ++++++++++++------- src/connection/types/index.ts | 32 ++++--------- src/content-scripts/api/index.ts | 9 +++- src/content-scripts/content-script.js | 34 ------------- src/content-scripts/content-script.ts | 26 ++++++++++ .../injection/inject-profiler.js | 29 ++++++----- src/devtools/api/index.ts | 16 ++++--- src/devtools/app.ts | 40 ++++++---------- src/devtools/context/connection.ts | 6 +++ src/devtools/store/EventList/slice.ts | 5 +- src/messages/messages-list.ts | 5 -- src/messages/messages.ts | 25 ---------- src/types/index.d.ts | 6 +++ tsconfig.json | 3 +- webpack.config.js | 6 ++- 17 files changed, 155 insertions(+), 153 deletions(-) delete mode 100644 src/content-scripts/content-script.js create mode 100644 src/content-scripts/content-script.ts create mode 100644 src/devtools/context/connection.ts delete mode 100644 src/messages/messages-list.ts delete mode 100644 src/messages/messages.ts diff --git a/package-lock.json b/package-lock.json index 76f817d..8965069 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0", "license": "ISC", "dependencies": { + "@lit/context": "^1.1.0", "@reduxjs/toolkit": "^1.9.5", "lit": "^3.1.0" }, @@ -206,6 +207,14 @@ "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz", "integrity": "sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g==" }, + "node_modules/@lit/context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.0.tgz", + "integrity": "sha512-fCyv4dsH05wCNm3AKbB+PdYbXGJd/XT8OOwo4hVmD4COq5wOWJlQreGAMDvmHZ7osqxuu06Y4nmP6ooXpN7ErA==", + "dependencies": { + "@lit/reactive-element": "^1.6.2 || ^2.0.0" + } + }, "node_modules/@lit/reactive-element": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.2.tgz", @@ -3099,6 +3108,14 @@ "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz", "integrity": "sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g==" }, + "@lit/context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.0.tgz", + "integrity": "sha512-fCyv4dsH05wCNm3AKbB+PdYbXGJd/XT8OOwo4hVmD4COq5wOWJlQreGAMDvmHZ7osqxuu06Y4nmP6ooXpN7ErA==", + "requires": { + "@lit/reactive-element": "^1.6.2 || ^2.0.0" + } + }, "@lit/reactive-element": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.2.tgz", diff --git a/package.json b/package.json index 0049a8b..eeb0851 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "webpack-cli": "^5.1.4" }, "dependencies": { + "@lit/context": "^1.1.0", "@reduxjs/toolkit": "^1.9.5", "lit": "^3.1.0" } diff --git a/src/connection/RuntimeConnection.ts b/src/connection/RuntimeConnection.ts index 9bfb6dc..b59ec58 100644 --- a/src/connection/RuntimeConnection.ts +++ b/src/connection/RuntimeConnection.ts @@ -1,41 +1,55 @@ -import { - MessageRegistry, - MessagePayload, - OnMessageHandler, - MessageTypes -} from './types' +import Message, { + BrowserMessages, + DevtoolsMessages +} from "./types" -export class RuntimeConnection { +class RuntimeConnection< + TListen extends object, + TEmit extends object +> { private connection: chrome.runtime.Port - private listeners: Record + private listeners: Partial> = {} connect(tab: string) { this.connection = chrome.runtime.connect({ name: tab }) this.connection.onMessage.addListener(this.onMessage) + + this.connection.onDisconnect.addListener(() => { + this.connection.onMessage.removeListener(this.onMessage) + }) } disconnect() { - this.connection.onMessage.removeListener(this.onMessage) - this.disconnect() } - on(type: Tkey, handler: OnMessageHandler) { + on(type: Tkey, handler: (payload: TListen[Tkey]) => void) { this.listeners[type] = handler } - post(type: TKey, payload: MessagePayload) { - this.connection.postMessage({ - type, - payload - }) + emit(type: TKey, payload: TEmit[TKey]) { + const message: Message = { + type: type, + payload: payload + } + + this.connection.postMessage(message) } - private onMessage(message: MessageRegistry) { + private onMessage = (message: Message) => { const handler = this.listeners[message.type] + if (!handler) { + console.warn(`there is no handler assosiated with ${String(message.type)}`) + return + } handler?.call(this, message.payload) } } + +export type DevtoolsConnection = RuntimeConnection +export type BrowserConnection = RuntimeConnection + +export default RuntimeConnection diff --git a/src/connection/types/index.ts b/src/connection/types/index.ts index d5e5e6e..7605c25 100644 --- a/src/connection/types/index.ts +++ b/src/connection/types/index.ts @@ -1,26 +1,14 @@ -import { - IncodingEventMessage, - IncodingEventExecutedMessage -} from "@devtools/api" +import type DevtoolsMessages from "@devtools/api" +import type BrowserMessages from "@content-scripts/api" - -export interface Message { - type: TKey, - payload: TPayload +type Message = { + type: keyof TMessages, + payload: TMessages[keyof TMessages] } -export type MessageRegistry = - IncodingEventMessage | - IncodingEventExecutedMessage - ; - -export type MessageTypes = MessageRegistry['type'] - -export type MessageByKey - = Extract - -export type MessagePayload - = MessageByKey['payload'] +export { + DevtoolsMessages, + BrowserMessages +} -export type OnMessageHandler - = (payload: MessagePayload) => void +export default Message diff --git a/src/content-scripts/api/index.ts b/src/content-scripts/api/index.ts index 348a4ec..7bbcbc2 100644 --- a/src/content-scripts/api/index.ts +++ b/src/content-scripts/api/index.ts @@ -1,3 +1,8 @@ -import { Message } from "src/connection/types"; -export type HighlightElement = Message<'highlight-element', string> +export type InspectDOMElementMessage = string + +type BrowserMessages = { + 'inspect-element': InspectDOMElementMessage +} + +export default BrowserMessages diff --git a/src/content-scripts/content-script.js b/src/content-scripts/content-script.js deleted file mode 100644 index 70fa385..0000000 --- a/src/content-scripts/content-script.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * content-script - * - * Establishing window listener to direct messages from injected script to background service-worker - */ - - -const connection = chrome.runtime.connect({ - name: 'content-script' -}) - -connection.onMessage.addListener(onDevoolsMessage) -connection.onDisconnect.addListener(onDevtoolsDisconnect) - -window.addEventListener('message', onWindowMessage) - - -function onDevoolsMessage(message) { - -} - - -function onDevtoolsDisconnect() { - window.removeEventListener('message', onWindowMessage) -} - - -function onWindowMessage({ source, data }) { - if (source !== this.window || !data) { - return; - } - - connection.postMessage(data) -} diff --git a/src/content-scripts/content-script.ts b/src/content-scripts/content-script.ts new file mode 100644 index 0000000..b0c2374 --- /dev/null +++ b/src/content-scripts/content-script.ts @@ -0,0 +1,26 @@ +/** + * content-script + * + * Establishing window listener to direct messages from injected script to background service-worker + */ + +import RuntimeConnection, { BrowserConnection } from "@connection/RuntimeConnection" +import Message, { DevtoolsMessages } from "@connection/types"; + + +const connection: BrowserConnection = new RuntimeConnection() + +connection.connect('content-script') + +window.addEventListener('message', function({ source, data }) { + if (source !== this.window || !data) { + return; + } + const message = data as Message + + connection.emit(message.type, message.payload) +}) + +connection.on('inspect-element', elementId => { + window.inspect(document.querySelector(`data-profiler-id="${elementId}"`)) +}) diff --git a/src/content-scripts/injection/inject-profiler.js b/src/content-scripts/injection/inject-profiler.js index bbaf378..637ec4b 100644 --- a/src/content-scripts/injection/inject-profiler.js +++ b/src/content-scripts/injection/inject-profiler.js @@ -4,7 +4,6 @@ * Script to intercept executing messages from incoding.framework and pass them to content-script */ -import sendMessage from "../../messages/messages" import { jqueryToSelector, uuidv4 } from "../../utils" /* eslint-disable */ @@ -12,13 +11,16 @@ import { jqueryToSelector, uuidv4 } from "../../utils" function interceptExecute(current, state) { const messageId = uuidv4() - sendMessage(window, 'execute-start', { - uuid: messageId, - action: current.name, - eventName: current.event.type, - jsonData: current.jsonData, - self: jqueryToSelector(current.self), - target: jqueryToSelector(current.target) + window.postMessage({ + type: 'event-execution-start', + payload: { + uuid: messageId, + action: current.name, + eventName: current.event.type, + jsonData: current.jsonData, + self: jqueryToSelector(current.self), + target: jqueryToSelector(current.target) + } }) const tick = performance.now() @@ -28,10 +30,13 @@ const tick = performance.now() const tock = performance.now() - sendMessage(window, 'execute-finish', { - uuid: messageId, - executionTimeMs: tock - tick, - jsonData: current.jsonData + window.postMessage({ + type: 'event-execution-finish', + payload: { + uuid: messageId, + executionTimeMs: tock - tick, + jsonData: current.jsonData + } }) } diff --git a/src/devtools/api/index.ts b/src/devtools/api/index.ts index 9300e24..95e61ad 100644 --- a/src/devtools/api/index.ts +++ b/src/devtools/api/index.ts @@ -1,12 +1,14 @@ import IncodingEvent from "@devtools/models/incodingEvent" -import { Message } from "src/connection/types" -export type IncodingEventMessage = Message< - 'event-execution-start', +export type IncodingEventMessage = Omit -> -export type IncodingEventExecutedMessage = Message< - 'event-execution-finish', +export type IncodingEventExecutedMessage = Pick -> + +type DevtoolsMessages = { + 'event-execution-start': IncodingEventMessage + 'event-execution-finish': IncodingEventExecutedMessage +} + +export default DevtoolsMessages diff --git a/src/devtools/app.ts b/src/devtools/app.ts index 4553e19..f9b5d3f 100644 --- a/src/devtools/app.ts +++ b/src/devtools/app.ts @@ -4,22 +4,22 @@ import '@devtools/pages/EventList/index' import { LitElement, html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { choose } from "lit/directives/choose.js" +import { provide } from "@lit/context"; import resources from "@devtools/resources"; -import { ProfilerMessage } from "../messages/messages"; import store from "./store"; import { addEvent, updateEvent } from "./store/EventList/slice"; -import { - IncodingEventExecutedMessage, - IncodingEventMessage -} from "src/messages/messages-list"; +import RuntimeConnection, { DevtoolsConnection } from "@connection/RuntimeConnection"; +import runtimeConnectionCtx from "./context/connection"; @customElement('incoding-profiler-devtools') export class IncodingProfilerDevtools extends LitElement { - @state() private status: 'loading' | 'started' | 'failed' + @state() + private status: 'loading' | 'started' | 'failed' - private connection: chrome.runtime.Port + @provide({ context: runtimeConnectionCtx }) + private connection: DevtoolsConnection = new RuntimeConnection() override connectedCallback(): void { super.connectedCallback() @@ -32,7 +32,6 @@ export class IncodingProfilerDevtools extends LitElement { override disconnectedCallback(): void { super.disconnectedCallback() - this.connection.onMessage.removeListener(this.onProfilerMessage) this.connection.disconnect() this.status = 'loading' @@ -53,26 +52,17 @@ export class IncodingProfilerDevtools extends LitElement { private startProfiler() { const tabId = String(chrome.devtools.inspectedWindow.tabId) - this.connection = chrome.runtime.connect({ name: tabId }) + this.connection.connect(tabId) - this.connection.onMessage.addListener(this.onProfilerMessage) + this.connection.on('event-execution-start', event => { + store.dispatch(addEvent(event)) + }) - this.status = 'started' - } - - private onProfilerMessage(message: ProfilerMessage) { - switch (message.name) { - case 'execute-start': - store.dispatch(addEvent(message.data)) - break; + this.connection.on('event-execution-finish', event => { + store.dispatch(updateEvent(event)) + }) - case 'execute-finish': - store.dispatch(updateEvent(message.data)) - break; - - default: - break; - } + this.status = 'started' } protected render() { diff --git a/src/devtools/context/connection.ts b/src/devtools/context/connection.ts new file mode 100644 index 0000000..690efc4 --- /dev/null +++ b/src/devtools/context/connection.ts @@ -0,0 +1,6 @@ +import { createContext } from "@lit/context" +import { DevtoolsConnection } from "@connection/RuntimeConnection" + +const runtimeConnectionCtx = createContext(Symbol('runtime-connection')) + +export default runtimeConnectionCtx diff --git a/src/devtools/store/EventList/slice.ts b/src/devtools/store/EventList/slice.ts index fb2f312..1de08b6 100644 --- a/src/devtools/store/EventList/slice.ts +++ b/src/devtools/store/EventList/slice.ts @@ -1,6 +1,9 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import IncodingEvent from "@devtools/models/incodingEvent"; -import { IncodingEventExecutedMessage, IncodingEventMessage } from "../../../messages/messages-list"; +import { + IncodingEventExecutedMessage, + IncodingEventMessage +} from "@devtools/api"; interface EventListState { events: IncodingEvent[], diff --git a/src/messages/messages-list.ts b/src/messages/messages-list.ts deleted file mode 100644 index c64f02e..0000000 --- a/src/messages/messages-list.ts +++ /dev/null @@ -1,5 +0,0 @@ -import IncodingEvent from "../devtools/models/incodingEvent" - -export type IncodingEventMessage = Omit - -export type IncodingEventExecutedMessage = Pick diff --git a/src/messages/messages.ts b/src/messages/messages.ts deleted file mode 100644 index 2784ee1..0000000 --- a/src/messages/messages.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - IncodingEventExecutedMessage, - IncodingEventMessage, -} from "./messages-list" - -interface Messages { - 'execute-start': IncodingEventMessage - 'execute-finish': IncodingEventExecutedMessage -} - -export interface ProfilerMessage { - name: keyof Messages, - data: Messages[keyof Messages] -} - -function sendMessage(source: Window | chrome.runtime.Port, name: TKey, data: Messages[TKey]) { - const message: ProfilerMessage = { - data: data, - name: name - } - - source.postMessage(message) -} - -export default sendMessage diff --git a/src/types/index.d.ts b/src/types/index.d.ts index da97db2..670da59 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,2 +1,8 @@ +interface Window { + + inspect(element: HTMLElement | null): void; + +} + declare module '*.css'; diff --git a/tsconfig.json b/tsconfig.json index 03c4488..fc5f242 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,8 @@ "baseUrl": "./", "paths": { "@devtools/*": ["./src/devtools/*"], - "@content-scripts/*": ["./src/content-scripts/*"] + "@content-scripts/*": ["./src/content-scripts/*"], + "@connection/*": ["./src/connection/*"] }, "resolveJsonModule": true } diff --git a/webpack.config.js b/webpack.config.js index c436a74..645d53b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -10,7 +10,7 @@ module.exports = (env) => { background: './src/background/background.js', devtools: './src/devtools/index.ts', inject_profiler: './src/content-scripts/injection/inject-profiler.js', - content_script: './src/content-scripts/content-script.js' + content_script: './src/content-scripts/content-script.ts' }, output: { filename: '[name].js', @@ -19,7 +19,9 @@ module.exports = (env) => { resolve: { extensions: ['.js', '.ts'], alias: { - '@devtools': path.resolve(__dirname, 'src/devtools/') + '@devtools': path.resolve(__dirname, 'src/devtools/'), + '@content-scripts': path.resolve(__dirname, 'src/content-scripts/'), + '@connection': path.resolve(__dirname, 'src/connection/') } }, module: { From eb66b8f054619dd8d2c81f4bad04a6ff92507b2e Mon Sep 17 00:00:00 2001 From: semyon Date: Mon, 18 Dec 2023 19:03:35 +0300 Subject: [PATCH 18/28] Add connected/disconnected messages, fix #40 --- src/connection/RuntimeConnection.ts | 15 +++++++++++---- src/connection/types/index.ts | 12 +++++++++++- src/content-scripts/api/index.ts | 5 ++++- src/content-scripts/content-script.ts | 21 +++++++++++++-------- src/devtools/app.ts | 3 ++- 5 files changed, 41 insertions(+), 15 deletions(-) diff --git a/src/connection/RuntimeConnection.ts b/src/connection/RuntimeConnection.ts index b59ec58..78fcd42 100644 --- a/src/connection/RuntimeConnection.ts +++ b/src/connection/RuntimeConnection.ts @@ -1,15 +1,18 @@ import Message, { BrowserMessages, - DevtoolsMessages + DevtoolsMessages, + SharedMessages } from "./types" +type ListenMessages = T & SharedMessages + class RuntimeConnection< TListen extends object, TEmit extends object > { private connection: chrome.runtime.Port - private listeners: Partial> = {} + private listeners: Partial, Function>> = {} connect(tab: string) { this.connection = chrome.runtime.connect({ name: tab }) @@ -17,15 +20,19 @@ class RuntimeConnection< this.connection.onMessage.addListener(this.onMessage) this.connection.onDisconnect.addListener(() => { + this.onMessage({ type: "disconnected" }) + this.connection.onMessage.removeListener(this.onMessage) }) + + this.onMessage({ type: "connected" }) } disconnect() { this.disconnect() } - on(type: Tkey, handler: (payload: TListen[Tkey]) => void) { + on>(type: Tkey, handler: (payload: ListenMessages[Tkey]) => void) { this.listeners[type] = handler } @@ -38,7 +45,7 @@ class RuntimeConnection< this.connection.postMessage(message) } - private onMessage = (message: Message) => { + private onMessage = (message: Message>) => { const handler = this.listeners[message.type] if (!handler) { diff --git a/src/connection/types/index.ts b/src/connection/types/index.ts index 7605c25..60a89a0 100644 --- a/src/connection/types/index.ts +++ b/src/connection/types/index.ts @@ -3,10 +3,20 @@ import type BrowserMessages from "@content-scripts/api" type Message = { type: keyof TMessages, - payload: TMessages[keyof TMessages] + payload?: TMessages[keyof TMessages] +} + +type ConnectedMessage = void +type DisconnectedMessage = number + +type SharedMessages = { + 'connected': ConnectedMessage, + 'disconnected': DisconnectedMessage } export { + SharedMessages, + DevtoolsMessages, BrowserMessages } diff --git a/src/content-scripts/api/index.ts b/src/content-scripts/api/index.ts index 7bbcbc2..dd06eb4 100644 --- a/src/content-scripts/api/index.ts +++ b/src/content-scripts/api/index.ts @@ -1,8 +1,11 @@ export type InspectDOMElementMessage = string +export type InspecDomElementsMessage = string[] + type BrowserMessages = { - 'inspect-element': InspectDOMElementMessage + 'inspect-element': InspectDOMElementMessage, + 'inspect-elements': InspecDomElementsMessage } export default BrowserMessages diff --git a/src/content-scripts/content-script.ts b/src/content-scripts/content-script.ts index b0c2374..a6e8b9a 100644 --- a/src/content-scripts/content-script.ts +++ b/src/content-scripts/content-script.ts @@ -5,22 +5,27 @@ */ import RuntimeConnection, { BrowserConnection } from "@connection/RuntimeConnection" -import Message, { DevtoolsMessages } from "@connection/types"; - const connection: BrowserConnection = new RuntimeConnection() -connection.connect('content-script') - -window.addEventListener('message', function({ source, data }) { - if (source !== this.window || !data) { +function onWindowMessage({ source, data }: any) { + if (source !== window || !data) { return; } - const message = data as Message - connection.emit(message.type, message.payload) + connection.emit(data.type, data.payload) +} + +connection.on('connected', () => { + window.addEventListener('message', onWindowMessage) +}) + +connection.on('disconnected', () => { + window.removeEventListener('message', onWindowMessage) }) connection.on('inspect-element', elementId => { window.inspect(document.querySelector(`data-profiler-id="${elementId}"`)) }) + +connection.connect('content-script') diff --git a/src/devtools/app.ts b/src/devtools/app.ts index f9b5d3f..9e9a3a1 100644 --- a/src/devtools/app.ts +++ b/src/devtools/app.ts @@ -52,7 +52,6 @@ export class IncodingProfilerDevtools extends LitElement { private startProfiler() { const tabId = String(chrome.devtools.inspectedWindow.tabId) - this.connection.connect(tabId) this.connection.on('event-execution-start', event => { store.dispatch(addEvent(event)) @@ -62,6 +61,8 @@ export class IncodingProfilerDevtools extends LitElement { store.dispatch(updateEvent(event)) }) + this.connection.connect(tabId) + this.status = 'started' } From fae9fd7e1c82c77afef2499c72cd384b515931a1 Mon Sep 17 00:00:00 2001 From: semen Date: Tue, 19 Dec 2023 02:01:18 +0300 Subject: [PATCH 19/28] Finish runtime-connection --- src/connection/RuntimeConnection.ts | 8 ++++---- src/connection/types/index.ts | 2 +- src/content-scripts/api/index.ts | 5 +---- src/content-scripts/content-script.ts | 16 ++++++++-------- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/connection/RuntimeConnection.ts b/src/connection/RuntimeConnection.ts index 78fcd42..473534c 100644 --- a/src/connection/RuntimeConnection.ts +++ b/src/connection/RuntimeConnection.ts @@ -32,10 +32,6 @@ class RuntimeConnection< this.disconnect() } - on>(type: Tkey, handler: (payload: ListenMessages[Tkey]) => void) { - this.listeners[type] = handler - } - emit(type: TKey, payload: TEmit[TKey]) { const message: Message = { type: type, @@ -45,6 +41,10 @@ class RuntimeConnection< this.connection.postMessage(message) } + on>(type: Tkey, handler: (payload: ListenMessages[Tkey]) => void) { + this.listeners[type] = handler + } + private onMessage = (message: Message>) => { const handler = this.listeners[message.type] diff --git a/src/connection/types/index.ts b/src/connection/types/index.ts index 60a89a0..86ec55a 100644 --- a/src/connection/types/index.ts +++ b/src/connection/types/index.ts @@ -7,7 +7,7 @@ type Message = { } type ConnectedMessage = void -type DisconnectedMessage = number +type DisconnectedMessage = void type SharedMessages = { 'connected': ConnectedMessage, diff --git a/src/content-scripts/api/index.ts b/src/content-scripts/api/index.ts index dd06eb4..7bbcbc2 100644 --- a/src/content-scripts/api/index.ts +++ b/src/content-scripts/api/index.ts @@ -1,11 +1,8 @@ export type InspectDOMElementMessage = string -export type InspecDomElementsMessage = string[] - type BrowserMessages = { - 'inspect-element': InspectDOMElementMessage, - 'inspect-elements': InspecDomElementsMessage + 'inspect-element': InspectDOMElementMessage } export default BrowserMessages diff --git a/src/content-scripts/content-script.ts b/src/content-scripts/content-script.ts index a6e8b9a..51b29d5 100644 --- a/src/content-scripts/content-script.ts +++ b/src/content-scripts/content-script.ts @@ -8,14 +8,6 @@ import RuntimeConnection, { BrowserConnection } from "@connection/RuntimeConnect const connection: BrowserConnection = new RuntimeConnection() -function onWindowMessage({ source, data }: any) { - if (source !== window || !data) { - return; - } - - connection.emit(data.type, data.payload) -} - connection.on('connected', () => { window.addEventListener('message', onWindowMessage) }) @@ -29,3 +21,11 @@ connection.on('inspect-element', elementId => { }) connection.connect('content-script') + +function onWindowMessage({ source, data }: any) { + if (source !== window || !data) { + return; + } + + connection.emit(data.type, data.payload) +} From a3b60921361720a851e2d68768ffe8a549109a4b Mon Sep 17 00:00:00 2001 From: semen Date: Thu, 28 Dec 2023 00:40:22 +0300 Subject: [PATCH 20/28] Fix typo --- src/background/background.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/background/background.js b/src/background/background.js index 4bd401d..a7133a0 100644 --- a/src/background/background.js +++ b/src/background/background.js @@ -31,7 +31,7 @@ chrome.runtime.onConnect.addListener(function onConnect(port) { if (connectionPorts[tab] == undefined) { connectionPorts[tab] = { - debtools: null, + devtools: null, contentScript: null } } From 281884e260001494caabd0fa901b8eecf923b41c Mon Sep 17 00:00:00 2001 From: semen Date: Sat, 13 Jan 2024 09:36:34 +0300 Subject: [PATCH 21/28] Add base class for components, add debounce fn --- .../components/action-marker/index.ts | 9 ++-- .../components/controls/button-group/index.ts | 5 +- .../controls/button-icon/ButtonClickEvent.ts | 9 ---- .../components/controls/button-icon/index.ts | 11 ++--- .../button-toggle/ButtonToggleEvent.ts | 13 ----- .../controls/button-toggle/index.ts | 47 ++++++++----------- .../controls/button-toggle/styles.css | 4 ++ src/devtools/components/icon/index.ts | 5 +- .../inputs/search/SearchChangeEvent.ts | 14 ------ .../components/inputs/search/index.ts | 37 +++++++++------ src/devtools/components/lit-component.ts | 14 ++++++ .../components/profiler-event/index.ts | 15 +++--- src/devtools/components/time-marker/index.ts | 5 +- .../EventList/panels/event-list-header.ts | 22 +++++---- .../pages/EventList/panels/event-list.ts | 9 +++- src/devtools/utils/debounce.ts | 19 ++++++++ 16 files changed, 125 insertions(+), 113 deletions(-) delete mode 100644 src/devtools/components/controls/button-icon/ButtonClickEvent.ts delete mode 100644 src/devtools/components/controls/button-toggle/ButtonToggleEvent.ts create mode 100644 src/devtools/components/controls/button-toggle/styles.css delete mode 100644 src/devtools/components/inputs/search/SearchChangeEvent.ts create mode 100644 src/devtools/components/lit-component.ts create mode 100644 src/devtools/utils/debounce.ts diff --git a/src/devtools/components/action-marker/index.ts b/src/devtools/components/action-marker/index.ts index f250110..98d78c6 100644 --- a/src/devtools/components/action-marker/index.ts +++ b/src/devtools/components/action-marker/index.ts @@ -1,12 +1,13 @@ -import { LitElement, html } from "lit"; +import { html } from "lit"; import { customElement, property } from "lit/decorators.js"; -import Actions from "../../models/actions"; -import getColorByAction from "../../utils/actionColors"; +import Actions from "@devtools/models/actions"; +import getColorByAction from "@devtools/utils/actionColors"; +import { LitComponentElement } from "../lit-component"; import styles from "./styles.css" @customElement('action-marker') -export class ActionMarkerElement extends LitElement { +export class ActionMarkerElement extends LitComponentElement { static styles = styles diff --git a/src/devtools/components/controls/button-group/index.ts b/src/devtools/components/controls/button-group/index.ts index df8c8c4..f7d9134 100644 --- a/src/devtools/components/controls/button-group/index.ts +++ b/src/devtools/components/controls/button-group/index.ts @@ -1,8 +1,9 @@ -import { LitElement, css, html } from "lit"; +import { LitComponentElement } from "@devtools/components/lit-component"; +import { css, html } from "lit"; import { customElement } from "lit/decorators.js"; @customElement('btn-group') -export class ButtonGroupElement extends LitElement { +export class ButtonGroupElement extends LitComponentElement { static styles = css` :host { diff --git a/src/devtools/components/controls/button-icon/ButtonClickEvent.ts b/src/devtools/components/controls/button-icon/ButtonClickEvent.ts deleted file mode 100644 index 1c54be9..0000000 --- a/src/devtools/components/controls/button-icon/ButtonClickEvent.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default class ButtonClickEvent extends Event { - - constructor() { - super('click', { - bubbles: false, - composed: true - }); - } -} diff --git a/src/devtools/components/controls/button-icon/index.ts b/src/devtools/components/controls/button-icon/index.ts index 153580e..7cb1c3b 100644 --- a/src/devtools/components/controls/button-icon/index.ts +++ b/src/devtools/components/controls/button-icon/index.ts @@ -1,11 +1,10 @@ -import { LitElement, html } from "lit"; +import { html } from "lit"; import { customElement, property } from "lit/decorators.js"; - import resetButtonStyles from "@devtools/styles/reset-button.css" -import ButtonClickEvent from "./ButtonClickEvent"; +import { LitComponentElement } from "@devtools/components/lit-component"; @customElement('btn-icon') -export class IconButtonElement extends LitElement { +export class IconButtonElement extends LitComponentElement { static styles = [resetButtonStyles] @@ -29,8 +28,4 @@ export class IconButtonElement extends LitElement { ` } - - private dispatchChange() { - this.dispatchEvent(new ButtonClickEvent()) - } } diff --git a/src/devtools/components/controls/button-toggle/ButtonToggleEvent.ts b/src/devtools/components/controls/button-toggle/ButtonToggleEvent.ts deleted file mode 100644 index 48183a0..0000000 --- a/src/devtools/components/controls/button-toggle/ButtonToggleEvent.ts +++ /dev/null @@ -1,13 +0,0 @@ -export default class ButtonToggleEvent extends Event { - - enabled: boolean - - constructor(value: boolean) { - super('toggle', { - bubbles: false, - composed: true, - }) - - this.enabled = value - } -} diff --git a/src/devtools/components/controls/button-toggle/index.ts b/src/devtools/components/controls/button-toggle/index.ts index d2eed0b..a909d8b 100644 --- a/src/devtools/components/controls/button-toggle/index.ts +++ b/src/devtools/components/controls/button-toggle/index.ts @@ -1,20 +1,25 @@ -import { LitElement, html } from "lit"; -import { customElement, queryAssignedElements, state } from "lit/decorators.js"; -import ButtonToggleEvent from "./ButtonToggleEvent"; +import { html } from "lit"; +import { customElement, property, queryAssignedElements, state } from "lit/decorators.js"; +import { classMap } from "lit/directives/class-map.js" +import { LitComponentElement } from "@devtools/components/lit-component"; -@customElement('btn-toggle') -export class ButtonToggleElement extends LitElement { +import styles from "./styles.css" + +export type ToggleEventDetails = { + disabled: boolean +} - @state() enabled: boolean = true +@customElement('btn-toggle') +export class ButtonToggleElement extends LitComponentElement { - @queryAssignedElements({ slot: 'enabled' }) enabledButton: HTMLElement[] + static styles = [styles] - @queryAssignedElements({ slot: 'disabled' }) disabledButton: HTMLElement[] + @property({ type: Boolean }) disabled: boolean = false protected render() { return html` - - + + ` } @@ -30,23 +35,11 @@ export class ButtonToggleElement extends LitElement { this.removeEventListener('click', this.toggleEnabled) } - private initSlot() { - this.enabledButton.forEach(el => el.style.display = 'block') - this.disabledButton.forEach(el => el.style.display = 'none') - } - private toggleEnabled() { - this.enabled = !this.enabled - - if (this.enabled) { - this.enabledButton.forEach(el => el.style.display = 'block') - this.disabledButton.forEach(el => el.style.display = 'none') - } - else { - this.enabledButton.forEach(el => el.style.display = 'none') - this.disabledButton.forEach(el => el.style.display = 'block') - } - - this.dispatchEvent(new ButtonToggleEvent(this.enabled)) + this.disabled = !this.disabled + + this.fireEvent('toggle', { + disabled: this.disabled + }) } } diff --git a/src/devtools/components/controls/button-toggle/styles.css b/src/devtools/components/controls/button-toggle/styles.css new file mode 100644 index 0000000..f5a471c --- /dev/null +++ b/src/devtools/components/controls/button-toggle/styles.css @@ -0,0 +1,4 @@ + +.hidden { + display: none; +} diff --git a/src/devtools/components/icon/index.ts b/src/devtools/components/icon/index.ts index 4df6b1f..39b0eaa 100644 --- a/src/devtools/components/icon/index.ts +++ b/src/devtools/components/icon/index.ts @@ -1,11 +1,12 @@ -import { LitElement, html } from "lit"; +import { html } from "lit"; import { customElement, property } from "lit/decorators.js"; import { styleMap } from 'lit/directives/style-map.js'; +import { LitComponentElement } from "../lit-component"; import styles from "./styles.css" @customElement('material-icon') -export class IconElement extends LitElement { +export class IconElement extends LitComponentElement { static styles = styles diff --git a/src/devtools/components/inputs/search/SearchChangeEvent.ts b/src/devtools/components/inputs/search/SearchChangeEvent.ts deleted file mode 100644 index 043a33b..0000000 --- a/src/devtools/components/inputs/search/SearchChangeEvent.ts +++ /dev/null @@ -1,14 +0,0 @@ - -export default class SearchEvent extends Event { - - public value: string - - constructor(value: string) { - super('search', { - bubbles: false, - composed: true - }) - - this.value = value - } -} diff --git a/src/devtools/components/inputs/search/index.ts b/src/devtools/components/inputs/search/index.ts index 70674b6..5be60e1 100644 --- a/src/devtools/components/inputs/search/index.ts +++ b/src/devtools/components/inputs/search/index.ts @@ -1,11 +1,16 @@ -import { LitElement, html } from "lit"; -import { customElement, property, query } from "lit/decorators.js"; -import SearchEvent from "./SearchChangeEvent"; +import { html } from "lit"; +import { customElement, property, query, state } from "lit/decorators.js"; +import { live } from "lit/directives/live.js" +import { LitComponentElement } from "@devtools/components/lit-component"; import styles from "./styles.css" +export type SearchEventDetail = { + search: string +} + @customElement('input-search') -export class InputSearchElement extends LitElement { +export class InputSearchElement extends LitComponentElement { static styles = [styles] @@ -13,23 +18,27 @@ export class InputSearchElement extends LitElement { @property({ type: Number }) delayMs: number = 100 - @query('input') searchInput: HTMLInputElement + @property() searchValue: string = '' - private delayTimeout: number + @query('input') input: HTMLInputElement protected render() { return html` - + ` } - private onsearch(ev: KeyboardEvent) { - const value = this.searchInput.value - - clearTimeout(this.delayTimeout) + private handleInput() { + this.searchValue = this.input.value + } - this.delayTimeout = window.setTimeout(() => { - this.dispatchEvent(new SearchEvent(value)) - }, this.delayMs) + private handleSearch() { + this.fireEvent('search', { + search: this.searchValue + }) } } diff --git a/src/devtools/components/lit-component.ts b/src/devtools/components/lit-component.ts new file mode 100644 index 0000000..466ca4f --- /dev/null +++ b/src/devtools/components/lit-component.ts @@ -0,0 +1,14 @@ +import { LitElement } from "lit" + +export class LitComponentElement extends LitElement { + + protected fireEvent(type: string, data: T) { + const event = new CustomEvent(type, { + bubbles: true, + composed: true, + detail: data + }) + + this.dispatchEvent(event) + } +} diff --git a/src/devtools/components/profiler-event/index.ts b/src/devtools/components/profiler-event/index.ts index d562f21..a1b1cfa 100644 --- a/src/devtools/components/profiler-event/index.ts +++ b/src/devtools/components/profiler-event/index.ts @@ -1,13 +1,14 @@ -import { LitElement, html } from "lit"; +import { html } from "lit"; import { customElement, property, query } from "lit/decorators.js" import IncodingEvent from '@devtools/models/incodingEvent'; -import { select } from '@devtools/store/EventViewer/slice'; -import store from '@devtools/store'; +import { LitComponentElement } from "../lit-component"; import styles from "./styles.css" + + @customElement('profiler-event') -export class ProfilerEventElement extends LitElement { +export class ProfilerEventElement extends LitComponentElement { static styles = styles @@ -17,7 +18,7 @@ export class ProfilerEventElement extends LitElement { protected render() { return html` -
+
${this.data.eventName}
@@ -25,7 +26,7 @@ export class ProfilerEventElement extends LitElement { ` } - _click() { - store.dispatch(select(this.data)) + private handleClick() { + this.fireEvent('data-selected', this.data) } } diff --git a/src/devtools/components/time-marker/index.ts b/src/devtools/components/time-marker/index.ts index 8aed988..6b67143 100644 --- a/src/devtools/components/time-marker/index.ts +++ b/src/devtools/components/time-marker/index.ts @@ -1,8 +1,9 @@ -import { LitElement, html } from "lit"; +import { html } from "lit"; import { customElement, property } from "lit/decorators.js"; +import { LitComponentElement } from "../lit-component"; @customElement('time-marker') -export class TimeMarkerElement extends LitElement { +export class TimeMarkerElement extends LitComponentElement { @property({ type: Number }) public timeInMs?: number | undefined diff --git a/src/devtools/pages/EventList/panels/event-list-header.ts b/src/devtools/pages/EventList/panels/event-list-header.ts index d7688e7..7992069 100644 --- a/src/devtools/pages/EventList/panels/event-list-header.ts +++ b/src/devtools/pages/EventList/panels/event-list-header.ts @@ -1,6 +1,5 @@ import { LitElement, css, html } from "lit"; import { customElement } from "lit/decorators.js" -import SearchEvent from '@devtools/components/inputs/search/SearchChangeEvent'; import store from '@devtools/store'; import { clearEvents, @@ -9,7 +8,9 @@ import { resumeEvents, searchEvents } from '@devtools/store/EventList/slice'; -import ButtonToggleEvent from '@devtools/components/controls/button-toggle/ButtonToggleEvent'; +import { debounce } from "@devtools/utils/debounce"; +import { ToggleEventDetails } from "@devtools/components/controls/button-toggle"; +import { SearchEventDetail } from "@devtools/components/inputs/search"; @customElement('event-list-header') @@ -50,7 +51,10 @@ export class EventListHeaderElement extends LitElement {
- + + ` } @@ -58,19 +62,19 @@ export class EventListHeaderElement extends LitElement { store.dispatch(clearEvents()) } - private togglePauseEvents(ev: ButtonToggleEvent) { - if (ev.enabled) { + private togglePauseEvents(ev: CustomEvent) { + if (!ev.detail.disabled) { store.dispatch(resumeEvents()) } else { store.dispatch(pauseEvents()) } } - private onSearchEvents(ev: SearchEvent) { - const searchValue = ev.value + private handleSearchEvents(ev: CustomEvent) { + const search = ev.detail.search - if (searchValue !== '') { - store.dispatch(searchEvents(searchValue)) + if (search !== '') { + store.dispatch(searchEvents(search)) } else { store.dispatch(resetSearch()) } diff --git a/src/devtools/pages/EventList/panels/event-list.ts b/src/devtools/pages/EventList/panels/event-list.ts index fbc5a72..6b56c85 100644 --- a/src/devtools/pages/EventList/panels/event-list.ts +++ b/src/devtools/pages/EventList/panels/event-list.ts @@ -2,11 +2,12 @@ import { css, html } from "lit"; import { customElement, state, query } from "lit/decorators.js"; import { repeat } from "lit/directives/repeat.js" import IncodingEvent from "@devtools/models/incodingEvent"; -import { RootState } from "@devtools/store"; +import store, { RootState } from "@devtools/store"; import scrollStyles from "@devtools/styles/scroll.css" import StatefulLitElement from "@devtools/core/StatefulLItElement"; import { selectEvents } from "@devtools/store/EventList/selectors"; import resources from "@devtools/resources"; +import { select } from "@devtools/store/EventViewer/slice"; const styles = css` @@ -43,7 +44,7 @@ export class EventListElement extends StatefulLitElement { const hasEvents = this.events.length != 0 return html` -
+
${hasEvents ? this.renderList() : this.renderEmpty()} @@ -70,4 +71,8 @@ export class EventListElement extends StatefulLitElement { this.scrollAttached = this.container.scrollTop === containerScroll; } + + private onDataClick(ev: CustomEvent) { + store.dispatch(select(ev.detail)) + } } diff --git a/src/devtools/utils/debounce.ts b/src/devtools/utils/debounce.ts new file mode 100644 index 0000000..58f806a --- /dev/null +++ b/src/devtools/utils/debounce.ts @@ -0,0 +1,19 @@ + +function debounce void>( + func: T, timeoutMs: number +): (...args: Parameters) => void { + + let timeoutId: ReturnType | null = null + + return (...args: Parameters) => { + if (timeoutId) { + clearTimeout(timeoutId) + } + + timeoutId = setTimeout(() => func(...args), timeoutMs) + } +} + +export { + debounce +} From d9c50bd246ad320e31dd28fec1934bb380a665f5 Mon Sep 17 00:00:00 2001 From: semen Date: Sun, 14 Jan 2024 00:19:01 +0300 Subject: [PATCH 22/28] Add default popup, fix naming to kebab-case --- platforms/chrome/manifest.json | 4 ++++ platforms/edge/manifest.json | 4 ++++ public/{panel.html => devtools.html} | 0 public/index.html | 2 +- {src => public}/index.js | 4 +++- public/popup.html | 12 ++++++++++ src/background/persist-connection.js | 2 -- src/content-scripts/api/index.ts | 6 +---- src/content-scripts/content-script.ts | 4 ---- src/devtools/api/index.ts | 2 +- src/devtools/app.ts | 4 ++-- .../components/action-marker/index.ts | 2 +- .../button-group/index.ts | 0 .../button-icon/index.ts | 0 .../button-toggle/index.ts | 0 .../button-toggle/styles.css | 0 src/devtools/components/index.ts | 17 ++++++++----- .../inputs/{search => text}/index.ts | 14 +++++------ .../inputs/{search => text}/styles.css | 0 .../components/profiler-event/index.ts | 2 +- src/devtools/models/actions.ts | 18 -------------- .../pages/{EventList => event-list}/index.css | 0 .../pages/{EventList => event-list}/index.ts | 0 .../panels/event-list-header.ts | 19 ++++++++------- .../panels/event-list.ts | 6 ++--- .../panels/event-viewer.ts | 4 ++-- .../{EventList => event-list}/selectors.ts | 2 +- .../store/{EventList => event-list}/slice.ts | 2 +- .../selectors.ts | 0 .../{EventViewer => event-viewer}/slice.ts | 2 +- src/devtools/store/index.ts | 4 ++-- src/devtools/types/actions.ts | 19 +++++++++++++++ .../incoding-event.ts} | 2 +- .../jsonData.ts => types/json-data.ts} | 0 src/devtools/utils/actionColors.ts | 2 +- src/popup/index.ts | 2 ++ .../connection/RuntimeConnection.ts | 0 src/{ => shared}/connection/types/index.ts | 0 src/types/declarations.d.ts | 24 ------------------- src/types/index.d.ts | 6 ----- tsconfig.json | 2 +- webpack.config.js | 9 +++---- 42 files changed, 98 insertions(+), 104 deletions(-) rename public/{panel.html => devtools.html} (100%) rename {src => public}/index.js (50%) create mode 100644 public/popup.html rename src/devtools/components/{controls => buttons}/button-group/index.ts (100%) rename src/devtools/components/{controls => buttons}/button-icon/index.ts (100%) rename src/devtools/components/{controls => buttons}/button-toggle/index.ts (100%) rename src/devtools/components/{controls => buttons}/button-toggle/styles.css (100%) rename src/devtools/components/inputs/{search => text}/index.ts (75%) rename src/devtools/components/inputs/{search => text}/styles.css (100%) delete mode 100644 src/devtools/models/actions.ts rename src/devtools/pages/{EventList => event-list}/index.css (100%) rename src/devtools/pages/{EventList => event-list}/index.ts (100%) rename src/devtools/pages/{EventList => event-list}/panels/event-list-header.ts (78%) rename src/devtools/pages/{EventList => event-list}/panels/event-list.ts (91%) rename src/devtools/pages/{EventList => event-list}/panels/event-viewer.ts (86%) rename src/devtools/store/{EventList => event-list}/selectors.ts (91%) rename src/devtools/store/{EventList => event-list}/slice.ts (96%) rename src/devtools/store/{EventViewer => event-viewer}/selectors.ts (100%) rename src/devtools/store/{EventViewer => event-viewer}/slice.ts (91%) create mode 100644 src/devtools/types/actions.ts rename src/devtools/{models/incodingEvent.ts => types/incoding-event.ts} (82%) rename src/devtools/{models/jsonData.ts => types/json-data.ts} (100%) create mode 100644 src/popup/index.ts rename src/{ => shared}/connection/RuntimeConnection.ts (100%) rename src/{ => shared}/connection/types/index.ts (100%) delete mode 100644 src/types/declarations.d.ts diff --git a/platforms/chrome/manifest.json b/platforms/chrome/manifest.json index 9c6201b..2fc4f6e 100644 --- a/platforms/chrome/manifest.json +++ b/platforms/chrome/manifest.json @@ -6,6 +6,10 @@ "version": "1.0", "devtools_page": "index.html", + "action": { + "default_popup": "popup.html" + }, + "background": { "service_worker": "background.js" }, diff --git a/platforms/edge/manifest.json b/platforms/edge/manifest.json index 9c6201b..2fc4f6e 100644 --- a/platforms/edge/manifest.json +++ b/platforms/edge/manifest.json @@ -6,6 +6,10 @@ "version": "1.0", "devtools_page": "index.html", + "action": { + "default_popup": "popup.html" + }, + "background": { "service_worker": "background.js" }, diff --git a/public/panel.html b/public/devtools.html similarity index 100% rename from public/panel.html rename to public/devtools.html diff --git a/public/index.html b/public/index.html index a9a9fa1..fff4fa4 100644 --- a/public/index.html +++ b/public/index.html @@ -1,4 +1,4 @@ - + diff --git a/src/index.js b/public/index.js similarity index 50% rename from src/index.js rename to public/index.js index f5a92c8..8b73677 100644 --- a/src/index.js +++ b/public/index.js @@ -1,7 +1,9 @@ /** * Devtools panel initialization */ -chrome.devtools.panels.create('Incoding profiler', '', 'panel.html', + + +chrome.devtools.panels.create('Incoding profiler', '', 'devtools.html', function(panel) { } diff --git a/public/popup.html b/public/popup.html new file mode 100644 index 0000000..68aa316 --- /dev/null +++ b/public/popup.html @@ -0,0 +1,12 @@ + + + + + + +
+ +
+ + + diff --git a/src/background/persist-connection.js b/src/background/persist-connection.js index 8f7e3bc..728e525 100644 --- a/src/background/persist-connection.js +++ b/src/background/persist-connection.js @@ -14,12 +14,10 @@ function keepBackgroundAlive() { } } - async function ping() { await chrome.storage.local.set({ '_': 'pong' + Date.now() }) } - export { keepBackgroundAlive } diff --git a/src/content-scripts/api/index.ts b/src/content-scripts/api/index.ts index 7bbcbc2..18491b8 100644 --- a/src/content-scripts/api/index.ts +++ b/src/content-scripts/api/index.ts @@ -1,8 +1,4 @@ -export type InspectDOMElementMessage = string - -type BrowserMessages = { - 'inspect-element': InspectDOMElementMessage -} +type BrowserMessages = { } export default BrowserMessages diff --git a/src/content-scripts/content-script.ts b/src/content-scripts/content-script.ts index 51b29d5..1cd937b 100644 --- a/src/content-scripts/content-script.ts +++ b/src/content-scripts/content-script.ts @@ -16,10 +16,6 @@ connection.on('disconnected', () => { window.removeEventListener('message', onWindowMessage) }) -connection.on('inspect-element', elementId => { - window.inspect(document.querySelector(`data-profiler-id="${elementId}"`)) -}) - connection.connect('content-script') function onWindowMessage({ source, data }: any) { diff --git a/src/devtools/api/index.ts b/src/devtools/api/index.ts index 95e61ad..f737715 100644 --- a/src/devtools/api/index.ts +++ b/src/devtools/api/index.ts @@ -1,4 +1,4 @@ -import IncodingEvent from "@devtools/models/incodingEvent" +import IncodingEvent from "@devtools/types/incoding-event" export type IncodingEventMessage = Omit diff --git a/src/devtools/app.ts b/src/devtools/app.ts index 9e9a3a1..509a42e 100644 --- a/src/devtools/app.ts +++ b/src/devtools/app.ts @@ -1,5 +1,5 @@ import "@devtools/components" -import '@devtools/pages/EventList/index' +import '@devtools/pages/event-list/index' import { LitElement, html } from "lit"; import { customElement, state } from "lit/decorators.js"; @@ -7,7 +7,7 @@ import { choose } from "lit/directives/choose.js" import { provide } from "@lit/context"; import resources from "@devtools/resources"; import store from "./store"; -import { addEvent, updateEvent } from "./store/EventList/slice"; +import { addEvent, updateEvent } from "./store/event-list/slice"; import RuntimeConnection, { DevtoolsConnection } from "@connection/RuntimeConnection"; import runtimeConnectionCtx from "./context/connection"; diff --git a/src/devtools/components/action-marker/index.ts b/src/devtools/components/action-marker/index.ts index 98d78c6..b7cecca 100644 --- a/src/devtools/components/action-marker/index.ts +++ b/src/devtools/components/action-marker/index.ts @@ -1,6 +1,6 @@ import { html } from "lit"; import { customElement, property } from "lit/decorators.js"; -import Actions from "@devtools/models/actions"; +import Actions from "@devtools/types/actions"; import getColorByAction from "@devtools/utils/actionColors"; import { LitComponentElement } from "../lit-component"; diff --git a/src/devtools/components/controls/button-group/index.ts b/src/devtools/components/buttons/button-group/index.ts similarity index 100% rename from src/devtools/components/controls/button-group/index.ts rename to src/devtools/components/buttons/button-group/index.ts diff --git a/src/devtools/components/controls/button-icon/index.ts b/src/devtools/components/buttons/button-icon/index.ts similarity index 100% rename from src/devtools/components/controls/button-icon/index.ts rename to src/devtools/components/buttons/button-icon/index.ts diff --git a/src/devtools/components/controls/button-toggle/index.ts b/src/devtools/components/buttons/button-toggle/index.ts similarity index 100% rename from src/devtools/components/controls/button-toggle/index.ts rename to src/devtools/components/buttons/button-toggle/index.ts diff --git a/src/devtools/components/controls/button-toggle/styles.css b/src/devtools/components/buttons/button-toggle/styles.css similarity index 100% rename from src/devtools/components/controls/button-toggle/styles.css rename to src/devtools/components/buttons/button-toggle/styles.css diff --git a/src/devtools/components/index.ts b/src/devtools/components/index.ts index 8a3e02c..83eb7d4 100644 --- a/src/devtools/components/index.ts +++ b/src/devtools/components/index.ts @@ -1,9 +1,14 @@ import "./action-marker" -import "./time-marker" -import "./icon" import "./profiler-event" -import "./controls/button-icon" -import "./controls/button-group" -import "./controls/button-toggle" -import "./inputs/search" +import "./time-marker" + import "./no-content" + +import "./icon" + +import "./inputs/text" + +import "./buttons/button-icon" +import "./buttons/button-group" +import "./buttons/button-toggle" + diff --git a/src/devtools/components/inputs/search/index.ts b/src/devtools/components/inputs/text/index.ts similarity index 75% rename from src/devtools/components/inputs/search/index.ts rename to src/devtools/components/inputs/text/index.ts index 5be60e1..ce28d38 100644 --- a/src/devtools/components/inputs/search/index.ts +++ b/src/devtools/components/inputs/text/index.ts @@ -5,11 +5,11 @@ import { LitComponentElement } from "@devtools/components/lit-component"; import styles from "./styles.css" -export type SearchEventDetail = { +export type ValueChangeEventDetail = { search: string } -@customElement('input-search') +@customElement('input-text') export class InputSearchElement extends LitComponentElement { static styles = [styles] @@ -18,7 +18,7 @@ export class InputSearchElement extends LitComponentElement { @property({ type: Number }) delayMs: number = 100 - @property() searchValue: string = '' + @property() value: string = '' @query('input') input: HTMLInputElement @@ -26,19 +26,19 @@ export class InputSearchElement extends LitComponentElement { return html` ` } private handleInput() { - this.searchValue = this.input.value + this.value = this.input.value } private handleSearch() { - this.fireEvent('search', { - search: this.searchValue + this.fireEvent('value-change', { + search: this.value }) } } diff --git a/src/devtools/components/inputs/search/styles.css b/src/devtools/components/inputs/text/styles.css similarity index 100% rename from src/devtools/components/inputs/search/styles.css rename to src/devtools/components/inputs/text/styles.css diff --git a/src/devtools/components/profiler-event/index.ts b/src/devtools/components/profiler-event/index.ts index a1b1cfa..86f5390 100644 --- a/src/devtools/components/profiler-event/index.ts +++ b/src/devtools/components/profiler-event/index.ts @@ -1,6 +1,6 @@ import { html } from "lit"; import { customElement, property, query } from "lit/decorators.js" -import IncodingEvent from '@devtools/models/incodingEvent'; +import IncodingEvent from '@devtools/types/incoding-event'; import { LitComponentElement } from "../lit-component"; import styles from "./styles.css" diff --git a/src/devtools/models/actions.ts b/src/devtools/models/actions.ts deleted file mode 100644 index f81c7af..0000000 --- a/src/devtools/models/actions.ts +++ /dev/null @@ -1,18 +0,0 @@ -type Actions = 'Direct' | - 'Eval' | - 'Ajax' | - 'Submit' | - 'Jquery' | - 'Trigger' | - 'Insert' | - 'Eval Method' | - 'Break' | - 'Store Insert' | - 'Store fetch' | - 'Store Manipulate' | - 'Form' | - 'Bind' | - 'Validation parse' | - 'Validation Refresh' - -export default Actions diff --git a/src/devtools/pages/EventList/index.css b/src/devtools/pages/event-list/index.css similarity index 100% rename from src/devtools/pages/EventList/index.css rename to src/devtools/pages/event-list/index.css diff --git a/src/devtools/pages/EventList/index.ts b/src/devtools/pages/event-list/index.ts similarity index 100% rename from src/devtools/pages/EventList/index.ts rename to src/devtools/pages/event-list/index.ts diff --git a/src/devtools/pages/EventList/panels/event-list-header.ts b/src/devtools/pages/event-list/panels/event-list-header.ts similarity index 78% rename from src/devtools/pages/EventList/panels/event-list-header.ts rename to src/devtools/pages/event-list/panels/event-list-header.ts index 7992069..6ec908e 100644 --- a/src/devtools/pages/EventList/panels/event-list-header.ts +++ b/src/devtools/pages/event-list/panels/event-list-header.ts @@ -7,10 +7,10 @@ import { resetSearch, resumeEvents, searchEvents -} from '@devtools/store/EventList/slice'; +} from '@devtools/store/event-list/slice'; import { debounce } from "@devtools/utils/debounce"; -import { ToggleEventDetails } from "@devtools/components/controls/button-toggle"; -import { SearchEventDetail } from "@devtools/components/inputs/search"; +import { ToggleEventDetails } from "@devtools/components/buttons/button-toggle"; +import { ValueChangeEventDetail } from "@devtools/components/inputs/text"; @customElement('event-list-header') @@ -51,10 +51,13 @@ export class EventListHeaderElement extends LitElement {
- - +
+ + +
` } @@ -70,7 +73,7 @@ export class EventListHeaderElement extends LitElement { } } - private handleSearchEvents(ev: CustomEvent) { + private handleSearch(ev: CustomEvent) { const search = ev.detail.search if (search !== '') { diff --git a/src/devtools/pages/EventList/panels/event-list.ts b/src/devtools/pages/event-list/panels/event-list.ts similarity index 91% rename from src/devtools/pages/EventList/panels/event-list.ts rename to src/devtools/pages/event-list/panels/event-list.ts index 6b56c85..d0453ef 100644 --- a/src/devtools/pages/EventList/panels/event-list.ts +++ b/src/devtools/pages/event-list/panels/event-list.ts @@ -1,13 +1,13 @@ import { css, html } from "lit"; import { customElement, state, query } from "lit/decorators.js"; import { repeat } from "lit/directives/repeat.js" -import IncodingEvent from "@devtools/models/incodingEvent"; +import IncodingEvent from "@devtools/types/incoding-event"; import store, { RootState } from "@devtools/store"; import scrollStyles from "@devtools/styles/scroll.css" import StatefulLitElement from "@devtools/core/StatefulLItElement"; -import { selectEvents } from "@devtools/store/EventList/selectors"; +import { selectEvents } from "@devtools/store/event-list/selectors"; import resources from "@devtools/resources"; -import { select } from "@devtools/store/EventViewer/slice"; +import { select } from "@devtools/store/event-viewer/slice"; const styles = css` diff --git a/src/devtools/pages/EventList/panels/event-viewer.ts b/src/devtools/pages/event-list/panels/event-viewer.ts similarity index 86% rename from src/devtools/pages/EventList/panels/event-viewer.ts rename to src/devtools/pages/event-list/panels/event-viewer.ts index 2c68184..c98796b 100644 --- a/src/devtools/pages/EventList/panels/event-viewer.ts +++ b/src/devtools/pages/event-list/panels/event-viewer.ts @@ -1,9 +1,9 @@ import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; -import JsonData from "@devtools/models/jsonData" +import JsonData from "@devtools/types/json-data" import { RootState } from '@devtools/store'; import StatefulLitElement from '@devtools/core/StatefulLItElement'; -import { selectSelectedJsonData } from '@devtools/store/EventViewer/selectors'; +import { selectSelectedJsonData } from '@devtools/store/event-viewer/selectors'; import resources from '@devtools/resources'; diff --git a/src/devtools/store/EventList/selectors.ts b/src/devtools/store/event-list/selectors.ts similarity index 91% rename from src/devtools/store/EventList/selectors.ts rename to src/devtools/store/event-list/selectors.ts index f212440..f273733 100644 --- a/src/devtools/store/EventList/selectors.ts +++ b/src/devtools/store/event-list/selectors.ts @@ -1,6 +1,6 @@ import { createSelector } from "@reduxjs/toolkit"; import { RootState } from ".."; -import IncodingEvent from "@devtools/models/incodingEvent"; +import IncodingEvent from "@devtools/types/incoding-event"; const eventsSelector = (state: RootState) => state.eventList.events const searchSelector = (state: RootState) => state.eventList.search diff --git a/src/devtools/store/EventList/slice.ts b/src/devtools/store/event-list/slice.ts similarity index 96% rename from src/devtools/store/EventList/slice.ts rename to src/devtools/store/event-list/slice.ts index 1de08b6..9b838e7 100644 --- a/src/devtools/store/EventList/slice.ts +++ b/src/devtools/store/event-list/slice.ts @@ -1,5 +1,5 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import IncodingEvent from "@devtools/models/incodingEvent"; +import IncodingEvent from "@devtools/types/incoding-event"; import { IncodingEventExecutedMessage, IncodingEventMessage diff --git a/src/devtools/store/EventViewer/selectors.ts b/src/devtools/store/event-viewer/selectors.ts similarity index 100% rename from src/devtools/store/EventViewer/selectors.ts rename to src/devtools/store/event-viewer/selectors.ts diff --git a/src/devtools/store/EventViewer/slice.ts b/src/devtools/store/event-viewer/slice.ts similarity index 91% rename from src/devtools/store/EventViewer/slice.ts rename to src/devtools/store/event-viewer/slice.ts index ac4a48d..98b356b 100644 --- a/src/devtools/store/EventViewer/slice.ts +++ b/src/devtools/store/event-viewer/slice.ts @@ -1,5 +1,5 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import IncodingEvent from "@devtools/models/incodingEvent"; +import IncodingEvent from "@devtools/types/incoding-event"; interface EventViewerState { selected: IncodingEvent | null diff --git a/src/devtools/store/index.ts b/src/devtools/store/index.ts index 90d6ec4..b6e8805 100644 --- a/src/devtools/store/index.ts +++ b/src/devtools/store/index.ts @@ -1,6 +1,6 @@ import { configureStore } from "@reduxjs/toolkit"; -import eventListReducer from "./EventList/slice"; -import eventViewerReducer from "./EventViewer/slice"; +import eventListReducer from "./event-list/slice"; +import eventViewerReducer from "./event-viewer/slice"; const store = configureStore({ reducer: { diff --git a/src/devtools/types/actions.ts b/src/devtools/types/actions.ts new file mode 100644 index 0000000..0f08a2f --- /dev/null +++ b/src/devtools/types/actions.ts @@ -0,0 +1,19 @@ +type Actions = + 'Direct' | + 'Eval' | + 'Ajax' | + 'Submit' | + 'Jquery' | + 'Trigger' | + 'Insert' | + 'Eval Method' | + 'Break' | + 'Store Insert' | + 'Store fetch' | + 'Store Manipulate' | + 'Form' | + 'Bind' | + 'Validation parse' | + 'Validation Refresh' + +export default Actions diff --git a/src/devtools/models/incodingEvent.ts b/src/devtools/types/incoding-event.ts similarity index 82% rename from src/devtools/models/incodingEvent.ts rename to src/devtools/types/incoding-event.ts index f90d96f..1329027 100644 --- a/src/devtools/models/incodingEvent.ts +++ b/src/devtools/types/incoding-event.ts @@ -1,4 +1,4 @@ -import JsonData from "@devtools/models/jsonData" +import JsonData from "@devtools/types/json-data" import Actions from "./actions" export default interface IncodingEvent { diff --git a/src/devtools/models/jsonData.ts b/src/devtools/types/json-data.ts similarity index 100% rename from src/devtools/models/jsonData.ts rename to src/devtools/types/json-data.ts diff --git a/src/devtools/utils/actionColors.ts b/src/devtools/utils/actionColors.ts index ec44f5e..0326f10 100644 --- a/src/devtools/utils/actionColors.ts +++ b/src/devtools/utils/actionColors.ts @@ -1,4 +1,4 @@ -import Actions from "@devtools/models/actions"; +import Actions from "@devtools/types/actions"; const actionColors: { [key in Actions]: string } = { 'Direct': 'rgb(238, 238, 238)', diff --git a/src/popup/index.ts b/src/popup/index.ts new file mode 100644 index 0000000..3df3080 --- /dev/null +++ b/src/popup/index.ts @@ -0,0 +1,2 @@ + +document.getElementById('root')!.innerText = '' diff --git a/src/connection/RuntimeConnection.ts b/src/shared/connection/RuntimeConnection.ts similarity index 100% rename from src/connection/RuntimeConnection.ts rename to src/shared/connection/RuntimeConnection.ts diff --git a/src/connection/types/index.ts b/src/shared/connection/types/index.ts similarity index 100% rename from src/connection/types/index.ts rename to src/shared/connection/types/index.ts diff --git a/src/types/declarations.d.ts b/src/types/declarations.d.ts deleted file mode 100644 index e984360..0000000 --- a/src/types/declarations.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ActionMarkerElement } from "../devtools/components/action-marker"; -import { IconElement } from "../devtools/components/icon"; -import { ProfilerEventElement } from "../devtools/components/profiler-event"; -import { TimeMarkerElement } from "../devtools/components/time-marker"; -import { EventListPage } from "../devtools/pages/EventList"; -import { EventListElement } from "../devtools/pages/EventList/panels/event-list"; -import { EventListHeaderElement } from "../devtools/pages/EventList/panels/event-list-header"; -import { EventViewerElement } from "../devtools/pages/EventList/panels/event-viewer"; - -declare global { - - interface HTMLElementTagNameMap { - "profiler-event": ProfilerEventElement; - "action-marker": ActionMarkerElement; - "time-marker": TimeMarkerElement; - "material-icon": IconElement; - - "event-list": EventListElement; - "event-list-page": EventListPage; - "event-list-header": EventListHeaderElement; - "event-viewer": EventViewerElement; - } - -} diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 670da59..da97db2 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,8 +1,2 @@ -interface Window { - - inspect(element: HTMLElement | null): void; - -} - declare module '*.css'; diff --git a/tsconfig.json b/tsconfig.json index fc5f242..e8f1511 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,7 @@ "paths": { "@devtools/*": ["./src/devtools/*"], "@content-scripts/*": ["./src/content-scripts/*"], - "@connection/*": ["./src/connection/*"] + "@connection/*": ["./src/shared/connection/*"] }, "resolveJsonModule": true } diff --git a/webpack.config.js b/webpack.config.js index 645d53b..b69ea43 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,11 +6,12 @@ module.exports = (env) => { return { entry: { - loader: './src/index.js', + popup: './src/popup/index.ts', background: './src/background/background.js', devtools: './src/devtools/index.ts', - inject_profiler: './src/content-scripts/injection/inject-profiler.js', - content_script: './src/content-scripts/content-script.ts' + content_script: './src/content-scripts/content-script.ts', + + inject_profiler: './src/content-scripts/injection/inject-profiler.js' }, output: { filename: '[name].js', @@ -21,7 +22,7 @@ module.exports = (env) => { alias: { '@devtools': path.resolve(__dirname, 'src/devtools/'), '@content-scripts': path.resolve(__dirname, 'src/content-scripts/'), - '@connection': path.resolve(__dirname, 'src/connection/') + '@connection': path.resolve(__dirname, 'src/shared/connection/') } }, module: { From a8e1020ba5f70e5641897ca2756ac264df7e1fc9 Mon Sep 17 00:00:00 2001 From: semyon Date: Fri, 2 Feb 2024 18:13:39 +0300 Subject: [PATCH 23/28] Add firefox inject-profiler.js injection with fetch Firefox currently lack support of `scripting` api and `main` execution world, so we need to fuck around with injecting this script. Maybe it will be better if we just insert this script manually and delegate it to end user --- package.json | 4 +++- platforms/firefox/manifest.json | 21 +++++++++++++++++++++ src/background/background.js | 26 +++++++++++++++++--------- src/content-scripts/content-script.ts | 11 +++++++++++ src/types/index.d.ts | 4 ++++ webpack.config.js | 12 +++++++++--- 6 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 platforms/firefox/manifest.json diff --git a/package.json b/package.json index eeb0851..2979861 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,12 @@ "scripts": { "prod:chrome": "npx webpack build --mode production --env mode=release --env platform=chrome", "prod:edge": "npx webpack build --mode production --env mode=release --env platform=edge", - "prod:firefox": "npx webpack build --mode production --env mode=release --env platform=edge", + "prod:firefox": "npx webpack build --mode production --env mode=release --env platform=firefox", + "dev:chrome": "npx webpack watch --mode production -d source-map --env mode=debug --env platform=chrome", "dev:edge": "npx webpack watch --mode production -d source-map --env mode=debug --env platform=edge", "dev:firefox": "npx webpack watch --mode production -d source-map --env mode=debug --env platform=firefox", + "lint": "eslint src/", "lintfix": "eslint src/ --fix" }, diff --git a/platforms/firefox/manifest.json b/platforms/firefox/manifest.json new file mode 100644 index 0000000..f70acb0 --- /dev/null +++ b/platforms/firefox/manifest.json @@ -0,0 +1,21 @@ +{ + "manifest_version": 2, + "name": "incoding.profiler", + "description": "Devtools extension for profiling incoding.framework.js", + + "version": "1.0", + "devtools_page": "index.html", + + "page_action": { + "default_popup": "popup.html" + }, + + "background": { + "scripts": ["background.js"] + }, + + "permissions": [ + "activeTab", "scripting", "storage", + "" + ] +} diff --git a/src/background/background.js b/src/background/background.js index a7133a0..4373542 100644 --- a/src/background/background.js +++ b/src/background/background.js @@ -45,15 +45,23 @@ chrome.runtime.onConnect.addListener(function onConnect(port) { async function dynamiclyInjectContentScript() { - const scriptsToInject = [ - { - id: 'inject-profiler', - matches: [''], - js: ['inject_profiler.js'], - world: 'MAIN', - runAt: 'document_end' - } - ] + const scriptsToInject = + __FIREFOX__ ? [ + { + id: 'inject-profiler', + matches: [''], + js: ['inject_profiler.js'], + runAt: 'document_end' + } + ] : [ + { + id: 'inject-profiler', + matches: [''], + js: ['inject_profiler.js'], + world: 'MAIN', + runAt: 'document_end' + } + ] try { await chrome.scripting.unregisterContentScripts() diff --git a/src/content-scripts/content-script.ts b/src/content-scripts/content-script.ts index 1cd937b..fd9ad82 100644 --- a/src/content-scripts/content-script.ts +++ b/src/content-scripts/content-script.ts @@ -8,6 +8,17 @@ import RuntimeConnection, { BrowserConnection } from "@connection/RuntimeConnect const connection: BrowserConnection = new RuntimeConnection() +fetch(chrome.runtime.getURL('inject_profiler.js')) + .then(res => res.text()) + .then(code => { + const script = document.createElement('script') + + script.innerHTML = code + + document.body.appendChild(script) + }) + .catch(alert) + connection.on('connected', () => { window.addEventListener('message', onWindowMessage) }) diff --git a/src/types/index.d.ts b/src/types/index.d.ts index da97db2..b9dcead 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,2 +1,6 @@ declare module '*.css'; + +declare var __FIREFOX__: boolean +declare var __CHROME__: boolean +declare var __EDGE__: boolean diff --git a/webpack.config.js b/webpack.config.js index b69ea43..fa9cae4 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,8 +1,14 @@ const path = require('path') +const webpack = require('webpack') const PrebuildExtensionPlugin = require('./platforms/PrebuildExtensionPlugin') module.exports = (env) => { - const plugin = new PrebuildExtensionPlugin(env.mode, env.platform) + const buildPlugin = new PrebuildExtensionPlugin(env.mode, env.platform) + const definePlugin = new webpack.DefinePlugin({ + __FIREFOX__: buildPlugin.platform === 'firefox', + __CHROME__: buildPlugin.platform === 'chrome', + __EDGE__: buildPlugin.platform === 'edge' + }) return { entry: { @@ -15,7 +21,7 @@ module.exports = (env) => { }, output: { filename: '[name].js', - path: plugin.getDestinationPath() + path: buildPlugin.getDestinationPath() }, resolve: { extensions: ['.js', '.ts'], @@ -41,6 +47,6 @@ module.exports = (env) => { } ], }, - plugins: [plugin] + plugins: [buildPlugin, definePlugin] } } From fd14df7edc79ef2bbb4a41362f4cbfa706d062f7 Mon Sep 17 00:00:00 2001 From: semen Date: Sat, 3 Feb 2024 21:50:44 +0300 Subject: [PATCH 24/28] Fix disconnect for runtime connection --- src/shared/connection/RuntimeConnection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/connection/RuntimeConnection.ts b/src/shared/connection/RuntimeConnection.ts index 473534c..0228b8f 100644 --- a/src/shared/connection/RuntimeConnection.ts +++ b/src/shared/connection/RuntimeConnection.ts @@ -29,7 +29,7 @@ class RuntimeConnection< } disconnect() { - this.disconnect() + this.connection.disconnect() } emit(type: TKey, payload: TEmit[TKey]) { From 9e1ae6a5256056d9f4e7977609a41d9ee298fa32 Mon Sep 17 00:00:00 2001 From: semen Date: Sun, 4 Feb 2024 13:56:46 +0300 Subject: [PATCH 25/28] Add support for firefox (#30) Add support for firefox browser, changed logic of intercepting events: now content-scripts.ts is declared statically in manifest.json, and will install inject-profiler script via document.createElement('script') and appending it to element if DOM, thus dynamicallyInjectContentScript is not needed anymore --- platforms/chrome/manifest.json | 16 ++++++ platforms/edge/manifest.json | 16 ++++++ platforms/firefox/manifest.json | 11 +++- src/background/background.js | 58 ++-------------------- src/content-scripts/content-script.ts | 34 ++++++++----- src/shared/connection/RuntimeConnection.ts | 7 ++- webpack.config.js | 3 +- 7 files changed, 72 insertions(+), 73 deletions(-) diff --git a/platforms/chrome/manifest.json b/platforms/chrome/manifest.json index 2fc4f6e..0a2ea53 100644 --- a/platforms/chrome/manifest.json +++ b/platforms/chrome/manifest.json @@ -14,6 +14,22 @@ "service_worker": "background.js" }, + "content_scripts": [ + { + "id": "content_script", + "js": ["content_script.js"], + "run_at": "document_start", + "matches": [""] + } + ], + + "web_accessible_resources": [ + { + "resources": ["inject_profiler.js"], + "matches": [""] + } + ], + "permissions": [ "activeTab", "scripting", "storage" ], diff --git a/platforms/edge/manifest.json b/platforms/edge/manifest.json index 2fc4f6e..0a2ea53 100644 --- a/platforms/edge/manifest.json +++ b/platforms/edge/manifest.json @@ -14,6 +14,22 @@ "service_worker": "background.js" }, + "content_scripts": [ + { + "id": "content_script", + "js": ["content_script.js"], + "run_at": "document_start", + "matches": [""] + } + ], + + "web_accessible_resources": [ + { + "resources": ["inject_profiler.js"], + "matches": [""] + } + ], + "permissions": [ "activeTab", "scripting", "storage" ], diff --git a/platforms/firefox/manifest.json b/platforms/firefox/manifest.json index f70acb0..9a57cc8 100644 --- a/platforms/firefox/manifest.json +++ b/platforms/firefox/manifest.json @@ -11,9 +11,18 @@ }, "background": { - "scripts": ["background.js"] + "scripts": ["background.js"], + "persistent": true }, + "content_scripts": [ + { + "js": ["content_script.js"], + "run_at": "document_start", + "matches": [""] + } + ], + "permissions": [ "activeTab", "scripting", "storage", "" diff --git a/src/background/background.js b/src/background/background.js index 4373542..051e5c7 100644 --- a/src/background/background.js +++ b/src/background/background.js @@ -10,9 +10,9 @@ import { keepBackgroundAlive } from "./persist-connection" const connectionPorts = {} -const killBackground = keepBackgroundAlive() - -dynamiclyInjectContentScript() +if (!__FIREFOX__) { + keepBackgroundAlive() +} chrome.runtime.onConnect.addListener(function onConnect(port) { let name = null @@ -21,8 +21,6 @@ chrome.runtime.onConnect.addListener(function onConnect(port) { if (Number.isInteger(+port.name)) { name = 'devtools' tab = +port.name - - installContentScript(tab) } else { name = 'contentScript' @@ -43,54 +41,6 @@ chrome.runtime.onConnect.addListener(function onConnect(port) { } }) - -async function dynamiclyInjectContentScript() { - const scriptsToInject = - __FIREFOX__ ? [ - { - id: 'inject-profiler', - matches: [''], - js: ['inject_profiler.js'], - runAt: 'document_end' - } - ] : [ - { - id: 'inject-profiler', - matches: [''], - js: ['inject_profiler.js'], - world: 'MAIN', - runAt: 'document_end' - } - ] - - try { - await chrome.scripting.unregisterContentScripts() - await chrome.scripting.registerContentScripts(scriptsToInject) - } - catch (error) { - console.error(error) - } -} - - -async function installContentScript(tab) { - const contentScript = { - files: ['content_script.js'], - target: { - tabId: tab - }, - world: "ISOLATED" - } - - try { - await chrome.scripting.executeScript(contentScript) - } - catch (error) { - console.error(error); - } -} - - function establishBidirectionalConnection(tabId, one, two) { const listen = (anotherPort) => function (message) { try { @@ -122,7 +72,5 @@ function establishBidirectionalConnection(tabId, one, two) { two.disconnect(); connectionPorts[tabId] = null; - - killBackground() } } diff --git a/src/content-scripts/content-script.ts b/src/content-scripts/content-script.ts index fd9ad82..65f7be7 100644 --- a/src/content-scripts/content-script.ts +++ b/src/content-scripts/content-script.ts @@ -4,21 +4,10 @@ * Establishing window listener to direct messages from injected script to background service-worker */ -import RuntimeConnection, { BrowserConnection } from "@connection/RuntimeConnection" +import RuntimeConnection, { BrowserConnection } from "@connection/RuntimeConnection"; const connection: BrowserConnection = new RuntimeConnection() -fetch(chrome.runtime.getURL('inject_profiler.js')) - .then(res => res.text()) - .then(code => { - const script = document.createElement('script') - - script.innerHTML = code - - document.body.appendChild(script) - }) - .catch(alert) - connection.on('connected', () => { window.addEventListener('message', onWindowMessage) }) @@ -27,7 +16,15 @@ connection.on('disconnected', () => { window.removeEventListener('message', onWindowMessage) }) -connection.connect('content-script') +document.onreadystatechange = () => { + if (document.readyState === 'interactive') { + injectProfilerToPage() + } + + if (document.readyState === 'complete') { + connection.connect('content-script') + } +}; function onWindowMessage({ source, data }: any) { if (source !== window || !data) { @@ -36,3 +33,14 @@ function onWindowMessage({ source, data }: any) { connection.emit(data.type, data.payload) } + +/** + * Firefox manifest does not support executionWorld.MAIN, so we have to + * manually inject profiling script as