From 24bbef833bfe068f0cac8374638027b6740a2d00 Mon Sep 17 00:00:00 2001 From: Emanuele Fenocchi Date: Wed, 17 Jun 2026 21:41:49 +0000 Subject: [PATCH 1/2] fix: decouple typecheck from optional transformers dependency --- embeddings/embed-daemon.js | 3 ++- src/embeddings/nomic.ts | 25 +++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/embeddings/embed-daemon.js b/embeddings/embed-daemon.js index a491a879..f2d0650a 100755 --- a/embeddings/embed-daemon.js +++ b/embeddings/embed-daemon.js @@ -34,7 +34,8 @@ async function _importFromCanonicalSharedDeps(sharedDir = join(homedir(), ".hive return _normalizeTransformersModule(mod); } async function _importFromBareSpecifier() { - const mod = await import("@huggingface/transformers"); + const spec = "@huggingface/transformers"; + const mod = await import(spec); return _normalizeTransformersModule(mod); } function _normalizeTransformersModule(mod) { diff --git a/src/embeddings/nomic.ts b/src/embeddings/nomic.ts index 9dcc7b20..a6d3785d 100644 --- a/src/embeddings/nomic.ts +++ b/src/embeddings/nomic.ts @@ -18,7 +18,24 @@ import { type Embedder = (input: string | string[], opts: Record) => Promise<{ data: Float32Array | number[] }>; -type TransformersModule = typeof import("@huggingface/transformers"); +// Minimal shape of @huggingface/transformers that this wrapper actually uses. +// Declared locally instead of `typeof import("@huggingface/transformers")` so +// the typecheck does NOT resolve the package at compile time: it's an +// optional, on-demand dependency (installed by `hivemind embeddings install`) +// and is absent on some platforms (e.g. Windows CI), where a `typeof import` +// query would make `tsc` fail with TS2307. +interface TransformersModule { + env: { + allowLocalModels: boolean; + useFSCache: boolean; + [key: string]: unknown; + }; + pipeline: ( + task: string, + model: string, + options?: Record, + ) => Promise; +} type TransformersImporter = () => Promise; export interface NomicOptions { @@ -51,7 +68,11 @@ export async function _importFromCanonicalSharedDeps( } export async function _importFromBareSpecifier(): Promise { - const mod = await import("@huggingface/transformers"); + // Non-literal specifier (typed as `string`) so `tsc` treats this as a + // dynamic import of `any` and does not resolve the optional package at + // compile time — see the TransformersModule comment above. + const spec: string = "@huggingface/transformers"; + const mod = await import(spec); return _normalizeTransformersModule(mod); } From 736be92eab4d96da83f97b347da9169b0a819c9c Mon Sep 17 00:00:00 2001 From: Emanuele Fenocchi Date: Wed, 17 Jun 2026 21:41:49 +0000 Subject: [PATCH 2/2] test: use non-literal specifier for optional transformers import --- tests/claude-code/embeddings-nomic.test.ts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/claude-code/embeddings-nomic.test.ts b/tests/claude-code/embeddings-nomic.test.ts index 8a16cda1..cc3e3279 100644 --- a/tests/claude-code/embeddings-nomic.test.ts +++ b/tests/claude-code/embeddings-nomic.test.ts @@ -12,6 +12,13 @@ import { _importFromCanonicalSharedDeps, } from "../../src/embeddings/nomic.js"; +// Non-literal specifier (typed `string`) so `tsc` does not resolve the optional +// @huggingface/transformers package at compile time — it's absent on some +// platforms (e.g. Windows CI). The `vi.mock(...)` below still intercepts it at +// runtime by resolved module id, so the dynamic `import(TRANSFORMERS_PKG)` +// calls return the mock. +const TRANSFORMERS_PKG: string = "@huggingface/transformers"; + // Mock the heavy transformers import so these tests don't pull in // onnxruntime-node or download any model weights. `load()` resolves // transformers via an injected importer (default goes through the canonical @@ -41,7 +48,7 @@ vi.mock("@huggingface/transformers", () => { beforeEach(() => { // Route the embedder's loader through the vi.mock-intercepted bare specifier // instead of the real canonical-shared-deps resolver. - _setTransformersImporterForTesting(() => import("@huggingface/transformers") as any); + _setTransformersImporterForTesting(() => import(TRANSFORMERS_PKG) as any); }); afterEach(() => { @@ -55,7 +62,7 @@ describe("NomicEmbedder", () => { await e.load(); // second call is a no-op (cached) // If load() didn't memoize, pipeline() would be invoked twice; the // mock would return a fresh spy whose call counts would differ. - const mod: any = await import("@huggingface/transformers"); + const mod: any = await import(TRANSFORMERS_PKG); expect((mod.pipeline as any).mock.calls.length).toBe(1); }); @@ -63,7 +70,7 @@ describe("NomicEmbedder", () => { const e = new NomicEmbedder({ dims: 4 }); const v = await e.embed("hello", "document"); expect(v).toHaveLength(4); - const mod: any = await import("@huggingface/transformers"); + const mod: any = await import(TRANSFORMERS_PKG); const pipeline = await (mod.pipeline as any).mock.results[0].value; const callArg = (pipeline as any).mock.calls.at(-1)[0]; expect(callArg).toBe("search_document: hello"); @@ -72,7 +79,7 @@ describe("NomicEmbedder", () => { it("embeds a query with the search_query: prefix", async () => { const e = new NomicEmbedder({ dims: 4 }); await e.embed("q", "query"); - const mod: any = await import("@huggingface/transformers"); + const mod: any = await import(TRANSFORMERS_PKG); const pipeline = await (mod.pipeline as any).mock.results[0].value; const callArg = (pipeline as any).mock.calls.at(-1)[0]; expect(callArg).toBe("search_query: q"); @@ -113,7 +120,7 @@ describe("NomicEmbedder", () => { it("handles a zero-norm truncation without dividing by zero", async () => { // Reach through the private helper via a custom mock that returns zeros. - const mod: any = await import("@huggingface/transformers"); + const mod: any = await import(TRANSFORMERS_PKG); const origPipeline = mod.pipeline; const wrapped = vi.fn(() => Promise.resolve(() => Promise.resolve({ data: [0, 0, 0, 0] }))); (mod as any).pipeline = wrapped; @@ -145,7 +152,7 @@ describe("NomicEmbedder", () => { it("coalesces concurrent load() calls onto a single pipeline build", async () => { // Replace pipeline with a slow one so the two load() calls overlap and // the second enters the `if (this.loading) return this.loading;` branch. - const mod: any = await import("@huggingface/transformers"); + const mod: any = await import(TRANSFORMERS_PKG); const orig = mod.pipeline; let calls = 0; mod.pipeline = vi.fn(async () => { @@ -168,7 +175,7 @@ describe("NomicEmbedder", () => { it("embeds a query in embedBatch with the search_query prefix", async () => { const e = new NomicEmbedder({ dims: 4 }); await e.embedBatch(["hi"], "query"); - const mod: any = await import("@huggingface/transformers"); + const mod: any = await import(TRANSFORMERS_PKG); const pipeline = await (mod.pipeline as any).mock.results[0].value; const lastCall = (pipeline as any).mock.calls.at(-1)[0]; expect(lastCall).toEqual(["search_query: hi"]);