diff --git a/src/config/env-preserve.ts b/src/config/env-preserve.ts index a882357b9e..0259781eee 100644 --- a/src/config/env-preserve.ts +++ b/src/config/env-preserve.ts @@ -1,3 +1,5 @@ +import { isPlainObject } from "../infra/plain-object.js"; + /** * Preserves `${VAR}` environment variable references during config write-back. * @@ -16,15 +18,6 @@ const ENV_VAR_PATTERN = /\$\{[A-Z_][A-Z0-9_]*\}/; -function isPlainObject(value: unknown): value is Record { - return ( - typeof value === "object" && - value !== null && - !Array.isArray(value) && - Object.prototype.toString.call(value) === "[object Object]" - ); -} - /** * Check if a string contains any `${VAR}` env var references. */ diff --git a/src/infra/plain-object.test.ts b/src/infra/plain-object.test.ts new file mode 100644 index 0000000000..b87e555b21 --- /dev/null +++ b/src/infra/plain-object.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { isPlainObject } from "./plain-object.js"; + +describe("isPlainObject", () => { + it("accepts plain objects", () => { + expect(isPlainObject({})).toBe(true); + expect(isPlainObject({ a: 1 })).toBe(true); + }); + + it("rejects non-plain values", () => { + expect(isPlainObject(null)).toBe(false); + expect(isPlainObject([])).toBe(false); + expect(isPlainObject(new Date())).toBe(false); + expect(isPlainObject(/re/)).toBe(false); + expect(isPlainObject("x")).toBe(false); + expect(isPlainObject(42)).toBe(false); + }); +}); diff --git a/src/infra/plain-object.ts b/src/infra/plain-object.ts new file mode 100644 index 0000000000..2b611ca33c --- /dev/null +++ b/src/infra/plain-object.ts @@ -0,0 +1,11 @@ +/** + * Strict plain-object guard (excludes arrays and host objects). + */ +export function isPlainObject(value: unknown): value is Record { + return ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + Object.prototype.toString.call(value) === "[object Object]" + ); +} diff --git a/src/utils.ts b/src/utils.ts index 66ed063cfa..49e14e0d04 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,6 +8,7 @@ import { resolveEffectiveHomeDir, resolveRequiredHomeDir, } from "./infra/home-dir.js"; +import { isPlainObject } from "./infra/plain-object.js"; export async function ensureDir(dir: string) { await fs.promises.mkdir(dir, { recursive: true }); @@ -54,18 +55,7 @@ export function safeParseJson(raw: string): T | null { } } -/** - * Type guard for plain objects (not arrays, null, Date, RegExp, etc.). - * Uses Object.prototype.toString for maximum safety. - */ -export function isPlainObject(value: unknown): value is Record { - return ( - typeof value === "object" && - value !== null && - !Array.isArray(value) && - Object.prototype.toString.call(value) === "[object Object]" - ); -} +export { isPlainObject }; /** * Type guard for Record (less strict than isPlainObject).