diff --git a/package-lock.json b/package-lock.json index a362979..1284ef6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "@digital-alchemy/type-writer", - "version": "0.3.5", + "version": "0.3.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@digital-alchemy/type-writer", - "version": "0.3.5", + "version": "0.3.6", "license": "MIT", "dependencies": { "@digital-alchemy/core": "^0.3.11", - "@digital-alchemy/hass": "^0.3.8", + "@digital-alchemy/hass": "^0.3.10", "js-yaml": "^4.1.0" }, "bin": { @@ -1136,13 +1136,13 @@ } }, "node_modules/@digital-alchemy/hass": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@digital-alchemy/hass/-/hass-0.3.8.tgz", - "integrity": "sha512-ECy13z94Xkz6gEiQe1rd30HKslBHVp5ZkjtgDCz8ich/ILT8hn32EJzfJsAVeO+ALl/Hc2AcLtDAS29r9GzFqQ==", + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@digital-alchemy/hass/-/hass-0.3.10.tgz", + "integrity": "sha512-HjPHZGWq+Lo2SPw41/xTosaA2hZTQE3ZSYxf7a0gZHhCjneGwIBL3wBduG8Xy8r3TA/KaAp9Z4EmvdMESrHxMg==", "dependencies": { - "@digital-alchemy/core": "^0.3.8", + "@digital-alchemy/core": "^0.3.11", "dayjs": "^1.11.10", - "prom-client": "^15.1.0", + "prom-client": "^15.1.1", "ws": "^8.16.0" }, "engines": { @@ -10350,9 +10350,9 @@ } }, "node_modules/prom-client": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.0.tgz", - "integrity": "sha512-cCD7jLTqyPdjEPBo/Xk4Iu8jxjuZgZJ3e/oET3L+ZwOuap/7Cw3dH/TJSsZKs1TQLZ2IHpIlRAKw82ef06kmMw==", + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.1.tgz", + "integrity": "sha512-GVA2H96QCg2q71rjc3VYvSrVG7OpnJxyryC7dMzvfJfpJJHzQVwF3TJLfHzKORcwJpElWs1TwXLthlJAFJxq2A==", "dependencies": { "@opentelemetry/api": "^1.4.0", "tdigest": "^0.1.1" diff --git a/package.json b/package.json index 0e984f4..eb3ffba 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@digital-alchemy/type-writer", "repository": "https://github.com/Digital-Alchemy-TS/type-writer", "homepage": "https://docs.digital-alchemy.app/Type-Writer", - "version": "0.3.5", + "version": "0.3.6", "scripts": { "build": "rm -rf dist/; tsc", "lint": "eslint src", @@ -26,7 +26,7 @@ "license": "MIT", "dependencies": { "@digital-alchemy/core": "^0.3.11", - "@digital-alchemy/hass": "^0.3.8", + "@digital-alchemy/hass": "^0.3.10", "js-yaml": "^4.1.0" }, "devDependencies": { diff --git a/src/build.extension.ts b/src/build.extension.ts index 67e62ef..44b0925 100644 --- a/src/build.extension.ts +++ b/src/build.extension.ts @@ -1,6 +1,7 @@ import { is, TServiceParams } from "@digital-alchemy/core"; import { existsSync, writeFileSync } from "fs"; import { join } from "path"; +import { exit } from "process"; export function BuildTypes({ logger, @@ -37,12 +38,13 @@ export function BuildTypes({ } catch (error) { logger.fatal({ error }, `failed to write type definitions file`); } + setImmediate(() => exit()); }); // see file - libs/home-assistant/src/dynamic.ts async function DoBuild() { logger.info(`Pulling information`); - const typeInterface = await type_writer.type_writer(); + const typeInterface = await type_writer.call_service(); const entities = await hass.fetch.getAllEntities(); const entitySetup = {}; entities.forEach(i => @@ -51,13 +53,14 @@ export function BuildTypes({ return [ `// This file is generated, and is automatically updated as a npm post install step`, "// Do not edit this file, it will only affect type definitions, not functional code", - "", `import { PICK_ENTITY } from "./helpers";`, - "", `export const ENTITY_SETUP = ${JSON.stringify(entitySetup, undefined, " ")};`, - "", typeInterface, - "", - ].join(`\n`); + type_writer.identifiers.area(), + type_writer.identifiers.device(), + type_writer.identifiers.floor(), + type_writer.identifiers.label(), + type_writer.identifiers.zone(), + ].join(`\n\n`); } } diff --git a/src/type-writer.extension.ts b/src/i-call-service.extension.ts similarity index 64% rename from src/type-writer.extension.ts rename to src/i-call-service.extension.ts index 7e172bf..d0a4a89 100644 --- a/src/type-writer.extension.ts +++ b/src/i-call-service.extension.ts @@ -1,139 +1,28 @@ -/* eslint-disable sonarjs/cognitive-complexity */ /* eslint-disable unicorn/consistent-function-scoping */ import { DOWN, is, TServiceParams, UP } from "@digital-alchemy/core"; import { + HassServiceDTO, + ServiceListField, ServiceListFieldDescription, ServiceListServiceTarget, } from "@digital-alchemy/hass"; import { dump } from "js-yaml"; import { addSyntheticLeadingComment, - createPrinter, - createSourceFile, - EmitHint, factory, - NewLineKind, - ScriptKind, - ScriptTarget, SyntaxKind, TypeElement, TypeNode, } from "typescript"; - -const printer = createPrinter({ newLine: NewLineKind.LineFeed }); -const resultFile = createSourceFile( - "", - "", - ScriptTarget.Latest, - false, - ScriptKind.TS, -); -let lastBuild: string; -let lastServices: string; - -export async function TypeWriter({ hass, logger }: TServiceParams) { +export async function ICallServiceExtension({ + hass, + logger, + type_writer, +}: TServiceParams) { return async function () { const domains = await hass.fetch.listServices(); - const stringified = JSON.stringify(domains); - if (stringified === lastServices) { - return lastBuild; - } - // logger.info(`Services updated`); - lastServices = stringified; - lastBuild = printer.printNode( - EmitHint.Unspecified, - // Wrap all this into a top level `interface iCallService` - factory.createTypeAliasDeclaration( - [factory.createModifier(SyntaxKind.ExportKeyword)], - factory.createIdentifier("iCallService"), - undefined, - // Create categories based off domain name - // { domain: {...services} } - factory.createTypeLiteralNode( - domains - .sort((a, b) => (a.domain > b.domain ? UP : DOWN)) - .map(({ domain, services }) => - factory.createPropertySignature( - undefined, - factory.createIdentifier(domain), - undefined, - factory.createTypeLiteralNode( - // Create functions based on provided services - // { [...service_name](service_data): Promise } - Object.entries(services) - .sort(([a], [b]) => (a > b ? UP : DOWN)) - .map(([key, value]) => - addSyntheticLeadingComment( - factory.createMethodSignature( - undefined, - factory.createIdentifier(key), - undefined, - undefined, - [ - // f( service_data: { ...definition } ) - // Provide this ^^^^^^ - factory.createParameterDeclaration( - undefined, - undefined, - factory.createIdentifier("service_data"), - // ? If all the parameters are optional, then don't require the data at all - Object.values(value.fields).some(i => - is.boolean(i.required) ? !i.required : true, - ) - ? factory.createToken(SyntaxKind.QuestionToken) - : undefined, - factory.createTypeLiteralNode( - [ - ...Object.entries(value.fields) - .sort(([a], [b]) => (a > b ? UP : DOWN)) - .map(([service, details]) => - fieldPropertySignature( - service, - details, - domain, - key, - ), - ), - createTarget( - value.target as ServiceListServiceTarget, - domain, - ), - ].filter( - i => !is.undefined(i), - ) as TypeElement[], - ), - ), - ], - factory.createTypeReferenceNode( - factory.createIdentifier("Promise"), - [ - factory.createKeywordTypeNode( - SyntaxKind.VoidKeyword, - ), - ], - ), - ), - SyntaxKind.MultiLineCommentTrivia, - `*\n` + - [ - `### ${value.name || key}`, - "", - ...value.description.split("\n").map(i => `> ${i}`), - ] - .map(i => ` * ${i}`) - .join(`\n`) + - "\n ", - true, - ), - ), - ), - ), - ), - ), - ), - resultFile, - ); + // #MARK: createTarget function createTarget( target: ServiceListServiceTarget, fallbackDomain: string, @@ -170,6 +59,7 @@ export async function TypeWriter({ hass, logger }: TServiceParams) { return undefined; } + // #MARK: generateEntityList /** * # entity_id * @@ -223,10 +113,7 @@ export async function TypeWriter({ hass, logger }: TServiceParams) { ); } - // function getDomain(target: ServiceListEntityTarget) { - // return target. - // } - + // #MARK: fieldPropertySignature function fieldPropertySignature( parameterName: string, { selector, ...details }: ServiceListFieldDescription, @@ -304,11 +191,12 @@ export async function TypeWriter({ hass, logger }: TServiceParams) { return addSyntheticLeadingComment( property, SyntaxKind.MultiLineCommentTrivia, - buildParameterMultilineComment(parameterName, { selector, ...details }), + parameterComment(parameterName, { selector, ...details }), true, ); } + // #MARK: handleSelectors function handleSelectors( serviceDomain: string, serviceName: string, @@ -353,7 +241,8 @@ export async function TypeWriter({ hass, logger }: TServiceParams) { ]); } - function buildParameterMultilineComment( + // #MARK: parameterComment + function parameterComment( parameterName: string, { selector, ...details }: ServiceListFieldDescription, ) { @@ -396,6 +285,102 @@ export async function TypeWriter({ hass, logger }: TServiceParams) { out = out + "`\n "; return out; } - return lastBuild; + + // #MARK: ServiceComment + function serviceComment(key: string, value: ServiceListField) { + return ( + `*\n` + + [ + `### ${value.name || key}`, + "", + ...value.description.split("\n").map(i => `> ${i}`), + ] + .map(i => ` * ${i}`) + .join(`\n`) + + "\n " + ); + } + + // #MARK: BuildServiceParameters + function serviceParameters( + domain: string, + key: string, + value: ServiceListField, + ) { + return [ + // f( service_data: { ...definition } ) + // Provide this ^^^^^^ + factory.createParameterDeclaration( + undefined, + undefined, + factory.createIdentifier("service_data"), + // ? If all the parameters are optional, then don't require the data at all + Object.values(value.fields).some(i => + is.boolean(i.required) ? !i.required : true, + ) + ? factory.createToken(SyntaxKind.QuestionToken) + : undefined, + factory.createTypeLiteralNode( + [ + ...Object.entries(value.fields) + .sort(([a], [b]) => (a > b ? UP : DOWN)) + .map(([service, details]) => + fieldPropertySignature(service, details, domain, key), + ), + createTarget(value.target as ServiceListServiceTarget, domain), + ].filter(i => !is.undefined(i)) as TypeElement[], + ), + ), + ]; + } + + // #MARK: BuildService + function buildService( + domain: string, + key: string, + value: ServiceListField, + ) { + return addSyntheticLeadingComment( + factory.createMethodSignature( + undefined, + factory.createIdentifier(key), + undefined, + undefined, + serviceParameters(domain, key, value), + factory.createTypeReferenceNode(factory.createIdentifier("Promise"), [ + factory.createKeywordTypeNode(SyntaxKind.VoidKeyword), + ]), + ), + SyntaxKind.MultiLineCommentTrivia, + serviceComment(key, value), + true, + ); + } + + // #MARK: BuildDomain + function buildDomain({ domain, services }: HassServiceDTO) { + return factory.createPropertySignature( + undefined, + factory.createIdentifier(domain), + undefined, + factory.createTypeLiteralNode( + // Create functions based on provided services + // { [...service_name](service_data): Promise } + Object.entries(services) + .sort(([a], [b]) => (a > b ? UP : DOWN)) + .map(([key, value]) => buildService(domain, key, value)), + ), + ); + } + + // #MARK: final build + return type_writer.printer( + "iCallService", + factory.createTypeLiteralNode( + domains + .sort((a, b) => (a.domain > b.domain ? UP : DOWN)) + .map(domain => buildDomain(domain)), + ), + ); }; } diff --git a/src/identifiers.extension.ts b/src/identifiers.extension.ts new file mode 100644 index 0000000..e9f0ffc --- /dev/null +++ b/src/identifiers.extension.ts @@ -0,0 +1,63 @@ +import { TServiceParams } from "@digital-alchemy/core"; +import { factory } from "typescript"; + +export function Identifiers({ hass, type_writer }: TServiceParams) { + return { + area() { + return type_writer.printer( + "TAreaId", + factory.createUnionTypeNode( + hass.area.current.map(i => + factory.createLiteralTypeNode( + factory.createStringLiteral(i.area_id), + ), + ), + ), + ); + }, + device() { + return type_writer.printer( + "TDeviceId", + factory.createUnionTypeNode( + hass.device.current.map(i => + factory.createLiteralTypeNode(factory.createStringLiteral(i.id)), + ), + ), + ); + }, + floor() { + return type_writer.printer( + "TFloorId", + factory.createUnionTypeNode( + hass.floor.current.map(i => + factory.createLiteralTypeNode( + factory.createStringLiteral(i.floor_id), + ), + ), + ), + ); + }, + label() { + return type_writer.printer( + "TLabelId", + factory.createUnionTypeNode( + hass.label.current.map(i => + factory.createLiteralTypeNode( + factory.createStringLiteral(i.label_id), + ), + ), + ), + ); + }, + zone() { + return type_writer.printer( + "TZoneId", + factory.createUnionTypeNode( + hass.zone.current.map(i => + factory.createLiteralTypeNode(factory.createStringLiteral(i.id)), + ), + ), + ); + }, + }; +} diff --git a/src/main.ts b/src/main.ts index 1ad43cd..f6cb181 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,7 +3,9 @@ import { CreateApplication } from "@digital-alchemy/core"; import { LIB_HASS } from "@digital-alchemy/hass"; import { BuildTypes } from "./build.extension"; -import { TypeWriter } from "./type-writer.extension"; +import { ICallServiceExtension } from "./i-call-service.extension"; +import { Identifiers } from "./identifiers.extension"; +import { Printer } from "./printer.extension"; export const TYPE_WRITER = CreateApplication({ configuration: { @@ -17,7 +19,9 @@ export const TYPE_WRITER = CreateApplication({ name: "type_writer", services: { build: BuildTypes, - type_writer: TypeWriter, + call_service: ICallServiceExtension, + identifiers: Identifiers, + printer: Printer, }, }); setImmediate(async () => { @@ -27,7 +31,6 @@ setImmediate(async () => { LOG_LEVEL: "warn", }, hass: { - AUTO_CONNECT_SOCKET: false, AUTO_SCAN_CALL_PROXY: false, }, }, diff --git a/src/printer.extension.ts b/src/printer.extension.ts new file mode 100644 index 0000000..cc6a0b4 --- /dev/null +++ b/src/printer.extension.ts @@ -0,0 +1,35 @@ +import { + createPrinter, + createSourceFile, + EmitHint, + factory, + NewLineKind, + ScriptKind, + ScriptTarget, + SyntaxKind, + TypeNode, +} from "typescript"; + +export function Printer() { + const printer = createPrinter({ newLine: NewLineKind.LineFeed }); + const resultFile = createSourceFile( + "", + "", + ScriptTarget.Latest, + false, + ScriptKind.TS, + ); + + return function (name: string, types: TypeNode) { + return printer.printNode( + EmitHint.Unspecified, + factory.createTypeAliasDeclaration( + [factory.createModifier(SyntaxKind.ExportKeyword)], + factory.createIdentifier(name), + undefined, + types, + ), + resultFile, + ); + }; +}