From dd7d9204803da75b609f5082e154a1e55d2a7e27 Mon Sep 17 00:00:00 2001 From: Long Zheng Date: Fri, 16 Feb 2024 10:17:31 +1100 Subject: [PATCH] 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 --- .github/workflows/build-sdk.yml | 25 +++++++++++ sdk/src/index.ts | 47 ++++++++++---------- sdk/src/storage-settings.ts | 6 ++- sdk/tsconfig.json | 3 +- sdk/types/package-lock.json | 76 +++++++++------------------------ sdk/types/package.json | 4 +- sdk/types/src/build.ts | 23 ++++++---- sdk/types/src/tsconfig.json | 3 +- sdk/types/src/types.input.ts | 2 +- sdk/types/tsconfig.json | 3 +- 10 files changed, 98 insertions(+), 94 deletions(-) create mode 100644 .github/workflows/build-sdk.yml diff --git a/.github/workflows/build-sdk.yml b/.github/workflows/build-sdk.yml new file mode 100644 index 0000000000..8717971352 --- /dev/null +++ b/.github/workflows/build-sdk.yml @@ -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 diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 500d8ec7c7..7a021e96cd 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -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 { 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(); constructor(options: MixinDeviceOptions) { 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 { } } - (function () { - function _createGetState(state: any) { + function _createGetState(deviceBase: ScryptedDeviceBase | MixinDeviceBase, 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(deviceBase: ScryptedDeviceBase | MixinDeviceBase, 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 { 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; diff --git a/sdk/src/storage-settings.ts b/sdk/src/storage-settings.ts index fee41aee1a..98c497b9e4 100644 --- a/sdk/src/storage-settings.ts +++ b/sdk/src/storage-settings.ts @@ -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') { diff --git a/sdk/tsconfig.json b/sdk/tsconfig.json index b91e4b6303..0675fcccff 100644 --- a/sdk/tsconfig.json +++ b/sdk/tsconfig.json @@ -6,7 +6,8 @@ "esModuleInterop": true, "sourceMap": true, "declaration": true, - "outDir": "dist" + "outDir": "dist", + "strict": true }, "include": [ "src/**/*", diff --git a/sdk/types/package-lock.json b/sdk/types/package-lock.json index e490f568f9..0bdfe51440 100644 --- a/sdk/types/package-lock.json +++ b/sdk/types/package-lock.json @@ -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", diff --git a/sdk/types/package.json b/sdk/types/package.json index 040e177de0..79b47d65a2 100644 --- a/sdk/types/package.json +++ b/sdk/types/package.json @@ -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" }, diff --git a/sdk/types/src/build.ts b/sdk/types/src/build.ts index fc85614a0c..c1d069be9c 100644 --- a/sdk/types/src/build.ts +++ b/sdk/types/src/build.ts @@ -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(); @@ -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(); diff --git a/sdk/types/src/tsconfig.json b/sdk/types/src/tsconfig.json index 5ee8bfdbfa..8746d57cb2 100644 --- a/sdk/types/src/tsconfig.json +++ b/sdk/types/src/tsconfig.json @@ -5,6 +5,7 @@ "noImplicitAny": true, "esModuleInterop": true, "resolveJsonModule": true, - "noEmit": true + "noEmit": true, + "strict": true } } \ No newline at end of file diff --git a/sdk/types/src/types.input.ts b/sdk/types/src/types.input.ts index 60fd4ae727..9c87a5883d 100644 --- a/sdk/types/src/types.input.ts +++ b/sdk/types/src/types.input.ts @@ -1201,7 +1201,7 @@ export interface AmbientLightSensor { /** * The ambient light in lux. */ - ambientLight: number; + ambientLight?: number; } export interface OccupancySensor { occupied?: boolean; diff --git a/sdk/types/tsconfig.json b/sdk/types/tsconfig.json index 6e0c3d5cd3..da9d8decd4 100644 --- a/sdk/types/tsconfig.json +++ b/sdk/types/tsconfig.json @@ -6,7 +6,8 @@ "esModuleInterop": true, "sourceMap": true, "declaration": true, - "outDir": "dist" + "outDir": "dist", + "strict": true }, "include": [ "gen/**/*"