sdk: Add strict types to sdk (#1308)

* Enable strict mode

* Add @types/node

Remove @types/rimraf

* Fix `include` path to be actual `src`

* Add strict to `sdk`

* Assert `getItem`

* Fix types in SDK

* Refactor SDK function to be type safe

* parseValue handle value null or undefined

* Fix types tsconfig

* Make getDeviceConsole required

* Add build-sdk workflow

* Set working directory

* Assert not undefined

* Remove optionals

* Undo addScryptedInterfaceProperties, revert to self executing function

* Use different type

* Make _deviceState private and add ts-ignore

* Remove unused function

* Remove non-null asserts

* Add tsconfig for sdk/types/src

* Get property isOptional from schema

Use typedoc types

* Type fixes

* Fix type

* Fix type

* Revert change
This commit is contained in:
Long Zheng
2024-02-16 10:17:31 +11:00
committed by GitHub
parent 426454f28f
commit dd7d920480
10 changed files with 98 additions and 94 deletions

25
.github/workflows/build-sdk.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Build SDK
on:
push:
branches: ["main"]
paths: ["sdk/**"]
pull_request:
paths: ["sdk/**"]
workflow_dispatch:
jobs:
build:
name: Build
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./sdk
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm ci
- run: npm run build

View File

@@ -6,10 +6,10 @@ import { DeviceBase, ScryptedInterfaceDescriptors, ScryptedInterfaceProperty, TY
* @category Core Reference
*/
export class ScryptedDeviceBase extends DeviceBase {
private _storage: Storage;
private _log: Logger;
private _console: Console;
private _deviceState: DeviceState;
private _storage: Storage | undefined;
private _log: Logger | undefined;
private _console: Console | undefined;
private _deviceState: DeviceState | undefined;
constructor(public readonly nativeId?: string) {
super();
@@ -43,7 +43,7 @@ export class ScryptedDeviceBase extends DeviceBase {
});
}
getMediaObjectConsole(mediaObject: MediaObject): Console {
getMediaObjectConsole(mediaObject: MediaObject): Console | undefined {
if (typeof mediaObject.sourceId !== 'string')
return this.console;
return deviceManager.getMixinConsole(mediaObject.sourceId, this.nativeId);
@@ -86,17 +86,17 @@ export interface MixinDeviceOptions<T> {
mixinProviderNativeId: ScryptedNativeId;
mixinDevice: T;
mixinDeviceInterfaces: ScryptedInterface[];
private _storage: Storage;
private mixinStorageSuffix: string;
private _log: Logger;
private _console: Console;
private _storage: Storage | undefined;
private mixinStorageSuffix: string | undefined;
private _log: Logger | undefined;
private _console: Console | undefined;
private _deviceState: WritableDeviceState;
private _listeners = new Set<EventListenerRegister>();
constructor(options: MixinDeviceOptions<T>) {
super();
this.nativeId = systemManager.getDeviceById(this.id)?.nativeId;
this.nativeId = systemManager.getDeviceById(this.id).nativeId;
this.mixinDevice = options.mixinDevice;
this.mixinDeviceInterfaces = options.mixinDeviceInterfaces;
this.mixinStorageSuffix = options.mixinStorageSuffix;
@@ -164,22 +164,24 @@ export interface MixinDeviceOptions<T> {
}
}
(function () {
function _createGetState(state: any) {
function _createGetState<T>(deviceBase: ScryptedDeviceBase | MixinDeviceBase<T>, state: ScryptedInterfaceProperty) {
return function () {
this._lazyLoadDeviceState();
return this._deviceState?.[state];
deviceBase._lazyLoadDeviceState();
// @ts-ignore: accessing private property
return deviceBase._deviceState?.[state];
};
}
function _createSetState(state: any) {
function _createSetState<T>(deviceBase: ScryptedDeviceBase | MixinDeviceBase<T>, state: ScryptedInterfaceProperty) {
return function (value: any) {
this._lazyLoadDeviceState();
if (!this._deviceState)
deviceBase._lazyLoadDeviceState();
// @ts-ignore: accessing private property
if (!deviceBase._deviceState)
console.warn('device state is unavailable. the device must be discovered with deviceManager.onDeviceDiscovered or deviceManager.onDevicesChanged before the state can be set.');
else
this._deviceState[state] = value;
// @ts-ignore: accessing private property
deviceBase._deviceState[state] = value;
};
}
@@ -187,17 +189,16 @@ export interface MixinDeviceOptions<T> {
if (field === ScryptedInterfaceProperty.nativeId)
continue;
Object.defineProperty(ScryptedDeviceBase.prototype, field, {
set: _createSetState(field),
get: _createGetState(field),
set: _createSetState(ScryptedDeviceBase.prototype, field),
get: _createGetState(ScryptedDeviceBase.prototype, field),
});
Object.defineProperty(MixinDeviceBase.prototype, field, {
set: _createSetState(field),
get: _createGetState(field),
set: _createSetState(MixinDeviceBase.prototype, field),
get: _createGetState(MixinDeviceBase.prototype, field),
});
}
})();
export const sdk: ScryptedStatic = {} as any;
declare const deviceManager: DeviceManager;
declare const endpointManager: EndpointManager;

View File

@@ -2,7 +2,11 @@ import sdk, { ScryptedInterface, Setting, Settings, SettingValue } from ".";
const { systemManager } = sdk;
function parseValue(value: string, setting: StorageSetting, readDefaultValue: () => any, rawDevice?: boolean) {
function parseValue(value: string | null | undefined, setting: StorageSetting, readDefaultValue: () => any, rawDevice?: boolean) {
if (value === null || value === undefined) {
return readDefaultValue();
}
const type = setting.multiple ? 'array' : setting.type;
if (type === 'boolean') {

View File

@@ -6,7 +6,8 @@
"esModuleInterop": true,
"sourceMap": true,
"declaration": true,
"outDir": "dist"
"outDir": "dist",
"strict": true
},
"include": [
"src/**/*",

View File

@@ -9,7 +9,7 @@
"version": "0.3.11",
"license": "ISC",
"devDependencies": {
"@types/rimraf": "^3.0.2",
"@types/node": "^18.19.15",
"rimraf": "^3.0.2",
"ts-node": "^10.9.1"
}
@@ -75,36 +75,13 @@
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
"dev": true
},
"node_modules/@types/glob": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.0.0.tgz",
"integrity": "sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA==",
"dev": true,
"dependencies": {
"@types/minimatch": "*",
"@types/node": "*"
}
},
"node_modules/@types/minimatch": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"dev": true
},
"node_modules/@types/node": {
"version": "18.7.20",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.20.tgz",
"integrity": "sha512-adzY4vLLr5Uivmx8+zfSJ5fbdgKxX8UMtjtl+17n0B1q1Nz8JEmE151vefMdpD+1gyh+77weN4qEhej/O7budQ==",
"dev": true
},
"node_modules/@types/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==",
"version": "18.19.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.15.tgz",
"integrity": "sha512-AMZ2UWx+woHNfM11PyAEQmfSxi05jm9OlkxczuHeEqmvwPkYj6MWv44gbzDPefYOLysTOFyI3ziiy2ONmUZfpA==",
"dev": true,
"dependencies": {
"@types/glob": "*",
"@types/node": "*"
"undici-types": "~5.26.4"
}
},
"node_modules/acorn": {
@@ -321,6 +298,12 @@
"node": ">=4.2.0"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
@@ -399,36 +382,13 @@
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
"dev": true
},
"@types/glob": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.0.0.tgz",
"integrity": "sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA==",
"dev": true,
"requires": {
"@types/minimatch": "*",
"@types/node": "*"
}
},
"@types/minimatch": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"dev": true
},
"@types/node": {
"version": "18.7.20",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.20.tgz",
"integrity": "sha512-adzY4vLLr5Uivmx8+zfSJ5fbdgKxX8UMtjtl+17n0B1q1Nz8JEmE151vefMdpD+1gyh+77weN4qEhej/O7budQ==",
"dev": true
},
"@types/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==",
"version": "18.19.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.15.tgz",
"integrity": "sha512-AMZ2UWx+woHNfM11PyAEQmfSxi05jm9OlkxczuHeEqmvwPkYj6MWv44gbzDPefYOLysTOFyI3ziiy2ONmUZfpA==",
"dev": true,
"requires": {
"@types/glob": "*",
"@types/node": "*"
"undici-types": "~5.26.4"
}
},
"acorn": {
@@ -586,6 +546,12 @@
"dev": true,
"peer": true
},
"undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",

View File

@@ -7,10 +7,10 @@
"license": "ISC",
"scripts": {
"prepublishOnly": "npm run build",
"build": "rimraf dist gen && typedoc && ts-node ./src/build.ts && tsc"
"build": "tsc --project src && rimraf dist gen && typedoc && ts-node ./src/build.ts && tsc"
},
"devDependencies": {
"@types/rimraf": "^3.0.2",
"@types/node": "^18.19.15",
"rimraf": "^3.0.2",
"ts-node": "^10.9.1"
},

View File

@@ -22,8 +22,8 @@ function toTypescriptType(type: any): string {
}
for (const name of Object.values(ScryptedInterface)) {
const td = schema.children.find((child) => child.name === name);
const children = td.children || [];
const td = schema.children?.find((child) => child.name === name);
const children = td?.children || [];
const properties = children.filter((child) => child.kindString === 'Property').map((child) => child.name);
const methods = children.filter((child) => child.kindString === 'Method').map((child) => child.name);
ScryptedInterfaceDescriptors[name] = {
@@ -46,7 +46,7 @@ ${Object.entries(allProperties).map(([property, {type, flags}]) => ` ${property
}
export class DeviceBase implements DeviceState {
${Object.entries(allProperties).map(([property, {type, flags}]) => ` ${property}${flags.isOptional ? '?' : ''}: ${toTypescriptType(type)}`).join('\n')};
${Object.entries(allProperties).map(([property, {type, flags}]) => ` ${property}${flags.isOptional ? '?' : '!'}: ${toTypescriptType(type)}`).join('\n')};
}
`;
@@ -138,12 +138,15 @@ function selfSignature(method: any) {
return params.join(', ');
}
const enums = schema.children.filter((child: any) => child.kindString === 'Enumeration');
const interfaces = schema.children.filter((child: any) => Object.values(ScryptedInterface).includes(child.name));
const enums = schema.children?.filter((child: any) => child.kindString === 'Enumeration') ?? [];
const interfaces = schema.children?.filter((child: any) => Object.values(ScryptedInterface).includes(child.name)) ?? [];
let python = '';
for (const iface of ['Logger', 'DeviceManager', 'SystemManager', 'MediaManager', 'EndpointManager']) {
interfaces.push(schema.children.find((child: any) => child.name === iface));
const child = schema.children?.find((child: any) => child.name === iface);
if (child)
interfaces.push(child);
}
let seen = new Set<string>();
@@ -226,14 +229,16 @@ for (const td of interfaces) {
let pythonEnums = ''
for (const e of enums) {
pythonEnums += `
if (e.children) {
pythonEnums += `
class ${e.name}(str, Enum):
${toDocstring(e)}
`
for (const val of e.children) {
if ('type' in val && 'value' in val.type)
if (val.type && 'value' in val.type)
pythonEnums += ` ${val.name} = "${val.type.value}"
`;
}
}
}
@@ -282,7 +287,7 @@ ScryptedInterfaceDescriptors = ${JSON.stringify(ScryptedInterfaceDescriptors, nu
`
while (discoveredTypes.size) {
const unknowns = schema.children.filter((child: any) => discoveredTypes.has(child.name) && !enums.find((e: any) => e.name === child.name));
const unknowns = schema.children?.filter((child: any) => discoveredTypes.has(child.name) && !enums.find((e: any) => e.name === child.name)) ?? [];
const newSeen = new Set([...seen, ...discoveredTypes]);
discoveredTypes.clear();

View File

@@ -5,6 +5,7 @@
"noImplicitAny": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"noEmit": true
"noEmit": true,
"strict": true
}
}

View File

@@ -1201,7 +1201,7 @@ export interface AmbientLightSensor {
/**
* The ambient light in lux.
*/
ambientLight: number;
ambientLight?: number;
}
export interface OccupancySensor {
occupied?: boolean;

View File

@@ -6,7 +6,8 @@
"esModuleInterop": true,
"sourceMap": true,
"declaration": true,
"outDir": "dist"
"outDir": "dist",
"strict": true
},
"include": [
"gen/**/*"