From c6d1cb96c398d22f995c726fefc4801ea498a0d5 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Sun, 10 Nov 2024 09:43:21 -0800 Subject: [PATCH] set up eslint and address some linting errors. add unix-mount test. --- eslint.config.mjs | 9 ++ package-lock.json | 151 ++++++++------------------- package.json | 13 +-- src/DeepFreeze.ts | 37 +++++-- src/__tests__/async-behavior.test.ts | 3 +- src/__tests__/unix-mount.test.ts | 122 ++++++++++++++++++++++ src/__tests__/unix.test.ts | 1 + src/__tests__/windows.test.ts | 5 +- src/index.ts | 3 +- src/linux/mtab.ts | 2 +- src/test-utils/assert.ts | 4 - 11 files changed, 217 insertions(+), 133 deletions(-) create mode 100644 eslint.config.mjs create mode 100644 src/__tests__/unix-mount.test.ts diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..7310d79 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,9 @@ +import ts_eslint from "typescript-eslint"; + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + { + files: ["src/**/*.ts}"], + }, + ...ts_eslint.configs.recommended, +]; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 574229f..b457aca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,20 +23,21 @@ "node-addon-api": "^8.2.2" }, "devDependencies": { + "@eslint/js": "^9.14.0", "@types/jest": "^29.5.14", "@types/node": "^22.9.0", "@typescript-eslint/eslint-plugin": "^8.13.0", "@typescript-eslint/parser": "^8.13.0", "eslint": "^9.14.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.2.1", + "globals": "^15.12.0", "jest": "^29.7.0", "node-gyp": "^10.2.0", "prettier": "^3.3.3", "prettier-plugin-organize-imports": "4.1.0", "ts-jest": "^29.2.5", "typedoc": "^0.26.11", - "typescript": "^5.6.3" + "typescript": "^5.6.3", + "typescript-eslint": "^8.13.0" } }, "node_modules/@ampproject/remapping": { @@ -682,6 +683,19 @@ "concat-map": "0.0.1" } }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1452,19 +1466,6 @@ "node": ">=14" } }, - "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, "node_modules/@shikijs/core": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.22.2.tgz", @@ -2878,50 +2879,6 @@ } } }, - "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", - "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": "*", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, "node_modules/eslint-scope": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", @@ -3144,13 +3101,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -3486,9 +3436,9 @@ } }, "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "15.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz", + "integrity": "sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==", "dev": true, "license": "MIT", "engines": { @@ -5695,19 +5645,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/prettier-plugin-organize-imports": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz", @@ -6335,23 +6272,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/synckit": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", - "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -6552,13 +6472,6 @@ } } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6632,6 +6545,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.13.0.tgz", + "integrity": "sha512-vIMpDRJrQd70au2G8w34mPps0ezFSPMEX4pXkTzUkrNbRX+36ais2ksGWN0esZL+ZMaFJEneOBHzCgSqle7DHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.13.0", + "@typescript-eslint/parser": "8.13.0", + "@typescript-eslint/utils": "8.13.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", diff --git a/package.json b/package.json index 39f45f3..54cba42 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "test:coverage": "jest --coverage", "test:clear": "jest --clearCache", "test:debug": "node --inspect-brk jest --runInBand", - "lint": "eslint . --ext .ts", - "lint:fix": "eslint . --ext .ts --fix", + "lint": "eslint", + "lint:fix": "eslint --fix", "fmt": "prettier --write \"src/**/*.ts\" && npm run fmt:cpp", "fmt:cpp": "clang-format --style=LLVM -i src/*/*.cpp src/*/*.h" }, @@ -36,19 +36,20 @@ "node-addon-api": "^8.2.2" }, "devDependencies": { + "@eslint/js": "^9.14.0", "@types/jest": "^29.5.14", "@types/node": "^22.9.0", "@typescript-eslint/eslint-plugin": "^8.13.0", "@typescript-eslint/parser": "^8.13.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.2.1", "eslint": "^9.14.0", + "globals": "^15.12.0", "jest": "^29.7.0", "node-gyp": "^10.2.0", - "prettier-plugin-organize-imports": "4.1.0", "prettier": "^3.3.3", + "prettier-plugin-organize-imports": "4.1.0", "ts-jest": "^29.2.5", "typedoc": "^0.26.11", - "typescript": "^5.6.3" + "typescript": "^5.6.3", + "typescript-eslint": "^8.13.0" } } diff --git a/src/DeepFreeze.ts b/src/DeepFreeze.ts index 3f6ed5f..0457faa 100644 --- a/src/DeepFreeze.ts +++ b/src/DeepFreeze.ts @@ -2,27 +2,46 @@ export type DeepReadonly = T extends (infer R)[] ? ReadonlyArray> + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type : T extends Function - ? T + ? T : T extends object ? { readonly [P in keyof T]: DeepReadonly } : T; export type OrDeepReadonly = T | DeepReadonly; +/** + * Type guard to check if a value is an object (excluding null and arrays) + */ +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + /** * Recursively freezes an object and all of its properties */ export function deepFreeze(obj: T): DeepReadonly { - if (obj == null || typeof obj !== "object" || Object.isFrozen(obj)) { - return obj as any; + // Handle primitive types and frozen objects + if (obj === null || typeof obj !== 'object' || Object.isFrozen(obj)) { + return obj as DeepReadonly; } + if (Array.isArray(obj)) { - return Object.freeze(obj.map(deepFreeze)) as any; + const frozenArray = Object.freeze(obj.map(deepFreeze)) as ReadonlyArray>; + return frozenArray as DeepReadonly; } - const result = {} as any; - for (const [key, value] of Object.entries(obj)) { - result[key] = deepFreeze(value); + + if (isObject(obj)) { + const result = {} as T; + + Object.entries(obj as Record).forEach(([key, value]) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (result as any)[key] = deepFreeze(value); + }); + + return Object.freeze(result) as DeepReadonly; } - return Object.freeze(result); -} + + return obj as DeepReadonly; +} \ No newline at end of file diff --git a/src/__tests__/async-behavior.test.ts b/src/__tests__/async-behavior.test.ts index 45422a2..8305352 100644 --- a/src/__tests__/async-behavior.test.ts +++ b/src/__tests__/async-behavior.test.ts @@ -5,10 +5,9 @@ import { describePlatform } from "../test-utils/platform"; describe("Filesystem API Async Behavior", () => { const describeLinux = describePlatform("linux"); - const describeWindows = describePlatform("win32"); // Helper to measure execution time - const timeExecution = async (fn: () => Promise): Promise => { + const timeExecution = async (fn: () => Promise): Promise => { const start = process.hrtime(); await fn(); const [seconds, nanoseconds] = process.hrtime(start); diff --git a/src/__tests__/unix-mount.test.ts b/src/__tests__/unix-mount.test.ts new file mode 100644 index 0000000..7e46336 --- /dev/null +++ b/src/__tests__/unix-mount.test.ts @@ -0,0 +1,122 @@ +// src/__tests__/unix-mount.test.ts + +import { getVolumeMountPoints } from "../index"; +import { describePlatform } from "../test-utils/platform"; +import { parseMount } from "../unix/mount"; + +const describeUnix = (name: string, fn: () => void) => { + const isUnix = process.platform === "linux" || process.platform === "darwin"; + return isUnix ? describe(name, fn) : describe.skip(name, fn); +}; + +const describeLinux = describePlatform("linux"); +const describeDarwin = describePlatform("darwin"); + +describeUnix("Unix Mount Points Parser", () => { + describe("parseMount()", () => { + it("should return an array of mount points", async () => { + const mountPoints = await parseMount(); + expect(Array.isArray(mountPoints)).toBe(true); + expect(mountPoints.length).toBeGreaterThan(0); + }); + + it("should have valid mount point objects", async () => { + const mountPoints = await parseMount(); + + for (const mount of mountPoints) { + expect(mount).toMatchObject({ + mountPoint: expect.any(String), + device: expect.any(String), + type: expect.any(String), + options: expect.any(Array), + }); + + // Mount point should be an absolute path + expect(mount.mountPoint.startsWith("/")).toBe(true); + + // Options should be non-empty array of strings + expect(Array.isArray(mount.options)).toBe(true); + mount.options.forEach((option) => { + expect(typeof option).toBe("string"); + expect(option.length).toBeGreaterThan(0); + }); + } + }); + + it("should match getVolumeMountPoints results", async () => { + const mountPoints = await parseMount(); + const volumeMountPoints = await getVolumeMountPoints(); + + // All volume mount points should exist in parseMount results + for (const volumePoint of volumeMountPoints) { + const found = mountPoints.some( + (mount) => mount.mountPoint === volumePoint, + ); + expect(found).toBe(true); + } + }); + + describeLinux("Linux-specific tests", () => { + it("should have expected Linux filesystem types", async () => { + const mountPoints = await parseMount(); + const expectedTypes = ["ext4", "ext3", "xfs", "btrfs", "zfs"]; + + // Root filesystem should be one of the expected types + const rootMount = mountPoints.find((mount) => mount.mountPoint === "/"); + expect(rootMount).toBeDefined(); + expect(expectedTypes).toContain(rootMount?.type); + }); + + it("should handle Linux-style mount options", async () => { + const mountPoints = await parseMount(); + const commonOptions = ["rw", "relatime", "defaults"]; + + mountPoints.forEach((mount) => { + // At least one common option should be present + expect(mount.options.some((opt) => commonOptions.includes(opt))).toBe( + true, + ); + }); + }); + }); + + describeDarwin("macOS-specific tests", () => { + it("should have expected macOS filesystem types", async () => { + const mountPoints = await parseMount(); + const expectedTypes = ["apfs", "hfs", "autofs", "devfs"]; + + // Root filesystem should be one of the expected types + const rootMount = mountPoints.find((mount) => mount.mountPoint === "/"); + expect(rootMount).toBeDefined(); + expect(expectedTypes).toContain(rootMount?.type); + }); + + it("should handle macOS-style mount options", async () => { + const mountPoints = await parseMount(); + console.log({ mountPoints }); + const commonOptions = [ + "read-only", + "local", + "nodev", + "nosuid", + "sealed", + "journaled", + "nobrowse", + "automounted", + "hidden", + "shadow", + "protect", + ]; + mountPoints.forEach((mount) => { + // Options should be properly parsed for macOS format + expect(Array.isArray(mount.options)).toBe(true); + if (mount.options.length > 0) { + expect( + mount.options.some((opt) => commonOptions.includes(opt)), + ).toBe(true); + } + }); + }); + }); + }); +}); diff --git a/src/__tests__/unix.test.ts b/src/__tests__/unix.test.ts index a95719c..f519f6c 100644 --- a/src/__tests__/unix.test.ts +++ b/src/__tests__/unix.test.ts @@ -120,6 +120,7 @@ describeUnix("Unix (Linux/macOS) File system metadata", () => { ]; for (const path of invalidPaths) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any await expect(getVolumeMetadata(path as any)).rejects.toThrow(); } }); diff --git a/src/__tests__/windows.test.ts b/src/__tests__/windows.test.ts index b369691..d81927f 100644 --- a/src/__tests__/windows.test.ts +++ b/src/__tests__/windows.test.ts @@ -65,12 +65,11 @@ describe("Filesystem Metadata", () => { }); it("should handle empty or null mountPoint", async () => { - // @ts-ignore - Testing invalid input await expect(getVolumeMetadata("")).rejects.toThrow( /mountPoint is required/i, ); - // @ts-ignore - Testing invalid input - await expect(getVolumeMetadata(null)).rejects.toThrow( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await expect(getVolumeMetadata(null as any)).rejects.toThrow( /mountpoint is required/i, ); }); diff --git a/src/index.ts b/src/index.ts index 45f4b99..e2986cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import { getLinuxMountPoints, TypedMountPoint } from "./linux/mtab"; export { DefaultConfig, getConfig, setConfig } from "./Config"; export type { Config } from "./Config"; +// eslint-disable-next-line @typescript-eslint/no-require-imports const native = require("bindings")("node_fs_meta"); /** @@ -137,7 +138,7 @@ export async function getVolumeMetadata( try { await stat(mountPoint); } catch (e) { - throw new Error(`mountPoint ${mountPoint} is not accessible`); + throw new Error(`mountPoint ${mountPoint} is not accessible: ${e}`); } const result = await native.getVolumeMetadata(mountPoint); diff --git a/src/linux/mtab.ts b/src/linux/mtab.ts index 88cc7a0..48912c9 100644 --- a/src/linux/mtab.ts +++ b/src/linux/mtab.ts @@ -20,7 +20,7 @@ export async function getLinuxMountPoints( const mtabContent = await readFile(input, "utf8"); const result: { mountPoint: string; fstype: string }[] = []; - for (let ea of mtabContent.split("\n")) { + for (const ea of mtabContent.split("\n")) { const line = ea.trim(); if (line.length === 0 || line.startsWith("#")) continue; const [fstype, mp] = line.split(/\s+/); diff --git a/src/test-utils/assert.ts b/src/test-utils/assert.ts index 8c7e37b..e946bd0 100644 --- a/src/test-utils/assert.ts +++ b/src/test-utils/assert.ts @@ -1,5 +1,4 @@ // src/test-utils/assert.ts -import { statSync } from "node:fs"; import { VolumeMetadata } from "../index"; /** @@ -20,9 +19,6 @@ export function assertMetadata(metadata: VolumeMetadata | undefined) { expect(typeof metadata.fileSystem).toBe("string"); } - // Validate device ID - const s = statSync(metadata.mountPoint); - // Size checks expect(metadata.size).toBeGreaterThan(0); expect(metadata.used).toBeGreaterThanOrEqual(0);