From 7787ce9449be4b2fb7f6970e3acb14800f45a835 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Mon, 15 Apr 2024 19:21:48 +0200 Subject: [PATCH] feat: initial devtools package implementation (#10484) --- packages/devtools/.gitignore | 1 + packages/devtools/package.json | 15 ++ packages/devtools/project.json | 4 + packages/devtools/src/devtoolsRuntime.ts | 108 ++++++++++++ .../ApplicationSettingsKeyValueProvider.ts | 29 ++++ packages/devtools/src/index.ts | 16 ++ .../src/providers/keyValueProvider.ts | 161 ++++++++++++++++++ packages/devtools/src/types.d.ts | 6 + packages/devtools/tsconfig.json | 26 +++ 9 files changed, 366 insertions(+) create mode 100644 packages/devtools/.gitignore create mode 100644 packages/devtools/package.json create mode 100644 packages/devtools/project.json create mode 100644 packages/devtools/src/devtoolsRuntime.ts create mode 100644 packages/devtools/src/impl/ApplicationSettingsKeyValueProvider.ts create mode 100644 packages/devtools/src/index.ts create mode 100644 packages/devtools/src/providers/keyValueProvider.ts create mode 100644 packages/devtools/src/types.d.ts create mode 100644 packages/devtools/tsconfig.json diff --git a/packages/devtools/.gitignore b/packages/devtools/.gitignore new file mode 100644 index 000000000..849ddff3b --- /dev/null +++ b/packages/devtools/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/packages/devtools/package.json b/packages/devtools/package.json new file mode 100644 index 000000000..54a2f5fa9 --- /dev/null +++ b/packages/devtools/package.json @@ -0,0 +1,15 @@ +{ + "name": "@nativescript/devtools", + "version": "0.0.0", + "private": true, + "files": [ + "./dist/" + ], + "main": "./dist/index", + "scripts": { + "prepack": "tsc || echo ok" + }, + "devDependencies": { + "@nativescript/core": "../core" + } +} diff --git a/packages/devtools/project.json b/packages/devtools/project.json new file mode 100644 index 000000000..ddc5f645d --- /dev/null +++ b/packages/devtools/project.json @@ -0,0 +1,4 @@ +{ + "name": "devtools", + "$schema": "../../node_modules/nx/schemas/project-schema.json" +} diff --git a/packages/devtools/src/devtoolsRuntime.ts b/packages/devtools/src/devtoolsRuntime.ts new file mode 100644 index 000000000..cd04ad7c0 --- /dev/null +++ b/packages/devtools/src/devtoolsRuntime.ts @@ -0,0 +1,108 @@ +// const domainDispatchers = new Map(); + +export function DomainDispatcher(domain: string): ClassDecorator { + return (klass) => { + __registerDomainDispatcher(domain, klass); + // if (!domainDispatchers.has(domain)) { + // domainDispatchers.set(domain, klass); + // } else { + // console.trace(`Domain dispatcher for ${domain} already registered!`); + // } + }; +} + +interface ProtocolMessage { + id: number; + method: string; + params: T; +} + +export class ProtocolWrapper { + constructor() { + return ProtocolWrapper.wrap(this); + } + + protected enabled: boolean = false; + + enable() { + this.enabled = true; + } + + disable() { + this.enabled = false; + } + + // private _enabled: boolean = false; + + // get enabled() { + // return this._enabled; + // } + + // enable() { + // console.log(this.constructor.name, 'enable!'); + // this._enabled = true; + // } + + // disable() { + // this._enabled = false; + // } + + public emit(name: string, params: any) { + try { + // console.info('[emit]', { method: name, params }); + __inspectorSendEvent( + JSON.stringify({ + method: name, + params, + }) + ); + } catch (err) { + console.error(err); + } + } + + public timestamp(): number { + return __inspectorTimestamp(); + } + + private static async sendResponseToDevtools(id: number, data: T) { + try { + const result = await data; + if (result) { + // console.info('[sendResponseToDevtools]', { id, result }); + __inspectorSendEvent( + JSON.stringify({ + id, + result, + }) + ); + } + } catch (err) { + console.error(err); + } + } + + private static wrap(handler: T): T { + return new Proxy(handler, { + get(target, prop) { + if (typeof target[prop] === 'function') { + return (params, message: ProtocolMessage) => { + try { + // console.warn('[incoming]', { + // id: message.id, + // method: message.method, + // params, + // message, + // }); + const res = target[prop](params, message); + ProtocolWrapper.sendResponseToDevtools(message.id, res); + return res; + } catch (err) { + console.error(err); + } + }; + } + }, + }) as T; + } +} diff --git a/packages/devtools/src/impl/ApplicationSettingsKeyValueProvider.ts b/packages/devtools/src/impl/ApplicationSettingsKeyValueProvider.ts new file mode 100644 index 000000000..3a8666a21 --- /dev/null +++ b/packages/devtools/src/impl/ApplicationSettingsKeyValueProvider.ts @@ -0,0 +1,29 @@ +import { ApplicationSettings } from '@nativescript/core'; +import { KeyValueProvider } from '../providers/keyValueProvider'; + +/** + * Exposes ApplicationSettings to Chrome Devtools + */ +export class ApplicationSettingsKeyValueProvider implements KeyValueProvider { + getName(): string { + return 'ApplicationSettings'; + } + getKeys(): string[] { + return ApplicationSettings.getAllKeys(); + } + getValue(key: string) { + return ApplicationSettings.getString(key); + } + setValue(key: string, value: string) { + ApplicationSettings.setString(key, value); + return true; + } + deleteKey(key: string) { + ApplicationSettings.remove(key); + return true; + } + clear() { + ApplicationSettings.clear(); + return true; + } +} diff --git a/packages/devtools/src/index.ts b/packages/devtools/src/index.ts new file mode 100644 index 000000000..9d95554c6 --- /dev/null +++ b/packages/devtools/src/index.ts @@ -0,0 +1,16 @@ +import { ApplicationSettingsKeyValueProvider } from './impl/ApplicationSettingsKeyValueProvider'; +import { KeyValueProviderRegistry } from './providers/keyValueProvider'; + +/** + * Initializes the default devtools providers. + */ +export function initDevtools() { + KeyValueProviderRegistry.registerKeyValueProvider(new ApplicationSettingsKeyValueProvider()); +} + +export { KeyValueProviderRegistry, KeyValueProvider } from './providers/keyValueProvider'; +// export { SqlDatabaseProviderRegistry, SqlDatabaseProvider, SqlDatabaseResult } from './providers/sqlDatabaseProvider'; +// export { IndexedDBDatabaseProviderRegistry, IndexedDBDatabaseProvider, IndexedDBDatabaseResult } from './providers/indexedDbDatabaseProvider'; +// export { NetworkProviderRegistry, NetworkProvider } from './providers/networkProvider'; + +export { DomainDispatcher, ProtocolWrapper } from './devtoolsRuntime'; diff --git a/packages/devtools/src/providers/keyValueProvider.ts b/packages/devtools/src/providers/keyValueProvider.ts new file mode 100644 index 000000000..93783a4db --- /dev/null +++ b/packages/devtools/src/providers/keyValueProvider.ts @@ -0,0 +1,161 @@ +import { DomainDispatcher, ProtocolWrapper } from '../devtoolsRuntime'; + +export interface KeyValueProvider { + getName(): string; + getKeys(): MaybePromise; + getValue(key: string): MaybePromise; + setValue(key: string, value: string): MaybePromise; + deleteKey(key: string): MaybePromise; + clear(): MaybePromise; +} + +@DomainDispatcher('Storage') +export class KeyValueProviderStorageHandler extends ProtocolWrapper { + getStorageKeyForFrame(params) { + if (params.frameId.startsWith('KeyValueProvider_')) { + return { + storageKey: `kv://${params.frameId.substr('KeyValueProvider_'.length)}`, + }; + } + + // return { storageKey: '' }; + } +} + +@DomainDispatcher('DOMStorage') +export class KeyValueProviderRegistry extends ProtocolWrapper { + private static providers: Map = new Map(); + + static registerKeyValueProvider(provider: KeyValueProvider) { + this.providers.set(provider.getName(), provider); + } + + static getProviderFrames() { + return Array.from(this.providers.keys()).map((name) => { + console.log('provider', name); + return { + frame: { + id: `KeyValueProvider_${name}`, + loaderId: 'NSLoaderIdentifier', + url: `kv://KV:${name}`, + securityOrigin: '', + mimeType: 'text/directory', + }, + resources: [], + }; + }); + } + + enable() { + super.enable(); + console.log('enable KeyValueProviderRegistry'); + } + + private getProviderFromParams(params) { + const { storageKey, isLocalStorage } = params.storageId; + if (!isLocalStorage) { + return; + } + + return KeyValueProviderRegistry.providers.get(storageKey.replace('kv://', '')); + } + + async getDOMStorageItems(params) { + const provider = this.getProviderFromParams(params); + if (!provider) { + return { entries: [] }; + } + const keys = await provider.getKeys(); + const entries = []; + for await (const key of keys) { + const value = await provider.getValue(key); + entries.push([key, value]); + } + + return { + entries, + }; + } + + async removeDOMStorageItem(params) { + const provider = this.getProviderFromParams(params); + if (!provider) { + return; + } + const res = await provider.deleteKey(params.key); + + if (res) { + this.emit('DOMStorage.domStorageItemRemoved', { + storageId: params.storageId, + key: params.key, + }); + } + } + + async setDOMStorageItem(params) { + const provider = this.getProviderFromParams(params); + if (!provider) { + return; + } + const oldValue = (await provider.getValue(params.key)) ?? undefined; + const res = await provider.setValue(params.key, params.value); + if (res) { + this.emit(oldValue ? 'DOMStorage.domStorageItemUpdated' : 'DOMStorage.domStorageItemAdded', { + storageId: params.storageId, + key: params.key, + oldValue, + newValue: params.value, + }); + } + } + + async clear(params) { + const provider = this.getProviderFromParams(params); + if (!provider) { + return; + } + + const res = await provider.clear(); + + if (res) { + this.emit('DOMStorage.domStorageItemsCleared', { + storageId: params.storageId, + }); + + // re-emit all remaining values after the clean (system may have added new values) + const keys = await provider.getKeys(); + for await (const key of keys) { + const value = await provider.getValue(key); + this.emit('DOMStorage.domStorageItemAdded', { + storageId: params.storageId, + key, + newValue: value, + }); + } + } + } +} + +@DomainDispatcher('Page') +export class KeyValueProviderPageHandler extends ProtocolWrapper { + enable(): void { + super.enable(); + KeyValueProviderRegistry.getProviderFrames().forEach(({ frame }) => { + this.emit('Page.frameStartedLoading', { + frameId: frame.id, + }); + // this.emit('Page.domContentEventFired', { + // timestamp: this.timestamp(), + // }); + // this.emit('Page.loadEventFired', { + // timestamp: this.timestamp(), + // }); + this.emit('Page.frameNavigated', { + frame, + }); + this.emit('Page.frameStoppedLoading', { + frameId: frame.id, + }); + }); + } +} diff --git a/packages/devtools/src/types.d.ts b/packages/devtools/src/types.d.ts new file mode 100644 index 000000000..f8517ddeb --- /dev/null +++ b/packages/devtools/src/types.d.ts @@ -0,0 +1,6 @@ +// Note: these functions are provided by the NativeScript runtimes (@nativescript/ios and @nativescript/android). +declare function __registerDomainDispatcher(domain: string, dispatcher: any): void; +declare function __inspectorSendEvent(data: string): void; +declare function __inspectorTimestamp(): number; + +type MaybePromise = T | Promise; diff --git a/packages/devtools/tsconfig.json b/packages/devtools/tsconfig.json new file mode 100644 index 000000000..709f194cf --- /dev/null +++ b/packages/devtools/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "rootDir": "./src", + "baseUrl": ".", + "target": "ES2020", + "module": "esnext", + "moduleResolution": "node", + "outDir": "./dist", + "declaration": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": ["es2017"], + "sourceMap": true, + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "diagnostics": true, + "paths": { + "@nativescript/devtools": ["src"] + }, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "stripInternal": true + }, + "include": ["src"], + "exclude": ["node_modules"] +}