diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 75221de60..5b5762d01 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -78,7 +78,7 @@ abstract class Extension { protected state: State; protected publishEntityState: PublishEntityState; protected eventBus: EventBus; - + async start(): Promise {} async stop(): Promise {} } @@ -181,7 +181,7 @@ logger.debug("message"); - Use TypeScript's strict mode features (`noImplicitAny`, `noImplicitThis`) ### Performance -- Use `rimrafSync` for synchronous file deletion when appropriate +- 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` - Cache computed values when appropriate (see device model patterns) @@ -217,10 +217,10 @@ describe("ComponentName", () => { it("Should do something specific", async () => { // Arrange const input = {}; - + // Act const result = await someFunction(input); - + // Assert expect(result).toBe(expected); }); @@ -414,7 +414,7 @@ this.eventBus.on('deviceMessage', this.onDeviceMessage, this); - Device operations through `zigbee-herdsman` API - Event handling through EventBus wrappers -### MQTT Integration +### MQTT Integration - Connect: `await this.mqtt.connect()` - Subscribe: `await this.mqtt.subscribe(topic)` - Publish: `await this.mqtt.publish(topic, message, options)` diff --git a/AGENTS.md b/AGENTS.md index b0b515bfc..fd0e57110 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ Zigbee2MQTT is a Zigbee to MQTT bridge that allows you to use your Zigbee device - **Language**: TypeScript 5.9.3 compiled to JavaScript (ES modules with NodeNext resolution) - **Runtime**: Node.js (versions 20, 22, or 24) - **Package Manager**: pnpm 10.12.1 (strictly enforced via `packageManager` field) -- **Core Dependencies**: +- **Core Dependencies**: - `zigbee-herdsman` (6.2.0 - exact version, handles Zigbee adapter communication) - `zigbee-herdsman-converters` (25.42.0 - exact version, device definitions) - `mqtt` (5.14.1 - MQTT client) @@ -227,7 +227,7 @@ abstract class Extension { protected state: State; protected publishEntityState: PublishEntityState; protected eventBus: EventBus; - + async start(): Promise {} // Initialize extension async stop(): Promise {} // Cleanup extension } @@ -326,7 +326,7 @@ https://www.zigbee2mqtt.io/guide/installation/01_linux.html ### Performance Considerations -- Use `rimrafSync` for synchronous file operations +- Use `fs.rmSync(path, {recursive: true, force: true})` for synchronous file operations - Leverage async/await to avoid blocking - Cache computed values in getters when appropriate - EventBus provides loose coupling between components @@ -344,7 +344,7 @@ These dependencies use **exact versions** (no semver ranges) - do not upgrade wi Only these Node.js versions are supported: - Node.js 20.x -- Node.js 22.x +- Node.js 22.x - Node.js 24.x Using other versions may cause runtime errors or incompatibilities. diff --git a/lib/util/logger.ts b/lib/util/logger.ts index 6668b4166..9e91d12e0 100644 --- a/lib/util/logger.ts +++ b/lib/util/logger.ts @@ -2,7 +2,6 @@ import assert from "node:assert"; import fs from "node:fs"; import path from "node:path"; -import {rimrafSync} from "rimraf"; import winston from "winston"; import * as settings from "./settings"; @@ -235,7 +234,7 @@ class Logger { for (const dir of directories) { this.debug(`Removing old log directory '${dir.path}'`); try { - rimrafSync(dir.path); + fs.rmSync(dir.path, {recursive: true, force: true}); } catch (e) { this.error(`Failed to remove old log directory '${dir.path}': ${e}`); } diff --git a/package.json b/package.json index 2047d6f04..a2420a61e 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "test:watch": "vitest watch --config ./test/vitest.config.mts", "bench": "vitest bench --run --config ./test/vitest.config.mts", "prepack": "pnpm run clean && pnpm run build", - "clean": "rimraf coverage dist tsconfig.tsbuildinfo" + "clean": "node -e \"for (const p of ['coverage', 'dist', 'tsconfig.tsbuildinfo']) require('node:fs').rmSync(p, {recursive: true, force: true, maxRetries: process.platform === 'win32' ? 10 : 0})\"" }, "author": "Koen Kanters", "license": "GPL-3.0", @@ -54,7 +54,6 @@ "jszip": "^3.10.1", "mqtt": "^5.15.2", "object-assign-deep": "^0.4.0", - "rimraf": "^6.1.3", "semver": "^7.8.5", "source-map-support": "^0.5.21", "throttleit": "^3.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 83c63af51..bb2d095ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,9 +47,6 @@ importers: object-assign-deep: specifier: ^0.4.0 version: 0.4.0 - rimraf: - specifier: ^6.1.3 - version: 6.1.3 semver: specifier: ^7.8.5 version: 7.8.5 @@ -376,10 +373,6 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@isaacs/cliui@9.0.0': - resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} - engines: {node: '>=18'} - '@istanbuljs/schema@0.1.3': resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} @@ -768,10 +761,6 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - balanced-match@4.0.2: - resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==} - engines: {node: 20 || >=22} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -790,10 +779,6 @@ packages: brace-expansion@2.1.0: resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} - brace-expansion@5.0.6: - resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} - engines: {node: 18 || 20 || >=22} - broker-factory@3.1.15: resolution: {integrity: sha512-ko+aWvgNuP49meGrdjUu7rC+Y+Wai3cCPxP3xWwHsHfehFjOh5ZQM2yC4gEB2UddeZ/YXhm0K1eG/L6fxym2Og==} @@ -987,10 +972,6 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true - glob@13.0.3: - resolution: {integrity: sha512-/g3B0mC+4x724v1TgtBlBtt2hPi/EWptsIAmXUx9Z2rvBYleQcsrmaOzd5LyL50jf/Soi83ZDJmw2+XqvH/EeA==} - engines: {node: 20 || >=22} - glossy@0.1.7: resolution: {integrity: sha512-mTCC51QFadK75MvAhrL5nPVIP291NjML1guo10Sa7Yj04tJU4V++Vgm780NIddg9etQD9D8FM67hFGqM8EE2HQ==} engines: {node: '>= 0.2.5'} @@ -1062,10 +1043,6 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jackspeak@4.2.3: - resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} - engines: {node: 20 || >=22} - js-sdsl@4.3.0: resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==} @@ -1101,10 +1078,6 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.5.1: - resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} - engines: {node: 20 || >=22} - magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -1123,10 +1096,6 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} - minimatch@10.2.0: - resolution: {integrity: sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==} - engines: {node: 20 || >=22} - minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -1201,10 +1170,6 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-scurry@2.0.1: - resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} - engines: {node: 20 || >=22} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1252,11 +1217,6 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rimraf@6.1.3: - resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} - engines: {node: 20 || >=22} - hasBin: true - rollup@4.44.0: resolution: {integrity: sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -1751,8 +1711,6 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@isaacs/cliui@9.0.0': {} - '@istanbuljs/schema@0.1.3': {} '@jridgewell/gen-mapping@0.3.8': @@ -2062,10 +2020,6 @@ snapshots: balanced-match@1.0.2: {} - balanced-match@4.0.2: - dependencies: - jackspeak: 4.2.3 - base64-js@1.5.1: {} bind-decorator@1.0.11: {} @@ -2091,10 +2045,6 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.6: - dependencies: - balanced-match: 4.0.2 - broker-factory@3.1.15: dependencies: '@babel/runtime': 7.29.7 @@ -2291,12 +2241,6 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - glob@13.0.3: - dependencies: - minimatch: 10.2.0 - minipass: 7.1.2 - path-scurry: 2.0.1 - glossy@0.1.7: {} has-flag@4.0.0: {} @@ -2362,10 +2306,6 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jackspeak@4.2.3: - dependencies: - '@isaacs/cliui': 9.0.0 - js-sdsl@4.3.0: {} js-tokens@9.0.1: {} @@ -2404,8 +2344,6 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.5.1: {} - magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -2426,10 +2364,6 @@ snapshots: dependencies: mime-db: 1.54.0 - minimatch@10.2.0: - dependencies: - brace-expansion: 5.0.6 - minimatch@9.0.5: dependencies: brace-expansion: 2.1.0 @@ -2515,11 +2449,6 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-scurry@2.0.1: - dependencies: - lru-cache: 11.5.1 - minipass: 7.1.2 - pathe@2.0.3: {} pathval@2.0.0: {} @@ -2568,11 +2497,6 @@ snapshots: rfdc@1.4.1: {} - rimraf@6.1.3: - dependencies: - glob: 13.0.3 - package-json-from-dist: 1.0.1 - rollup@4.44.0: dependencies: '@types/estree': 1.0.8 diff --git a/test/logger.test.ts b/test/logger.test.ts index 297e991c5..09dfda433 100644 --- a/test/logger.test.ts +++ b/test/logger.test.ts @@ -5,20 +5,13 @@ import * as data from "./mocks/data"; import fs from "node:fs"; import {platform} from "node:os"; import path from "node:path"; -import {rimrafSync} from "rimraf"; import tmp from "tmp"; import type {MockInstance} from "vitest"; import Transport from "winston-transport"; import logger from "../lib/util/logger"; import * as settings from "../lib/util/settings"; -vi.mock("rimraf", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - rimrafSync: vi.fn(actual.rimrafSync), - }; -}); +const rmSync = (target: string): void => fs.rmSync(target, {recursive: true, force: true}); describe("Logger", () => { let consoleWriteSpy: MockInstance; @@ -56,7 +49,7 @@ describe("Logger", () => { it("Should cleanup (default setting)", () => { for (const d of fs.readdirSync(dir.name)) { - rimrafSync(path.join(dir.name, d)); + rmSync(path.join(dir.name, d)); } for (let i = 0; i < 20; i++) { @@ -70,25 +63,30 @@ describe("Logger", () => { it("Should handle cleanup error", () => { for (const d of fs.readdirSync(dir.name)) { - rimrafSync(path.join(dir.name, d)); + rmSync(path.join(dir.name, d)); } for (let i = 0; i < 20; i++) { fs.mkdirSync(path.join(dir.name, `log_${i}`)); } - vi.mocked(rimrafSync).mockImplementationOnce(() => { + const rmSyncSpy = vi.spyOn(fs, "rmSync").mockImplementationOnce(() => { throw new Error("EACCES: permission denied"); }); const errorSpy = vi.spyOn(logger, "error"); - logger.init(); - expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/Failed to remove old log directory '.*': Error: EACCES: permission denied/)); + + try { + logger.init(); + expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/Failed to remove old log directory '.*': Error: EACCES: permission denied/)); + } finally { + rmSyncSpy.mockRestore(); + } }); it("Should cleanup (15 folders setting)", () => { for (const d of fs.readdirSync(dir.name)) { - rimrafSync(path.join(dir.name, d)); + rmSync(path.join(dir.name, d)); } for (let i = 0; i < 20; i++) { @@ -103,7 +101,7 @@ describe("Logger", () => { it("Should not cleanup when there is no timestamp set", () => { for (const d of fs.readdirSync(dir.name)) { - rimrafSync(path.join(dir.name, d)); + rmSync(path.join(dir.name, d)); } for (let i = 30; i < 50; i++) {