From 6017ace0c692765de241f3397b550426a5007837 Mon Sep 17 00:00:00 2001 From: Demir Yerli Date: Sun, 5 Apr 2026 09:51:35 +0300 Subject: [PATCH 01/12] remove obsolete QuadrantClient and related interfaces --- packages/quadrant-node/index.d.ts | 41 -------- packages/quadrant-node/index.js | 161 ------------------------------ 2 files changed, 202 deletions(-) delete mode 100644 packages/quadrant-node/index.d.ts delete mode 100644 packages/quadrant-node/index.js diff --git a/packages/quadrant-node/index.d.ts b/packages/quadrant-node/index.d.ts deleted file mode 100644 index bf31c2b3..00000000 --- a/packages/quadrant-node/index.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -export interface QuadrantClientOptions { - dataDir: string; - mcFolder?: string | null; - apiBaseUrl?: string | null; - oauthClientId: string; - oauthClientSecret: string; - quadrantApiKey: string; - configStoreName?: string | null; - updateStoreName?: string | null; - keyringServiceName?: string | null; - appVersion?: string | null; - osName?: string | null; - userAgent?: string | null; -} - -export interface QuadrantEventEnvelope { - event: string; - payload: unknown; -} - -export interface QuadrantClient { - startBackgroundWorkers(): Promise; - stopBackgroundWorkers(): Promise; - shutdown(): Promise; - initConfig(): Promise; - getMinecraftFolder(): Promise; - invoke(command: string, payload?: unknown): Promise; - getModpacks(hideFree?: boolean): Promise; - getAccountInfo(): Promise; - getNews(): Promise; - installMod(args: unknown): Promise; - syncModpack(modpack: unknown, overwrite?: boolean): Promise; - on(eventName: string, listener: (payload: unknown) => void): () => void; - off(eventName: string, listener: (payload: unknown) => void): void; - once(eventName: string, listener: (payload: unknown) => void): void; -} - -export function createQuadrantClient( - options: QuadrantClientOptions, - nativeModule?: any, -): QuadrantClient; diff --git a/packages/quadrant-node/index.js b/packages/quadrant-node/index.js deleted file mode 100644 index d9dd1d6c..00000000 --- a/packages/quadrant-node/index.js +++ /dev/null @@ -1,161 +0,0 @@ -/** @format */ - -import { EventEmitter } from "node:events"; -import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; - -function normalizeNativeModule(module) { - if (!module) { - return module; - } - - if (typeof module.QuadrantHostAddon === "function") { - return module; - } - - if ( - module.default && - typeof module.default.QuadrantHostAddon === "function" - ) { - return module.default; - } - - return module; -} - -function resolveModuleSpecifier(specifier) { - if ( - specifier.startsWith("./") || - specifier.startsWith("../") || - specifier.startsWith("/") || - /^[A-Za-z]:[\\/]/.test(specifier) - ) { - return pathToFileURL(path.resolve(process.cwd(), specifier)).href; - } - - return specifier; -} - -async function loadNativeModule() { - if (process.env.QUADRANT_NAPI_MODULE) { - try { - return normalizeNativeModule( - await import(resolveModuleSpecifier(process.env.QUADRANT_NAPI_MODULE)), - ); - } catch (error) { - throw new Error( - `Failed to load Quadrant native module from ${process.env.QUADRANT_NAPI_MODULE}: ${error}`, - ); - } - } - - try { - return normalizeNativeModule(await import("./native/index.js")); - } catch (error) { - const packageDir = path.dirname(fileURLToPath(import.meta.url)); - throw new Error( - `Failed to load Quadrant native module from ${packageDir}: ${error}`, - ); - } -} - -const defaultNativeModule = await loadNativeModule(); - -function nativeMethod(target, ...names) { - for (const name of names) { - if (typeof target[name] === "function") { - return target[name].bind(target); - } - } - throw new Error(`Native method not found. Tried: ${names.join(", ")}`); -} - -function mapOptions(options) { - const mappedOptions = { - dataDir: options.dataDir, - oauthClientId: options.oauthClientId, - oauthClientSecret: options.oauthClientSecret, - quadrantApiKey: options.quadrantApiKey, - }; - - if (options.mcFolder != null) { - mappedOptions.mcFolder = options.mcFolder; - } - if (options.apiBaseUrl != null) { - mappedOptions.apiBaseUrl = options.apiBaseUrl; - } - if (options.configStoreName != null) { - mappedOptions.configStoreName = options.configStoreName; - } - if (options.updateStoreName != null) { - mappedOptions.updateStoreName = options.updateStoreName; - } - if (options.keyringServiceName != null) { - mappedOptions.keyringServiceName = options.keyringServiceName; - } - if (options.appVersion != null) { - mappedOptions.appVersion = options.appVersion; - } - if (options.osName != null) { - mappedOptions.osName = options.osName; - } - if (options.userAgent != null) { - mappedOptions.userAgent = options.userAgent; - } - - return mappedOptions; -} - -export function createQuadrantClient( - options, - nativeModule = defaultNativeModule, -) { - const host = new nativeModule.QuadrantHostAddon(mapOptions(options)); - const emitter = new EventEmitter(); - - nativeMethod( - host, - "onEvent", - "on_event", - )((rawEvent) => { - const event = - typeof rawEvent === "string" ? JSON.parse(rawEvent) : rawEvent; - emitter.emit("event", event); - emitter.emit(event.event, event.payload); - }); - - return { - startBackgroundWorkers: () => - nativeMethod( - host, - "startBackgroundWorkers", - "start_background_workers", - )(), - stopBackgroundWorkers: () => - nativeMethod(host, "stopBackgroundWorkers", "stop_background_workers")(), - shutdown: () => nativeMethod(host, "shutdown")(), - initConfig: () => nativeMethod(host, "initConfig", "init_config")(), - getMinecraftFolder: () => - nativeMethod(host, "getMinecraftFolder", "get_minecraft_folder")(), - invoke: (command, payload = null) => - nativeMethod(host, "invoke")(command, payload), - getModpacks: (hideFree = false) => - nativeMethod(host, "getModpacks", "get_modpacks")(hideFree), - getAccountInfo: () => - nativeMethod(host, "getAccountInfo", "get_account_info")(), - getNews: () => nativeMethod(host, "getNews", "get_news")(), - installMod: (args) => nativeMethod(host, "installMod", "install_mod")(args), - syncModpack: (modpack, overwrite = true) => - nativeMethod(host, "syncModpack", "sync_modpack")(modpack, overwrite), - on: (eventName, listener) => { - emitter.on(eventName, listener); - return () => emitter.off(eventName, listener); - }, - off: (eventName, listener) => { - emitter.off(eventName, listener); - }, - once: (eventName, listener) => { - emitter.once(eventName, listener); - }, - }; -} From b0877f290f7c4fd69b87cfa41b53e3a5f8b036bd Mon Sep 17 00:00:00 2001 From: Demir Yerli Date: Sun, 5 Apr 2026 09:51:48 +0300 Subject: [PATCH 02/12] Add sync functionality for quadrant-node package and update related scripts --- .gitignore | 2 + DEVELOP.md | 2 +- packages/quadrant-node/package.json | 2 +- scripts/build-napi.mjs | 3 + scripts/dev-electron.mjs | 3 +- scripts/quadrant-node/index.d.ts | 41 +++++++ scripts/quadrant-node/index.js | 161 +++++++++++++++++++++++++ scripts/sync-quadrant-node-package.mjs | 43 +++++++ 8 files changed, 254 insertions(+), 3 deletions(-) create mode 100644 scripts/quadrant-node/index.d.ts create mode 100644 scripts/quadrant-node/index.js create mode 100644 scripts/sync-quadrant-node-package.mjs diff --git a/.gitignore b/.gitignore index b90e8dab..1a190c59 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,6 @@ target/ AppxContent AppxBundles electron/runtime-config.generated.json +packages/quadrant-node/index.js +packages/quadrant-node/index.d.ts packages/quadrant-node/native/ diff --git a/DEVELOP.md b/DEVELOP.md index 0d60d695..5487b5e8 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -8,6 +8,6 @@ You can develop the Quadrant Next client by: - Tauri development is available through `bun run dev:tauri`. - Electron development is available through `bun run dev:electron`. - To build the shared renderer only, run `bun run build`. -- To build the native Electron addon explicitly, run `bun run build:napi`. +- To build the native Electron addon explicitly, run `bun run build:napi`. This also regenerates the `packages/quadrant-node` wrapper files. - To package the Electron app, run `bun run package:electron`. - To build the Tauri app without any of the proprietary features, run `bun tauri dev -- -- --no-default-features`. diff --git a/packages/quadrant-node/package.json b/packages/quadrant-node/package.json index 9f3a203f..b2766512 100644 --- a/packages/quadrant-node/package.json +++ b/packages/quadrant-node/package.json @@ -1,6 +1,6 @@ { "name": "@quadrant/quadrant-node", - "version": "26.4.1-preview.0", + "private": true, "type": "module", "main": "index.js", "exports": { diff --git a/scripts/build-napi.mjs b/scripts/build-napi.mjs index e487523c..1feb9f73 100644 --- a/scripts/build-napi.mjs +++ b/scripts/build-napi.mjs @@ -1,6 +1,7 @@ import { cpSync, existsSync, mkdirSync, readdirSync } from "node:fs"; import path from "node:path"; import { resolveCargoCommand, runCommand } from "./command-utils.mjs"; +import { syncQuadrantNodePackage } from "./sync-quadrant-node-package.mjs"; const rootDir = path.resolve(import.meta.dirname, ".."); const srcTauriDir = path.join(rootDir, "src-tauri"); @@ -108,6 +109,8 @@ const rustTarget = getArgValue("--target") ?? getRustTargetTriple(targetPlatform, targetArch); const cargoCommand = resolveCargoCommand(); +syncQuadrantNodePackage(); + runCommand( cargoCommand, [ diff --git a/scripts/dev-electron.mjs b/scripts/dev-electron.mjs index 52709826..a13135eb 100644 --- a/scripts/dev-electron.mjs +++ b/scripts/dev-electron.mjs @@ -8,9 +8,10 @@ const devUrl = "http://127.0.0.1:1420"; const electronWatchGlobs = [ "electron/**/*", "package.json", - "packages/quadrant-node/**/*", "scripts/build-napi.mjs", "scripts/command-utils.mjs", + "scripts/sync-quadrant-node-package.mjs", + "scripts/quadrant-node/**/*", "scripts/write-electron-runtime-config.mjs", "src-tauri/Cargo.toml", "src-tauri/Cargo.lock", diff --git a/scripts/quadrant-node/index.d.ts b/scripts/quadrant-node/index.d.ts new file mode 100644 index 00000000..bf31c2b3 --- /dev/null +++ b/scripts/quadrant-node/index.d.ts @@ -0,0 +1,41 @@ +export interface QuadrantClientOptions { + dataDir: string; + mcFolder?: string | null; + apiBaseUrl?: string | null; + oauthClientId: string; + oauthClientSecret: string; + quadrantApiKey: string; + configStoreName?: string | null; + updateStoreName?: string | null; + keyringServiceName?: string | null; + appVersion?: string | null; + osName?: string | null; + userAgent?: string | null; +} + +export interface QuadrantEventEnvelope { + event: string; + payload: unknown; +} + +export interface QuadrantClient { + startBackgroundWorkers(): Promise; + stopBackgroundWorkers(): Promise; + shutdown(): Promise; + initConfig(): Promise; + getMinecraftFolder(): Promise; + invoke(command: string, payload?: unknown): Promise; + getModpacks(hideFree?: boolean): Promise; + getAccountInfo(): Promise; + getNews(): Promise; + installMod(args: unknown): Promise; + syncModpack(modpack: unknown, overwrite?: boolean): Promise; + on(eventName: string, listener: (payload: unknown) => void): () => void; + off(eventName: string, listener: (payload: unknown) => void): void; + once(eventName: string, listener: (payload: unknown) => void): void; +} + +export function createQuadrantClient( + options: QuadrantClientOptions, + nativeModule?: any, +): QuadrantClient; diff --git a/scripts/quadrant-node/index.js b/scripts/quadrant-node/index.js new file mode 100644 index 00000000..d9dd1d6c --- /dev/null +++ b/scripts/quadrant-node/index.js @@ -0,0 +1,161 @@ +/** @format */ + +import { EventEmitter } from "node:events"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +function normalizeNativeModule(module) { + if (!module) { + return module; + } + + if (typeof module.QuadrantHostAddon === "function") { + return module; + } + + if ( + module.default && + typeof module.default.QuadrantHostAddon === "function" + ) { + return module.default; + } + + return module; +} + +function resolveModuleSpecifier(specifier) { + if ( + specifier.startsWith("./") || + specifier.startsWith("../") || + specifier.startsWith("/") || + /^[A-Za-z]:[\\/]/.test(specifier) + ) { + return pathToFileURL(path.resolve(process.cwd(), specifier)).href; + } + + return specifier; +} + +async function loadNativeModule() { + if (process.env.QUADRANT_NAPI_MODULE) { + try { + return normalizeNativeModule( + await import(resolveModuleSpecifier(process.env.QUADRANT_NAPI_MODULE)), + ); + } catch (error) { + throw new Error( + `Failed to load Quadrant native module from ${process.env.QUADRANT_NAPI_MODULE}: ${error}`, + ); + } + } + + try { + return normalizeNativeModule(await import("./native/index.js")); + } catch (error) { + const packageDir = path.dirname(fileURLToPath(import.meta.url)); + throw new Error( + `Failed to load Quadrant native module from ${packageDir}: ${error}`, + ); + } +} + +const defaultNativeModule = await loadNativeModule(); + +function nativeMethod(target, ...names) { + for (const name of names) { + if (typeof target[name] === "function") { + return target[name].bind(target); + } + } + throw new Error(`Native method not found. Tried: ${names.join(", ")}`); +} + +function mapOptions(options) { + const mappedOptions = { + dataDir: options.dataDir, + oauthClientId: options.oauthClientId, + oauthClientSecret: options.oauthClientSecret, + quadrantApiKey: options.quadrantApiKey, + }; + + if (options.mcFolder != null) { + mappedOptions.mcFolder = options.mcFolder; + } + if (options.apiBaseUrl != null) { + mappedOptions.apiBaseUrl = options.apiBaseUrl; + } + if (options.configStoreName != null) { + mappedOptions.configStoreName = options.configStoreName; + } + if (options.updateStoreName != null) { + mappedOptions.updateStoreName = options.updateStoreName; + } + if (options.keyringServiceName != null) { + mappedOptions.keyringServiceName = options.keyringServiceName; + } + if (options.appVersion != null) { + mappedOptions.appVersion = options.appVersion; + } + if (options.osName != null) { + mappedOptions.osName = options.osName; + } + if (options.userAgent != null) { + mappedOptions.userAgent = options.userAgent; + } + + return mappedOptions; +} + +export function createQuadrantClient( + options, + nativeModule = defaultNativeModule, +) { + const host = new nativeModule.QuadrantHostAddon(mapOptions(options)); + const emitter = new EventEmitter(); + + nativeMethod( + host, + "onEvent", + "on_event", + )((rawEvent) => { + const event = + typeof rawEvent === "string" ? JSON.parse(rawEvent) : rawEvent; + emitter.emit("event", event); + emitter.emit(event.event, event.payload); + }); + + return { + startBackgroundWorkers: () => + nativeMethod( + host, + "startBackgroundWorkers", + "start_background_workers", + )(), + stopBackgroundWorkers: () => + nativeMethod(host, "stopBackgroundWorkers", "stop_background_workers")(), + shutdown: () => nativeMethod(host, "shutdown")(), + initConfig: () => nativeMethod(host, "initConfig", "init_config")(), + getMinecraftFolder: () => + nativeMethod(host, "getMinecraftFolder", "get_minecraft_folder")(), + invoke: (command, payload = null) => + nativeMethod(host, "invoke")(command, payload), + getModpacks: (hideFree = false) => + nativeMethod(host, "getModpacks", "get_modpacks")(hideFree), + getAccountInfo: () => + nativeMethod(host, "getAccountInfo", "get_account_info")(), + getNews: () => nativeMethod(host, "getNews", "get_news")(), + installMod: (args) => nativeMethod(host, "installMod", "install_mod")(args), + syncModpack: (modpack, overwrite = true) => + nativeMethod(host, "syncModpack", "sync_modpack")(modpack, overwrite), + on: (eventName, listener) => { + emitter.on(eventName, listener); + return () => emitter.off(eventName, listener); + }, + off: (eventName, listener) => { + emitter.off(eventName, listener); + }, + once: (eventName, listener) => { + emitter.once(eventName, listener); + }, + }; +} diff --git a/scripts/sync-quadrant-node-package.mjs b/scripts/sync-quadrant-node-package.mjs new file mode 100644 index 00000000..09ed54f8 --- /dev/null +++ b/scripts/sync-quadrant-node-package.mjs @@ -0,0 +1,43 @@ +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const rootDir = path.resolve(import.meta.dirname, ".."); +const sourceDir = path.join(rootDir, "scripts", "quadrant-node"); +const targetDir = path.join(rootDir, "packages", "quadrant-node"); + +function readUtf8(filePath) { + return readFileSync(filePath, "utf8"); +} + +function writeIfChanged(filePath, contents) { + try { + if (readUtf8(filePath) === contents) { + return false; + } + } catch { + // Fall through and create the file when it does not exist yet. + } + + writeFileSync(filePath, contents); + return true; +} + +export function syncQuadrantNodePackage() { + mkdirSync(targetDir, { recursive: true }); + + return { + indexJs: writeIfChanged( + path.join(targetDir, "index.js"), + readUtf8(path.join(sourceDir, "index.js")), + ), + indexTypes: writeIfChanged( + path.join(targetDir, "index.d.ts"), + readUtf8(path.join(sourceDir, "index.d.ts")), + ), + }; +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + syncQuadrantNodePackage(); +} From a003a8438b97bdf6003dd50cf34e5ea867dd511a Mon Sep 17 00:00:00 2001 From: Demir Yerli Date: Sun, 5 Apr 2026 09:57:57 +0300 Subject: [PATCH 03/12] Update release workflow dependencies and streamline Flathub sync requirements --- .github/workflows/release.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4d843ddc..83904407 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,7 +106,7 @@ jobs: if: matrix.os == 'linux' run: | sudo apt update - sudo apt install -y libsecret-1-dev libarchive-tools rpm + sudo apt install -y xdg-utils libappindicator3-dev librsvg2-dev patchelf libdbus-1-dev pkg-config libsecret-1-dev libarchive-tools rpm - name: install frontend dependencies (bun) run: bun install @@ -135,9 +135,7 @@ jobs: sync_flathub: name: Sync Flathub - needs: - - publish-tauri - - publish-electron + needs: publish-electron if: endsWith(github.ref_name, '-stable') || endsWith(github.ref_name, '-flatpak') uses: ./.github/workflows/flatpak.yml secrets: inherit From 0206f1aad85207bffeb0419bdf914ba776fd8b79 Mon Sep 17 00:00:00 2001 From: Demir Yerli Date: Sun, 5 Apr 2026 10:04:24 +0300 Subject: [PATCH 04/12] Disable electron-builder publishing by default - Detect explicit `--publish` flags and skip the default `--publish never` - Preserve caller-provided publish settings when packaging Electron --- scripts/package-electron.mjs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/package-electron.mjs b/scripts/package-electron.mjs index df2a4c4d..9bdb6ee9 100644 --- a/scripts/package-electron.mjs +++ b/scripts/package-electron.mjs @@ -33,6 +33,12 @@ function normalizeArch(arch) { } } +function hasPublishFlag(args) { + return args.some( + (arg) => arg === "--publish" || arg.startsWith("--publish="), + ); +} + function run(command, args) { runCommand(command, args, { cwd: rootDir, @@ -43,6 +49,7 @@ function run(command, args) { const targetArch = normalizeArch(getArgValue("--arch")); const electronBuilderArchFlag = `--${targetArch}`; +const publishArgs = hasPublishFlag(extraArgs) ? [] : ["--publish", "never"]; run("node", ["scripts/build-electron.mjs", ...extraArgs]); run("bunx", [ @@ -50,4 +57,5 @@ run("bunx", [ "--config", "electron-builder.json", electronBuilderArchFlag, + ...publishArgs, ]); From 0d9285d691a29b9d1b5cc073eb4078b45fb41eb0 Mon Sep 17 00:00:00 2001 From: Demir Yerli Date: Sun, 5 Apr 2026 10:10:51 +0300 Subject: [PATCH 05/12] modified: .github/workflows/release.yml --- .github/workflows/release.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 83904407..8677ab42 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,34 @@ on: # This workflow will trigger on each push to the `release` branch to create or update a GitHub release, build your app, and upload the artifacts to the release. jobs: + ensure-release: + permissions: + contents: write + runs-on: ubuntu-24.04 + steps: + - name: Ensure GitHub release exists + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG_NAME: ${{ github.ref_name }} + shell: bash + run: | + if gh release view "$TAG_NAME" >/dev/null 2>&1; then + exit 0 + fi + + version="${TAG_NAME#v}" + prerelease_flag=() + if [[ "$TAG_NAME" != *stable ]]; then + prerelease_flag+=(--prerelease) + fi + + gh release create "$TAG_NAME" \ + --title "Quadrant v${version}" \ + --notes "See the assets to download this version and install. Check out the [changelog on my blog](https://blog.mrquantumoff.dev)" \ + "${prerelease_flag[@]}" + publish-tauri: + needs: ensure-release permissions: contents: write strategy: @@ -74,6 +101,7 @@ jobs: tagName: ${{github.ref_name}} publish-electron: + needs: ensure-release permissions: contents: write strategy: From 7501c3671391c02c1840c88b88acfdadc8c5b4ed Mon Sep 17 00:00:00 2001 From: Demir Yerli Date: Sun, 5 Apr 2026 10:15:47 +0300 Subject: [PATCH 06/12] Add repository context to GitHub release commands in workflow --- .github/workflows/release.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8677ab42..933f49ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,9 +20,10 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG_NAME: ${{ github.ref_name }} + REPO: ${{ github.repository }} shell: bash run: | - if gh release view "$TAG_NAME" >/dev/null 2>&1; then + if gh release view "$TAG_NAME" --repo "$REPO" >/dev/null 2>&1; then exit 0 fi @@ -33,6 +34,7 @@ jobs: fi gh release create "$TAG_NAME" \ + --repo "$REPO" \ --title "Quadrant v${version}" \ --notes "See the assets to download this version and install. Check out the [changelog on my blog](https://blog.mrquantumoff.dev)" \ "${prerelease_flag[@]}" @@ -156,10 +158,11 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG_NAME: ${{ github.ref_name }} + REPO: ${{ github.repository }} shell: bash run: | mapfile -d '' files < <(find dist-electron -maxdepth 1 -type f -print0) - gh release upload "$TAG_NAME" "${files[@]}" --clobber + gh release upload "$TAG_NAME" "${files[@]}" --clobber --repo "$REPO" sync_flathub: name: Sync Flathub From 9a5abba133a69832deef5d41456dd5520ce4d545 Mon Sep 17 00:00:00 2001 From: Demir Yerli Date: Sun, 5 Apr 2026 10:24:02 +0300 Subject: [PATCH 07/12] Update dependencies: add chokidar and electron-updater --- bun.lock | 41 ++++++++++++++++++++++++++--------------- package.json | 4 ++-- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/bun.lock b/bun.lock index f9a30a92..3e924592 100644 --- a/bun.lock +++ b/bun.lock @@ -21,6 +21,8 @@ "@tauri-apps/plugin-os": "~2.3.2", "@tauri-apps/plugin-store": "2.4.2", "@tauri-apps/plugin-updater": "2.10.1", + "chokidar": "^5.0.0", + "electron-updater": "^6.8.3", "i18next": "26.0.3", "motion": "12.38.0", "react": "19.2.4", @@ -41,10 +43,8 @@ "@vitejs/plugin-react": "^6.0.1", "autoprefixer": "^10.4.27", "babel-plugin-react-compiler": "1.0.0", - "chokidar": "^5.0.0", "electron": "^41.1.1", "electron-builder": "^26.8.1", - "electron-updater": "^6.8.3", "eslint": "^10.2.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", @@ -64,7 +64,6 @@ }, "packages/quadrant-node": { "name": "@quadrant/quadrant-node", - "version": "26.4.0", }, }, "trustedDependencies": [ @@ -1328,7 +1327,7 @@ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], @@ -1546,12 +1545,8 @@ "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], - "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], "@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -1562,12 +1557,12 @@ "@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], - "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], "@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="], + "@electron/rebuild/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "@electron/universal/fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="], "@electron/universal/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -1594,6 +1589,8 @@ "@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "@npmcli/fs/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], @@ -1636,6 +1633,8 @@ "@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="], + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="], "@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="], @@ -1646,6 +1645,8 @@ "app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], + "app-builder-lib/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -1658,6 +1659,8 @@ "electron/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "electron-updater/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], "eslint/@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.0", "", { "dependencies": { "@eslint/core": "^1.2.0", "levn": "^0.4.1" } }, "sha512-ejvBr8MQCbVsWNZnCwDXjUKq40MDmHalq7cJ6e9s/qzTUFIIo/afzt1Vui9T97FM/V/pN4YsFVoed5NIa96RDg=="], @@ -1670,20 +1673,18 @@ "eslint-plugin-import/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "eslint-plugin-jsx-a11y/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "eslint-plugin-react/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "global-agent/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "has-ansi/ansi-regex": ["ansi-regex@2.1.1", "", {}, "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA=="], "hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], @@ -1700,7 +1701,11 @@ "ml-random/ml-xsadd": ["ml-xsadd@2.0.0", "", {}, "sha512-VoAYUqmPRmzKbbqRejjqceGFp3VF81Qe8XXFGU0UXLxB7Mf4GGvyGq5Qn3k4AiQgDEV6WzobqlPOd+j0+m6IrA=="], - "node-exports-info/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "node-abi/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "node-api-version/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "node-gyp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], @@ -1716,6 +1721,8 @@ "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], + "simple-update-notifier/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -1734,6 +1741,8 @@ "vue-eslint-parser/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], + "vue-eslint-parser/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="], @@ -1774,6 +1783,8 @@ "@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="], + "@typescript-eslint/parser/@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "@typescript-eslint/parser/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="], "@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="], diff --git a/package.json b/package.json index 09d33ea5..7ef00c9b 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,8 @@ "@tauri-apps/plugin-os": "~2.3.2", "@tauri-apps/plugin-store": "2.4.2", "@tauri-apps/plugin-updater": "2.10.1", + "chokidar": "^5.0.0", + "electron-updater": "^6.8.3", "i18next": "26.0.3", "motion": "12.38.0", "react": "19.2.4", @@ -51,10 +53,8 @@ "@eslint/js": "^10.0.1", "@eslint/json": "^1.2.0", "@tailwindcss/postcss": "4.2.2", - "chokidar": "^5.0.0", "electron": "^41.1.1", "electron-builder": "^26.8.1", - "electron-updater": "^6.8.3", "@tauri-apps/cli": "^2.10.1", "@types/node": "^25.5.2", "@types/react": "^19.2.14", From 0acdc377b6e70d57498add05aa39c0af4ba3a525 Mon Sep 17 00:00:00 2001 From: Demir Yerli Date: Sun, 5 Apr 2026 10:30:58 +0300 Subject: [PATCH 08/12] v26.4.1-preview.1 --- bun.lock | 3 ++ ...mrquantumoff.mcmodpackmanager.metainfo.xml | 2 +- package.json | 3 +- src-tauri/Cargo.lock | 8 ++-- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 38 +++++++++++++++---- src/App.css | 2 +- vite.config.ts | 25 +++--------- 8 files changed, 48 insertions(+), 35 deletions(-) diff --git a/bun.lock b/bun.lock index 3e924592..355cd6a6 100644 --- a/bun.lock +++ b/bun.lock @@ -35,6 +35,7 @@ "@eslint/css": "^1.1.0", "@eslint/js": "^10.0.1", "@eslint/json": "^1.2.0", + "@rolldown/plugin-babel": "^0.2.0", "@tailwindcss/postcss": "4.2.2", "@tauri-apps/cli": "^2.10.1", "@types/node": "^25.5.2", @@ -271,6 +272,8 @@ "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "x64" }, "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw=="], + "@rolldown/plugin-babel": ["@rolldown/plugin-babel@0.2.2", "", { "dependencies": { "picomatch": "^4.0.3" }, "peerDependencies": { "@babel/core": "^7.29.0 || ^8.0.0-rc.1", "@babel/plugin-transform-runtime": "^7.29.0 || ^8.0.0-rc.1", "@babel/runtime": "^7.27.0 || ^8.0.0-rc.1", "rolldown": "^1.0.0-rc.5", "vite": "^8.0.0" }, "optionalPeers": ["@babel/plugin-transform-runtime", "@babel/runtime", "vite"] }, "sha512-q9pE8+47bQNHb5eWVcE6oXppA+JTSwvnrhH53m0ZuHuK5MLvwsLoWrWzBTFQqQ06BVxz1gp0HblLsch8o6pvZw=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], diff --git a/dev.mrquantumoff.mcmodpackmanager.metainfo.xml b/dev.mrquantumoff.mcmodpackmanager.metainfo.xml index e66866ee..6e2805db 100644 --- a/dev.mrquantumoff.mcmodpackmanager.metainfo.xml +++ b/dev.mrquantumoff.mcmodpackmanager.metainfo.xml @@ -44,7 +44,7 @@ - + https://blog.mrquantumoff.dev

Add Electron support as an alternative runtime

diff --git a/package.json b/package.json index 7ef00c9b..4aa7ca3e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "quadrant-next", "private": true, - "version": "26.4.1-preview.0", + "version": "26.4.1-preview.1", "type": "module", "description": "An easy way to manage your Minecraft mods and modpacks.", "author": "Demir Yerli ", @@ -52,6 +52,7 @@ "@eslint/css": "^1.1.0", "@eslint/js": "^10.0.1", "@eslint/json": "^1.2.0", + "@rolldown/plugin-babel": "^0.2.0", "@tailwindcss/postcss": "4.2.2", "electron": "^41.1.1", "electron-builder": "^26.8.1", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 010d1fd8..14f6ee1e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4406,7 +4406,7 @@ checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" [[package]] name = "quadrant-core" -version = "26.4.1-preview.0" +version = "26.4.1-preview.1" dependencies = [ "anyhow", "chrono", @@ -4433,7 +4433,7 @@ dependencies = [ [[package]] name = "quadrant-host" -version = "26.4.1-preview.0" +version = "26.4.1-preview.1" dependencies = [ "anyhow", "colog", @@ -4452,7 +4452,7 @@ dependencies = [ [[package]] name = "quadrant-napi" -version = "26.4.1-preview.0" +version = "26.4.1-preview.1" dependencies = [ "napi", "napi-build", @@ -4464,7 +4464,7 @@ dependencies = [ [[package]] name = "quadrant_next" -version = "26.4.1-preview.0" +version = "26.4.1-preview.1" dependencies = [ "anyhow", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e235e400..352df189 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -13,7 +13,7 @@ members = [ ] [workspace.package] -version = "26.4.1-preview.0" +version = "26.4.1-preview.1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a9276f10..9be4cafb 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,14 +1,17 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Quadrant", - "version": "26.4.1-preview.0", + "version": "26.4.1-preview.1", "identifier": "dev.mrquantumoff.mcmodpackmanager", "build": { "beforeDevCommand": "bun dev", "devUrl": "http://localhost:1420", "beforeBuildCommand": "bun run build", "frontendDist": "../dist", - "features": ["proprietary", "updater"] + "features": [ + "proprietary", + "updater" + ] }, "app": { "windows": [ @@ -39,7 +42,11 @@ "plugins": { "deep-link": { "desktop": { - "schemes": ["quadrantnext", "curseforge", "modrinth"] + "schemes": [ + "quadrantnext", + "curseforge", + "modrinth" + ] } }, "cli": { @@ -67,7 +74,12 @@ }, "bundle": { "active": true, - "targets": ["nsis", "appimage", "deb", "rpm"], + "targets": [ + "nsis", + "appimage", + "deb", + "rpm" + ], "linux": { "appimage": { "bundleMediaFramework": false @@ -111,11 +123,17 @@ "longDescription": "This is a powerful tool to manage your Minecraft: Java Edition mods. With this app you can easily install mods, resourcepacks, shaders. You can also share you modpacks with your friends, and you can back up your modpacks to the cloud!", "fileAssociations": [ { - "ext": ["modpackconfig.json", "modpackConfig", "quadrantModpack.json"], + "ext": [ + "modpackconfig.json", + "modpackConfig", + "quadrantModpack.json" + ], "description": "Quadrant Modpack Config File" } ], - "resources": ["../public"], + "resources": [ + "../public" + ], "windows": { "webviewInstallMode": { "type": "embedBootstrapper", @@ -123,7 +141,11 @@ }, "allowDowngrades": true, "nsis": { - "languages": ["Ukrainian", "English", "Turkish"] + "languages": [ + "Ukrainian", + "English", + "Turkish" + ] } }, "publisher": "MrQuantumOFF (Demir Yerli)", @@ -132,4 +154,4 @@ "license": "MPL-2.0", "createUpdaterArtifacts": true } -} +} \ No newline at end of file diff --git a/src/App.css b/src/App.css index 0fda02b2..d8047632 100644 --- a/src/App.css +++ b/src/App.css @@ -24,7 +24,7 @@ option { @font-face { font-family: "Inter"; - src: local("Inter"), url("$assets/Inter-Font.ttf"), url("./Inter-Font.ttf"); + src: local("Inter"), url("/Inter-Font.ttf") format("truetype"); } ::-webkit-scrollbar { diff --git a/vite.config.ts b/vite.config.ts index 03c54501..5f30e8ce 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,24 +1,16 @@ /** @format */ +import process from "node:process"; +import babel from "@rolldown/plugin-babel"; import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; +import react, { reactCompilerPreset } from "@vitejs/plugin-react"; -// @ts-expect-error process is a nodejs global const host = process.env.TAURI_DEV_HOST; -const ReactCompilerConfig = { - /* ... */ -}; - // https://vitejs.dev/config/ -export default defineConfig(async () => ({ - plugins: [ - react({ - babel: { - plugins: ["react-compiler", ReactCompilerConfig], - }, - }), - ], +export default defineConfig(async ({ command }) => ({ + plugins: [react(), babel({ presets: [reactCompilerPreset()] })], + base: command === "build" ? "./" : "/", // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // @@ -41,11 +33,6 @@ export default defineConfig(async () => ({ ignored: ["**/src-tauri/**"], }, }, - resolve: { - alias: { - $assets: "./public", - }, - }, build: { chunkSizeWarningLimit: 4096, }, From 12d0bea4150cdcd7ca26099ef3f9190d3ac83ff6 Mon Sep 17 00:00:00 2001 From: Demir Yerli Date: Sun, 5 Apr 2026 10:33:30 +0300 Subject: [PATCH 09/12] use checkout v6 in actions --- .github/workflows/flatpak.yml | 2 +- .github/workflows/msstore.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/validate-desktop.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 2a01d78a..8780994f 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -45,7 +45,7 @@ jobs: METAINFO_PATH: dev.mrquantumoff.mcmodpackmanager.metainfo.xml GH_TOKEN: ${{ secrets.FLATHUB_GH_TOKEN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ env.TAG }} diff --git a/.github/workflows/msstore.yml b/.github/workflows/msstore.yml index 03d99203..9c918d97 100644 --- a/.github/workflows/msstore.yml +++ b/.github/workflows/msstore.yml @@ -22,7 +22,7 @@ jobs: # args: "" runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup bun uses: oven-sh/setup-bun@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 933f49ec..35eb8d18 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,7 +61,7 @@ jobs: assets: ${{steps.tauri.outputs.releaseUploadUrl}} version: ${{steps.tauri.outputs.appVersion}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup bun uses: oven-sh/setup-bun@v2 @@ -124,7 +124,7 @@ jobs: arch: "arm64" runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup bun uses: oven-sh/setup-bun@v2 diff --git a/.github/workflows/validate-desktop.yml b/.github/workflows/validate-desktop.yml index fde1430a..5a924b90 100644 --- a/.github/workflows/validate-desktop.yml +++ b/.github/workflows/validate-desktop.yml @@ -19,7 +19,7 @@ jobs: QUADRANT_OAUTH2_CLIENT_SECRET: dev ETERNAL_API_TOKEN: dev steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup bun uses: oven-sh/setup-bun@v2 From 259fb0075365a674ac5bbcd67ba5ffd17e58e982 Mon Sep 17 00:00:00 2001 From: Demir Yerli Date: Sun, 5 Apr 2026 10:58:02 +0300 Subject: [PATCH 10/12] try to fix latest yml --- .github/workflows/release.yml | 53 ++++++++++- electron/main.mjs | 6 +- electron/tsconfig.json | 12 --- scripts/merge-electron-latest-yml.py | 133 +++++++++++++++++++++++++++ 4 files changed, 189 insertions(+), 15 deletions(-) delete mode 100644 electron/tsconfig.json create mode 100644 scripts/merge-electron-latest-yml.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 35eb8d18..86a5e3e6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -161,12 +161,61 @@ jobs: REPO: ${{ github.repository }} shell: bash run: | - mapfile -d '' files < <(find dist-electron -maxdepth 1 -type f -print0) + mapfile -d '' files < <( + if [[ "${{ matrix.os }}" == "windows" ]]; then + find dist-electron -maxdepth 1 -type f -not -name 'latest.yml' -print0 + else + find dist-electron -maxdepth 1 -type f -print0 + fi + ) gh release upload "$TAG_NAME" "${files[@]}" --clobber --repo "$REPO" + - name: Upload Windows Electron update metadata + if: matrix.os == 'windows' + uses: actions/upload-artifact@v4 + with: + name: electron-windows-${{ matrix.arch }}-latest-yml + path: dist-electron/latest.yml + if-no-files-found: error + retention-days: 1 + + publish-electron-windows-metadata: + needs: publish-electron + permissions: + contents: write + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + + - name: Download Windows x64 update metadata + uses: actions/download-artifact@v4 + with: + name: electron-windows-x64-latest-yml + path: artifacts/windows-x64 + + - name: Download Windows arm64 update metadata + uses: actions/download-artifact@v4 + with: + name: electron-windows-arm64-latest-yml + path: artifacts/windows-arm64 + + - name: Merge Windows Electron update metadata + run: | + python3 scripts/merge-electron-latest-yml.py \ + --base artifacts/windows-x64/latest.yml \ + --extra artifacts/windows-arm64/latest.yml \ + --output artifacts/windows/latest.yml + + - name: Upload merged Windows latest.yml to release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG_NAME: ${{ github.ref_name }} + REPO: ${{ github.repository }} + run: gh release upload "$TAG_NAME" artifacts/windows/latest.yml --clobber --repo "$REPO" + sync_flathub: name: Sync Flathub - needs: publish-electron + needs: publish-electron-windows-metadata if: endsWith(github.ref_name, '-stable') || endsWith(github.ref_name, '-flatpak') uses: ./.github/workflows/flatpak.yml secrets: inherit diff --git a/electron/main.mjs b/electron/main.mjs index f03f23e0..85a0c46d 100644 --- a/electron/main.mjs +++ b/electron/main.mjs @@ -1,4 +1,8 @@ -/** @format */ +/** + * eslint-disable no-undef + * + * @format + */ import { app, diff --git a/electron/tsconfig.json b/electron/tsconfig.json deleted file mode 100644 index 1a96a3a8..00000000 --- a/electron/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "allowJs": true, - "checkJs": false, - "types": ["node"], - "skipLibCheck": true - }, - "include": ["./*.mjs"] -} diff --git a/scripts/merge-electron-latest-yml.py b/scripts/merge-electron-latest-yml.py new file mode 100644 index 00000000..30b70102 --- /dev/null +++ b/scripts/merge-electron-latest-yml.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import copy +from datetime import datetime +from pathlib import Path +from typing import Any + +import yaml + + +def load_metadata(path: Path) -> dict[str, Any]: + data = yaml.safe_load(path.read_text(encoding="utf-8")) + if not isinstance(data, dict): + raise ValueError(f"{path} did not contain a YAML object") + return data + + +def extract_files(metadata: dict[str, Any]) -> list[dict[str, Any]]: + files = metadata.get("files") + if isinstance(files, list) and files: + return [copy.deepcopy(item) for item in files if isinstance(item, dict)] + + path = metadata.get("path") + sha512 = metadata.get("sha512") + if not path or not sha512: + raise ValueError("metadata must contain either a non-empty files list or path/sha512 fields") + + file_info: dict[str, Any] = {"url": path, "sha512": sha512} + for key in ("size", "blockMapSize"): + value = metadata.get(key) + if value is not None: + file_info[key] = value + return [file_info] + + +def merge_files(base: dict[str, Any], extra: dict[str, Any]) -> list[dict[str, Any]]: + merged: dict[str, dict[str, Any]] = {} + + for metadata in (base, extra): + for file_info in extract_files(metadata): + url = file_info.get("url") + if not isinstance(url, str) or not url: + raise ValueError(f"invalid file entry without a url: {file_info!r}") + existing = merged.get(url) + if existing is not None and existing != file_info: + raise ValueError(f"conflicting file metadata for {url}") + merged[url] = file_info + + return list(merged.values()) + + +def merge_packages(base: dict[str, Any], extra: dict[str, Any]) -> dict[str, Any] | None: + merged: dict[str, Any] = {} + + for metadata in (base, extra): + packages = metadata.get("packages") + if packages is None: + continue + if not isinstance(packages, dict): + raise ValueError("packages must be a mapping when present") + for arch, package_info in packages.items(): + existing = merged.get(arch) + if existing is not None and existing != package_info: + raise ValueError(f"conflicting package metadata for architecture {arch}") + merged[arch] = copy.deepcopy(package_info) + + return merged or None + + +def merge_release_date(base: dict[str, Any], extra: dict[str, Any]) -> str | None: + candidates = [] + for metadata in (base, extra): + release_date = metadata.get("releaseDate") + if isinstance(release_date, str): + candidates.append(release_date) + + if not candidates: + return None + + try: + return max(candidates, key=lambda value: datetime.fromisoformat(value.replace("Z", "+00:00"))) + except ValueError: + return candidates[0] + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Merge two electron-builder update metadata files into one Windows latest.yml", + ) + parser.add_argument("--base", required=True, help="The canonical metadata file to keep as top-level path/sha512, usually x64 latest.yml") + parser.add_argument("--extra", required=True, help="The secondary metadata file to merge in, usually arm64 latest.yml") + parser.add_argument("--output", required=True, help="Path to the merged YAML file") + args = parser.parse_args() + + base_path = Path(args.base) + extra_path = Path(args.extra) + output_path = Path(args.output) + + base = load_metadata(base_path) + extra = load_metadata(extra_path) + + base_version = base.get("version") + extra_version = extra.get("version") + if base_version != extra_version: + raise ValueError( + f"refusing to merge metadata from different versions: {base_version!r} != {extra_version!r}", + ) + + merged = copy.deepcopy(base) + merged["files"] = merge_files(base, extra) + + packages = merge_packages(base, extra) + if packages is not None: + merged["packages"] = packages + elif "packages" in merged: + del merged["packages"] + + release_date = merge_release_date(base, extra) + if release_date is not None: + merged["releaseDate"] = release_date + + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text( + yaml.safe_dump(merged, sort_keys=False, allow_unicode=False), + encoding="utf-8", + ) + + +if __name__ == "__main__": + main() From 43359b388d6e7c74e4ecf63c11dc173cf7dbbc7a Mon Sep 17 00:00:00 2001 From: Demir Yerli Date: Sun, 5 Apr 2026 10:59:11 +0300 Subject: [PATCH 11/12] bump version --- package.json | 4 +-- scripts/command-utils.mjs | 76 ++++++++++++++++++++------------------- scripts/tsconfig.json | 12 ------- src-tauri/Cargo.lock | 8 ++--- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 6 files changed, 47 insertions(+), 57 deletions(-) delete mode 100644 scripts/tsconfig.json diff --git a/package.json b/package.json index 4aa7ca3e..8f6a053d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "quadrant-next", "private": true, - "version": "26.4.1-preview.1", + "version": "26.4.1-preview.2", "type": "module", "description": "An easy way to manage your Minecraft mods and modpacks.", "author": "Demir Yerli ", @@ -85,4 +85,4 @@ "@tailwindcss/oxide", "electron" ] -} +} \ No newline at end of file diff --git a/scripts/command-utils.mjs b/scripts/command-utils.mjs index 8ca22340..60ef9ca8 100644 --- a/scripts/command-utils.mjs +++ b/scripts/command-utils.mjs @@ -1,50 +1,52 @@ +/** @format */ + import { existsSync } from "node:fs"; import { homedir } from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; function formatCommand(command, args) { - return [command, ...args].join(" "); + return [command, ...args].join(" "); } export function resolveCargoCommand() { - if (process.env.CARGO?.trim()) { - return process.env.CARGO.trim(); - } - - const rustupCargo = path.join( - homedir(), - ".cargo", - "bin", - process.platform === "win32" ? "cargo.exe" : "cargo", - ); - if (existsSync(rustupCargo)) { - return rustupCargo; - } - - return "cargo"; + if (process.env.CARGO?.trim()) { + return process.env.CARGO.trim(); + } + + const rustupCargo = path.join( + homedir(), + ".cargo", + "bin", + process.platform === "win32" ? "cargo.exe" : "cargo", + ); + if (existsSync(rustupCargo)) { + return rustupCargo; + } + + return "cargo"; } export function runCommand(command, args, options = {}) { - const { notFoundMessage, ...spawnOptions } = options; - const result = spawnSync(command, args, spawnOptions); - - if (result.error) { - if (result.error.code === "ENOENT") { - throw new Error( - notFoundMessage ?? - `Failed to run \`${formatCommand(command, args)}\`: command not found.`, - ); - } - - throw new Error( - `Failed to run \`${formatCommand(command, args)}\`: ${result.error.message}`, - ); - } - - if (result.status !== 0) { - process.exit(result.status ?? 1); - } - - return result; + const { notFoundMessage, ...spawnOptions } = options; + const result = spawnSync(command, args, spawnOptions); + + if (result.error) { + if (result.error.code === "ENOENT") { + throw new Error( + notFoundMessage ?? + `Failed to run \`${formatCommand(command, args)}\`: command not found.`, + ); + } + + throw new Error( + `Failed to run \`${formatCommand(command, args)}\`: ${result.error.message}`, + ); + } + + if (result.status !== 0) { + process.exit(result.status ?? 1); + } + + return result; } diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json deleted file mode 100644 index 1a96a3a8..00000000 --- a/scripts/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "allowJs": true, - "checkJs": false, - "types": ["node"], - "skipLibCheck": true - }, - "include": ["./*.mjs"] -} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 14f6ee1e..8396d47a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4406,7 +4406,7 @@ checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" [[package]] name = "quadrant-core" -version = "26.4.1-preview.1" +version = "26.4.1-preview.2" dependencies = [ "anyhow", "chrono", @@ -4433,7 +4433,7 @@ dependencies = [ [[package]] name = "quadrant-host" -version = "26.4.1-preview.1" +version = "26.4.1-preview.2" dependencies = [ "anyhow", "colog", @@ -4452,7 +4452,7 @@ dependencies = [ [[package]] name = "quadrant-napi" -version = "26.4.1-preview.1" +version = "26.4.1-preview.2" dependencies = [ "napi", "napi-build", @@ -4464,7 +4464,7 @@ dependencies = [ [[package]] name = "quadrant_next" -version = "26.4.1-preview.1" +version = "26.4.1-preview.2" dependencies = [ "anyhow", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 352df189..72b59b73 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -13,7 +13,7 @@ members = [ ] [workspace.package] -version = "26.4.1-preview.1" +version = "26.4.1-preview.2" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9be4cafb..824f62ae 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Quadrant", - "version": "26.4.1-preview.1", + "version": "26.4.1-preview.2", "identifier": "dev.mrquantumoff.mcmodpackmanager", "build": { "beforeDevCommand": "bun dev", From 92b302d5db67ce090c088dc663bf146a5f7f3d21 Mon Sep 17 00:00:00 2001 From: Demir Yerli Date: Sun, 5 Apr 2026 11:12:43 +0300 Subject: [PATCH 12/12] use TS --- .gitignore | 2 + docs/desktop-backends.md | 5 +- electron-builder.json | 4 +- electron/{main.mjs => main.ts} | 491 +++++++++++------- electron/{preload.mjs => preload.ts} | 107 +++- package.json | 13 +- scripts/build-electron.mjs | 19 - scripts/build-electron.ts | 22 + scripts/{build-napi.mjs => build-napi.ts} | 23 +- scripts/command-utils.mjs | 52 -- scripts/command-utils.ts | 61 +++ scripts/{dev-electron.mjs => dev-electron.ts} | 111 ++-- ...ckage-electron.mjs => package-electron.ts} | 16 +- ...kage.mjs => sync-quadrant-node-package.ts} | 12 +- ...g.mjs => write-electron-runtime-config.ts} | 10 +- tsconfig.electron.json | 17 + tsconfig.scripts.json | 16 + 17 files changed, 635 insertions(+), 346 deletions(-) rename electron/{main.mjs => main.ts} (58%) rename electron/{preload.mjs => preload.ts} (54%) delete mode 100644 scripts/build-electron.mjs create mode 100644 scripts/build-electron.ts rename scripts/{build-napi.mjs => build-napi.ts} (86%) delete mode 100644 scripts/command-utils.mjs create mode 100644 scripts/command-utils.ts rename scripts/{dev-electron.mjs => dev-electron.ts} (65%) rename scripts/{package-electron.mjs => package-electron.ts} (68%) rename scripts/{sync-quadrant-node-package.mjs => sync-quadrant-node-package.ts} (74%) rename scripts/{write-electron-runtime-config.mjs => write-electron-runtime-config.ts} (75%) create mode 100644 tsconfig.electron.json create mode 100644 tsconfig.scripts.json diff --git a/.gitignore b/.gitignore index 1a190c59..dc53890b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ lerna-debug.log* node_modules dist dist-electron +dist-electron-shell dist-ssr *.local @@ -41,6 +42,7 @@ target/ AppxContent AppxBundles electron/runtime-config.generated.json +dist-electron-shell/runtime-config.generated.json packages/quadrant-node/index.js packages/quadrant-node/index.d.ts packages/quadrant-node/native/ diff --git a/docs/desktop-backends.md b/docs/desktop-backends.md index fbae66e8..7a42eaf6 100644 --- a/docs/desktop-backends.md +++ b/docs/desktop-backends.md @@ -24,8 +24,9 @@ Quadrant supports two desktop shells that share one renderer and one backend con ## Electron Backend -- Shell entrypoint: `electron/main.mjs` -- Preload bridge: `electron/preload.mjs` +- Shell entrypoint source: `electron/main.ts` +- Preload bridge source: `electron/preload.ts` +- Built Electron shell output: `dist-electron-shell/*.js` - Renderer bridge: `src/desktop/electron.ts` - Backend implementation: `@quadrant/quadrant-node` -> `quadrant-napi` -> `quadrant-host` diff --git a/electron-builder.json b/electron-builder.json index 2e07e4f1..b7569beb 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -6,13 +6,13 @@ }, "files": [ "dist/**/*", - "electron/**/*", + "dist-electron-shell/**/*", "packages/quadrant-node/**/*", "public/tray.png", "package.json" ], "extraMetadata": { - "main": "electron/main.mjs" + "main": "dist-electron-shell/main.js" }, "asar": true, "asarUnpack": ["packages/quadrant-node/native/**/*.node"], diff --git a/electron/main.mjs b/electron/main.ts similarity index 58% rename from electron/main.mjs rename to electron/main.ts index 85a0c46d..33ac8784 100644 --- a/electron/main.mjs +++ b/electron/main.ts @@ -4,7 +4,25 @@ * @format */ +import electron, { + type BrowserWindow as BrowserWindowType, + type OpenDialogOptions, + type Tray as TrayType, +} from "electron"; +import chokidar, { type FSWatcher } from "chokidar"; +import electronUpdater from "electron-updater"; import { + createQuadrantClient, + type QuadrantClient, +} from "@quadrant/quadrant-node"; +import fs from "node:fs"; +import fsPromises from "node:fs/promises"; +import http from "node:http"; +import { randomUUID } from "node:crypto"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const { app, BrowserWindow, Menu, @@ -14,25 +32,57 @@ import { ipcMain, nativeTheme, shell, -} from "electron"; -import electronUpdater from "electron-updater"; -import chokidar from "chokidar"; -import { createQuadrantClient } from "@quadrant/quadrant-node"; -import fs from "node:fs"; -import fsPromises from "node:fs/promises"; -import http from "node:http"; -import { randomUUID } from "node:crypto"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; +} = electron; +const { autoUpdater } = electronUpdater; + +interface RuntimeConfig { + oauthClientId: string; + oauthClientSecret: string; + quadrantApiKey: string; + apiBaseUrl: string | null; +} + +type RuntimeConfigFile = Partial; + +interface StoreData { + [key: string]: unknown; +} + +interface FsWatchOptions { + delayMs?: number; +} + +interface DesktopDialogOptions { + mode?: "open" | "save"; + multiple?: boolean; + directory?: boolean; + recursive?: boolean; + title?: string; + defaultPath?: string; +} + +interface WindowProgressState { + progress: number; + status?: "none" | "normal" | "error" | "paused" | "indeterminate"; +} + +interface OAuthStartOptions { + response?: string; + ports?: number[]; +} + +interface BackendEventEnvelope { + event: string; + payload: T; +} const __dirname = path.dirname(fileURLToPath(import.meta.url)); const rootDir = path.resolve(__dirname, ".."); const appId = "dev.mrquantumoff.mcmodpackmanager"; const schemes = ["quadrantnext", "curseforge", "modrinth"]; -const { autoUpdater } = electronUpdater; const packageMetadata = JSON.parse( fs.readFileSync(path.join(rootDir, "package.json"), "utf8"), -); +) as { version?: unknown }; const quadrantAppVersion = ( typeof packageMetadata.version === "string" && @@ -48,31 +98,32 @@ const isAutostart = argv.includes("--autostart"); const updaterDisabledByCli = argv.includes("--noupdater"); const devServerUrl = process.env.QUADRANT_ELECTRON_DEV_SERVER_URL; -let mainWindow = null; -let tray = null; -let quadrantClient = null; +let mainWindow: BrowserWindowType | null = null; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +let tray: TrayType | null = null; +let quadrantClient: QuadrantClient | null = null; let updaterConfigured = false; let updateDownloaded = false; -const storeCache = new Map(); -const storeWriteQueues = new Map(); -const watchRegistry = new Map(); -const oauthServers = new Map(); -const pendingDeepLinks = []; +const storeCache = new Map(); +const storeWriteQueues = new Map>(); +const watchRegistry = new Map(); +const oauthServers = new Map(); +const pendingDeepLinks: string[] = []; let openUrlRendererReady = false; -function broadcast(channel, payload) { +function broadcast(channel: string, payload: unknown): void { for (const window of BrowserWindow.getAllWindows()) { window.webContents.send(channel, payload); } } -function parseDeepLinkUrls(values) { +function parseDeepLinkUrls(values: string[]): string[] { return values.filter((value) => schemes.some((scheme) => value.startsWith(`${scheme}:`)), ); } -function flushPendingDeepLinks() { +function flushPendingDeepLinks(): void { if (!mainWindow || !openUrlRendererReady || pendingDeepLinks.length === 0) { return; } @@ -80,10 +131,9 @@ function flushPendingDeepLinks() { mainWindow.webContents.send("quadrant:open-url", urls); } -function loadGeneratedRuntimeConfig() { +function loadGeneratedRuntimeConfig(): RuntimeConfigFile { const runtimeConfigPath = path.join( - rootDir, - "electron", + __dirname, "runtime-config.generated.json", ); @@ -91,19 +141,21 @@ function loadGeneratedRuntimeConfig() { return {}; } - return JSON.parse(fs.readFileSync(runtimeConfigPath, "utf8")); + return JSON.parse( + fs.readFileSync(runtimeConfigPath, "utf8"), + ) as RuntimeConfigFile; } -function normalizeOptionalConfigValue(value) { +function normalizeOptionalConfigValue(value: unknown): string | null { if (typeof value !== "string") { - return value ?? null; + return value == null ? null : String(value); } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } -function normalizeRequiredConfigValue(value) { +function normalizeRequiredConfigValue(value: unknown): string { if (typeof value !== "string") { return ""; } @@ -111,7 +163,7 @@ function normalizeRequiredConfigValue(value) { return value.trim(); } -function getRuntimeConfig() { +function getRuntimeConfig(): RuntimeConfig { const generated = loadGeneratedRuntimeConfig(); return { oauthClientId: normalizeRequiredConfigValue( @@ -129,7 +181,7 @@ function getRuntimeConfig() { }; } -function ensureRuntimeSecrets(config) { +function ensureRuntimeSecrets(config: RuntimeConfig): void { if ( !config.oauthClientId || !config.oauthClientSecret || @@ -141,23 +193,23 @@ function ensureRuntimeSecrets(config) { } } -function getWindow() { +function getWindow(): BrowserWindowType { if (!mainWindow) { throw new Error("Main window is not ready"); } return mainWindow; } -function resolveIconPath() { +function resolveIconPath(): string { return path.join(rootDir, "src-tauri", "icons", "128x128.png"); } -function resolveTrayIconPath() { +function resolveTrayIconPath(): string { const trayIconPath = path.join(rootDir, "public", "tray.png"); return fs.existsSync(trayIconPath) ? trayIconPath : resolveIconPath(); } -function normalizeDesktopPlatform(platform) { +function normalizeDesktopPlatform(platform: NodeJS.Platform): string { switch (platform) { case "win32": return "windows"; @@ -168,22 +220,24 @@ function normalizeDesktopPlatform(platform) { } } -function storeFilePath(storeName) { +function storeFilePath(storeName: string): string { return path.join(app.getPath("userData"), storeName); } -async function readStore(storeName) { +async function readStore(storeName: string): Promise { const filePath = storeFilePath(storeName); await fsPromises.mkdir(path.dirname(filePath), { recursive: true }); - let data = {}; + let data: StoreData = {}; if (fs.existsSync(filePath)) { try { - data = JSON.parse(await fsPromises.readFile(filePath, "utf8")); + data = JSON.parse( + await fsPromises.readFile(filePath, "utf8"), + ) as StoreData; } catch (error) { if (storeCache.has(storeName)) { console.warn(`Failed to parse ${filePath}, using cached copy`, error); - return storeCache.get(storeName); + return storeCache.get(storeName) ?? {}; } console.warn(`Failed to parse ${filePath}`, error); } @@ -193,13 +247,19 @@ async function readStore(storeName) { return data; } -async function writeStoreFile(filePath, data) { +async function writeStoreFile( + filePath: string, + data: StoreData, +): Promise { const tempPath = `${filePath}.${randomUUID()}.tmp`; await fsPromises.writeFile(tempPath, JSON.stringify(data, null, 2)); await fsPromises.rename(tempPath, filePath); } -async function saveStore(storeName, dataOverride) { +async function saveStore( + storeName: string, + dataOverride?: StoreData, +): Promise { const data = dataOverride ?? storeCache.get(storeName) ?? (await readStore(storeName)); const filePath = storeFilePath(storeName); @@ -207,9 +267,12 @@ async function saveStore(storeName, dataOverride) { await writeStoreFile(filePath, data); } -function queueStoreWrite(storeName, operation) { +function queueStoreWrite( + storeName: string, + operation: () => Promise, +): Promise { const previous = storeWriteQueues.get(storeName) ?? Promise.resolve(); - const next = previous.catch(() => {}).then(operation); + const next = previous.catch(() => undefined).then(operation); storeWriteQueues.set(storeName, next); return next.finally(() => { if (storeWriteQueues.get(storeName) === next) { @@ -218,7 +281,11 @@ function queueStoreWrite(storeName, operation) { }); } -async function setStoreValue(storeName, key, value) { +async function setStoreValue( + storeName: string, + key: string, + value: unknown, +): Promise { await queueStoreWrite(storeName, async () => { const data = await readStore(storeName); data[key] = value; @@ -228,7 +295,7 @@ async function setStoreValue(storeName, key, value) { broadcast("quadrant:store:changed", { storeName, key, value }); } -function queueDeepLinks(urls) { +function queueDeepLinks(urls: string[]): void { if (urls.length === 0) { return; } @@ -238,13 +305,11 @@ function queueDeepLinks(urls) { } } -async function createQuadrantHostClient() { +async function createQuadrantHostClient(): Promise { if (quadrantClient) { return quadrantClient; } - // Electron talks to the same QuadrantHost backend contract as Tauri, but it - // instantiates it through the N-API addon instead of Rust invoke handlers. const runtimeConfig = getRuntimeConfig(); ensureRuntimeSecrets(runtimeConfig); @@ -279,9 +344,10 @@ async function createQuadrantHostClient() { return quadrantClient; } -async function configureUpdater() { +async function configureUpdater(): Promise { const updateStore = await readStore("updateConfig.json"); - const channel = updateStore.channel ?? "stable"; + const channel = + typeof updateStore.channel === "string" ? updateStore.channel : "stable"; autoUpdater.allowPrerelease = channel !== "stable"; if (updaterConfigured) { @@ -296,7 +362,7 @@ async function configureUpdater() { broadcast("quadrant:backend-event", { event: "updateDownloadProgress", payload: progress.percent / 100, - }); + } satisfies BackendEventEnvelope); }); autoUpdater.on("update-downloaded", () => { @@ -304,7 +370,7 @@ async function configureUpdater() { broadcast("quadrant:backend-event", { event: "updateDownloadProgress", payload: 1, - }); + } satisfies BackendEventEnvelope); }); autoUpdater.on("update-not-available", () => { @@ -312,11 +378,11 @@ async function configureUpdater() { }); } -function isAutoupdateEnabled() { +function isAutoupdateEnabled(): boolean { return app.isPackaged && !updaterDisabledByCli; } -async function requestCheckForUpdates() { +async function requestCheckForUpdates(): Promise { if (!isAutoupdateEnabled()) { return; } @@ -325,7 +391,7 @@ async function requestCheckForUpdates() { await autoUpdater.checkForUpdates(); } -function createMainWindow() { +function createMainWindow(): BrowserWindowType { openUrlRendererReady = false; nativeTheme.themeSource = "dark"; @@ -340,7 +406,7 @@ function createMainWindow() { backgroundColor: "#020617", icon: resolveIconPath(), webPreferences: { - preload: path.join(__dirname, "preload.mjs"), + preload: path.join(__dirname, "preload.js"), contextIsolation: true, nodeIntegration: false, sandbox: false, @@ -369,16 +435,16 @@ function createMainWindow() { }); if (devServerUrl) { - window.loadURL(devServerUrl); + void window.loadURL(devServerUrl); window.webContents.openDevTools({ mode: "detach" }); } else { - window.loadFile(path.join(rootDir, "dist", "index.html")); + void window.loadFile(path.join(rootDir, "dist", "index.html")); } return window; } -function showMainWindow() { +function showMainWindow(): void { if (!mainWindow) { return; } @@ -389,7 +455,7 @@ function showMainWindow() { } } -function toggleMainWindowVisibility() { +function toggleMainWindowVisibility(): void { if (!mainWindow) { return; } @@ -401,7 +467,7 @@ function toggleMainWindowVisibility() { } } -function createTray() { +function createTray(): TrayType { const nextTray = new Tray(resolveTrayIconPath()); const menu = Menu.buildFromTemplate([ { @@ -427,12 +493,12 @@ function createTray() { return nextTray; } -function registerProtocolHandlers() { +function registerProtocolHandlers(): void { for (const scheme of schemes) { if (process.defaultApp) { if (process.argv.length >= 2) { app.setAsDefaultProtocolClient(scheme, process.execPath, [ - path.resolve(process.argv[1]), + path.resolve(process.argv[1] ?? ""), ]); } continue; @@ -500,101 +566,131 @@ app.on("before-quit", async () => { } }); -ipcMain.handle("quadrant:invoke", async (_event, { command, payload }) => { - const client = await createQuadrantHostClient(); - return client.invoke(command, payload ?? null); -}); +ipcMain.handle( + "quadrant:invoke", + async (_event, payload: { command: string; payload?: unknown }) => { + const client = await createQuadrantHostClient(); + return client.invoke(payload.command, payload.payload ?? null); + }, +); -ipcMain.handle("quadrant:store:get", async (_event, { storeName, key }) => { - const store = await readStore(storeName); - return store[key]; -}); +ipcMain.handle( + "quadrant:store:get", + async (_event, payload: { storeName: string; key: string }) => { + const store = await readStore(payload.storeName); + return store[payload.key]; + }, +); ipcMain.handle( "quadrant:store:set", - async (_event, { storeName, key, value }) => { - await setStoreValue(storeName, key, value); + async ( + _event, + payload: { storeName: string; key: string; value: unknown }, + ) => { + await setStoreValue(payload.storeName, payload.key, payload.value); }, ); -ipcMain.handle("quadrant:store:save", async (_event, { storeName }) => { - await queueStoreWrite(storeName, async () => { - await saveStore(storeName); - }); -}); +ipcMain.handle( + "quadrant:store:save", + async (_event, payload: { storeName: string }) => { + await queueStoreWrite(payload.storeName, async () => { + await saveStore(payload.storeName); + }); + }, +); ipcMain.handle( "quadrant:fs-watch:start", - async (event, { watchId, targetPath, options }) => { - const watcher = chokidar.watch(targetPath, { + async ( + event, + payload: { watchId: string; targetPath: string; options?: FsWatchOptions }, + ) => { + const watcher = chokidar.watch(payload.targetPath, { ignoreInitial: true, awaitWriteFinish: - options?.delayMs ? + payload.options?.delayMs ? { - stabilityThreshold: options.delayMs, - pollInterval: Math.max(50, Math.floor(options.delayMs / 2)), + stabilityThreshold: payload.options.delayMs, + pollInterval: Math.max(50, Math.floor(payload.options.delayMs / 2)), } : false, }); const sendChange = () => { - event.sender.send("quadrant:fs-watch:event", { watchId }); + event.sender.send("quadrant:fs-watch:event", { + watchId: payload.watchId, + }); }; watcher.on("add", sendChange); watcher.on("change", sendChange); watcher.on("unlink", sendChange); watcher.on("addDir", sendChange); watcher.on("unlinkDir", sendChange); - watchRegistry.set(watchId, watcher); + watchRegistry.set(payload.watchId, watcher); }, ); -ipcMain.handle("quadrant:fs-watch:stop", async (_event, { watchId }) => { - const watcher = watchRegistry.get(watchId); - if (watcher) { - await watcher.close(); - watchRegistry.delete(watchId); - } -}); +ipcMain.handle( + "quadrant:fs-watch:stop", + async (_event, payload: { watchId: string }) => { + const watcher = watchRegistry.get(payload.watchId); + if (watcher) { + await watcher.close(); + watchRegistry.delete(payload.watchId); + } + }, +); -ipcMain.handle("quadrant:path:join", async (_event, { segments }) => { - return path.join(...segments); -}); +ipcMain.handle( + "quadrant:path:join", + async (_event, payload: { segments: string[] }) => + path.join(...payload.segments), +); -ipcMain.handle("quadrant:dialog:open", async (_event, { options }) => { - if (options?.mode === "save") { - const result = await dialog.showSaveDialog(getWindow(), { +ipcMain.handle( + "quadrant:dialog:open", + async (_event, payload: { options?: DesktopDialogOptions }) => { + const { options } = payload; + + if (options?.mode === "save") { + const result = await dialog.showSaveDialog(getWindow(), { + title: options.title, + defaultPath: options.defaultPath, + }); + + if (result.canceled) { + return null; + } + + return result.filePath ?? null; + } + + const properties: OpenDialogOptions["properties"] = [ + options?.directory ? "openDirectory" : "openFile", + ...(options?.multiple ? (["multiSelections"] as const) : []), + ...(options?.directory && options?.recursive ? + (["createDirectory"] as const) + : []), + ]; + + const result = await dialog.showOpenDialog(getWindow(), { title: options?.title, + properties, defaultPath: options?.defaultPath, - showOverwriteConfirmation: true, }); if (result.canceled) { return null; } - return result.filePath ?? null; - } - - const result = await dialog.showOpenDialog(getWindow(), { - title: options?.title, - properties: [ - options?.directory ? "openDirectory" : "openFile", - ...(options?.multiple ? ["multiSelections"] : []), - ...(options?.directory && options?.recursive ? ["createDirectory"] : []), - ], - defaultPath: options?.defaultPath, - }); - - if (result.canceled) { - return null; - } - - if (options?.multiple) { - return result.filePaths; - } + if (options?.multiple) { + return result.filePaths; + } - return result.filePaths[0] ?? null; -}); + return result.filePaths[0] ?? null; + }, +); ipcMain.handle("quadrant:open-url:listener-ready", (event) => { if (mainWindow && event.sender.id === mainWindow.webContents.id) { @@ -603,23 +699,32 @@ ipcMain.handle("quadrant:open-url:listener-ready", (event) => { } }); -ipcMain.handle("quadrant:shell:open-external", async (_event, { url }) => { - await shell.openExternal(url); -}); +ipcMain.handle( + "quadrant:shell:open-external", + async (_event, payload: { url: string }) => { + await shell.openExternal(payload.url); + }, +); -ipcMain.handle("quadrant:shell:open-path", async (_event, { targetPath }) => { - const errorMessage = await shell.openPath(targetPath); - if (errorMessage) { - throw new Error(errorMessage); - } -}); +ipcMain.handle( + "quadrant:shell:open-path", + async (_event, payload: { targetPath: string }) => { + const errorMessage = await shell.openPath(payload.targetPath); + if (errorMessage) { + throw new Error(errorMessage); + } + }, +); ipcMain.handle("quadrant:clipboard:read-text", async () => clipboard.readText(), ); -ipcMain.handle("quadrant:clipboard:write-text", async (_event, { text }) => { - clipboard.writeText(text); -}); +ipcMain.handle( + "quadrant:clipboard:write-text", + async (_event, payload: { text: string }) => { + clipboard.writeText(payload.text); + }, +); ipcMain.handle("quadrant:platform", async () => normalizeDesktopPlatform(process.platform), @@ -653,9 +758,12 @@ ipcMain.handle("quadrant:window:hide", async () => { getWindow().hide(); }); -ipcMain.handle("quadrant:window:set-enabled", async (_event, { enabled }) => { - getWindow().setEnabled(enabled); -}); +ipcMain.handle( + "quadrant:window:set-enabled", + async (_event, payload: { enabled: boolean }) => { + getWindow().setEnabled(payload.enabled); + }, +); ipcMain.handle("quadrant:window:set-focus", async () => { getWindow().focus(); @@ -670,61 +778,74 @@ ipcMain.handle("quadrant:window:unminimize", async () => { ipcMain.handle( "quadrant:window:set-progress-bar", - async (_event, { state }) => { + async (_event, payload: { state: WindowProgressState }) => { const window = getWindow(); const normalizedProgress = - typeof state.progress === "number" && state.progress <= 1 ? - state.progress - : state.progress / 100; - const progress = state.status === "none" ? -1 : normalizedProgress; + ( + typeof payload.state.progress === "number" && + payload.state.progress <= 1 + ) ? + payload.state.progress + : payload.state.progress / 100; + const progress = payload.state.status === "none" ? -1 : normalizedProgress; window.setProgressBar(progress); }, ); -ipcMain.handle("quadrant:oauth:start", async (_event, { options }) => { - const responseHtml = - options?.response ?? - "

You can return to Quadrant now.

"; - const ports = options?.ports ?? [4000, 4001, 4002, 4003, 4004, 4005]; - - for (const port of ports) { - try { - const server = http.createServer((request, response) => { - const url = `http://127.0.0.1:${port}${request.url ?? "/"}`; - broadcast("quadrant:oauth:url", url); - response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); - response.end(responseHtml); - }); - - await new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(port, "127.0.0.1", () => resolve(undefined)); - }); - - oauthServers.set(port, server); - return port; - } catch (error) { - console.warn(`Failed to bind OAuth server on ${port}`, error); +ipcMain.handle( + "quadrant:oauth:start", + async (_event, payload: { options?: OAuthStartOptions }) => { + const responseHtml = + payload.options?.response ?? + "

You can return to Quadrant now.

"; + const ports = payload.options?.ports ?? [ + 4000, 4001, 4002, 4003, 4004, 4005, + ]; + + for (const port of ports) { + try { + const server = http.createServer((request, response) => { + const url = `http://127.0.0.1:${port}${request.url ?? "/"}`; + broadcast("quadrant:oauth:url", url); + response.writeHead(200, { + "Content-Type": "text/html; charset=utf-8", + }); + response.end(responseHtml); + }); + + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(port, "127.0.0.1", () => resolve()); + }); + + oauthServers.set(port, server); + return port; + } catch (error) { + console.warn(`Failed to bind OAuth server on ${port}`, error); + } } - } - throw new Error("No available OAuth callback port"); -}); + throw new Error("No available OAuth callback port"); + }, +); -ipcMain.handle("quadrant:oauth:cancel", async (_event, { port }) => { - const server = oauthServers.get(port); - if (!server) { - return; - } +ipcMain.handle( + "quadrant:oauth:cancel", + async (_event, payload: { port: number }) => { + const server = oauthServers.get(payload.port); + if (!server) { + return; + } - await new Promise((resolve, reject) => { - server.close((error) => { - if (error) { - reject(error); - return; - } - resolve(undefined); + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); }); - }); - oauthServers.delete(port); -}); + oauthServers.delete(payload.port); + }, +); diff --git a/electron/preload.mjs b/electron/preload.ts similarity index 54% rename from electron/preload.mjs rename to electron/preload.ts index 6686340f..c2e80640 100644 --- a/electron/preload.mjs +++ b/electron/preload.ts @@ -1,15 +1,101 @@ -import { contextBridge, ipcRenderer } from "electron"; +import electron from "electron"; import { randomUUID } from "node:crypto"; -function listen(channel, listener) { - const wrapped = (_event, payload) => listener(payload); +const { contextBridge, ipcRenderer } = electron; + +type UnlistenFn = () => void | Promise; + +interface DesktopWatchOptions { + delayMs?: number; +} + +interface DesktopDialogOptions { + mode?: "open" | "save"; + multiple?: boolean; + directory?: boolean; + recursive?: boolean; + title?: string; + defaultPath?: string; +} + +interface DesktopWindowProgressState { + progress: number; + status?: "none" | "normal" | "error" | "paused" | "indeterminate"; +} + +interface DesktopOAuthStartOptions { + response?: string; + ports?: number[]; +} + +interface DesktopBackendEventEnvelope { + event: string; + payload: T; +} + +interface DesktopStoreChangeEvent { + storeName: string; + key: string; + value: unknown; +} + +interface FsWatchEventPayload { + watchId: string; +} + +interface ElectronBridge { + invoke(command: string, payload?: unknown): Promise; + addBackendEventListener( + listener: (event: DesktopBackendEventEnvelope) => void, + ): UnlistenFn; + storeGet(storeName: string, key: string): Promise; + storeSet(storeName: string, key: string, value: unknown): Promise; + storeSave(storeName: string): Promise; + addStoreChangeListener( + listener: (event: DesktopStoreChangeEvent) => void, + ): UnlistenFn; + watchPath( + targetPath: string, + options: DesktopWatchOptions | undefined, + listener: () => void, + ): Promise; + joinPath(...segments: string[]): Promise; + openDialog(options: DesktopDialogOptions): Promise; + openExternal(url: string): Promise; + openPath(targetPath: string): Promise; + readClipboardText(): Promise; + writeClipboardText(text: string): Promise; + platform(): Promise; + getAppVersion(): Promise; + getRuntimeVersion(): Promise; + requestCheckForUpdates(): Promise; + installUpdate(): Promise; + isAutoupdateEnabled(): Promise; + windowMinimize(): Promise; + windowHide(): Promise; + windowSetEnabled(enabled: boolean): Promise; + windowSetFocus(): Promise; + windowUnminimize(): Promise; + windowSetProgressBar(state: DesktopWindowProgressState): Promise; + addOpenUrlListener(listener: (urls: string[]) => void): UnlistenFn; + startOAuthServer(options: DesktopOAuthStartOptions): Promise; + cancelOAuthServer(port: number): Promise; + addOAuthUrlListener(listener: (url: string) => void): UnlistenFn; +} + +function listen( + channel: string, + listener: (payload: T) => void, +): UnlistenFn { + const wrapped = (_event: Electron.IpcRendererEvent, payload: T) => + listener(payload); ipcRenderer.on(channel, wrapped); return () => { ipcRenderer.removeListener(channel, wrapped); }; } -const api = { +const api: ElectronBridge = { invoke(command, payload) { return ipcRenderer.invoke("quadrant:invoke", { command, payload }); }, @@ -30,11 +116,14 @@ const api = { }, async watchPath(targetPath, options, listener) { const watchId = randomUUID(); - const stopListening = listen("quadrant:fs-watch:event", (payload) => { - if (payload.watchId === watchId) { - listener(); - } - }); + const stopListening = listen( + "quadrant:fs-watch:event", + (payload) => { + if (payload.watchId === watchId) { + listener(); + } + }, + ); await ipcRenderer.invoke("quadrant:fs-watch:start", { watchId, targetPath, diff --git a/package.json b/package.json index 8f6a053d..6911ad3d 100644 --- a/package.json +++ b/package.json @@ -11,14 +11,17 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", + "build:electron-shell": "tsc -p tsconfig.electron.json", "lint": "eslint .", "preview": "vite preview", - "build:napi": "node scripts/build-napi.mjs", + "typecheck:scripts": "tsc -p tsconfig.scripts.json", + "typecheck:electron": "tsc -p tsconfig.electron.json --noEmit", + "build:napi": "bun scripts/build-napi.ts", "dev:tauri": "tauri dev", "build:tauri": "tauri build", - "dev:electron": "node scripts/dev-electron.mjs", - "build:electron": "node scripts/build-electron.mjs", - "package:electron": "node scripts/package-electron.mjs", + "dev:electron": "bun scripts/dev-electron.ts", + "build:electron": "bun scripts/build-electron.ts", + "package:electron": "bun scripts/package-electron.ts", "tauri": "tauri", "tauri:windows:build": "tauri-windows-bundle build" }, @@ -85,4 +88,4 @@ "@tailwindcss/oxide", "electron" ] -} \ No newline at end of file +} diff --git a/scripts/build-electron.mjs b/scripts/build-electron.mjs deleted file mode 100644 index 4537bc88..00000000 --- a/scripts/build-electron.mjs +++ /dev/null @@ -1,19 +0,0 @@ -/** @format */ - -import path from "node:path"; -import { runCommand } from "./command-utils.mjs"; - -const rootDir = path.resolve(import.meta.dirname, ".."); -const extraArgs = process.argv.slice(2); - -function run(command, args) { - runCommand(command, args, { - cwd: rootDir, - stdio: "inherit", - shell: process.platform === "win32", - }); -} - -run("node", ["scripts/write-electron-runtime-config.mjs"]); -run("bun", ["run", "build"]); -run("node", ["scripts/build-napi.mjs", "--release", ...extraArgs]); diff --git a/scripts/build-electron.ts b/scripts/build-electron.ts new file mode 100644 index 00000000..8b6cc6cd --- /dev/null +++ b/scripts/build-electron.ts @@ -0,0 +1,22 @@ +/** @format */ + +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { runCommand } from "./command-utils.ts"; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(scriptDir, ".."); +const extraArgs = process.argv.slice(2); + +function run(command: string, args: string[]): void { + runCommand(command, args, { + cwd: rootDir, + stdio: "inherit", + shell: process.platform === "win32", + }); +} + +run("bun", ["run", "build"]); +run("bun", ["run", "build:electron-shell"]); +run("bun", ["scripts/write-electron-runtime-config.ts"]); +run("bun", ["scripts/build-napi.ts", "--release", ...extraArgs]); diff --git a/scripts/build-napi.mjs b/scripts/build-napi.ts similarity index 86% rename from scripts/build-napi.mjs rename to scripts/build-napi.ts index 1feb9f73..da30489c 100644 --- a/scripts/build-napi.mjs +++ b/scripts/build-napi.ts @@ -1,15 +1,17 @@ import { cpSync, existsSync, mkdirSync, readdirSync } from "node:fs"; import path from "node:path"; -import { resolveCargoCommand, runCommand } from "./command-utils.mjs"; -import { syncQuadrantNodePackage } from "./sync-quadrant-node-package.mjs"; +import { fileURLToPath } from "node:url"; +import { resolveCargoCommand, runCommand } from "./command-utils.ts"; +import { syncQuadrantNodePackage } from "./sync-quadrant-node-package.ts"; -const rootDir = path.resolve(import.meta.dirname, ".."); +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(scriptDir, ".."); const srcTauriDir = path.join(rootDir, "src-tauri"); const args = process.argv.slice(2); const release = args.includes("--release"); const profile = release ? "release" : "debug"; -function getArgValue(flag) { +function getArgValue(flag: string): string | undefined { const exactMatch = args.find((arg) => arg.startsWith(`${flag}=`)); if (exactMatch) { return exactMatch.slice(flag.length + 1); @@ -23,7 +25,7 @@ function getArgValue(flag) { return undefined; } -function normalizePlatform(platform) { +function normalizePlatform(platform: string | undefined): string { if (!platform) { return process.platform; } @@ -39,7 +41,7 @@ function normalizePlatform(platform) { } } -function normalizeArch(arch) { +function normalizeArch(arch: string | undefined): string { if (!arch) { return process.arch; } @@ -54,7 +56,7 @@ function normalizeArch(arch) { } } -function getRustTargetTriple(platform, arch) { +function getRustTargetTriple(platform: string, arch: string): string { const key = `${platform}-${arch}`; switch (key) { case "win32-x64": @@ -76,7 +78,7 @@ function getRustTargetTriple(platform, arch) { } } -function findNativeBinary(directory) { +function findNativeBinary(directory: string): string | null { const entries = readdirSync(directory, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(directory, entry.name); @@ -152,11 +154,12 @@ try { cpSync(builtNode, path.join(nativeDir, "index.node")); } catch (error) { if ( - error && + error instanceof Error && + "code" in error && (error.code === "EIO" || error.code === "EPERM" || error.code === "EBUSY") ) { throw new Error( - `Built Quadrant N-API successfully, but could not update packages/quadrant-node/native/index.node because it is locked. Close Electron or any process using the addon, then rerun \`node scripts/build-napi.mjs\`. Original error: ${error.message}`, + `Built Quadrant N-API successfully, but could not update packages/quadrant-node/native/index.node because it is locked. Close Electron or any process using the addon, then rerun \`bun scripts/build-napi.ts\`. Original error: ${error.message}`, ); } throw error; diff --git a/scripts/command-utils.mjs b/scripts/command-utils.mjs deleted file mode 100644 index 60ef9ca8..00000000 --- a/scripts/command-utils.mjs +++ /dev/null @@ -1,52 +0,0 @@ -/** @format */ - -import { existsSync } from "node:fs"; -import { homedir } from "node:os"; -import path from "node:path"; -import { spawnSync } from "node:child_process"; - -function formatCommand(command, args) { - return [command, ...args].join(" "); -} - -export function resolveCargoCommand() { - if (process.env.CARGO?.trim()) { - return process.env.CARGO.trim(); - } - - const rustupCargo = path.join( - homedir(), - ".cargo", - "bin", - process.platform === "win32" ? "cargo.exe" : "cargo", - ); - if (existsSync(rustupCargo)) { - return rustupCargo; - } - - return "cargo"; -} - -export function runCommand(command, args, options = {}) { - const { notFoundMessage, ...spawnOptions } = options; - const result = spawnSync(command, args, spawnOptions); - - if (result.error) { - if (result.error.code === "ENOENT") { - throw new Error( - notFoundMessage ?? - `Failed to run \`${formatCommand(command, args)}\`: command not found.`, - ); - } - - throw new Error( - `Failed to run \`${formatCommand(command, args)}\`: ${result.error.message}`, - ); - } - - if (result.status !== 0) { - process.exit(result.status ?? 1); - } - - return result; -} diff --git a/scripts/command-utils.ts b/scripts/command-utils.ts new file mode 100644 index 00000000..ff992cc7 --- /dev/null +++ b/scripts/command-utils.ts @@ -0,0 +1,61 @@ +/** @format */ + +import { spawnSync, type SpawnSyncOptions } from "node:child_process"; +import { existsSync } from "node:fs"; +import { homedir } from "node:os"; +import path from "node:path"; + +export interface RunCommandOptions extends SpawnSyncOptions { + notFoundMessage?: string; +} + +function formatCommand(command: string, args: readonly string[]): string { + return [command, ...args].join(" "); +} + +export function resolveCargoCommand(): string { + if (process.env.CARGO?.trim()) { + return process.env.CARGO.trim(); + } + + const rustupCargo = path.join( + homedir(), + ".cargo", + "bin", + process.platform === "win32" ? "cargo.exe" : "cargo", + ); + if (existsSync(rustupCargo)) { + return rustupCargo; + } + + return "cargo"; +} + +export function runCommand( + command: string, + args: string[], + options: RunCommandOptions = {}, +): ReturnType { + const { notFoundMessage, ...spawnOptions } = options; + const result = spawnSync(command, args, spawnOptions); + + if (result.error) { + const error = result.error as NodeJS.ErrnoException; + if (error.code === "ENOENT") { + throw new Error( + notFoundMessage ?? + `Failed to run \`${formatCommand(command, args)}\`: command not found.`, + ); + } + + throw new Error( + `Failed to run \`${formatCommand(command, args)}\`: ${error.message}`, + ); + } + + if (result.status !== 0) { + process.exit(result.status ?? 1); + } + + return result; +} diff --git a/scripts/dev-electron.mjs b/scripts/dev-electron.ts similarity index 65% rename from scripts/dev-electron.mjs rename to scripts/dev-electron.ts index a13135eb..f640e5da 100644 --- a/scripts/dev-electron.mjs +++ b/scripts/dev-electron.ts @@ -1,18 +1,26 @@ -import { spawn } from "node:child_process"; +import { spawn, type ChildProcess } from "node:child_process"; import http from "node:http"; import path from "node:path"; -import chokidar from "chokidar"; +import { fileURLToPath } from "node:url"; +import chokidar, { type FSWatcher } from "chokidar"; -const rootDir = path.resolve(import.meta.dirname, ".."); +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(scriptDir, ".."); const devUrl = "http://127.0.0.1:1420"; const electronWatchGlobs = [ - "electron/**/*", + "electron/**/*.ts", "package.json", - "scripts/build-napi.mjs", - "scripts/command-utils.mjs", - "scripts/sync-quadrant-node-package.mjs", + "electron-builder.json", + "tsconfig.electron.json", + "tsconfig.scripts.json", + "scripts/build-napi.ts", + "scripts/build-electron.ts", + "scripts/command-utils.ts", + "scripts/dev-electron.ts", + "scripts/package-electron.ts", + "scripts/sync-quadrant-node-package.ts", "scripts/quadrant-node/**/*", - "scripts/write-electron-runtime-config.mjs", + "scripts/write-electron-runtime-config.ts", "src-tauri/Cargo.toml", "src-tauri/Cargo.lock", "src-tauri/crates/quadrant-napi/**/*", @@ -20,7 +28,7 @@ const electronWatchGlobs = [ const ignoredWatchGlobs = [ "**/.DS_Store", "**/node_modules/**", - "electron/runtime-config.generated.json", + "dist-electron-shell/runtime-config.generated.json", "packages/quadrant-node/native/**", "src-tauri/target/**", ]; @@ -28,10 +36,14 @@ const ignoredWatchGlobs = [ let isShuttingDown = false; let isRestartingElectron = false; let electronRestartQueued = false; -let electronRestartTimer = null; -let electronProcess = null; - -function spawnProcess(command, args, extraEnv = {}) { +let electronRestartTimer: ReturnType | null = null; +let electronProcess: ChildProcess | null = null; + +function spawnProcess( + command: string, + args: string[], + extraEnv: NodeJS.ProcessEnv = {}, +): ChildProcess { return spawn(command, args, { cwd: rootDir, stdio: "inherit", @@ -43,14 +55,17 @@ function spawnProcess(command, args, extraEnv = {}) { }); } -function waitForChildProcess(childProcess, label) { +function waitForChildProcess( + childProcess: ChildProcess, + label: string, +): Promise { return new Promise((resolve, reject) => { childProcess.on("error", (error) => { reject(new Error(`Failed to start ${label}: ${error.message}`)); }); childProcess.on("exit", (code, signal) => { if (code === 0) { - resolve(undefined); + resolve(); return; } if (signal) { @@ -62,17 +77,17 @@ function waitForChildProcess(childProcess, label) { }); } -async function waitForUrl(url, timeoutMs = 120000) { +async function waitForUrl(url: string, timeoutMs = 120_000): Promise { const startedAt = Date.now(); while (Date.now() - startedAt < timeoutMs) { - const isReady = await new Promise((resolve) => { + const isReady = await new Promise((resolve) => { const request = http.get(url, (response) => { response.resume(); resolve(response.statusCode !== undefined && response.statusCode < 500); }); request.on("error", () => resolve(false)); - request.setTimeout(2000, () => { + request.setTimeout(2_000, () => { request.destroy(); resolve(false); }); @@ -82,34 +97,39 @@ async function waitForUrl(url, timeoutMs = 120000) { return; } - await new Promise((resolve) => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 1_000)); } throw new Error(`Timed out waiting for ${url}`); } -async function runPrepareStep(command, args, label) { +async function runPrepareStep( + command: string, + args: string[], + label: string, +): Promise { const childProcess = spawnProcess(command, args); await waitForChildProcess(childProcess, label); } -async function runElectronPrepareSteps() { +async function runElectronPrepareSteps(): Promise { + await runPrepareStep("bun", ["run", "build:electron-shell"], "build:electron-shell"); await runPrepareStep( - "node", - ["scripts/write-electron-runtime-config.mjs"], - "scripts/write-electron-runtime-config.mjs", + "bun", + ["scripts/write-electron-runtime-config.ts"], + "scripts/write-electron-runtime-config.ts", ); await runPrepareStep( - "node", - ["scripts/build-napi.mjs"], - "scripts/build-napi.mjs", + "bun", + ["scripts/build-napi.ts"], + "scripts/build-napi.ts", ); } -function launchElectron() { +function launchElectron(): void { const nextElectronProcess = spawnProcess( "bunx", - ["electron", "electron/main.mjs"], + ["electron", "dist-electron-shell/main.js"], { QUADRANT_ELECTRON_DEV_SERVER_URL: devUrl, }, @@ -137,7 +157,7 @@ function launchElectron() { }); } -async function stopElectronProcess() { +async function stopElectronProcess(): Promise { if (!electronProcess || electronProcess.exitCode !== null) { electronProcess = null; return; @@ -145,7 +165,7 @@ async function stopElectronProcess() { const runningElectronProcess = electronProcess; - await new Promise((resolve) => { + await new Promise((resolve) => { let resolved = false; const finalize = () => { @@ -154,14 +174,14 @@ async function stopElectronProcess() { } resolved = true; clearTimeout(forceKillTimeout); - resolve(undefined); + resolve(); }; const forceKillTimeout = setTimeout(() => { if (!runningElectronProcess.killed) { runningElectronProcess.kill("SIGKILL"); } - }, 10000); + }, 10_000); runningElectronProcess.once("exit", finalize); runningElectronProcess.kill("SIGTERM"); @@ -172,7 +192,7 @@ async function stopElectronProcess() { } } -async function restartElectron(reason) { +async function restartElectron(reason: string): Promise { if (isShuttingDown) { return; } @@ -195,16 +215,16 @@ async function restartElectron(reason) { launchElectron(); } } catch (error) { - console.error( - `[dev:electron] Failed to restart Electron: ${error.message}`, - ); + const message = + error instanceof Error ? error.message : `Unknown error: ${String(error)}`; + console.error(`[dev:electron] Failed to restart Electron: ${message}`); } } while (electronRestartQueued && !isShuttingDown); isRestartingElectron = false; } -function scheduleElectronRestart(reason) { +function scheduleElectronRestart(reason: string): void { if (isShuttingDown) { return; } @@ -216,9 +236,9 @@ function scheduleElectronRestart(reason) { electronRestartTimer = setTimeout(() => { electronRestartTimer = null; restartElectron(reason).catch((error) => { - console.error( - `[dev:electron] Unexpected restart failure: ${error.message}`, - ); + const message = + error instanceof Error ? error.message : `Unknown error: ${String(error)}`; + console.error(`[dev:electron] Unexpected restart failure: ${message}`); }); }, 250); } @@ -231,7 +251,7 @@ const viteProcess = spawnProcess("bun", [ "127.0.0.1", ]); -const electronWatcher = chokidar.watch(electronWatchGlobs, { +const electronWatcher: FSWatcher = chokidar.watch(electronWatchGlobs, { cwd: rootDir, ignoreInitial: true, ignored: ignoredWatchGlobs, @@ -241,7 +261,7 @@ electronWatcher.on("all", (eventName, filePath) => { scheduleElectronRestart(`${eventName} ${filePath}`); }); -const cleanup = async () => { +const cleanup = async (): Promise => { if (isShuttingDown) { return; } @@ -253,10 +273,7 @@ const cleanup = async () => { electronRestartTimer = null; } - await Promise.allSettled([ - electronWatcher.close(), - stopElectronProcess(), - ]); + await Promise.allSettled([electronWatcher.close(), stopElectronProcess()]); if (viteProcess.exitCode === null) { viteProcess.kill("SIGTERM"); diff --git a/scripts/package-electron.mjs b/scripts/package-electron.ts similarity index 68% rename from scripts/package-electron.mjs rename to scripts/package-electron.ts index 9bdb6ee9..6878d71d 100644 --- a/scripts/package-electron.mjs +++ b/scripts/package-electron.ts @@ -1,10 +1,12 @@ import path from "node:path"; -import { runCommand } from "./command-utils.mjs"; +import { fileURLToPath } from "node:url"; +import { runCommand } from "./command-utils.ts"; -const rootDir = path.resolve(import.meta.dirname, ".."); +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(scriptDir, ".."); const extraArgs = process.argv.slice(2); -function getArgValue(flag) { +function getArgValue(flag: string): string | undefined { const exactMatch = extraArgs.find((arg) => arg.startsWith(`${flag}=`)); if (exactMatch) { return exactMatch.slice(flag.length + 1); @@ -18,7 +20,7 @@ function getArgValue(flag) { return undefined; } -function normalizeArch(arch) { +function normalizeArch(arch: string | undefined): string { if (!arch) { return process.arch; } @@ -33,13 +35,13 @@ function normalizeArch(arch) { } } -function hasPublishFlag(args) { +function hasPublishFlag(args: string[]): boolean { return args.some( (arg) => arg === "--publish" || arg.startsWith("--publish="), ); } -function run(command, args) { +function run(command: string, args: string[]): void { runCommand(command, args, { cwd: rootDir, stdio: "inherit", @@ -51,7 +53,7 @@ const targetArch = normalizeArch(getArgValue("--arch")); const electronBuilderArchFlag = `--${targetArch}`; const publishArgs = hasPublishFlag(extraArgs) ? [] : ["--publish", "never"]; -run("node", ["scripts/build-electron.mjs", ...extraArgs]); +run("bun", ["scripts/build-electron.ts", ...extraArgs]); run("bunx", [ "electron-builder", "--config", diff --git a/scripts/sync-quadrant-node-package.mjs b/scripts/sync-quadrant-node-package.ts similarity index 74% rename from scripts/sync-quadrant-node-package.mjs rename to scripts/sync-quadrant-node-package.ts index 09ed54f8..3822193a 100644 --- a/scripts/sync-quadrant-node-package.mjs +++ b/scripts/sync-quadrant-node-package.ts @@ -2,15 +2,16 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -const rootDir = path.resolve(import.meta.dirname, ".."); +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(scriptDir, ".."); const sourceDir = path.join(rootDir, "scripts", "quadrant-node"); const targetDir = path.join(rootDir, "packages", "quadrant-node"); -function readUtf8(filePath) { +function readUtf8(filePath: string): string { return readFileSync(filePath, "utf8"); } -function writeIfChanged(filePath, contents) { +function writeIfChanged(filePath: string, contents: string): boolean { try { if (readUtf8(filePath) === contents) { return false; @@ -23,7 +24,10 @@ function writeIfChanged(filePath, contents) { return true; } -export function syncQuadrantNodePackage() { +export function syncQuadrantNodePackage(): { + indexJs: boolean; + indexTypes: boolean; +} { mkdirSync(targetDir, { recursive: true }); return { diff --git a/scripts/write-electron-runtime-config.mjs b/scripts/write-electron-runtime-config.ts similarity index 75% rename from scripts/write-electron-runtime-config.mjs rename to scripts/write-electron-runtime-config.ts index 71daabf8..c97aae53 100644 --- a/scripts/write-electron-runtime-config.mjs +++ b/scripts/write-electron-runtime-config.ts @@ -1,14 +1,16 @@ import { mkdirSync, writeFileSync } from "node:fs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; -const rootDir = path.resolve(import.meta.dirname, ".."); +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(scriptDir, ".."); const outputPath = path.join( rootDir, - "electron", + "dist-electron-shell", "runtime-config.generated.json", ); -function normalizeOptionalConfigValue(value) { +function normalizeOptionalConfigValue(value: unknown): string | null { if (typeof value !== "string") { return null; } @@ -17,7 +19,7 @@ function normalizeOptionalConfigValue(value) { return trimmed.length > 0 ? trimmed : null; } -function normalizeRequiredConfigValue(value) { +function normalizeRequiredConfigValue(value: unknown): string { if (typeof value !== "string") { return ""; } diff --git a/tsconfig.electron.json b/tsconfig.electron.json new file mode 100644 index 00000000..a79f9e8e --- /dev/null +++ b/tsconfig.electron.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": ["node"], + "lib": ["ES2023"], + "strict": true, + "skipLibCheck": true, + "outDir": "dist-electron-shell", + "rootDir": "electron", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "resolveJsonModule": true + }, + "include": ["electron/**/*.ts"] +} diff --git a/tsconfig.scripts.json b/tsconfig.scripts.json new file mode 100644 index 00000000..b9cdeba6 --- /dev/null +++ b/tsconfig.scripts.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": ["node"], + "lib": ["ES2023"], + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true + }, + "include": ["scripts/**/*.ts"] +}