diff --git a/app/libs/promise-assembler.js b/app/libs/promise-assembler.js deleted file mode 100644 index 870e2fef8d..0000000000 --- a/app/libs/promise-assembler.js +++ /dev/null @@ -1,179 +0,0 @@ -import { assert } from '@ember/debug'; -import { later } from '@ember/runloop'; -import EmberObject from '@ember/object'; -import EventedMixin from '@ember/object/evented'; -import Promise from 'ember-inspector/models/promise'; - -import { TrackedArray } from 'tracked-built-ins'; -import { tracked } from '@glimmer/tracking'; - -export default class PromiseAssembler extends EmberObject.extend(EventedMixin) { - // Used to track whether current message received - // is the first in the request - // Mainly helps in triggering 'firstMessageReceived' event - @tracked firstMessageReceived = false; - - all = new TrackedArray([]); - topSort = new TrackedArray([]); - - init() { - super.init(...arguments); - - this.topSortMeta = {}; - this.promiseIndex = {}; - } - - start() { - this.port.on('promise:promisesUpdated', this, this.addOrUpdatePromises); - this.port.send('promise:getAndObservePromises'); - } - - stop() { - this.port.off('promise:promisesUpdated', this, this.addOrUpdatePromises); - this.port.send('promise:releasePromises'); - this.reset(); - } - - reset() { - this.set('topSortMeta', {}); - this.set('promiseIndex', {}); - this.topSort.splice(0, this.topSort.length); - - this.firstMessageReceived = false; - let all = this.all; - // Lazily destroy promises - // Allows for a smooth transition on deactivate, - // and thus providing the illusion of better perf - later( - this, - function () { - this.destroyPromises(all); - }, - 500, - ); - this.set('all', new TrackedArray([])); - } - - destroyPromises(promises) { - promises.forEach(function (item) { - item.destroy(); - }); - } - - addOrUpdatePromises(message) { - this.rebuildPromises(message.promises); - - if (!this.firstMessageReceived) { - this.firstMessageReceived = true; - this.trigger('firstMessageReceived'); - } - } - - rebuildPromises(promises) { - promises.forEach((props) => { - props = Object.assign({}, props); - let childrenIds = props.children; - let parentId = props.parent; - delete props.children; - delete props.parent; - if (parentId && parentId !== props.guid) { - props.parent = this.updateOrCreate({ guid: parentId }); - } - let promise = this.updateOrCreate(props); - if (childrenIds) { - childrenIds.forEach((childId) => { - // avoid infinite recursion - if (childId === props.guid) { - return; - } - let child = this.updateOrCreate({ guid: childId, parent: promise }); - promise.get('children').pushObject(child); - }); - } - }); - } - - updateTopSort(promise) { - let topSortMeta = this.topSortMeta; - let guid = promise.get('guid'); - let meta = topSortMeta[guid]; - let isNew = !meta; - let hadParent = false; - let hasParent = !!promise.get('parent'); - let topSort = this.topSort; - let parentChanged = isNew; - - if (isNew) { - meta = topSortMeta[guid] = {}; - } else { - hadParent = meta.hasParent; - } - if (!isNew && hasParent !== hadParent) { - // todo: implement recursion to reposition children - const index = topSort.indexOf(promise); - if (index !== -1) { - topSort.splice(index, 1); - } - parentChanged = true; - } - meta.hasParent = hasParent; - if (parentChanged) { - this.insertInTopSort(promise); - } - } - - insertInTopSort(promise) { - let topSort = this.topSort; - if (promise.get('parent')) { - let parentIndex = topSort.indexOf(promise.get('parent')); - topSort.splice(parentIndex + 1, 0, promise); - } else { - this.topSort.push(promise); - } - promise.get('children').forEach((child) => { - const index = topSort.indexOf(child); - if (index !== -1) { - topSort.splice(index, 1); - } - this.insertInTopSort(child); - }); - } - - updateOrCreate(props) { - let guid = props.guid; - let promise = this.findOrCreate(guid); - - promise.setProperties(props); - - this.updateTopSort(promise); - - return promise; - } - - createPromise(props) { - let promise = Promise.create(props); - let index = this.get('all.length'); - - this.all.push(promise); - this.promiseIndex[promise.get('guid')] = index; - return promise; - } - - find(guid) { - if (guid) { - let index = this.promiseIndex[guid]; - if (index !== undefined) { - return this.all.at(index); - } - } else { - return this.all; - } - } - - findOrCreate(guid) { - if (!guid) { - assert('You have tried to findOrCreate without a guid'); - } - return this.find(guid) || this.createPromise({ guid }); - } -} diff --git a/app/libs/promise-assembler.ts b/app/libs/promise-assembler.ts new file mode 100644 index 0000000000..0f7e56cfa2 --- /dev/null +++ b/app/libs/promise-assembler.ts @@ -0,0 +1,240 @@ +import { assert } from '@ember/debug'; +import { later } from '@ember/runloop'; +import EmberObject, { action, setProperties } from '@ember/object'; +import { addListener, removeListener, sendEvent } from '@ember/object/events'; +import type { AnyFn } from 'ember/-private/type-utils'; + +import { TrackedArray, TrackedObject } from 'tracked-built-ins'; +import { tracked } from '@glimmer/tracking'; + +import PromiseModel from '../models/promise'; +import type PortService from '../services/port'; + +interface SerializedPromise { + children?: Array; + guid: string; + label: string; + parent?: string; + reason: string; + state: string; + value: string; +} + +export default class PromiseAssembler extends EmberObject { + declare port: PortService; + // Used to track whether current message received + // is the first in the request + // Mainly helps in triggering 'firstMessageReceived' event + @tracked firstMessageReceived = false; + + all = new TrackedArray([]); + promiseIndex = new TrackedObject>({}); + topSort = new TrackedArray([]); + topSortMeta = new TrackedObject>({}); + + start() { + this.port.on('promise:promisesUpdated', this, this.addOrUpdatePromises); + this.port.send('promise:getAndObservePromises'); + } + + stop() { + this.port.off('promise:promisesUpdated', this, this.addOrUpdatePromises); + this.port.send('promise:releasePromises'); + this.reset(); + } + + reset() { + this.topSortMeta = new TrackedObject< + Record + >({}); + this.promiseIndex = new TrackedObject>({}); + this.topSort.splice(0, this.topSort.length); + + this.firstMessageReceived = false; + let all = this.all; + // Lazily destroy promises + // Allows for a smooth transition on deactivate, + // and thus providing the illusion of better perf + later( + this, + function () { + this.destroyPromises(all); + }, + 500, + ); + this.set('all', new TrackedArray([])); + } + + destroyPromises(promises: Array) { + promises.forEach(function (item) { + item.destroy(); + }); + } + + addOrUpdatePromises(message: { promises: Array }) { + this.rebuildPromises(message.promises); + + if (!this.firstMessageReceived) { + this.firstMessageReceived = true; + this.trigger('firstMessageReceived'); + } + } + + rebuildPromises(promises: Array) { + promises.forEach((props) => { + props = Object.assign({}, props); + let childrenIds = props.children; + let parentId = props.parent; + delete props.children; + delete props.parent; + if (parentId && parentId !== props.guid) { + props.parent = this.updateOrCreate({ guid: parentId }); + } + let promise = this.updateOrCreate(props); + if (childrenIds) { + childrenIds.forEach((childId) => { + // avoid infinite recursion + if (childId === props.guid) { + return; + } + let child = this.updateOrCreate({ guid: childId, parent: promise }); + promise.children.push(child); + }); + } + }); + } + + updateTopSort(promise: PromiseModel) { + let topSortMeta = this.topSortMeta; + let guid = promise.guid; + let meta = topSortMeta[guid] ?? {}; + let isNew = !meta; + let hadParent: boolean | undefined = false; + let hasParent = !!promise.parent; + let topSort = this.topSort; + let parentChanged = isNew; + + if (isNew) { + meta = topSortMeta[guid] = {}; + } else { + hadParent = meta.hasParent; + } + if (!isNew && hasParent !== hadParent) { + // todo: implement recursion to reposition children + const index = topSort.indexOf(promise); + if (index !== -1) { + topSort.splice(index, 1); + } + parentChanged = true; + } + meta.hasParent = hasParent; + if (parentChanged) { + this.insertInTopSort(promise); + } + } + + insertInTopSort(promise: PromiseModel) { + let topSort = this.topSort; + if (promise.parent) { + let parentIndex = topSort.indexOf(promise.parent); + topSort.splice(parentIndex + 1, 0, promise); + } else { + this.topSort.push(promise); + } + promise.children.forEach((child) => { + const index = topSort.indexOf(child); + if (index !== -1) { + topSort.splice(index, 1); + } + this.insertInTopSort(child); + }); + } + + updateOrCreate(props: any) { + let guid = props.guid; + let promise = this.findOrCreate(guid); + + setProperties(promise, props); + + this.updateTopSort(promise); + + return promise; + } + + createPromise(props: any): PromiseModel { + let promise = PromiseModel.create(props); + let index = this.all.length; + + this.all.push(promise); + this.promiseIndex[promise.guid as keyof object] = index; + return promise; + } + + find(guid?: string) { + if (guid) { + let index = this.promiseIndex[guid as keyof object]; + if (index !== undefined) { + return this.all.at(index); + } + } else { + return this.all; + } + } + + findOrCreate(guid?: string) { + if (!guid) { + assert('You have tried to findOrCreate without a guid'); + } + return (this.find(guid) as PromiseModel) || this.createPromise({ guid }); + } + + // Manually implement Evented functionality, so we can move away from the mixin + + on(eventName: string, method: AnyFn): void; + on(eventName: string, target: unknown, method: AnyFn): void; + + @action + on(eventName: string, targetOrMethod: unknown | AnyFn, method?: AnyFn): void { + if (typeof targetOrMethod === 'function') { + // If we did not pass a target, default to `this` + addListener(this, eventName, this, targetOrMethod as AnyFn); + } else { + addListener(this, eventName, targetOrMethod, method!); + } + } + + one(eventName: string, method: AnyFn): void; + one(eventName: string, target: unknown, method: AnyFn): void; + + @action + one(eventName: string, targetOrMethod: unknown | AnyFn, method?: AnyFn) { + if (typeof targetOrMethod === 'function') { + // If we did not pass a target, default to `this` + addListener(this, eventName, this, targetOrMethod as AnyFn, true); + } else { + addListener(this, eventName, targetOrMethod, method!, true); + } + } + + off(eventName: string, method: AnyFn): void; + off(eventName: string, target: unknown, method: AnyFn): void; + + @action + off(eventName: string, targetOrMethod: unknown | AnyFn, method?: AnyFn) { + try { + if (typeof targetOrMethod === 'function') { + // If we did not pass a target, default to `this` + removeListener(this, eventName, this, targetOrMethod as AnyFn); + } else { + removeListener(this, eventName, targetOrMethod, method!); + } + } catch (e) { + console.error(e); + } + } + + @action + trigger(eventName: string, ...args: Array) { + sendEvent(this, eventName, args); + } +} diff --git a/app/models/promise.js b/app/models/promise.ts similarity index 71% rename from app/models/promise.js rename to app/models/promise.ts index 40680c1052..22dceedd43 100644 --- a/app/models/promise.js +++ b/app/models/promise.ts @@ -1,17 +1,22 @@ +// @ts-expect-error This does not seem to be typed import { observes } from '@ember-decorators/object'; import { once } from '@ember/runloop'; -import { typeOf, isEmpty } from '@ember/utils'; +import { typeOf } from '@ember/utils'; // eslint-disable-next-line ember/no-observers import EmberObject, { computed } from '@ember/object'; -import escapeRegExp from 'ember-inspector/utils/escape-reg-exp'; import { tracked } from '@glimmer/tracking'; +import { TrackedArray } from 'tracked-built-ins'; + +import escapeRegExp from '../utils/escape-reg-exp'; +import { isNullish } from '../utils/nullish'; + const dateComputed = function () { return computed({ get() { return null; }, - set(key, date) { + set(_key, date: Date | number | string) { if (typeOf(date) === 'date') { return date; } else if (typeof date === 'number' || typeof date === 'string') { @@ -22,25 +27,27 @@ const dateComputed = function () { }); }; -export default class Promise extends EmberObject { - @dateComputed() - createdAt; - - @dateComputed() - settledAt; +export default class PromiseModel extends EmberObject { + children = new TrackedArray([]); + declare label?: string; + declare guid: string; + declare state: string; + // @ts-expect-error TODO: figure out types for this + @dateComputed() createdAt; + // @ts-expect-error TODO: figure out types for this + @dateComputed() settledAt; @tracked branchLabel = ''; @tracked isExpanded = false; @tracked isManuallyExpanded = undefined; - @tracked parent = null; + @tracked parent: PromiseModel | null = null; - @computed('parent.level') - get level() { + get level(): number { let parent = this.parent; if (!parent) { return 0; } - return parent.get('level') + 1; + return parent.level + 1; } get isSettled() { @@ -59,29 +66,24 @@ export default class Promise extends EmberObject { return !this.isSettled; } - children = []; - - @computed('isPending', 'children.@each.pendingBranch') get pendingBranch() { return this.recursiveState('isPending', 'pendingBranch'); } - @computed('isRejected', 'children.@each.rejectedBranch') get rejectedBranch() { return this.recursiveState('isRejected', 'rejectedBranch'); } - @computed('isFulfilled', 'children.@each.fulfilledBranch') get fulfilledBranch() { return this.recursiveState('isFulfilled', 'fulfilledBranch'); } - recursiveState(prop, cp) { + recursiveState(prop: keyof PromiseModel, cp: keyof PromiseModel) { if (this[prop]) { return true; } for (let i = 0; i < this.children.length; i++) { - if (this.children.at(i)[cp]) { + if (this.children.at(i)?.[cp]) { return true; } } @@ -97,9 +99,9 @@ export default class Promise extends EmberObject { return; } if ( - (this.pendingBranch && !this.get('parent.pendingBranch')) || - (this.fulfilledBranch && !this.get('parent.fulfilledBranch')) || - (this.rejectedBranch && !this.get('parent.rejectedBranch')) + (this.pendingBranch && !this.parent.pendingBranch) || + (this.fulfilledBranch && !this.parent.fulfilledBranch) || + (this.rejectedBranch && !this.parent.rejectedBranch) ) { this.parent.notifyPropertyChange('fulfilledBranch'); this.parent.notifyPropertyChange('rejectedBranch'); @@ -113,8 +115,8 @@ export default class Promise extends EmberObject { this.addBranchLabel(this.label, true); } - addBranchLabel(label, replace) { - if (isEmpty(label)) { + addBranchLabel(label?: string, replace?: boolean) { + if (isNullish(label)) { return; } if (replace) { @@ -129,13 +131,13 @@ export default class Promise extends EmberObject { } } - matches(val) { + matches(val: string) { return !!this.branchLabel .toLowerCase() .match(new RegExp(`.*${escapeRegExp(val.toLowerCase())}.*`)); } - matchesExactly(val) { + matchesExactly(val: string) { return !!(this.label || '') .toLowerCase() .match(new RegExp(`.*${escapeRegExp(val.toLowerCase())}.*`)); @@ -152,7 +154,7 @@ export default class Promise extends EmberObject { } } - _findTopParent() { + _findTopParent(): PromiseModel { let parent = this.parent; if (!parent) { return this; @@ -167,12 +169,12 @@ export default class Promise extends EmberObject { isExpanded = this.isManuallyExpanded; } else { let children = this._allChildren(); - for (let i = 0, l = children.length; i < l; i++) { - let child = children[i]; - if (child.get('isRejected')) { + for (let i = 0; i < children.length; i++) { + let child = children[i] as PromiseModel; + if (child.isRejected) { isExpanded = true; } - if (child.get('isPending') && !child.get('parent.isPending')) { + if (child.isPending && !child.parent!.isPending) { isExpanded = true; } if (isExpanded) { @@ -184,7 +186,7 @@ export default class Promise extends EmberObject { parents.forEach((parent) => { parent.set('isExpanded', true); }); - } else if (this.get('parent.isExpanded')) { + } else if (this.parent?.isExpanded) { this.parent.recalculateExpanded(); } } @@ -192,10 +194,9 @@ export default class Promise extends EmberObject { return isExpanded; } - @computed('parent.{isExpanded,isVisible}', 'parent') - get isVisible() { + get isVisible(): boolean { if (this.parent) { - return this.get('parent.isExpanded') && this.get('parent.isVisible'); + return this.parent.isExpanded && this.parent.isVisible; } return true; } @@ -208,7 +209,7 @@ export default class Promise extends EmberObject { return children; } - _allParents() { + _allParents(): Array { let parent = this.parent; if (parent) { return [parent, ...parent._allParents()]; diff --git a/app/routes/promise-tree.js b/app/routes/promise-tree.ts similarity index 63% rename from app/routes/promise-tree.js rename to app/routes/promise-tree.ts index f8dfe2d99b..fd9c0ea19d 100644 --- a/app/routes/promise-tree.js +++ b/app/routes/promise-tree.ts @@ -1,22 +1,31 @@ -import { get, set } from '@ember/object'; +import { set } from '@ember/object'; import { inject as service } from '@ember/service'; import { Promise } from 'rsvp'; +// @ts-expect-error TODO: not yet typed import TabRoute from 'ember-inspector/routes/tab'; -import PromiseAssembler from 'ember-inspector/libs/promise-assembler'; + +import PromiseAssembler from '../libs/promise-assembler'; +import type PortService from '../services/port'; export default class PromiseTreeRoute extends TabRoute { - @service port; + @service declare port: PortService; + + assembler: PromiseAssembler; + + constructor() { + super(...arguments); - assembler = PromiseAssembler.create({ - port: this.port, - }); + this.assembler = PromiseAssembler.create({ + port: this.port, + }); + } model() { // block rendering until first batch arrives // Helps prevent flashing of "please refresh the page" return new Promise((resolve) => { this.assembler.one('firstMessageReceived', () => { - resolve(get(this, 'assembler.topSort')); + resolve(this.assembler.topSort); }); this.assembler.start(); }); @@ -42,7 +51,8 @@ export default class PromiseTreeRoute extends TabRoute { ); } - setInstrumentWithStack(message) { + setInstrumentWithStack(message: { instrumentWithStack: boolean }) { + // @ts-expect-error TODO: fix this type later set(this, 'controller.instrumentWithStack', message.instrumentWithStack); } } diff --git a/app/services/adapters/basic.ts b/app/services/adapters/basic.ts index ff12ae3482..7d05937c3d 100644 --- a/app/services/adapters/basic.ts +++ b/app/services/adapters/basic.ts @@ -19,7 +19,7 @@ import config from 'ember-inspector/config/environment'; import type PortService from '../port'; import type { Message } from '../port'; -export default abstract class Basic extends Service { +export default class Basic extends Service { @service declare port: PortService; _messageCallbacks: Array; @@ -100,7 +100,7 @@ export default abstract class Basic extends Service { }); } - abstract reloadTab(): void; + reloadTab?(): void; // Called when the "Reload" is clicked by the user willReload() {} openResource(_file: string, _line: number) {} diff --git a/app/services/port.ts b/app/services/port.ts index c521b839fd..9d5f047859 100644 --- a/app/services/port.ts +++ b/app/services/port.ts @@ -107,20 +107,44 @@ export default class PortService extends Service { // Manually implement Evented functionality, so we can move away from the mixin + on(eventName: string, method: AnyFn): void; + on(eventName: string, target: unknown, method: AnyFn): void; + @action - on(eventName: string, target: unknown, method: AnyFn) { - addListener(this, eventName, target, method); + on(eventName: string, targetOrMethod: unknown | AnyFn, method?: AnyFn): void { + if (typeof targetOrMethod === 'function') { + // If we did not pass a target, default to `this` + addListener(this, eventName, this, targetOrMethod as AnyFn); + } else { + addListener(this, eventName, targetOrMethod, method!); + } } + one(eventName: string, method: AnyFn): void; + one(eventName: string, target: unknown, method: AnyFn): void; + @action - one(eventName: string, target: unknown, method: AnyFn) { - addListener(this, eventName, target, method, true); + one(eventName: string, targetOrMethod: unknown | AnyFn, method?: AnyFn) { + if (typeof targetOrMethod === 'function') { + // If we did not pass a target, default to `this` + addListener(this, eventName, this, targetOrMethod as AnyFn, true); + } else { + addListener(this, eventName, targetOrMethod, method!, true); + } } + off(eventName: string, method: AnyFn): void; + off(eventName: string, target: unknown, method: AnyFn): void; + @action - off(eventName: string, target: unknown, method: AnyFn) { + off(eventName: string, targetOrMethod: unknown | AnyFn, method?: AnyFn) { try { - removeListener(this, eventName, target, method); + if (typeof targetOrMethod === 'function') { + // If we did not pass a target, default to `this` + removeListener(this, eventName, this, targetOrMethod as AnyFn); + } else { + removeListener(this, eventName, targetOrMethod, method!); + } } catch (e) { console.error(e); } diff --git a/app/services/storage.js b/app/services/storage.ts similarity index 83% rename from app/services/storage.js rename to app/services/storage.ts index a7fe5b940d..63c5872ac9 100644 --- a/app/services/storage.js +++ b/app/services/storage.ts @@ -1,5 +1,7 @@ import Service, { inject as service } from '@ember/service'; import { LOCAL_STORAGE_SUPPORTED } from './storage/local'; +import type LocalStorageService from './storage/local'; +import type MemoryStorageService from './storage/memory'; /** * Service that wraps either the LocalStorageService or @@ -12,16 +14,14 @@ import { LOCAL_STORAGE_SUPPORTED } from './storage/local'; */ export default class StorageService extends Service { @service(LOCAL_STORAGE_SUPPORTED ? 'storage/local' : 'storage/memory') - backend; + declare backend: LocalStorageService | MemoryStorageService; /** * Reads a stored object for a give key, if any. * - * @method getItem - * @param {String} key * @return {Option} The value, if found */ - getItem(key) { + getItem(key: string) { let serialized = this.backend.getItem(key); if (serialized === null) { @@ -34,12 +34,8 @@ export default class StorageService extends Service { /** * Store a string for a given key. - * - * @method setItem - * @param {String} key - * @param {String} value */ - setItem(key, value) { + setItem(key: string, value: string) { if (value === undefined) { this.removeItem(key); } else { @@ -50,18 +46,14 @@ export default class StorageService extends Service { /** * Deletes the stored string for a given key. - * - * @method removeItem - * @param {String} key */ - removeItem(key) { + removeItem(key: string) { this.backend.removeItem(key); } /** * Returns the list of stored keys. * - * @method keys * @return {Array} The array of keys */ keys() { diff --git a/app/services/storage/local.js b/app/services/storage/local.ts similarity index 75% rename from app/services/storage/local.js rename to app/services/storage/local.ts index 9d4d89ccee..b98a3f9b59 100644 --- a/app/services/storage/local.js +++ b/app/services/storage/local.ts @@ -6,48 +6,35 @@ export let LOCAL_STORAGE_SUPPORTED = false; * Service that wraps local storage. Only store strings. This * is not intended to be used directly, use StorageServeice * instead. - * - * @class LocalStorageService - * @extends Service */ export default class LocalStorageService extends Service { /** * Reads a stored string for a give key, if any. * - * @method getItem - * @param {String} key * @return {Option} The value, if found */ - getItem(key) { + getItem(key: string) { return localStorage.getItem(key); } /** * Store a string for a given key. - * - * @method setItem - * @param {String} key - * @param {String} value */ - setItem(key, value) { + setItem(key: string, value: string) { localStorage.setItem(key, value); } /** * Deletes the stored string for a given key. - * - * @method removeItem - * @param {String} key */ - removeItem(key) { + removeItem(key: string) { localStorage.removeItem(key); } /** * Returns the list of stored keys. * - * @method keys - * @return {Array} The array of keys + * @return The array of keys */ keys() { let keys = []; diff --git a/app/services/storage/memory.js b/app/services/storage/memory.ts similarity index 67% rename from app/services/storage/memory.js rename to app/services/storage/memory.ts index a912fb3874..a49deec626 100644 --- a/app/services/storage/memory.js +++ b/app/services/storage/memory.ts @@ -4,17 +4,12 @@ import Service from '@ember/service'; * Service that wraps an in-memory object with the same APIs as * the LocalStorageService, usually as a fallback. Only store * strings. This is not intended to be used directly, use - * StorageServeice instead. - * - * @class MemoryStorageService - * @extends Service + * StorageService instead. */ export default class MemoryStorageService extends Service { /** * Where data is stored. * - * @property store - * @type {Object} * @private */ store = Object.create(null); @@ -22,11 +17,9 @@ export default class MemoryStorageService extends Service { /** * Reads a stored string for a give key, if any. * - * @method getItem - * @param {String} key * @return {Option} The value, if found */ - getItem(key) { + getItem(key: string) { if (key in this.store) { return this.store[key]; } else { @@ -36,30 +29,22 @@ export default class MemoryStorageService extends Service { /** * Store a string for a given key. - * - * @method setItem - * @param {String} key - * @param {String} value */ - setItem(key, value) { + setItem(key: string, value: string) { this.store[key] = value; } /** * Deletes the stored string for a given key. - * - * @method removeItem - * @param {String} key */ - removeItem(key) { + removeItem(key: string) { delete this.store[key]; } /** * Returns the list of stored keys. * - * @method keys - * @return {Array} The array of keys + * @return The array of keys */ keys() { return Object.keys(this.store); diff --git a/app/utils/nullish.ts b/app/utils/nullish.ts new file mode 100644 index 0000000000..2b42a1c412 --- /dev/null +++ b/app/utils/nullish.ts @@ -0,0 +1,7 @@ +export const isNullish = ( + argument: T | undefined | null, +): argument is undefined | null => argument === null || argument === undefined; + +export const nonNullish = ( + argument: T | undefined | null, +): argument is NonNullable => !isNullish(argument); diff --git a/package.json b/package.json index d93b0d141a..48fc2244c7 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,10 @@ "lint:hbs": "ember-template-lint .", "lint:hbs:fix": "ember-template-lint . --fix", "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "lint:types": "tsc --noEmit", "lock-version": "pnpm build:production && pnpm compress:panes", "serve:bookmarklet": "ember serve --port 9191", - "lint:js:fix": "eslint . --fix", "start": "ember serve", "test": "concurrently \"pnpm:lint\" \"pnpm:test:*\" --names \"lint,test:\"", "test:ember": "COVERAGE=true ember test", diff --git a/tsconfig.json b/tsconfig.json index 406c4eb44a..becf7bb970 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ // layout, which is not resolvable with the Node resolution algorithm, to // work with TypeScript. "baseUrl": ".", + "lib": ["ES2023", "DOM"], "paths": { "ember-debug/deps/*": ["node_modules/*"], "ember-debug/*": ["ember_debug/*"],