From 3125bcf400f098fcac07faa94e9314a3327b6d64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zachary=20Gu=C3=A9not?= Date: Wed, 9 Sep 2026 19:51:27 +0200 Subject: [PATCH] fix(integrations): keep unifi-os-types client-safe Co-Authored-By: Claude Opus 5 --- .../src/unifi-os/test/unifi-os-types.spec.ts | 19 ++++++++++++++++++ .../integrations/src/unifi-os/unifi-os-api.ts | 2 +- .../src/unifi-os/unifi-os-parse.ts | 20 +++++++++++++++++++ .../src/unifi-os/unifi-os-types.ts | 13 ------------ .../integrations/src/unifi-os/unifi-sm-api.ts | 8 ++------ 5 files changed, 42 insertions(+), 20 deletions(-) create mode 100644 packages/integrations/src/unifi-os/unifi-os-parse.ts diff --git a/packages/integrations/src/unifi-os/test/unifi-os-types.spec.ts b/packages/integrations/src/unifi-os/test/unifi-os-types.spec.ts index 1ed03e845..eddeb0574 100644 --- a/packages/integrations/src/unifi-os/test/unifi-os-types.spec.ts +++ b/packages/integrations/src/unifi-os/test/unifi-os-types.spec.ts @@ -1,4 +1,7 @@ // @vitest-environment node +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + import { describe, expect, test, vi } from "vitest"; vi.hoisted(() => { @@ -147,6 +150,22 @@ describe("matchCloudSite", () => { }); }); +describe("client-safety", () => { + // `unifi-os-types.ts` is re-exported by `packages/integrations/src/types.ts`, + // which client components import (e.g. via the spotlight search group). An + // import of `@homarr/common/server` here drags `ioredis` — and transitively + // Node's `dns` module — into the browser bundle and breaks `pnpm dev`/build. + // Only a source-text check catches this: a type-only "does it compile" check + // would not, since the breakage is a bundler resolution failure, not a type + // error. This regression guard is what would have caught the original bug. + test("does not import @homarr/common/server", () => { + const path = fileURLToPath(new URL("../unifi-os-types.ts", import.meta.url)); + const source = readFileSync(path, "utf-8"); + + expect(source).not.toContain("@homarr/common/server"); + }); +}); + describe("unifiOsClientSchema", () => { test("keeps fields the published OpenAPI does not document", () => { const parsed = unifiOsClientSchema.parse({ diff --git a/packages/integrations/src/unifi-os/unifi-os-api.ts b/packages/integrations/src/unifi-os/unifi-os-api.ts index 6d784dc95..23fdb1499 100644 --- a/packages/integrations/src/unifi-os/unifi-os-api.ts +++ b/packages/integrations/src/unifi-os/unifi-os-api.ts @@ -2,11 +2,11 @@ import { ResponseError } from "@homarr/common/server"; import type { RequestInit, Response } from "undici"; import type { z } from "zod/v4"; +import { parseUnifiOsAsync } from "./unifi-os-parse"; import type { UnifiOsClientCounts, UnifiOsDevice, UnifiOsDeviceStatistics, UnifiOsSite } from "./unifi-os-types"; import { countClients, createUnifiOsPageSchema, - parseUnifiOsAsync, unifiOsClientSchema, unifiOsDeviceSchema, unifiOsDeviceStatisticsSchema, diff --git a/packages/integrations/src/unifi-os/unifi-os-parse.ts b/packages/integrations/src/unifi-os/unifi-os-parse.ts new file mode 100644 index 000000000..6a23f14d3 --- /dev/null +++ b/packages/integrations/src/unifi-os/unifi-os-parse.ts @@ -0,0 +1,20 @@ +import { ParseError } from "@homarr/common/server"; +import type { z } from "zod/v4"; + +/** + * Server-only: pulled out of `unifi-os-types.ts` because `ParseError` comes + * from `@homarr/common/server`, which drags in `ioredis` and Node's `dns`. + * `unifi-os-types.ts` is re-exported by `packages/integrations/src/types.ts`, + * which client components import, so it must stay free of server-only code. + */ +export const parseUnifiOsAsync = async ( + value: unknown, + schema: z.ZodType, + message: string, +): Promise => { + const result = await schema.safeParseAsync(value); + if (!result.success) { + throw new ParseError(message, { cause: result.error }); + } + return result.data; +}; diff --git a/packages/integrations/src/unifi-os/unifi-os-types.ts b/packages/integrations/src/unifi-os/unifi-os-types.ts index a3d5af7d4..8480464bd 100644 --- a/packages/integrations/src/unifi-os/unifi-os-types.ts +++ b/packages/integrations/src/unifi-os/unifi-os-types.ts @@ -1,4 +1,3 @@ -import { ParseError } from "@homarr/common/server"; import { z } from "zod/v4"; /** @@ -153,18 +152,6 @@ export interface UnifiOsSummary { gateway: UnifiOsGateway | null; } -export const parseUnifiOsAsync = async ( - value: unknown, - schema: z.ZodType, - message: string, -): Promise => { - const result = await schema.safeParseAsync(value); - if (!result.success) { - throw new ParseError(message, { cause: result.error }); - } - return result.data; -}; - /** * L'API locale renvoie `aa:bb:cc:11:22:33`, le cloud peut renvoyer une autre * casse ou d'autres séparateurs. La comparaison se fait sur une forme canonique. diff --git a/packages/integrations/src/unifi-os/unifi-sm-api.ts b/packages/integrations/src/unifi-os/unifi-sm-api.ts index 62fafb544..aa7443d56 100644 --- a/packages/integrations/src/unifi-os/unifi-sm-api.ts +++ b/packages/integrations/src/unifi-os/unifi-sm-api.ts @@ -1,13 +1,9 @@ import { createLogger } from "@homarr/core/infrastructure/logs"; import type { UnifiOsFetch } from "./unifi-os-api"; +import { parseUnifiOsAsync } from "./unifi-os-parse"; import type { UnifiOsIspMetrics } from "./unifi-os-types"; -import { - matchCloudSite, - parseUnifiOsAsync, - unifiSmIspMetricsResponseSchema, - unifiSmSiteSchema, -} from "./unifi-os-types"; +import { matchCloudSite, unifiSmIspMetricsResponseSchema, unifiSmSiteSchema } from "./unifi-os-types"; import { z } from "zod/v4"; const logger = createLogger({ module: "unifiSmApi" });