cli/src/pack.ts
2022-08-16 00:24:29 +02:00

149 lines
5.5 KiB
TypeScript

import { findPackDirectoryPath, HorizrFile, ModFile, ModFileModrinthSource, readHorizrFile, readModFile, readModIds, writeModFile } from "./files.js"
import { resolve } from "path"
import { output } from "./output.js"
import pLimit from "p-limit"
import kleur from "kleur"
import { getModFileDataForModrinthVersion, modrinthApi, ReleaseChannel, sortModrinthVersionsByPreference } from "./modrinth.js"
import semver from "semver"
export type Loader = "fabric" | "quilt"
export interface Update {
mod: Mod
activeVersion: string
availableVersion: string
apply(): Promise<void>
}
export interface Pack {
path: string
horizrFile: HorizrFile
mods: Mod[]
addMod(id: string, file: ModFile): Promise<void>
findModByCode(code: string): Mod | null
findModByCodeOrFail(code: string): Mod
checkForUpdates(allowedReleaseChannels: ReleaseChannel[]): Promise<Update[]>
resolvePath(...segments: string[]): string
}
export interface Mod {
id: string
modFile: ModFile
saveModFile(): Promise<void>
checkForUpdate(allowedReleaseChannels: ReleaseChannel[]): Promise<Update | null>
}
let pack: Pack
export async function usePack(): Promise<Pack> {
if (pack === undefined) {
const path = await findPackDirectoryPath()
pack = {
path,
horizrFile: await readHorizrFile(path),
mods: await Promise.all((await readModIds(path)).map(async id => {
const mod: Mod = {
id,
modFile: (await readModFile(path, id))!,
async saveModFile() {
await writeModFile(path, id, this.modFile)
},
async checkForUpdate(allowedReleaseChannels: ReleaseChannel[]): Promise<Update | null> {
if (mod.modFile.ignoreUpdates) return null
if (mod.modFile.source.type === "modrinth") {
const activeVersionString = mod.modFile.file.version
const activeSemver = semver.parse(activeVersionString)
if (activeSemver === null)
output.warn(`${kleur.yellow(mod.modFile.name)} has no valid semantic version: ${kleur.yellow(mod.modFile.file.version)}. The publication date will instead be used.`)
const versions = await modrinthApi.listVersions(mod.modFile.source.modId, pack.horizrFile.loader, pack.horizrFile.versions.minecraft)
const allowedVersions = versions.filter(version => allowedReleaseChannels.includes(version.releaseChannel))
const newerVersions = activeSemver === null ? allowedVersions : allowedVersions.filter(version => {
const thisSemver = semver.parse(version.versionString)
if (thisSemver === null) return false
return thisSemver.compare(activeSemver) === 1
})
if (newerVersions.length === 0) return null
const sortedNewerVersions = sortModrinthVersionsByPreference(newerVersions)
const newestVersion = sortedNewerVersions[0]
if (activeSemver === null ? activeVersionString === newestVersion.versionString : semver.eq(activeSemver, newestVersion.versionString)) return null
return {
mod,
activeVersion: activeVersionString,
availableVersion: newestVersion.versionString,
async apply() {
const modrinthMod = (await modrinthApi.getMod(newestVersion.projectId))!
mod.modFile.file = getModFileDataForModrinthVersion(modrinthMod, newestVersion)
;(mod.modFile.source as ModFileModrinthSource).versionId = newestVersion.id
await mod.saveModFile()
}
}
} else {
output.warn(`${kleur.yellow(mod.modFile.name)} has no source information attached.`)
}
return null
}
}
return mod
})),
async addMod(id: string, file: ModFile) {
await writeModFile(path, id, file)
},
findModByCode(code: string): Mod | null {
if (code.startsWith("mrv:")) {
return this.mods.find(mod => mod.modFile.source.type === "modrinth" && mod.modFile.source.versionId === code.slice(4)) ?? null
} else if (code.startsWith("mr:")) {
return this.mods.find(mod => mod.modFile.source.type === "modrinth" && mod.modFile.source.modId === code.slice(3)) ?? null
} else if (code.endsWith(".json")) {
return this.mods.find(mod => mod.id === code.slice(0, -5)) ?? null
} else {
return this.mods.find(mod => mod.id === code) ?? null
}
},
findModByCodeOrFail(code: string): Mod {
const mod = this.findModByCode(code)
if (mod === null) return output.failAndExit("The mod could not be found.")
return mod
},
async checkForUpdates(allowedReleaseChannels: ReleaseChannel[]): Promise<Update[]> {
const limit = pLimit(5)
const loader = output.startLoading(`Checking for updates (0/${this.mods.length})`)
let finishedCount = 0
const updates: Array<Update | null> = await Promise.all(this.mods.map(mod => limit(async () => {
const update = await mod.checkForUpdate(allowedReleaseChannels)
finishedCount++
loader.setText(`Checking for updates (${finishedCount}/${this.mods.length})`)
return update
})))
loader.stop()
return updates.filter(info => info !== null) as Update[]
},
resolvePath(...segments): string {
return resolve(path, ...segments)
}
}
}
return pack
}