feat: initial devtools package implementation (#10484)

This commit is contained in:
Igor Randjelovic
2024-04-15 19:21:48 +02:00
committed by GitHub
parent 873f711a6b
commit 7787ce9449
9 changed files with 366 additions and 0 deletions

1
packages/devtools/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist/

View File

@ -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"
}
}

View File

@ -0,0 +1,4 @@
{
"name": "devtools",
"$schema": "../../node_modules/nx/schemas/project-schema.json"
}

View File

@ -0,0 +1,108 @@
// const domainDispatchers = new Map<string, any>();
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<T = any> {
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<T = any>(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<T extends Object>(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;
}
}

View File

@ -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;
}
}

View File

@ -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';

View File

@ -0,0 +1,161 @@
import { DomainDispatcher, ProtocolWrapper } from '../devtoolsRuntime';
export interface KeyValueProvider {
getName(): string;
getKeys(): MaybePromise<string[]>;
getValue(key: string): MaybePromise<string | undefined>;
setValue(key: string, value: string): MaybePromise<boolean>;
deleteKey(key: string): MaybePromise<boolean>;
clear(): MaybePromise<boolean>;
}
@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<string, KeyValueProvider> = 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,
});
});
}
}

6
packages/devtools/src/types.d.ts vendored Normal file
View File

@ -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> = T | Promise<T>;

View File

@ -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"]
}