From 5d01fe884ee222597fcec5087019d814189b6596 Mon Sep 17 00:00:00 2001 From: Gregor Adams <1148334+pixelass@users.noreply.github.com> Date: Wed, 1 May 2024 18:44:51 +0200 Subject: [PATCH] refactor: cleanup types (#255) ## Motivation removes all instances of lazy typing (`any`) --- jest.setup.client.ts | 13 +- package-lock.json | 120 ++++++++++++++++++ src/client/apps/story/components.tsx | 27 ++-- src/client/atoms/icons/dynamic.tsx | 12 +- .../ions/handlers/__tests__/action.test.ts | 6 +- src/electron/handlers.ts | 4 +- ...g-face-transformers-embeddings.test.e2e.ts | 14 +- .../__tests__/download-manager.test.ts | 15 ++- .../services/download-manager/index.ts | 76 ++++++----- src/electron/helpers/services/vector-store.ts | 43 +++++-- src/electron/helpers/stores/utils.ts | 4 +- src/electron/helpers/stores/watchers.ts | 24 ++-- .../helpers/utils/__tests__/git.test.ts | 11 +- src/electron/helpers/utils/git.ts | 35 ++--- 14 files changed, 300 insertions(+), 104 deletions(-) diff --git a/jest.setup.client.ts b/jest.setup.client.ts index 763166d9a..2137c5ec7 100644 --- a/jest.setup.client.ts +++ b/jest.setup.client.ts @@ -1,7 +1,14 @@ -import "@testing-library/jest-dom"; +// Define an interface for the mock if TypeScript doesn't include CSS or is missing properties/methods you need +interface CSSMock { + escape: (input: string) => string; +} +// Assigning the mock to global.CSS with proper typing if (typeof CSS === "undefined") { - global.CSS = { + const mockCSS: CSSMock = { escape: (string_: string) => string_.replaceAll(/([()\\{}])/g, "\\$1"), - } as any; // Cast to 'any' to bypass TypeScript's type checking for the mock. + }; + + // Extend the existing global interface (if necessary) and assign the mock + global.CSS = mockCSS as typeof CSS; } diff --git a/package-lock.json b/package-lock.json index 5e0665f5a..dc5924941 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26327,6 +26327,126 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.6.tgz", + "integrity": "sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.6.tgz", + "integrity": "sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.6.tgz", + "integrity": "sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.6.tgz", + "integrity": "sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.6.tgz", + "integrity": "sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.6.tgz", + "integrity": "sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.6.tgz", + "integrity": "sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.6.tgz", + "integrity": "sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/src/client/apps/story/components.tsx b/src/client/apps/story/components.tsx index 3a959fe49..c95df303d 100644 --- a/src/client/apps/story/components.tsx +++ b/src/client/apps/story/components.tsx @@ -21,10 +21,11 @@ import Box from "@mui/joy/Box"; import Button from "@mui/joy/Button"; import Sheet from "@mui/joy/Sheet"; import { useAtom } from "jotai/index"; -import type { CSSProperties } from "react"; +import type { CSSProperties, RefCallback } from "react"; import { useMemo } from "react"; import { useState } from "react"; import { forwardRef, type LegacyRef, type ReactNode, useCallback, useRef } from "react"; +import type { ScrollbarProps } from "react-custom-scrollbars"; import Scrollbars from "react-custom-scrollbars"; import AutoSizer from "react-virtualized-auto-sizer"; import { FixedSizeGrid } from "react-window"; @@ -172,13 +173,13 @@ export function LegacyCustomScrollbars({ style, children, }: { - onScroll?: any; - forwardedRef?: any; - style?: any; - children?: any; + onScroll?: ScrollbarProps["onScroll"]; + forwardedRef?: RefCallback; + style?: CSSProperties; + children?: ReactNode; }) { - const referenceSetter: LegacyRef = useCallback( - (scrollbarsReference: { view: any }) => { + const referenceSetter: LegacyRef = useCallback( + (scrollbarsReference: { view: HTMLDivElement }) => { if (forwardedRef) { if (scrollbarsReference) { forwardedRef(scrollbarsReference.view); @@ -214,10 +215,14 @@ export function LegacyCustomScrollbars({ ); } -export const CustomScrollbarsVirtualList = forwardRef< - HTMLDivElement, - { onScroll: any; forwardedRef: any; style: any; children: any } ->((properties, reference) => ); +export const CustomScrollbarsVirtualList = forwardRef( + (properties, reference) => ( + } + /> + ) +); CustomScrollbarsVirtualList.displayName = "CustomScrollbarsVirtualList"; diff --git a/src/client/atoms/icons/dynamic.tsx b/src/client/atoms/icons/dynamic.tsx index 61c3d2e67..57e1159b5 100644 --- a/src/client/atoms/icons/dynamic.tsx +++ b/src/client/atoms/icons/dynamic.tsx @@ -1,5 +1,8 @@ -import type { SvgIconProps } from "@mui/joy/SvgIcon"; +import type { DefaultComponentProps } from "@mui/material/OverridableComponent"; +import type { SvgIconProps } from "@mui/material/SvgIcon"; +import type { SvgIconTypeMap } from "@mui/material/SvgIcon"; import dynamic from "next/dynamic"; +import type { ComponentType } from "react"; import { createElement } from "react"; const Brush = dynamic(() => import("@mui/icons-material/Brush")); @@ -17,7 +20,10 @@ const ShoppingBag = dynamic(() => import("@mui/icons-material/ShoppingBag")); const Stream = dynamic(() => import("@mui/icons-material/Stream")); const QuestionMark = dynamic(() => import("@mui/icons-material/QuestionMark")); -const iconCache: Record = { +const iconCache: Record< + string, + ComponentType>> +> = { Brush, DarkMode, Dashboard, @@ -39,5 +45,5 @@ const iconCache: Record = { export function DynamicIcon({ icon, ...rest }: { icon: string } & SvgIconProps) { const component = iconCache[icon] ?? null; - return component && createElement(component, rest); + return component && createElement(component, rest as SvgIconProps); } diff --git a/src/client/ions/handlers/__tests__/action.test.ts b/src/client/ions/handlers/__tests__/action.test.ts index f3a77c509..03cae6d6a 100644 --- a/src/client/ions/handlers/__tests__/action.test.ts +++ b/src/client/ions/handlers/__tests__/action.test.ts @@ -1,4 +1,4 @@ -import type { VectorStoreResponse } from "@captn/utils/types"; +import type { IPCHandlers, VectorStoreResponse } from "@captn/utils/types"; import { handleCaptainAction } from "../action"; @@ -10,11 +10,9 @@ jest.mock("#/build-key", () => ({ })); describe("handleCaptainAction", () => { - // Mocking window.ipc.send const mockSend = jest.fn(); beforeAll(() => { - // Ensure window.ipc exists - global.window.ipc = { send: mockSend } as any; + global.window.ipc = { send: mockSend } as unknown as IPCHandlers; }); beforeEach(() => { diff --git a/src/electron/handlers.ts b/src/electron/handlers.ts index 25ff056b9..53704670f 100644 --- a/src/electron/handlers.ts +++ b/src/electron/handlers.ts @@ -64,8 +64,8 @@ export const handlers = { send(channel: string, value?: unknown) { ipcRenderer.send(channel, value); }, - on(channel: string, callback: (...arguments_: any[]) => void) { - function subscription(_event: IpcRendererEvent, ...arguments_: any[]) { + on(channel: string, callback: (...arguments_: unknown[]) => void) { + function subscription(_event: IpcRendererEvent, ...arguments_: unknown[]) { return callback(...arguments_); } diff --git a/src/electron/helpers/langchain/__tests__/custom-hugging-face-transformers-embeddings.test.e2e.ts b/src/electron/helpers/langchain/__tests__/custom-hugging-face-transformers-embeddings.test.e2e.ts index 52585e9a3..494e32bee 100644 --- a/src/electron/helpers/langchain/__tests__/custom-hugging-face-transformers-embeddings.test.e2e.ts +++ b/src/electron/helpers/langchain/__tests__/custom-hugging-face-transformers-embeddings.test.e2e.ts @@ -1,7 +1,11 @@ import path from "node:path"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore import { env } from "@xenova/transformers"; +import { CustomHuggingFaceTransformersEmbeddings } from "../custom-hugging-face-transformers-embeddings"; + const originalImplementation = Array.isArray; // @ts-expect-error we just want to mock this Array.isArray = jest.fn(type => { @@ -16,10 +20,8 @@ Array.isArray = jest.fn(type => { return originalImplementation(type); }); -import { CustomHuggingFaceTransformersEmbeddings } from "../custom-hugging-face-transformers-embeddings"; - describe("CustomHuggingFaceEmbeddings", () => { - let embeddings: any; + let embeddings: CustomHuggingFaceTransformersEmbeddings; beforeAll(() => { env.localModelPath = path.join(process.cwd(), "models"); @@ -48,14 +50,14 @@ describe("CustomHuggingFaceEmbeddings", () => { expect(results.length).toEqual(texts.length); // Check each result to ensure it's an array and not empty - for (const [index, embedding] of results.entries()) { + for (const [, embedding] of results.entries()) { expect(Array.isArray(embedding)).toBeTruthy(); expect(embedding.length).toBeGreaterThan(0); } }); describe("without maxTokens defined", () => { - let defaultEmbeddings: any; + let defaultEmbeddings: CustomHuggingFaceTransformersEmbeddings; beforeAll(() => { defaultEmbeddings = new CustomHuggingFaceTransformersEmbeddings({ @@ -79,7 +81,7 @@ describe("CustomHuggingFaceEmbeddings", () => { expect(Array.isArray(results)).toBeTruthy(); expect(results.length).toEqual(texts.length); - for (const [index, embedding] of results.entries()) { + for (const [, embedding] of results.entries()) { expect(Array.isArray(embedding)).toBeTruthy(); expect(embedding.length).toBeGreaterThan(0); } diff --git a/src/electron/helpers/services/__tests__/download-manager.test.ts b/src/electron/helpers/services/__tests__/download-manager.test.ts index 1689e7630..70e25772b 100644 --- a/src/electron/helpers/services/__tests__/download-manager.test.ts +++ b/src/electron/helpers/services/__tests__/download-manager.test.ts @@ -16,6 +16,10 @@ import { unpack } from "@/utils/unpack"; const testDownloadFile = "https://example.com/test.jpg"; +interface DownloadEventArguments { + action: DownloadEvent; +} + jest.mock("electron", () => { const originalModule = jest.requireActual("electron"); @@ -377,11 +381,16 @@ describe("DownloadManager", () => { }); expect( - spySend.mock.calls.some((call: any) => call[1].action === DownloadEvent.PROGRESS) + spySend.mock.calls.some( + (call: [string, unknown]) => + (call[1] as DownloadEventArguments).action === DownloadEvent.PROGRESS + ) ).toBe(true); expect( - spySend.mock.calls.filter((call: any) => call[1].action === DownloadEvent.PROGRESS) - .length + spySend.mock.calls.filter( + (call: [string, unknown]) => + (call[1] as DownloadEventArguments).action === DownloadEvent.PROGRESS + ).length ).toBeGreaterThan(1); }); diff --git a/src/electron/helpers/services/download-manager/index.ts b/src/electron/helpers/services/download-manager/index.ts index fb19d2710..cb4338747 100644 --- a/src/electron/helpers/services/download-manager/index.ts +++ b/src/electron/helpers/services/download-manager/index.ts @@ -1,6 +1,7 @@ import type { DownloadItem } from "@captn/utils/constants"; import { DOWNLOADS_MESSAGE_KEY, DownloadEvent, DownloadState } from "@captn/utils/constants"; import { download } from "electron-dl"; +import type ElectronStore from "electron-store"; import { jsonStringify } from "#/object"; import { apps } from "@/apps"; @@ -16,6 +17,33 @@ import { } from "@/utils/path-helpers"; import { unpack } from "@/utils/unpack"; +function handleDownloadCompletion( + item: DownloadItem, + { + isApp, + downloadKeyPath, + keyPath, + targetDestination, + }: { isApp: boolean; downloadKeyPath: string; keyPath: string; targetDestination: string } +) { + item.state = DownloadState.DONE; + if (isApp) { + item.state = DownloadState.RESTART; + appSettingsStore.set("requiresRestart", true); + } + + downloadsStore.set(downloadKeyPath, item.state); + pushToStore(inventoryStore as unknown as ElectronStore>, keyPath, { + id: item.id, + modelPath: targetDestination, + label: item.label, + }); + sendToAllWindows(DOWNLOADS_MESSAGE_KEY, { + action: DownloadEvent.COMPLETED, + payload: item, + }); +} + /** * The DownloadManager class serves as a centralized manager for handling all download-related activities within an application. * It is designed to manage a queue of downloads, allowing for operations such as adding to the queue, checking the queue's status, @@ -110,7 +138,7 @@ export class DownloadManager { action: DownloadEvent.QUEUED, payload: item, }); - this.processQueue(); + this.processQueue().catch(); } } @@ -177,8 +205,8 @@ export class DownloadManager { queueItem => queueItem.id !== item.id ); - // Check if more downloads can be started - this.processQueue(); + this.processQueue().catch(); + if (item.unzip) { try { item.state = DownloadState.UNPACKING; @@ -192,20 +220,11 @@ export class DownloadManager { targetDestination, true ); - item.state = isApp ? DownloadState.RESTART : DownloadState.DONE; - if (isApp) { - appSettingsStore.set("requiresRestart", true); - } - - downloadsStore.set(downloadKeyPath, item.state); - pushToStore(inventoryStore, keyPath, { - id: item.id, - modelPath: targetDestination, - label: item.label, - }); - sendToAllWindows(DOWNLOADS_MESSAGE_KEY, { - action: DownloadEvent.COMPLETED, - payload: item, + handleDownloadCompletion(item, { + isApp, + keyPath, + downloadKeyPath, + targetDestination, }); } catch { item.state = DownloadState.FAILED; @@ -215,23 +234,14 @@ export class DownloadManager { action: DownloadEvent.ERROR, payload: item, }); - this.processQueue(); + this.processQueue().catch(); } } else { - item.state = isApp ? DownloadState.RESTART : DownloadState.DONE; - if (isApp) { - appSettingsStore.set("requiresRestart", true); - } - - downloadsStore.set(downloadKeyPath, item.state); - pushToStore(inventoryStore, keyPath, { - id: item.id, - modelPath: targetDestination, - label: item.label, - }); - sendToAllWindows(DOWNLOADS_MESSAGE_KEY, { - action: DownloadEvent.COMPLETED, - payload: item, + handleDownloadCompletion(item, { + isApp, + keyPath, + downloadKeyPath, + targetDestination, }); } }, @@ -257,7 +267,7 @@ export class DownloadManager { action: DownloadEvent.ERROR, payload: item, }); - this.processQueue(); + this.processQueue().catch(); } } } diff --git a/src/electron/helpers/services/vector-store.ts b/src/electron/helpers/services/vector-store.ts index 2e913d713..088ab42c0 100644 --- a/src/electron/helpers/services/vector-store.ts +++ b/src/electron/helpers/services/vector-store.ts @@ -174,9 +174,20 @@ export class VectorStore { * * @param {string} collectionName - The name of the collection where documents will be upserted. * @param {VectorStoreDocument[]} documents - An array of documents to be upserted. - * @returns {Promise} A promise that resolves when all upsert operations are completed. + * @returns {Promise | ReturnType[] | undefined>} A promise that resolves when all upsert operations are completed. */ - public async upsert(collectionName: string, documents: VectorStoreDocument[]) { + public async upsert( + collectionName: string, + documents: VectorStoreDocument[] + ): Promise< + Awaited< + | { + operation_id?: number | null | undefined; + status: "acknowledged" | "completed"; + } + | undefined + >[] + > { await this.ensureCollection(collectionName); const contentArray = documents.map(document_ => document_.content); @@ -226,9 +237,13 @@ export class VectorStore { * @param {string} collectionName - The name of the collection to search in. * @param {string} query - The text query to search for similar documents. * @param {SearchOptions} [options] - Optional search parameters. - * @returns {Promise} A promise that resolves with the search results. + * @returns {Promise|undefined>} A promise that resolves with the search results. */ - public async search(collectionName: string, query: string, options?: SearchOptions) { + public async search( + collectionName: string, + query: string, + options?: SearchOptions + ): Promise | undefined> { await this.ensureCollection(collectionName); const queryVector = await this.embeddings.embedDocuments([query]); @@ -243,12 +258,15 @@ export class VectorStore { * * @param {string} collectionName - The name of the collection to search in. * @param {ScrollOptions} [options] - Optional scroll parameters. - * @returns {Promise} A promise that resolves with the search results. + * @returns {Promise|undefined>} A promise that resolves with the search results. */ - public async scroll(collectionName: string, options?: ScrollOptions) { + public async scroll( + collectionName: string, + options?: ScrollOptions + ): Promise | undefined> { await this.ensureCollection(collectionName); - return this.client!.scroll(collectionName, options); + return this.client?.scroll(collectionName, options); } /** @@ -259,7 +277,10 @@ export class VectorStore { * @param {boolean} autoCreate - Whether to automatically create the collection if it does not exist. * @returns {Promise} */ - private async ensureCollection(collectionName: string, autoCreate: boolean = true) { + private async ensureCollection( + collectionName: string, + autoCreate: boolean = true + ): Promise { if (!this.client) { throw new Error("Qdrant client is not initialized."); } @@ -281,7 +302,7 @@ export class VectorStore { await this.client.createCollection(collectionName, collectionConfig); // Create the index for the internal id - this.client.createPayloadIndex(collectionName, { + await this.client.createPayloadIndex(collectionName, { field_name: "id", field_schema: "keyword", }); @@ -296,9 +317,9 @@ export class VectorStore { * Deletes the specified collection from the Qdrant database. * * @param {string} collectionName - The name of the collection to be deleted. - * @returns {Promise} A promise that resolves when the collection has been deleted. + * @returns { Promise)} A promise that resolves when the collection has been deleted. */ - public async deleteCollection(collectionName: string) { + public async deleteCollection(collectionName: string): Promise { await this.ensureCollection(collectionName, false); return this.client?.deleteCollection(collectionName); diff --git a/src/electron/helpers/stores/utils.ts b/src/electron/helpers/stores/utils.ts index 213c1fec4..b15705da7 100644 --- a/src/electron/helpers/stores/utils.ts +++ b/src/electron/helpers/stores/utils.ts @@ -1,8 +1,8 @@ import type Store from "electron-store"; import { uniqBy } from "lodash"; -export function pushToStore(store: Store, keyPath: string, data: T) { - const items = store.get(keyPath, []); +export function pushToStore(store: Store>, keyPath: string, data: T) { + const items = store.get(keyPath, []) as unknown[]; items.push(data); store.set(keyPath, uniqBy(items, "id")); } diff --git a/src/electron/helpers/stores/watchers.ts b/src/electron/helpers/stores/watchers.ts index 0e1b7af0f..3e473e6e8 100644 --- a/src/electron/helpers/stores/watchers.ts +++ b/src/electron/helpers/stores/watchers.ts @@ -35,21 +35,27 @@ export function watchStores() { } }), // We need to cast the type to any to allow dot-prop - inventoryStore.onDidChange("files.image", images => { + inventoryStore.onDidChange("files.image" as keyof typeof inventoryStore.store, images => { if (images) { sendToAllWindows("images", images); } }), - inventoryStore.onDidChange("stable-diffusion.checkpoints", checkpoints => { - if (checkpoints) { - sendToAllWindows("stable-diffusion.checkpoints", checkpoints); + inventoryStore.onDidChange( + "stable-diffusion.checkpoints" as keyof typeof inventoryStore.store, + checkpoints => { + if (checkpoints) { + sendToAllWindows("stable-diffusion.checkpoints", checkpoints); + } } - }), - inventoryStore.onDidChange("stable-diffusion.vae", vae => { - if (vae) { - sendToAllWindows("stable-diffusion.vae", vae); + ), + inventoryStore.onDidChange( + "stable-diffusion.vae" as keyof typeof inventoryStore.store, + vae => { + if (vae) { + sendToAllWindows("stable-diffusion.vae", vae); + } } - }), + ), inventoryStore.onDidAnyChange(inventory => { sendToAllWindows("allInventory", inventory); }), diff --git a/src/electron/helpers/utils/__tests__/git.test.ts b/src/electron/helpers/utils/__tests__/git.test.ts index 64218f97d..d042db658 100644 --- a/src/electron/helpers/utils/__tests__/git.test.ts +++ b/src/electron/helpers/utils/__tests__/git.test.ts @@ -1,5 +1,6 @@ import fs from "node:fs"; +import type { ExecaChildProcess } from "execa"; import { execa } from "execa"; jest.mock("execa"); @@ -14,6 +15,12 @@ jest.mock("@/utils/path-helpers", () => ({ const mockedExeca = execa as jest.MockedFunction; +interface MockChildProcess { + stderr: { + on: (event: string, handler: (data: unknown) => void) => void; + }; +} + describe("lfs", () => { const fakePath = "path/to/git"; const successMessage = "Git LFS has been set up successfully."; @@ -180,7 +187,7 @@ describe("clone", () => { it("should report progress during clone operation", async () => { jest.spyOn(fs, "existsSync").mockReturnValueOnce(false); - const mockChildProcess = { + const mockChildProcess: MockChildProcess = { stderr: { on: jest.fn((event, handler) => { if (event === "data") { @@ -189,7 +196,7 @@ describe("clone", () => { }), }, }; - mockedExeca.mockReturnValueOnce(mockChildProcess as any); + mockedExeca.mockReturnValueOnce(mockChildProcess as unknown as ExecaChildProcess); const onProgressMock = jest.fn(); diff --git a/src/electron/helpers/utils/git.ts b/src/electron/helpers/utils/git.ts index 5ae247db2..399406f8b 100644 --- a/src/electron/helpers/utils/git.ts +++ b/src/electron/helpers/utils/git.ts @@ -8,7 +8,7 @@ import { createDirectory } from "@/utils/fs"; import { getCaptainData, getCaptainDownloads } from "@/utils/path-helpers"; export interface GitCloneOptions { - onStarted?: (item: any) => void; + onStarted?: (item: { repository: string; destination: string; cancel(): void }) => void; onProgress?: (progress: GitCloneProgress) => void; onCompleted?: (completed: GitCloneCompleted) => void; } @@ -39,11 +39,11 @@ export async function clone(repository: string, destination: string, options?: G createDirectory(destinationPath); - let process: ExecaChildProcess | null = null; + let process_: ExecaChildProcess | null = null; function cancel() { - if (process) { - process.kill("SIGTERM"); + if (process_) { + process_.kill("SIGTERM"); } } @@ -56,8 +56,8 @@ export async function clone(repository: string, destination: string, options?: G // Update repo if it already exists if (fs.existsSync(gitDirectory)) { try { - process = execa(git(), ["pull"], { cwd: destinationPath }); - await process; + process_ = execa(git(), ["pull"], { cwd: destinationPath }); + await process_; options?.onProgress?.({ percent: 1, @@ -69,9 +69,14 @@ export async function clone(repository: string, destination: string, options?: G } } else { // Proceed with cloning - process = execa(git(), ["clone", "--progress", `git@hf.co:${repository}`, destinationPath]); - - process.stderr?.on("data", (buffer: Buffer) => { + process_ = execa(git(), [ + "clone", + "--progress", + `git@hf.co:${repository}`, + destinationPath, + ]); + + process_.stderr?.on("data", (buffer: Buffer) => { const output = buffer.toString(); const progressMatch = output.match(/Receiving objects:\s+(\d+)%/); if (progressMatch) { @@ -87,16 +92,16 @@ export async function clone(repository: string, destination: string, options?: G // Clone try { - await process; + await process_; } catch (error) { throw new Error(`Cloning repository failed: ${error}`); } } // Fetch LFS objects - process = execa(git(), ["lfs", "fetch"], { cwd: destinationPath }); + process_ = execa(git(), ["lfs", "fetch"], { cwd: destinationPath }); - process.stderr?.on("data", (buffer: Buffer) => { + process_.stderr?.on("data", (buffer: Buffer) => { const output = buffer.toString(); const progressMatch = output.match(/(\d+)% \((\d+)\/(\d+)\)/); if (progressMatch) { @@ -111,15 +116,15 @@ export async function clone(repository: string, destination: string, options?: G }); try { - await process; + await process_; } catch (error) { throw new Error(`Fetching Git LFS objects failed: ${error}`); } // Checkout LFS objects try { - process = execa(git(), ["lfs", "checkout"], { cwd: destinationPath }); - await process; + process_ = execa(git(), ["lfs", "checkout"], { cwd: destinationPath }); + await process_; options?.onCompleted?.({ path: destinationPath,