From 03c3fe26a015e6a5ec7e35c92e6f8fcf3d16c6a8 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Fri, 3 Apr 2026 20:39:24 +0100 Subject: [PATCH 1/2] install recursive deps properly --- apps/app-frontend/src/store/install.js | 156 ++++++++++++++++++++----- 1 file changed, 124 insertions(+), 32 deletions(-) diff --git a/apps/app-frontend/src/store/install.js b/apps/app-frontend/src/store/install.js index 2df6da020b..315ea32d82 100644 --- a/apps/app-frontend/src/store/install.js +++ b/apps/app-frontend/src/store/install.js @@ -2,7 +2,7 @@ import dayjs from 'dayjs' -import { get_project, get_version_many } from '@/helpers/cache.js' +import { get_project, get_version, get_version_many } from '@/helpers/cache.js' import { add_project_from_version, check_installed } from '@/helpers/profile.js' import { add_server_to_profile, @@ -11,20 +11,8 @@ import { } from '@/helpers/worlds.ts' export const findPreferredVersion = (versions, project, instance) => { - // When `project` is passed in from this stack trace: - // - `installVersionDependencies` - // - `install.js/install` - `installVersionDependencies` call - // - // ..then `project` is actually a `Dependency` struct of a cached `Version`. - // `Dependency` does not have a `project_type` field, - // so we default it to `mod`. - // - // If we don't default here, then this `.find` will ignore version/instance - // loader mismatches, and you'll end up e.g. installing NeoForge mods for a - // Fabric instance. const projectType = project.project_type ?? 'mod' - // If we can find a version using strictly the instance loader then prefer that let version = versions.find( (v) => v.game_versions.includes(instance.game_version) && @@ -32,7 +20,6 @@ export const findPreferredVersion = (versions, project, instance) => { ) if (!version) { - // Otherwise use first compatible version (in addition to versions with the instance loader this includes datapacks) version = versions.find((v) => isVersionCompatible(v, project, instance)) } @@ -49,33 +36,138 @@ export const isVersionCompatible = (version, project, instance) => { } export const installVersionDependencies = async (profile, version, onDepInstalling) => { - for (const dep of version.dependencies) { - if (dep.dependency_type !== 'required') continue - // disallow fabric api install on quilt - if (dep.project_id === 'P7dR8mSH' && profile.loader === 'quilt') continue + const projectNames = new Map() + const storeProjectName = (p) => { + if (p?.id && p.title) projectNames.set(p.id, p.title) + } + + const visitedVersions = new Set() + const announcedProjects = new Set() + const queuedVersionIds = new Set() + const queuedProjectVersions = new Map() + const queuedInstalls = [] + const installedProjectCache = new Map() + + const isProjectInstalled = async (projectId) => { + if (!projectId) return false + if (installedProjectCache.has(projectId)) { + return installedProjectCache.get(projectId) + } + const installed = await check_installed(profile.path, projectId) + installedProjectCache.set(projectId, installed) + return installed + } + + const queueInstall = async (projectId, resolvedVersion) => { + if (!resolvedVersion?.id) return false + + const versionId = resolvedVersion.id + const resolvedProjectId = projectId ?? resolvedVersion.project_id ?? null + + if (resolvedProjectId) { + if (await isProjectInstalled(resolvedProjectId)) return false + + const existingVersionId = queuedProjectVersions.get(resolvedProjectId) + if (existingVersionId && existingVersionId !== versionId) return false + if (existingVersionId === versionId) return false + } + + if (queuedVersionIds.has(versionId)) return false + + queuedVersionIds.add(versionId) + if (resolvedProjectId) { + queuedProjectVersions.set(resolvedProjectId, versionId) + } + queuedInstalls.push({ versionId, projectId: resolvedProjectId }) + return true + } + + const announceDependency = async (projectId, resolvedVersion) => { + if (!onDepInstalling || !projectId) return + if (announcedProjects.has(projectId)) return + + const depProject = await get_project(projectId, 'bypass').catch(() => null) + if (!depProject) return + + storeProjectName(depProject) + onDepInstalling(depProject, resolvedVersion ?? undefined) + announcedProjects.add(projectId) + } + + const resolveDependency = async (dep, inputVersion) => { + let depVersion = null + let depProjectId = dep.project_id ?? null + if (dep.version_id) { - if (dep.project_id && (await check_installed(profile.path, dep.project_id))) continue - if (dep.project_id && onDepInstalling) { - const depProject = await get_project(dep.project_id, 'bypass').catch(() => null) - if (depProject) onDepInstalling(depProject) + depVersion = await get_version(dep.version_id, 'bypass').catch(() => null) + if (!depVersion) return null + + depProjectId = depProjectId ?? depVersion.project_id ?? null + if (depProjectId && !projectNames.has(depProjectId)) { + const p = await get_project(depProjectId, 'bypass').catch(() => null) + storeProjectName(p) } - await add_project_from_version(profile.path, dep.version_id) - } else { - if (dep.project_id && (await check_installed(profile.path, dep.project_id))) continue + } else if (dep.project_id) { + const depProject = await get_project(dep.project_id, 'bypass').catch(() => null) + if (!depProject) return null - const depProject = await get_project(dep.project_id, 'bypass') - if (onDepInstalling) onDepInstalling(depProject) + storeProjectName(depProject) - const depVersions = (await get_version_many(depProject.versions, 'bypass')).sort( - (a, b) => dayjs(b.date_published) - dayjs(a.date_published), + const depVersions = await get_version_many(depProject.versions, 'bypass').catch(() => []) + depVersion = findPreferredVersion( + depVersions.sort((a, b) => dayjs(b.date_published) - dayjs(a.date_published)), + dep, + profile, ) + if (!depVersion) return null + + depProjectId = dep.project_id + } else { + return null + } + + return { depVersion, depProjectId } + } + + const collectDependenciesForVersion = async (inputVersion) => { + if (!inputVersion?.id || visitedVersions.has(inputVersion.id)) return + visitedVersions.add(inputVersion.id) - const latest = findPreferredVersion(depVersions, dep, profile) - if (latest) { - await add_project_from_version(profile.path, latest.id) + if (inputVersion.project_id && !projectNames.has(inputVersion.project_id)) { + const p = await get_project(inputVersion.project_id, 'bypass').catch(() => null) + storeProjectName(p) + } + + for (const dep of inputVersion.dependencies ?? []) { + if (dep.dependency_type !== 'required') continue + if (dep.project_id === 'P7dR8mSH' && profile.loader === 'quilt') continue + + const resolved = await resolveDependency(dep, inputVersion) + if (!resolved) continue + + const { depVersion, depProjectId } = resolved + const queued = await queueInstall(depProjectId, depVersion) + if (queued && depProjectId) { + await announceDependency(depProjectId, depVersion) } + + await collectDependenciesForVersion(depVersion) } } + + await collectDependenciesForVersion(version) + + if (queuedInstalls.length === 0) return + + const batchSize = 8 + for (let i = 0; i < queuedInstalls.length; i += batchSize) { + const batch = queuedInstalls.slice(i, i + batchSize) + await Promise.all( + batch.map(async ({ versionId }) => { + await add_project_from_version(profile.path, versionId) + }), + ) + } } export const getServerAddress = (javaServer) => { From 116618a12da93862376f0f7cc907a3b52c4dea61 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Fri, 3 Apr 2026 20:47:47 +0100 Subject: [PATCH 2/2] fix up --- apps/app-frontend/src/store/install.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/app-frontend/src/store/install.js b/apps/app-frontend/src/store/install.js index 315ea32d82..3e836c4368 100644 --- a/apps/app-frontend/src/store/install.js +++ b/apps/app-frontend/src/store/install.js @@ -11,8 +11,20 @@ import { } from '@/helpers/worlds.ts' export const findPreferredVersion = (versions, project, instance) => { + // When `project` is passed in from this stack trace: + // - `installVersionDependencies` + // - `install.js/install` - `installVersionDependencies` call + // + // ..then `project` is actually a `Dependency` struct of a cached `Version`. + // `Dependency` does not have a `project_type` field, + // so we default it to `mod`. + // + // If we don't default here, then this `.find` will ignore version/instance + // loader mismatches, and you'll end up e.g. installing NeoForge mods for a + // Fabric instance. const projectType = project.project_type ?? 'mod' + // If we can find a version using strictly the instance loader then prefer that let version = versions.find( (v) => v.game_versions.includes(instance.game_version) && @@ -20,6 +32,7 @@ export const findPreferredVersion = (versions, project, instance) => { ) if (!version) { + // Otherwise use first compatible version (in addition to versions with the instance loader this includes datapacks) version = versions.find((v) => isVersionCompatible(v, project, instance)) } @@ -94,7 +107,7 @@ export const installVersionDependencies = async (profile, version, onDepInstalli announcedProjects.add(projectId) } - const resolveDependency = async (dep, inputVersion) => { + const resolveDependency = async (dep) => { let depVersion = null let depProjectId = dep.project_id ?? null