diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5b5762d0..abec45bb 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -183,7 +183,7 @@ logger.debug("message"); ### Performance - Use `fs.rmSync(path, {recursive: true, force: true})` for synchronous file deletion when appropriate - Leverage async/await for I/O operations to avoid blocking -- Use JSON stable stringify for consistent object serialization: `json-stable-stringify-without-jsonify` +- Use JSON stable stringify util for consistent object serialization - Cache computed values when appropriate (see device model patterns) - Use getter methods for computed properties that should be cached diff --git a/lib/controller.ts b/lib/controller.ts index 1bd694c7..deb21601 100644 --- a/lib/controller.ts +++ b/lib/controller.ts @@ -1,5 +1,4 @@ import bind from "bind-decorator"; -import stringify from "json-stable-stringify-without-jsonify"; import {setLogger as zhSetLogger} from "zigbee-herdsman"; import {setLogger as zhcSetLogger} from "zigbee-herdsman-converters"; import EventBus from "./eventBus"; @@ -24,6 +23,7 @@ import type {Zigbee2MQTTAPI} from "./types/api"; import logger from "./util/logger"; import {initSdNotify} from "./util/sd-notify"; import * as settings from "./util/settings"; +import {stringify} from "./util/stringify"; import utils from "./util/utils"; import Zigbee from "./zigbee"; @@ -462,7 +462,7 @@ export class Controller { async iteratePayloadAttributeOutput(topicRoot: string, payload: KeyValue, options: Partial): Promise { for (const [key, value] of Object.entries(payload)) { let subPayload = value; - let message = null; + let message: string | undefined; // Special cases if (key === "color" && utils.objectHasProperties(subPayload, ["r", "g", "b"])) { @@ -480,7 +480,7 @@ export class Controller { message = typeof subPayload === "string" ? subPayload : stringify(subPayload); } - if (message !== null) { + if (message !== undefined) { await this.mqtt.publish(`${topicRoot}${key}`, message, options); } } diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index 444ff261..1a66a6fa 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -1,7 +1,6 @@ import assert from "node:assert"; import bind from "bind-decorator"; import debounce from "debounce"; -import stringify from "json-stable-stringify-without-jsonify"; import {Zcl} from "zigbee-herdsman"; import type {TClusterAttributeKeys} from "zigbee-herdsman/dist/zspec/zcl/definition/clusters-types"; import type {ClusterName} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype"; @@ -10,6 +9,7 @@ import Group from "../model/group"; import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from "../types/api"; import logger from "../util/logger"; import * as settings from "../util/settings"; +import {stringify} from "../util/stringify"; import utils, {DEFAULT_BIND_GROUP_ID} from "../util/utils"; import Extension from "./extension"; diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index e86b8359..1fb51377 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -1,7 +1,6 @@ import fs from "node:fs"; import path from "node:path"; import bind from "bind-decorator"; -import stringify from "json-stable-stringify-without-jsonify"; import JSZip from "jszip"; import objectAssignDeep from "object-assign-deep"; import type winston from "winston"; @@ -15,6 +14,7 @@ import type {Zigbee2MQTTAPI, Zigbee2MQTTDevice, Zigbee2MQTTResponse, Zigbee2MQTT import data from "../util/data"; import logger from "../util/logger"; import * as settings from "../util/settings"; +import {stringify} from "../util/stringify"; import utils, {assertString, DEFAULT_BIND_GROUP_ID} from "../util/utils"; import Extension from "./extension"; diff --git a/lib/extension/configure.ts b/lib/extension/configure.ts index 68316ca7..53226a0c 100644 --- a/lib/extension/configure.ts +++ b/lib/extension/configure.ts @@ -1,9 +1,9 @@ import bind from "bind-decorator"; -import stringify from "json-stable-stringify-without-jsonify"; import Device from "../model/device"; import type {Zigbee2MQTTAPI} from "../types/api"; import logger from "../util/logger"; import * as settings from "../util/settings"; +import {stringify} from "../util/stringify"; import utils from "../util/utils"; import Extension from "./extension"; diff --git a/lib/extension/externalJS.ts b/lib/extension/externalJS.ts index 528ce7c4..a848b126 100644 --- a/lib/extension/externalJS.ts +++ b/lib/extension/externalJS.ts @@ -2,12 +2,11 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import bind from "bind-decorator"; -import stringify from "json-stable-stringify-without-jsonify"; import type {Zigbee2MQTTAPI, Zigbee2MQTTResponse} from "../types/api"; - import data from "../util/data"; import logger from "../util/logger"; import * as settings from "../util/settings"; +import {stringify} from "../util/stringify"; import utils from "../util/utils"; import Extension from "./extension"; diff --git a/lib/extension/frontend.ts b/lib/extension/frontend.ts index 257a8ace..db0f4206 100644 --- a/lib/extension/frontend.ts +++ b/lib/extension/frontend.ts @@ -8,12 +8,11 @@ import {posix} from "node:path"; import bind from "bind-decorator"; import expressStaticGzip from "express-static-gzip"; import finalhandler from "finalhandler"; -import stringify from "json-stable-stringify-without-jsonify"; import WebSocket from "ws"; - import data from "../util/data"; import logger from "../util/logger"; import * as settings from "../util/settings"; +import {stringify} from "../util/stringify"; import utils from "../util/utils"; import Extension from "./extension"; diff --git a/lib/extension/groups.ts b/lib/extension/groups.ts index ebcd9954..a7de0806 100644 --- a/lib/extension/groups.ts +++ b/lib/extension/groups.ts @@ -1,13 +1,13 @@ import assert from "node:assert"; import bind from "bind-decorator"; import equals from "fast-deep-equal/es6"; -import stringify from "json-stable-stringify-without-jsonify"; import type * as zhc from "zigbee-herdsman-converters"; import Device from "../model/device"; import Group from "../model/group"; import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from "../types/api"; import logger from "../util/logger"; import * as settings from "../util/settings"; +import {stringify} from "../util/stringify"; import utils, {isLightExpose} from "../util/utils"; import Extension from "./extension"; diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index 2eb13ae5..b8a5e665 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -1,10 +1,10 @@ import assert from "node:assert"; import bind from "bind-decorator"; -import stringify from "json-stable-stringify-without-jsonify"; import type * as zhc from "zigbee-herdsman-converters"; import type {Zh} from "zigbee-herdsman-converters/lib/types"; import logger from "../util/logger"; import * as settings from "../util/settings"; +import {stringify} from "../util/stringify"; import utils, {assertBinaryExpose, assertEnumExpose, assertNumericExpose, isBinaryExpose, isEnumExpose, isNumericExpose} from "../util/utils"; import Extension from "./extension"; diff --git a/lib/extension/networkMap.ts b/lib/extension/networkMap.ts index 01a49a81..68a187e0 100644 --- a/lib/extension/networkMap.ts +++ b/lib/extension/networkMap.ts @@ -1,10 +1,10 @@ import bind from "bind-decorator"; -import stringify from "json-stable-stringify-without-jsonify"; import type {Eui64} from "zigbee-herdsman/dist/zspec/tstypes"; import type {LQITableEntry, RoutingTableEntry} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes"; import type {Zigbee2MQTTAPI, Zigbee2MQTTNetworkMap} from "../types/api"; import logger from "../util/logger"; import * as settings from "../util/settings"; +import {stringify} from "../util/stringify"; import utils from "../util/utils"; import Extension from "./extension"; diff --git a/lib/extension/otaUpdate.ts b/lib/extension/otaUpdate.ts index 6b906ccb..d3bf1914 100644 --- a/lib/extension/otaUpdate.ts +++ b/lib/extension/otaUpdate.ts @@ -2,7 +2,6 @@ import assert from "node:assert"; import {existsSync, mkdirSync, rmSync, writeFileSync} from "node:fs"; import {join} from "node:path"; import bind from "bind-decorator"; -import stringify from "json-stable-stringify-without-jsonify"; import {setOtaConfiguration, Zcl} from "zigbee-herdsman"; import type {OtaDataSettings, OtaSource, OtaUpdateAvailableResult} from "zigbee-herdsman/dist/controller/tstype"; import Device from "../model/device"; @@ -10,6 +9,7 @@ import type {Zigbee2MQTTAPI} from "../types/api"; import dataDir from "../util/data"; import logger from "../util/logger"; import * as settings from "../util/settings"; +import {stringify} from "../util/stringify"; import utils from "../util/utils"; import Extension from "./extension"; diff --git a/lib/extension/publish.ts b/lib/extension/publish.ts index d0998292..d6b814e1 100644 --- a/lib/extension/publish.ts +++ b/lib/extension/publish.ts @@ -1,11 +1,10 @@ import bind from "bind-decorator"; -import stringify from "json-stable-stringify-without-jsonify"; import type * as zhc from "zigbee-herdsman-converters"; - import Device from "../model/device"; import Group from "../model/group"; import logger from "../util/logger"; import * as settings from "../util/settings"; +import {stringify} from "../util/stringify"; import utils from "../util/utils"; import Extension from "./extension"; diff --git a/lib/extension/receive.ts b/lib/extension/receive.ts index e273e988..f897d187 100755 --- a/lib/extension/receive.ts +++ b/lib/extension/receive.ts @@ -1,14 +1,11 @@ import assert from "node:assert"; - import bind from "bind-decorator"; import debounce from "debounce"; -import stringify from "json-stable-stringify-without-jsonify"; import throttle from "throttleit"; - import * as zhc from "zigbee-herdsman-converters"; - import logger from "../util/logger"; import * as settings from "../util/settings"; +import {stringify} from "../util/stringify"; import utils from "../util/utils"; import Extension from "./extension"; diff --git a/lib/types/json-stable-stringify-without-jsonify.d.ts b/lib/types/json-stable-stringify-without-jsonify.d.ts deleted file mode 100644 index b19418cf..00000000 --- a/lib/types/json-stable-stringify-without-jsonify.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module "json-stable-stringify-without-jsonify" { - export default function (obj: unknown): string; -} diff --git a/lib/util/onboarding.ts b/lib/util/onboarding.ts index e8e2c408..a268287c 100644 --- a/lib/util/onboarding.ts +++ b/lib/util/onboarding.ts @@ -4,10 +4,10 @@ import {createServer} from "node:http"; import path from "node:path"; import expressStaticGzip from "express-static-gzip"; import finalhandler from "finalhandler"; -import stringify from "json-stable-stringify-without-jsonify"; import JSZip from "jszip"; import {findAllDevices} from "zigbee-herdsman/dist/adapter/adapterDiscovery"; import type {OnboardData, OnboardFailureData, OnboardSubmitResponse, Zigbee2MQTTSettings} from "../types/api"; +import {stringify} from "../util/stringify"; import data from "./data"; import * as settings from "./settings"; import {YAMLFileException} from "./yaml"; diff --git a/lib/util/stringify.ts b/lib/util/stringify.ts new file mode 100644 index 00000000..89ebeaac --- /dev/null +++ b/lib/util/stringify.ts @@ -0,0 +1,153 @@ +// Stable stringify inspired by https://github.com/BridgeAR/safe-stable-stringify +// Takes advantage of Node env and Z2M's object-only use-case. + +// biome-ignore lint/suspicious/noControlCharactersInRegex: escape regex +const STR_ESC_SEQ_REGEXP = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/; + +// Escape C0 control characters, double quotes, the backslash and every code +// unit with a numeric value in the inclusive range 0xD800 to 0xDFFF. +function strEscape(str: string): string { + // Some magic numbers that worked out fine while benchmarking with v8 8.0 + if (str.length < 5000 && !STR_ESC_SEQ_REGEXP.test(str)) { + return `"${str}"`; + } + + return JSON.stringify(str); +} + +function sort(array: string[]) { + // Insertion sort is very efficient for small input sizes, but it has a bad + // worst case complexity. Thus, use native array sort for bigger values. + if (array.length > 2e2) { + return array.sort(); + } + + for (let i = 1; i < array.length; i++) { + const currentValue = array[i]; + let position = i; + + while (position !== 0 && array[position - 1] > currentValue) { + array[position] = array[position - 1]; + position--; + } + + array[position] = currentValue; + } +} + +function isTypedArray(value: unknown): value is unknown[] { + return ArrayBuffer.isView(value) && !(value instanceof DataView); +} + +function stringifyTypedArray(array: unknown[]): string { + const isBigInt = typeof array[0] === "bigint"; + let res = `"0":${isBigInt ? `"${array[0]}"` : array[0]}`; + + for (let i = 1; i < array.length; i++) { + res += `,"${i}":${isBigInt ? `"${array[1]}"` : array[1]}`; + } + + return res; +} + +function stringifySimple(key: string, value: unknown, stack: unknown[]): string | undefined { + switch (typeof value) { + case "string": + return strEscape(value); + case "object": { + if (value === null) { + return "null"; + } + + if ("toJSON" in value && typeof value.toJSON === "function") { + value = value.toJSON(key); + + // Prevent calling `toJSON` again + if (typeof value !== "object") { + return stringifySimple(key, value, stack); + } + + if (value === null) { + return "null"; + } + } + + if (stack.indexOf(value) !== -1) { + return '"[Circular]"'; + } + + let res = ""; + + if (Array.isArray(value)) { + if (value.length === 0) { + return "[]"; + } + + stack.push(value); + + let i = 0; + + for (; i < value.length - 1; i++) { + const tmp = stringifySimple(`${i}`, value[i], stack); + res += tmp !== undefined ? tmp : "null"; + res += ","; + } + + const tmp = stringifySimple(`${i}`, value[i], stack); + res += tmp !== undefined ? tmp : "null"; + + stack.pop(); + + return `[${res}]`; + } + + let keys = Object.keys(value); + const keysLength = keys.length; + + if (keysLength === 0) { + return "{}"; + } + + let separator = ""; + let propsToStringify = keysLength; + + if (isTypedArray(value)) { + res += stringifyTypedArray(value); + keys = keys.slice(value.length); + propsToStringify -= value.length; + separator = ","; + } + + sort(keys); + stack.push(value); + + for (let i = 0; i < propsToStringify; i++) { + const valKey = keys[i]; + const tmp = stringifySimple(valKey, (value as Record)[valKey], stack); + + if (tmp !== undefined) { + res += `${separator}${strEscape(valKey)}:${tmp}`; + separator = ","; + } + } + + stack.pop(); + + return `{${res}}`; + } + case "number": + return Number.isFinite(value) ? `${value}` : "null"; + case "boolean": + return value === true ? "true" : "false"; + case "undefined": + return undefined; + case "bigint": + return `"${value}"`; + default: + return undefined; + } +} + +export function stringify(value: object): string { + return stringifySimple("", value, []) ?? "null"; +} diff --git a/lib/zigbee.ts b/lib/zigbee.ts index 61d1075f..78e343ce 100644 --- a/lib/zigbee.ts +++ b/lib/zigbee.ts @@ -1,6 +1,5 @@ import {randomInt} from "node:crypto"; import bind from "bind-decorator"; -import stringify from "json-stable-stringify-without-jsonify"; import type {Events as ZHEvents} from "zigbee-herdsman"; import {Controller} from "zigbee-herdsman"; import type {StartResult} from "zigbee-herdsman/dist/adapter/tstype"; @@ -9,6 +8,7 @@ import Group from "./model/group"; import data from "./util/data"; import logger from "./util/logger"; import * as settings from "./util/settings"; +import {stringify} from "./util/stringify"; import utils from "./util/utils"; const entityIDRegex = /^(.+?)(?:\/([^/]+))?$/; @@ -61,7 +61,7 @@ export default class Zigbee { logger.debug( () => - `Using zigbee-herdsman with settings: '${stringify(JSON.stringify(herdsmanSettings).replaceAll(JSON.stringify(herdsmanSettings.network.networkKey), '"HIDDEN"'))}'`, + `Using zigbee-herdsman with settings: '${stringify(herdsmanSettings).replaceAll(stringify(herdsmanSettings.network.networkKey), '"HIDDEN"')}'`, ); let startResult: StartResult; diff --git a/package.json b/package.json index da4832f0..de93cdd2 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "finalhandler": "^2.1.1", "humanize-duration": "^3.34.0", "js-yaml": "^5.2.1", - "json-stable-stringify-without-jsonify": "^1.0.1", "jszip": "^3.10.1", "mqtt": "^5.15.2", "object-assign-deep": "^0.4.0", @@ -96,4 +95,4 @@ "optionalDependencies": { "unix-dgram": "^2.0.7" } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cbd74dd3..9781f40a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,9 +35,6 @@ importers: js-yaml: specifier: ^5.2.1 version: 5.2.1 - json-stable-stringify-without-jsonify: - specifier: ^1.0.1 - version: 1.0.1 jszip: specifier: ^3.10.1 version: 3.10.1 @@ -1047,9 +1044,6 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} @@ -2298,8 +2292,6 @@ snapshots: json-schema-traverse@1.0.0: {} - json-stable-stringify-without-jsonify@1.0.1: {} - jszip@3.10.1: dependencies: lie: 3.3.0 diff --git a/test/controller.bench.ts b/test/controller.bench.ts index 1f79b3eb..c4bf5291 100644 --- a/test/controller.bench.ts +++ b/test/controller.bench.ts @@ -1,5 +1,4 @@ import {existsSync, mkdirSync} from "node:fs"; -import stringify from "json-stable-stringify-without-jsonify"; import {bench, describe, vi} from "vitest"; import {type Controller, Zcl, Zdo, ZSpec} from "zigbee-herdsman"; import type Adapter from "zigbee-herdsman/dist/adapter/adapter"; @@ -12,6 +11,7 @@ import type {DeviceType} from "zigbee-herdsman/dist/controller/tstype"; import {Foundation} from "zigbee-herdsman/dist/zspec/zcl/definition/foundation"; import type {RequestToResponseMap} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes"; import data from "../lib/util/data"; +import {stringify} from "../lib/util/stringify"; import {BENCH_OPTIONS} from "./benchOptions"; vi.doMock("zigbee-herdsman", async (importOriginal) => { @@ -257,7 +257,7 @@ const adapter = { switch (zclFrame.command.ID) { case Foundation.read.ID: { for (const attr of zclFrame.payload) { - const attribute = zclFrame.cluster.getAttribute(attr.attrId); + const attribute = Zcl.Utils.getClusterAttribute(zclFrame.cluster, attr.attrId, undefined); if (attribute && attribute.type !== Zcl.DataType.NO_DATA && attribute.type < Zcl.DataType.OCTET_STR) { payload.push({ @@ -421,7 +421,6 @@ const initController = async () => { disconnecting: false, disconnected: false, endAsync: async () => {}, - // @ts-expect-error Z2M does not make use of return publishAsync: async () => {}, }; controller.mqtt.connect = async () => { diff --git a/test/controller.test.ts b/test/controller.test.ts index 70fcbae8..de43d0e2 100644 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -17,9 +17,9 @@ import {devices, mockController as mockZHController, events as mockZHEvents, ret import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import stringify from "json-stable-stringify-without-jsonify"; import tmp from "tmp"; +import {stringify} from "../lib/util/stringify"; import type {Mock, MockInstance} from "vitest"; import {Controller as ZHController} from "zigbee-herdsman"; import {Controller} from "../lib/controller"; diff --git a/test/extensions/availability.test.ts b/test/extensions/availability.test.ts index 22735214..96eb7626 100644 --- a/test/extensions/availability.test.ts +++ b/test/extensions/availability.test.ts @@ -7,7 +7,7 @@ import {flushPromises} from "../mocks/utils"; import {devices, events as mockZHEvents, returnDevices} from "../mocks/zigbeeHerdsman"; import assert from "node:assert"; -import stringify from "json-stable-stringify-without-jsonify"; +import {stringify} from "../../lib/util/stringify"; import {Controller} from "../../lib/controller"; import Availability from "../../lib/extension/availability"; import * as settings from "../../lib/util/settings"; diff --git a/test/extensions/bind.test.ts b/test/extensions/bind.test.ts index 448fc11d..09bee490 100644 --- a/test/extensions/bind.test.ts +++ b/test/extensions/bind.test.ts @@ -7,7 +7,7 @@ import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt"; import {flushPromises} from "../mocks/utils"; import {type Device, devices, groups, events as mockZHEvents} from "../mocks/zigbeeHerdsman"; -import stringify from "json-stable-stringify-without-jsonify"; +import {stringify} from "../../lib/util/stringify"; import {Controller} from "../../lib/controller"; import Bind from "../../lib/extension/bind"; import * as settings from "../../lib/util/settings"; diff --git a/test/extensions/bridge.test.ts b/test/extensions/bridge.test.ts index dd5e66b8..3e091d4f 100644 --- a/test/extensions/bridge.test.ts +++ b/test/extensions/bridge.test.ts @@ -12,7 +12,7 @@ import assert from "node:assert"; import fs from "node:fs"; import {platform} from "node:os"; import path from "node:path"; -import stringify from "json-stable-stringify-without-jsonify"; +import {stringify} from "../../lib/util/stringify"; import type {Mock} from "vitest"; import {Controller} from "../../lib/controller"; import Bridge from "../../lib/extension/bridge"; diff --git a/test/extensions/configure.test.ts b/test/extensions/configure.test.ts index 3c6046e7..29a89a92 100644 --- a/test/extensions/configure.test.ts +++ b/test/extensions/configure.test.ts @@ -6,7 +6,7 @@ import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt"; import {flushPromises} from "../mocks/utils"; import {devices, type Endpoint, events as mockZHEvents, type Device as ZhDevice} from "../mocks/zigbeeHerdsman"; -import stringify from "json-stable-stringify-without-jsonify"; +import {stringify} from "../../lib/util/stringify"; import {InterviewState} from "zigbee-herdsman/dist/controller/model/device"; import {Controller} from "../../lib/controller"; import Device from "../../lib/model/device"; diff --git a/test/extensions/externalConverters.test.ts b/test/extensions/externalConverters.test.ts index 4406779c..5108dfa3 100644 --- a/test/extensions/externalConverters.test.ts +++ b/test/extensions/externalConverters.test.ts @@ -9,7 +9,7 @@ import {devices, mockController as mockZHController, returnDevices} from "../moc import fs from "node:fs"; import path from "node:path"; -import stringify from "json-stable-stringify-without-jsonify"; +import {stringify} from "../../lib/util/stringify"; import * as zhc from "zigbee-herdsman-converters"; import {Controller} from "../../lib/controller"; import ExternalConverters from "../../lib/extension/externalConverters"; diff --git a/test/extensions/externalExtensions.test.ts b/test/extensions/externalExtensions.test.ts index 983cf0b0..e5fdc231 100644 --- a/test/extensions/externalExtensions.test.ts +++ b/test/extensions/externalExtensions.test.ts @@ -8,7 +8,7 @@ import {devices, mockController as mockZHController, returnDevices} from "../moc import fs from "node:fs"; import path from "node:path"; -import stringify from "json-stable-stringify-without-jsonify"; +import {stringify} from "../../lib/util/stringify"; import {Controller} from "../../lib/controller"; import ExternalExtensions from "../../lib/extension/externalExtensions"; import * as settings from "../../lib/util/settings"; diff --git a/test/extensions/frontend.test.ts b/test/extensions/frontend.test.ts index 4f061ac2..9a98901f 100644 --- a/test/extensions/frontend.test.ts +++ b/test/extensions/frontend.test.ts @@ -7,7 +7,7 @@ import {type EventHandler, flushPromises} from "../mocks/utils"; import {devices, events as mockZHEvents} from "../mocks/zigbeeHerdsman"; import path from "node:path"; -import stringify from "json-stable-stringify-without-jsonify"; +import {stringify} from "../../lib/util/stringify"; import type {Mock} from "vitest"; import ws from "ws"; import {Controller} from "../../lib/controller"; diff --git a/test/extensions/groups.test.ts b/test/extensions/groups.test.ts index b466e26f..07878639 100644 --- a/test/extensions/groups.test.ts +++ b/test/extensions/groups.test.ts @@ -6,7 +6,7 @@ import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt"; import {flushPromises} from "../mocks/utils"; import {devices, groups, events as mockZHEvents, resetGroupMembers, returnDevices} from "../mocks/zigbeeHerdsman"; -import stringify from "json-stable-stringify-without-jsonify"; +import {stringify} from "../../lib/util/stringify"; import * as zhcGlobalStore from "zigbee-herdsman-converters/lib/store"; import {Controller} from "../../lib/controller"; import * as settings from "../../lib/util/settings"; diff --git a/test/extensions/homeassistant.test.ts b/test/extensions/homeassistant.test.ts index 2bd6617f..e69ebab8 100644 --- a/test/extensions/homeassistant.test.ts +++ b/test/extensions/homeassistant.test.ts @@ -9,7 +9,7 @@ import type {Device as ZhDevice} from "../mocks/zigbeeHerdsman"; import {devices, groups, events as mockZHEvents} from "../mocks/zigbeeHerdsman"; import assert from "node:assert"; -import stringify from "json-stable-stringify-without-jsonify"; +import {stringify} from "../../lib/util/stringify"; import type {MockInstance} from "vitest"; import * as zhc from "zigbee-herdsman-converters"; import type {KeyValueAny} from "zigbee-herdsman-converters/lib/types"; diff --git a/test/extensions/networkMap.test.ts b/test/extensions/networkMap.test.ts index 96b5e024..ddcb93b1 100644 --- a/test/extensions/networkMap.test.ts +++ b/test/extensions/networkMap.test.ts @@ -10,7 +10,7 @@ import {devices, events as mockZHEvents, returnDevices} from "../mocks/zigbeeHer import fs from "node:fs"; import path from "node:path"; -import stringify from "json-stable-stringify-without-jsonify"; +import {stringify} from "../../lib/util/stringify"; import {Controller} from "../../lib/controller"; import * as settings from "../../lib/util/settings"; diff --git a/test/extensions/otaUpdate.test.ts b/test/extensions/otaUpdate.test.ts index 09916aff..419783a5 100644 --- a/test/extensions/otaUpdate.test.ts +++ b/test/extensions/otaUpdate.test.ts @@ -9,7 +9,7 @@ import {devices, events as mockZHEvents} from "../mocks/zigbeeHerdsman"; import {join} from "node:path"; import {existsSync, readFileSync, rmSync} from "node:fs"; -import stringify from "json-stable-stringify-without-jsonify"; +import {stringify} from "../../lib/util/stringify"; import {Controller} from "../../lib/controller"; import OTAUpdate from "../../lib/extension/otaUpdate"; import * as settings from "../../lib/util/settings"; diff --git a/test/extensions/publish.test.ts b/test/extensions/publish.test.ts index c9b284e2..8c8507e5 100644 --- a/test/extensions/publish.test.ts +++ b/test/extensions/publish.test.ts @@ -7,7 +7,7 @@ import * as mockSleep from "../mocks/sleep"; import {flushPromises} from "../mocks/utils"; import {devices, groups, events as mockZHEvents} from "../mocks/zigbeeHerdsman"; -import stringify from "json-stable-stringify-without-jsonify"; +import {stringify} from "../../lib/util/stringify"; import {clearGlobalStore} from "zigbee-herdsman-converters"; import {Controller} from "../../lib/controller"; import {loadTopicGetSetRegex} from "../../lib/extension/publish"; diff --git a/test/extensions/receive.test.ts b/test/extensions/receive.test.ts index 954e10d6..6051e535 100644 --- a/test/extensions/receive.test.ts +++ b/test/extensions/receive.test.ts @@ -7,7 +7,7 @@ import * as mockSleep from "../mocks/sleep"; import {flushPromises} from "../mocks/utils"; import {devices, events as mockZHEvents} from "../mocks/zigbeeHerdsman"; -import stringify from "json-stable-stringify-without-jsonify"; +import {stringify} from "../../lib/util/stringify"; import {Controller} from "../../lib/controller"; import * as settings from "../../lib/util/settings"; diff --git a/test/mocks/data.ts b/test/mocks/data.ts index f6012abb..1a7e9f1f 100644 --- a/test/mocks/data.ts +++ b/test/mocks/data.ts @@ -1,8 +1,8 @@ import fs from "node:fs"; import path from "node:path"; -import stringify from "json-stable-stringify-without-jsonify"; import tmp from "tmp"; import {vi} from "vitest"; +import {stringify} from "../../lib/util/stringify"; import yaml from "../../lib/util/yaml"; export const mockDir: string = tmp.dirSync().name; diff --git a/test/mocks/types.d.ts b/test/mocks/types.d.ts index 6361a14f..e71a6144 100644 --- a/test/mocks/types.d.ts +++ b/test/mocks/types.d.ts @@ -1,7 +1,3 @@ -declare module "json-stable-stringify-without-jsonify" { - export default function (obj: unknown): string; -} - declare module "tmp" { export function dirSync(): { name: string; diff --git a/test/utils.test.ts b/test/utils.test.ts index 3c3e41e9..e4c35018 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -2,6 +2,7 @@ import {exec} from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import {describe, expect, it, vi} from "vitest"; +import {stringify} from "../lib/util/stringify"; import utils, {assertString} from "../lib/util/utils"; // keep the implementations, just spy @@ -155,4 +156,37 @@ describe("Utils", () => { }, }); }); + + it("stable stringify", () => { + expect( + stringify({ + a: "a", + b: 2, + 3: "c", + d: Buffer.from([1, 2]), + e: new Int16Array([0xfffd, 0xff11]), + beef: 0xfacen, + zed: new BigUint64Array([1n, 0xffffffffn]), + ris: [1, undefined, "b", 0xfeefn, Number.NaN, undefined], + ls: undefined, + }), + ).toStrictEqual( + `{"3":"c","a":"a","b":2,"beef":"64206","d":{"data":[1,2],"type":"Buffer"},"e":{"0":-3,"1":-239},"ris":[1,null,"b","65263",null,null],"zed":{"0":"1","1":"4294967295"}}`, + ); + // @ts-expect-error intentional to reach code for coverage + expect(stringify(undefined)).toStrictEqual("null"); + + const circularObj: Record = {a: 1, b: undefined}; + circularObj.b = circularObj; + + expect(stringify(circularObj)).toStrictEqual(`{"a":1,"b":"[Circular]"}`); + + const toJSONIsString = {a: 1, toJSON: () => `{"a":1}`}; + + expect(stringify(toJSONIsString)).toStrictEqual(`"{\\"a\\":1}"`); + + const toJSONIsNull = {a: 1, toJSON: () => null}; + + expect(stringify(toJSONIsNull)).toStrictEqual("null"); + }); });