From 641eb2fcd550129a61c3ead60b8e26092da291a2 Mon Sep 17 00:00:00 2001 From: Simeon Radivoev Date: Fri, 15 May 2026 17:38:38 +0300 Subject: [PATCH] fix: Moved to manual plugin version checking and fixed some steam deck issues. --- bun.lock | 3 - package.json | 8 +- src/bun/api/auth.ts | 6 + .../com.simeonradivoev.gameflow.romm/romm.ts | 2 +- .../store.ts | 2 +- src/bun/api/plugins/plugin-manager.ts | 4 +- src/bun/api/plugins/register-plugins.ts | 23 ++- src/bun/api/plugins/services.ts | 8 +- src/bun/api/plugins/update-check.ts | 169 ++++++++++++++++++ .../routes/store/details.plugin.$id.tsx | 4 +- src/mainview/routes/store/tab/download.tsx | 9 +- src/mainview/scripts/queries/romm.ts | 9 +- src/packages/gameflow-sdk/build.ts | 0 13 files changed, 219 insertions(+), 28 deletions(-) create mode 100644 src/bun/api/plugins/update-check.ts mode change 100644 => 100755 src/packages/gameflow-sdk/build.ts diff --git a/bun.lock b/bun.lock index 5fbc781..f005808 100644 --- a/bun.lock +++ b/bun.lock @@ -25,7 +25,6 @@ "node-downloader-helper": "^2.1.11", "node-stream-zip": "^1.15.0", "node-unrar-js": "^2.0.2", - "npm-check-updates": "^22.2.0", "open": "^11.0.0", "p-queue": "^9.2.0", "pathe": "^2.0.3", @@ -1436,8 +1435,6 @@ "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - "npm-check-updates": ["npm-check-updates@22.2.0", "", { "bin": { "npm-check-updates": "build/cli.js", "ncu": "build/cli.js" } }, "sha512-kaxgbkGkCOtoSrsUXShgcEiEfrRPqmOGk6Yeya+5hoNptblu9vuE8/PLABUSJz+IeNgKJBFxcC3UrBYmKsB8iA=="], - "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], "nypm": ["nypm@0.6.4", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw=="], diff --git a/package.json b/package.json index d770d67..913be66 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "node-downloader-helper": "^2.1.11", "node-stream-zip": "^1.15.0", "node-unrar-js": "^2.0.2", - "npm-check-updates": "^22.2.0", "open": "^11.0.0", "p-queue": "^9.2.0", "pathe": "^2.0.3", @@ -90,6 +89,11 @@ "webview-bun": "^2.4.0", "zod": "^4.4.3" }, + "overrides": { + "@tanstack/router-generator": { + "zod": "^3.23.8" + } + }, "devDependencies": { "@ap0nia/eden": "^1.6.1", "@ap0nia/eden-tanstack-query": "^1.0.0-next.22", @@ -155,4 +159,4 @@ "vite-static-assets-plugin": "^1.2.2", "vite-tsconfig-paths": "^6.1.1" } -} +} \ No newline at end of file diff --git a/src/bun/api/auth.ts b/src/bun/api/auth.ts index 47ea019..2cd6e3f 100644 --- a/src/bun/api/auth.ts +++ b/src/bun/api/auth.ts @@ -138,6 +138,12 @@ export async function checkLoginAndRefreshTwitch () export async function checkLoginAndRefreshRomm () { + //TODO: move to plugin logic + if (plugins.plugins['com.simeonradivoev.gameflow.romm'].config?.get('clientApiToken')) + { + return { hasLogin: true }; + } + const access_token = await secrets.get({ service: 'gameflow', name: 'romm_access_token' }); if (!access_token) { diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts index 2e63269..2be6e68 100644 --- a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts +++ b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts @@ -44,7 +44,7 @@ export default class RommIntegration implements PluginType async getAccessToken (config: Conf) { if (process.env.ROMM_CLIENT_TOKEN) return process.env.ROMM_CLIENT_TOKEN; - const client_token = await config.get('clientApiToken'); + const client_token = config.get('clientApiToken'); if (client_token) return client_token; return (await secrets.get({ service: 'gameflow', name: 'romm_access_token' })) ?? undefined; } diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/store.ts b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/store.ts index 90d1cbc..92ce1f9 100644 --- a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/store.ts +++ b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/store.ts @@ -2,7 +2,7 @@ import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-s import desc from './package.json'; import path, { } from 'node:path'; import { buildStoreFrontendEmulatorSystems, getAllStoreEmulatorPackages, getStoreEmulatorPackage, getStoreFolder } from "@/bun/api/store/services/gamesService"; -import { Glob, pathToFileURL, sleep, which } from "bun"; +import { Glob, pathToFileURL, which } from "bun"; import { and, eq } from "drizzle-orm"; import * as emulatorSchema from '@schema/emulators'; diff --git a/src/bun/api/plugins/plugin-manager.ts b/src/bun/api/plugins/plugin-manager.ts index 1fab907..8b2a1b5 100644 --- a/src/bun/api/plugins/plugin-manager.ts +++ b/src/bun/api/plugins/plugin-manager.ts @@ -91,7 +91,7 @@ export class PluginManager return true; } - private async reload (name: string, reloadCtx: { setProgress: (progress: number, state: string) => void; }, update: string | undefined) + private async reload (name: string, reloadCtx: { setProgress: (progress: number, state: string) => void; }, update: string | undefined | null) { const plugin = this.plugins[name]; if (plugin) @@ -149,7 +149,7 @@ export class PluginManager for await (const id of Object.keys(this.plugins)) { ctx.setProgress(0, `Loading ${id}`); - await this.reload(id, ctx, outdated?.[id]); + await this.reload(id, ctx, outdated.find(i => i.package === id)?.update); } } diff --git a/src/bun/api/plugins/register-plugins.ts b/src/bun/api/plugins/register-plugins.ts index a4b5666..5746947 100644 --- a/src/bun/api/plugins/register-plugins.ts +++ b/src/bun/api/plugins/register-plugins.ts @@ -122,19 +122,24 @@ export default async function register (pluginManager: PluginManager) if (outdated) { - for await (const plugin of validPlugins) + for (let i = 0; i < validPlugins.length; i++) { - const newVersion = outdated[plugin.name]; + const plugin = validPlugins[i]; + const newVersion = outdated.find(i => i.package === plugin.name); if (newVersion) { - console.log("Plugin", plugin.name, "has update", plugin.version, "=>", newVersion); - } + console.log("Plugin", plugin.name, "has update", plugin.version, "=>", newVersion.update); - if (plugin.autoUpdate) - { - console.log("Auto Updating Plugin", plugin.name); - let response = await runBunPackageCommand(["add", `${plugin.name}@${newVersion}`, "--registry", PluginRegistry, '--omit', 'peer']); - console.log(response); + if (plugin.autoUpdate || plugin.name === '@simeonradivoev/gameflow-store') + { + console.log("Auto Updating Plugin", plugin.name); + let response = await runBunPackageCommand(["add", `${plugin.name}@${newVersion?.update}`, "--registry", PluginRegistry, '--omit', 'peer']); + console.log(response); + // Update plugin package + const newPlugin = await getPlugin(plugin.name, pluginManager); + if (newPlugin) + validPlugins[i] = newPlugin; + } } } } diff --git a/src/bun/api/plugins/services.ts b/src/bun/api/plugins/services.ts index 0ba40b3..8878809 100644 --- a/src/bun/api/plugins/services.ts +++ b/src/bun/api/plugins/services.ts @@ -2,8 +2,8 @@ import path from 'node:path'; import os from 'node:os'; import { getStoreRootFolder } from '../store/services/gamesService'; import { PluginDescriptionType } from '@simeonradivoev/gameflow-sdk'; -import { run } from 'npm-check-updates'; import { existsSync } from 'node:fs'; +import { checkOutdated } from './update-check'; export function canDisable (description: PluginDescriptionType) { @@ -16,9 +16,9 @@ export function canDisable (description: PluginDescriptionType) export async function getUpdates () { - if (!existsSync(getStoreRootFolder())) return {}; - const updated = await run({ packageManager: 'bun', peer: true, cwd: getStoreRootFolder(), jsonUpgraded: true, reject: ['@simeonradivoev/gameflow-sdk'] }); - return updated as Record; + if (!existsSync(getStoreRootFolder())) return []; + const results = await checkOutdated(getStoreRootFolder()); + return results; } export function canUninstall (description: PluginDescriptionType, source: string) diff --git a/src/bun/api/plugins/update-check.ts b/src/bun/api/plugins/update-check.ts new file mode 100644 index 0000000..66cf381 --- /dev/null +++ b/src/bun/api/plugins/update-check.ts @@ -0,0 +1,169 @@ +import { semver } from "bun"; +import { readFile } from "fs/promises"; +import { join } from "path"; +import { getOrCached } from "../cache"; +import { PluginRegistry } from "@/shared/constants"; +import sdkPkg from '@/packages/gameflow-sdk/package.json'; + +interface UpdateInfo +{ + package: string, + current: string, + update: string | null, + latest: string, + sdkConstrained: boolean, + sdkRange: string, + note: string | null; +} + +function parseBunOutdated (cwd: string) +{ + const proc = Bun.spawnSync([process.execPath, "outdated"], { + stderr: "inherit", env: { + BUN_BE_BUN: "1", + NO_COLOR: "1", + }, cwd: cwd + }); + const output = proc.stdout.toString(); + const lines = output.split("\n").filter(Boolean); + + const headerIndex = lines.findIndex( + (l) => l.includes("Package") && l.includes("Current") + ); + if (headerIndex === -1) return []; + + return lines + .slice(headerIndex + 1) + .filter((line) => !/^[-─╌| ]+$/.test(line)) + .map((line) => + { + const [, pkg, current, , latest] = line.split("|").map((c) => c.trim()); + return pkg ? { package: pkg, current, latest } : null; + }) + .filter(p => p !== null); +} + +async function getInstalledVersion (cwd: string, pkg: string) +{ + try + { + const raw = await readFile(join(cwd, "node_modules", pkg, "package.json"), "utf8"); + return JSON.parse(raw).version ?? null; + } catch + { + return null; + } +} + +async function fetchAllVersions (pkg: string) +{ + const res = await fetch(`${PluginRegistry}/${pkg}`); + if (!res.ok) return []; + const data = await res.json(); + return Object.keys(data.versions ?? {}); +} + +async function fetchPeerDeps (pkg: string, version: string) +{ + const peerDependencies = await getOrCached(`npm-${pkg}-${version}`, async () => + { + const res = await fetch(`${PluginRegistry}/${pkg}/${version}`); + if (!res.ok) + { + throw new Error(`Error while fetching peer deps for ${pkg} ${version} ${res.status} ${res.statusText}`); + } + const data = await res.json(); + return data.peerDependencies ?? {}; + }, { + //5 days + expireMs: 1000 * 60 * 60 * 24 * 5 + }); + + + return peerDependencies; +} + +async function findBestVersion (pkg: string, allVersions: string[], sdkVersion: string) +{ + // Sort descending so we find the highest compatible version first + const sorted = [...allVersions].sort((a, b) => semver.order(b, a)); + + for (const version of sorted) + { + const peers = await fetchPeerDeps(pkg, version); + const sdkRange = peers[sdkPkg.name]; + + if (!sdkRange) + { + // No peer dep on SDK — compatible by default + return { version, sdkRange: null }; + } + + if (semver.satisfies(sdkVersion, sdkRange)) + { + return { version, sdkRange }; + } + } + + return null; +} + +export async function checkOutdated (cwd: string) +{ + const outdated = parseBunOutdated(cwd); + + if (outdated.length === 0) + { + return []; + } + + const sdkVersion = await getInstalledVersion(cwd, sdkPkg.name); + if (!sdkVersion) + { + console.error(`Could not find installed version of ${sdkPkg.name} in node_modules.`); + process.exit(1); + } + + const results = await Promise.all( + outdated.map(async ({ package: pkg, current, latest }) => + { + const allVersions = await fetchAllVersions(pkg); + + // Check if the outright latest is already SDK compatible + const latestPeers = await fetchPeerDeps(pkg, latest); + const latestSdkRange = latestPeers[sdkPkg.name]; + + const latestCompatible = + !latestSdkRange || semver.satisfies(sdkVersion, latestSdkRange); + + if (latestCompatible) + { + return { + package: pkg, + current, + update: latest, + latest, + sdkConstrained: false, + sdkRange: latestSdkRange ?? null, + note: null + } satisfies UpdateInfo as UpdateInfo; + } + + const best = await findBestVersion(pkg, allVersions, sdkVersion); + + return { + package: pkg, + current, + update: best?.version ?? null, + latest, + sdkConstrained: true, + sdkRange: best?.sdkRange ?? null, + note: best + ? `Latest (${latest}) requires incompatible SDK range; best compatible: ${best.version}` + : `No version of ${pkg} is compatible with ${sdkPkg.name}@${sdkVersion}`, + } satisfies UpdateInfo as UpdateInfo; + }) + ); + + return results; +} \ No newline at end of file diff --git a/src/mainview/routes/store/details.plugin.$id.tsx b/src/mainview/routes/store/details.plugin.$id.tsx index 5e5168c..9e35cdb 100644 --- a/src/mainview/routes/store/details.plugin.$id.tsx +++ b/src/mainview/routes/store/details.plugin.$id.tsx @@ -112,10 +112,10 @@ function Details () {!!data.update && } - - diff --git a/src/mainview/routes/store/tab/download.tsx b/src/mainview/routes/store/tab/download.tsx index 062698a..a1f0172 100644 --- a/src/mainview/routes/store/tab/download.tsx +++ b/src/mainview/routes/store/tab/download.tsx @@ -1,5 +1,6 @@ import DotsLoading from '@/mainview/components/backgrounds/dots'; import LoadMoreButton from '@/mainview/components/LoadMoreButton'; +import { Button } from '@/mainview/components/options/Button'; import { SideDownloadFilters } from '@/mainview/components/SideFilters'; import { downloadLookupFiltersQuery, downloadsLookupQuery } from '@/mainview/scripts/queries/romm'; import { scrollIntoViewHandler } from '@/mainview/scripts/utils'; @@ -7,7 +8,7 @@ import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-naviga import { DownloadLookupEntry, DownloadsLookupFilter } from '@simeonradivoev/gameflow-sdk/shared'; import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; import { createFileRoute, useNavigate } from '@tanstack/react-router'; -import { DownloadIcon, Eye, MessageCircle, Save, Star } from 'lucide-react'; +import { ArrowRight, DownloadIcon, Eye, MessageCircle, Puzzle, Save, Star } from 'lucide-react'; import prettyBytes from 'pretty-bytes'; import { useSessionStorage } from 'usehooks-ts'; @@ -49,6 +50,7 @@ function Downloads (data: { pages: { data: DownloadLookupEntry[]; totalCount: number; + hadMatchers: boolean; nextPage: number; }[]; hasNextPage: boolean, @@ -58,12 +60,15 @@ function Downloads (data: { error: string | undefined; }) { + const navigate = useNavigate(); const { ref, focusKey } = useFocusable({ focusKey: 'downloads-list' }); return
    {data.pages.flatMap((page, p) => page.data.map((match, i) => ))} - {data.hasNextPage && } + {data.hasNextPage && data.pages[0].hadMatchers &&