From 0f4fdaf7dd72d7d5b69ac48d23c9dc2a561d9e83 Mon Sep 17 00:00:00 2001 From: Robbie Wagner Date: Mon, 13 Jan 2025 12:02:26 -0500 Subject: [PATCH] Convert adapters and port to TS, remove Evented from port (#2617) --- ...operty-field.js => date-property-field.ts} | 6 +- app/config/environment.d.ts | 2 + app/services/adapters/{basic.js => basic.ts} | 51 +++---- .../{bookmarklet.js => bookmarklet.ts} | 25 ++-- .../adapters/{chrome.js => chrome.ts} | 3 +- .../adapters/{firefox.js => firefox.ts} | 0 .../{web-extension.js => web-extension.ts} | 54 ++++--- .../adapters/{websocket.js => websocket.ts} | 15 +- app/services/{layout.js => layout.ts} | 13 +- app/services/port.js | 90 ------------ app/services/port.ts | 133 ++++++++++++++++++ tsconfig.json | 3 +- 12 files changed, 211 insertions(+), 184 deletions(-) rename app/components/{date-property-field.js => date-property-field.ts} (68%) rename app/services/adapters/{basic.js => basic.ts} (75%) rename app/services/adapters/{bookmarklet.js => bookmarklet.ts} (75%) rename app/services/adapters/{chrome.js => chrome.ts} (86%) rename app/services/adapters/{firefox.js => firefox.ts} (100%) rename app/services/adapters/{web-extension.js => web-extension.ts} (82%) rename app/services/adapters/{websocket.js => websocket.ts} (56%) rename app/services/{layout.js => layout.ts} (80%) delete mode 100644 app/services/port.js create mode 100644 app/services/port.ts diff --git a/app/components/date-property-field.js b/app/components/date-property-field.ts similarity index 68% rename from app/components/date-property-field.js rename to app/components/date-property-field.ts index a0cf28173e..4bc5259c56 100644 --- a/app/components/date-property-field.js +++ b/app/components/date-property-field.ts @@ -1,16 +1,16 @@ import { scheduleOnce } from '@ember/runloop'; import { action } from '@ember/object'; -import DatePicker from 'ember-inspector/components/ember-flatpickr'; +import DatePicker from 'ember-flatpickr/components/ember-flatpickr'; export default class DatePropertyFieldComponent extends DatePicker { @action - onInsert(element) { + onInsert(element: HTMLInputElement) { super.onInsert(element); scheduleOnce('afterRender', this, this._openFlatpickr); } _openFlatpickr() { - this.flatpickrRef.open(); + this.flatpickrRef?.open(); } } diff --git a/app/config/environment.d.ts b/app/config/environment.d.ts index a8f92b9feb..082d955e4b 100644 --- a/app/config/environment.d.ts +++ b/app/config/environment.d.ts @@ -3,10 +3,12 @@ * import config from 'my-app/config/environment' */ declare const config: { + emberVersionsSupported: [fromVersion: string, tillVersion: string]; environment: string; modulePrefix: string; podModulePrefix: string; locationType: 'history' | 'hash' | 'none' | 'auto'; + previousEmberVersionsSupported: Array; rootURL: string; APP: Record; }; diff --git a/app/services/adapters/basic.js b/app/services/adapters/basic.ts similarity index 75% rename from app/services/adapters/basic.js rename to app/services/adapters/basic.ts index d72c84d16e..ff12ae3482 100644 --- a/app/services/adapters/basic.js +++ b/app/services/adapters/basic.ts @@ -14,22 +14,25 @@ import Service, { inject as service } from '@ember/service'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; +import type { AnyFn } from 'ember/-private/type-utils'; import config from 'ember-inspector/config/environment'; +import type PortService from '../port'; +import type { Message } from '../port'; -export default class Basic extends Service { - @service port; +export default abstract class Basic extends Service { + @service declare port: PortService; - @tracked canOpenResource = false; + _messageCallbacks: Array; name = 'basic'; + @tracked canOpenResource = false; + /** * Called when the adapter is created (when * the inspector app boots). - * - * @method init */ - init() { - super.init(...arguments); + constructor(properties?: object) { + super(properties); this._messageCallbacks = []; this._checkVersion(); } @@ -41,7 +44,6 @@ export default class Basic extends Service { * Ember version and needs to switch to an inspector version * that does. * - * @method _checkVersion * @private */ _checkVersion() { @@ -73,35 +75,35 @@ export default class Basic extends Service { * to switch to an older/new inspector version * that supports this Ember version. * - * @method onVersionMismatch - * @param {String} neededVersion (The version to go to) + * @param _neededVersion (The version to go to) */ - onVersionMismatch() {} + onVersionMismatch(_neededVersion?: string) {} /** Used to send messages to EmberDebug - @param type {Object} the message to the send + @param _message the message to send **/ - sendMessage() {} + sendMessage(_message: Partial) {} /** Register functions to be called when a message from EmberDebug is received **/ - onMessageReceived(callback) { + onMessageReceived(callback: AnyFn) { this._messageCallbacks.push(callback); } - _messageReceived(...args) { + _messageReceived(...args: Array) { this._messageCallbacks.forEach((callback) => { callback(...args); }); } + abstract reloadTab(): void; // Called when the "Reload" is clicked by the user willReload() {} - openResource /* file, line */() {} + openResource(_file: string, _line: number) {} @action refreshPage() { @@ -126,15 +128,14 @@ export default class Basic extends Service { * 0 if version1 == version2 * 1 if version1 > version2 * - * @param {String} version1 - * @param {String} version2 - * @return {Boolean} result of the comparison + * @return result of the comparison */ -function compareVersion(version1, version2) { - version1 = cleanupVersion(version1).split('.'); - version2 = cleanupVersion(version2).split('.'); +function compareVersion(version1: string, version2: string) { + const v1 = cleanupVersion(version1).split('.'); + const v2 = cleanupVersion(version2).split('.'); for (let i = 0; i < 3; i++) { - let compared = compare(+version1[i], +version2[i]); + // @ts-expect-error TODO: refactor this to make TS happy + let compared = compare(+v1[i], +v2[i]); if (compared !== 0) { return compared; } @@ -143,11 +144,11 @@ function compareVersion(version1, version2) { } /* Remove -alpha, -beta, etc from versions */ -function cleanupVersion(version) { +function cleanupVersion(version: string) { return version.replace(/-.*/g, ''); } -function compare(val, number) { +function compare(val: number, number: number) { if (val === number) { return 0; } else if (val < number) { diff --git a/app/services/adapters/bookmarklet.js b/app/services/adapters/bookmarklet.ts similarity index 75% rename from app/services/adapters/bookmarklet.js rename to app/services/adapters/bookmarklet.ts index d42b538cf7..46879a802b 100644 --- a/app/services/adapters/bookmarklet.js +++ b/app/services/adapters/bookmarklet.ts @@ -1,43 +1,34 @@ /* eslint-disable no-useless-escape */ -import { computed } from '@ember/object'; - import BasicAdapter from './basic'; +import type { Message } from '../port'; export default class Bookmarklet extends BasicAdapter { name = 'bookmarklet'; /** * Called when the adapter is created. - * - * @method init */ - init() { + constructor(properties?: object) { + super(properties); this._connect(); - return super.init(...arguments); } - @computed get inspectedWindow() { return window.opener || window.parent; } - @computed get inspectedWindowURL() { return loadPageVar('inspectedWindowURL'); } - sendMessage(options) { - options = options || {}; - this.inspectedWindow.postMessage(options, this.inspectedWindowURL); + sendMessage(message?: Partial) { + this.inspectedWindow.postMessage(message ?? {}, this.inspectedWindowURL); } /** * Redirect to the correct inspector version. - * - * @method onVersionMismatch - * @param {String} goToVersion */ - onVersionMismatch(goToVersion) { + onVersionMismatch(goToVersion: string) { this.sendMessage({ name: 'version-mismatch', version: goToVersion }); window.location.href = `../panes-${goToVersion.replace( /\./g, @@ -47,7 +38,7 @@ export default class Bookmarklet extends BasicAdapter { _connect() { window.addEventListener('message', (e) => { - let message = e.data; + let message = e.data as Message; if (e.origin !== this.inspectedWindowURL) { return; } @@ -62,7 +53,7 @@ export default class Bookmarklet extends BasicAdapter { } } -function loadPageVar(sVar) { +function loadPageVar(sVar: string) { return decodeURI( window.location.search.replace( new RegExp( diff --git a/app/services/adapters/chrome.js b/app/services/adapters/chrome.ts similarity index 86% rename from app/services/adapters/chrome.js rename to app/services/adapters/chrome.ts index d5a68a2076..65905a98e3 100644 --- a/app/services/adapters/chrome.js +++ b/app/services/adapters/chrome.ts @@ -5,8 +5,7 @@ export default class Chrome extends WebExtension { name = 'chrome'; @tracked canOpenResource = true; - openResource(file, line) { - /*global chrome */ + openResource(file: string, line: number) { // For some reason it opens the line after the one specified chrome.devtools.panels.openResource(file, line - 1); } diff --git a/app/services/adapters/firefox.js b/app/services/adapters/firefox.ts similarity index 100% rename from app/services/adapters/firefox.js rename to app/services/adapters/firefox.ts diff --git a/app/services/adapters/web-extension.js b/app/services/adapters/web-extension.ts similarity index 82% rename from app/services/adapters/web-extension.js rename to app/services/adapters/web-extension.ts index 15ad8dbddb..d9cb186e62 100644 --- a/app/services/adapters/web-extension.js +++ b/app/services/adapters/web-extension.ts @@ -1,11 +1,10 @@ -/* globals chrome */ -import { computed } from '@ember/object'; import { tracked } from '@glimmer/tracking'; import BasicAdapter from './basic'; import config from 'ember-inspector/config/environment'; +import type { Message } from '../port'; -let emberDebug = null; +let emberDebug: string | null = null; export default class WebExtension extends BasicAdapter { @tracked canOpenResource = false; @@ -13,22 +12,19 @@ export default class WebExtension extends BasicAdapter { /** * Called when the adapter is created. - * - * @method init */ - init() { + constructor(properties?: object) { + super(properties); + this._connect(); this._handleReload(); this._setThemeColors(); Promise.resolve().then(() => this._sendEmberDebug()); - - return super.init(...arguments); } - sendMessage(options) { - options = options || {}; - this._chromePort.postMessage(options); + sendMessage(message?: Partial) { + this._chromePort.postMessage(message ?? {}); } _sendEmberDebug() { @@ -44,17 +40,16 @@ export default class WebExtension extends BasicAdapter { this.onMessageReceived((message, sender) => { if (message === 'ember-content-script-ready') { this.sendMessage({ + frameId: sender.frameId, from: 'devtools', + tabId: chrome.devtools.inspectedWindow.tabId, type: 'inject-ember-debug', value: url, - tabId: chrome.devtools.inspectedWindow.tabId, - frameId: sender.frameId, }); } }); } - @computed get _chromePort() { return chrome.runtime.connect(); } @@ -77,17 +72,20 @@ export default class WebExtension extends BasicAdapter { let self = this; chrome.devtools.network.onNavigated.addListener(function () { self._injectDebugger(); - location.reload(true); + location.reload(); }); } _injectDebugger() { loadEmberDebug().then((emberDebug) => { - chrome.devtools.inspectedWindow.eval(emberDebug, (success, error) => { - if (success === undefined && error) { - throw error; - } - }); + chrome.devtools.inspectedWindow.eval( + emberDebug as string, + (success, error) => { + if (success === undefined && error) { + throw error; + } + }, + ); }); } @@ -108,11 +106,8 @@ export default class WebExtension extends BasicAdapter { /** * Open the devtools "Elements" or "Sources" tab and select a specific DOM node or function. - * - * @method inspectJSValue - * @param {String} name */ - inspectJSValue(name) { + inspectJSValue(name: string) { chrome.devtools.inspectedWindow.eval(` inspect(window[${JSON.stringify(name)}]); delete window[${JSON.stringify(name)}]; @@ -121,11 +116,8 @@ export default class WebExtension extends BasicAdapter { /** * Redirect to the correct inspector version. - * - * @method onVersionMismatch - * @param {String} goToVersion */ - onVersionMismatch(goToVersion) { + onVersionMismatch(goToVersion: string) { window.location.href = `../panes-${goToVersion.replace( /\./g, '-', @@ -138,14 +130,16 @@ export default class WebExtension extends BasicAdapter { */ reloadTab() { loadEmberDebug().then((emberDebug) => { - chrome.devtools.inspectedWindow.reload({ injectedScript: emberDebug }); + chrome.devtools.inspectedWindow.reload({ + injectedScript: emberDebug as string, + }); }); } } function loadEmberDebug() { let minimumVersion = config.emberVersionsSupported[0].replace(/\./g, '-'); - let xhr; + let xhr: XMLHttpRequest; return new Promise((resolve) => { if (!emberDebug) { diff --git a/app/services/adapters/websocket.js b/app/services/adapters/websocket.ts similarity index 56% rename from app/services/adapters/websocket.js rename to app/services/adapters/websocket.ts index dca2460502..4f2ad26c80 100644 --- a/app/services/adapters/websocket.js +++ b/app/services/adapters/websocket.ts @@ -1,20 +1,23 @@ import { run } from '@ember/runloop'; import BasicAdapter from './basic'; +import type { Message } from '../port'; export default class Websocket extends BasicAdapter { - init() { - super.init(); + socket: any; + + constructor(properties?: object) { + super(properties); + // @ts-expect-error TODO: figure out how to type this stuff this.socket = window.EMBER_INSPECTOR_CONFIG.remoteDebugSocket; this._connect(); } - sendMessage(options) { - options = options || {}; - this.socket.emit('emberInspectorMessage', options); + sendMessage(message?: Partial) { + this.socket.emit('emberInspectorMessage', message ?? {}); } _connect() { - this.socket.on('emberInspectorMessage', (message) => { + this.socket.on('emberInspectorMessage', (message: Message) => { run(() => { this._messageReceived(message); }); diff --git a/app/services/layout.js b/app/services/layout.ts similarity index 80% rename from app/services/layout.js rename to app/services/layout.ts index 05a65456e2..cf1195550f 100644 --- a/app/services/layout.js +++ b/app/services/layout.ts @@ -6,9 +6,6 @@ * when the main nav is resized. * Elements dependant on the app's layout listen to events on this service. For * example the `list` component. - * - * @class Layout - * @extends Service */ import Service from '@ember/service'; import { tracked } from '@glimmer/tracking'; @@ -18,21 +15,17 @@ export default class LayoutService extends Service.extend(Evented) { /** * Stores the app's content height. This property is kept up-to-date * by the `monitor-content-height` component. - * - * @property contentHeight - * @type {Number} */ - @tracked contentHeight = null; + @tracked contentHeight: number | null = null; /** * This is called by `monitor-content-height` whenever a window resize is detected * and the app's content height has changed. We therefore update the * `contentHeight` property and notify all listeners (mostly lists). * - * @method updateContentHeight - * @param {Number} height The new app content height + * @param height The new app content height */ - updateContentHeight(height) { + updateContentHeight(height: number) { this.contentHeight = height; this.trigger('content-height-update', height); } diff --git a/app/services/port.js b/app/services/port.js deleted file mode 100644 index 9bee0b7a8f..0000000000 --- a/app/services/port.js +++ /dev/null @@ -1,90 +0,0 @@ -import { set } from '@ember/object'; -import Evented from '@ember/object/evented'; -import Service, { inject as service } from '@ember/service'; - -export default class PortService extends Service.extend(Evented) { - @service adapter; - @service router; - - applicationId = undefined; - applicationName = undefined; - - init() { - super.init(...arguments); - - /* - * A dictionary of the form: - * { applicationId: applicationName } - */ - this.detectedApplications = {}; - this.applicationId = undefined; - this.applicationName = undefined; - - this.adapter.onMessageReceived((message) => { - if (message.type === 'apps-loaded') { - message.apps.forEach(({ applicationId, applicationName }) => { - set(this.detectedApplications, applicationId, applicationName); - }); - - return; - } - - let { applicationId, applicationName } = message; - - if (!applicationId) { - return; - } - - // save the application, in case we haven't seen it yet - set(this.detectedApplications, applicationId, applicationName); - - if (!this.applicationId) { - this.selectApplication(applicationId); - } - - if (this.applicationId === applicationId) { - if (!this.has(message.type)) { - throw new Error('unknown message type ' + message.type); - } - this.trigger(message.type, message, applicationId); - } - }); - - this.on('view:inspectJSValue', this, ({ name }) => - this.adapter.inspectJSValue(name), - ); - } - - selectApplication(applicationId) { - if ( - applicationId in this.detectedApplications && - applicationId !== this.applicationId - ) { - let applicationName = this.detectedApplications[applicationId]; - const currentApplication = this.applicationId; - this.setProperties({ applicationId, applicationName }); - if (currentApplication) { - // this is only required when switching apps - this.router.transitionTo('app-detected'); - } - this.send('app-selected', { applicationId, applicationName }); - } - } - - send(type, message) { - message = message || {}; - message.type = type; - message.from = 'devtools'; - message.applicationId = this.applicationId; - message.applicationName = this.applicationName; - this.adapter.sendMessage(message); - } - - off(...args) { - try { - super.off(...args); - } catch (e) { - console.error(e); - } - } -} diff --git a/app/services/port.ts b/app/services/port.ts new file mode 100644 index 0000000000..c521b839fd --- /dev/null +++ b/app/services/port.ts @@ -0,0 +1,133 @@ +import { action, set } from '@ember/object'; +import { addListener, removeListener, sendEvent } from '@ember/object/events'; +// @ts-expect-error TODO: maybe move away from this one day, but for now import from secret location +import { hasListeners } from '@ember/-internals/metal'; +import Service, { inject as service } from '@ember/service'; +import type RouterService from '@ember/routing/router-service'; + +import type WebExtension from './adapters/web-extension'; +import type { AnyFn } from 'ember/-private/type-utils'; + +export interface Message { + applicationId: string; + applicationName: string; + frameId?: any; + from: string; + name?: string; + tabId?: number; + type: string; + unloading?: boolean; + value: string; + version?: string; +} + +export default class PortService extends Service { + @service declare adapter: WebExtension; + @service declare router: RouterService; + + applicationId?: string; + applicationName?: string; + detectedApplications: { [key: string]: string }; + + constructor() { + super(...arguments); + + /* + * A dictionary of the form: + * { applicationId: applicationName } + */ + this.detectedApplications = {}; + this.applicationId = undefined; + this.applicationName = undefined; + + this.adapter.onMessageReceived( + (message: Message & { apps: Array }) => { + if (message.type === 'apps-loaded') { + message.apps.forEach( + ({ applicationId, applicationName }: Message) => { + set(this.detectedApplications, applicationId, applicationName); + }, + ); + + return; + } + + let { applicationId, applicationName } = message; + + if (!applicationId) { + return; + } + + // save the application, in case we haven't seen it yet + set(this.detectedApplications, applicationId, applicationName); + + if (!this.applicationId) { + this.selectApplication(applicationId); + } + + if (this.applicationId === applicationId) { + if (!hasListeners(this, message.type)) { + throw new Error('unknown message type ' + message.type); + } + this.trigger(message.type, message, applicationId); + } + }, + ); + + addListener(this, 'view:inspectJSValue', this, ({ name }) => + this.adapter.inspectJSValue(name), + ); + } + + selectApplication(applicationId: string) { + if ( + applicationId in this.detectedApplications && + applicationId !== this.applicationId + ) { + let applicationName = this.detectedApplications[applicationId] as string; + const currentApplication = this.applicationId; + this.setProperties({ applicationId, applicationName }); + if (currentApplication) { + // this is only required when switching apps + this.router.transitionTo('app-detected'); + } + this.send('app-selected', { applicationId, applicationName }); + } + } + + @action + send(type: string, message?: Partial) { + message = message || {}; + message.type = type; + message.from = 'devtools'; + message.applicationId = this.applicationId as string; + message.applicationName = this.applicationName as string; + this.adapter.sendMessage(message); + } + + // Manually implement Evented functionality, so we can move away from the mixin + + @action + on(eventName: string, target: unknown, method: AnyFn) { + addListener(this, eventName, target, method); + } + + @action + one(eventName: string, target: unknown, method: AnyFn) { + addListener(this, eventName, target, method, true); + } + + @action + off(eventName: string, target: unknown, method: AnyFn) { + try { + removeListener(this, eventName, target, method); + } catch (e) { + console.error(e); + } + } + + @action + trigger(eventName: string, ...args: Array) { + sendEvent(this, eventName, args); + } +} diff --git a/tsconfig.json b/tsconfig.json index ffbd5cd286..406c4eb44a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,8 @@ "ui/test-support": ["lib/ui/addon-test-support"], "ui/test-support/*": ["lib/ui/addon-test-support/*"], "*": ["types/*"] - } + }, + "types": ["chrome"] }, "include": ["app/**/*", "tests/**/*", "types/**/*", "lib/ui/**/*"] }