Implement Modrinth export

This commit is contained in:
Moritz Ruth 2022-08-16 10:23:45 +02:00
parent 81f60d9471
commit 862048d169
12 changed files with 291 additions and 161 deletions

View file

@ -23,6 +23,7 @@
},
"dependencies": {
"commander": "^9.4.0",
"cross-zip": "^4.0.0",
"dedent": "^0.7.0",
"env-paths": "^3.0.0",
"figures": "^5.0.0",
@ -43,6 +44,7 @@
"zod": "^3.18.0"
},
"devDependencies": {
"@types/cross-zip": "^4.0.0",
"@types/dedent": "^0.7.0",
"@types/fs-extra": "^9.0.13",
"@types/lodash-es": "^4.17.6",

13
pnpm-lock.yaml generated
View file

@ -1,6 +1,7 @@
lockfileVersion: 5.4
specifiers:
'@types/cross-zip': ^4.0.0
'@types/dedent': ^0.7.0
'@types/fs-extra': ^9.0.13
'@types/lodash-es': ^4.17.6
@ -8,6 +9,7 @@ specifiers:
'@types/semver': ^7.3.12
'@types/wrap-ansi': ^8.0.1
commander: ^9.4.0
cross-zip: ^4.0.0
dedent: ^0.7.0
env-paths: ^3.0.0
figures: ^5.0.0
@ -32,6 +34,7 @@ specifiers:
dependencies:
commander: 9.4.0
cross-zip: 4.0.0
dedent: 0.7.0
env-paths: 3.0.0
figures: 5.0.0
@ -52,6 +55,7 @@ dependencies:
zod: 3.18.0
devDependencies:
'@types/cross-zip': 4.0.0
'@types/dedent': 0.7.0
'@types/fs-extra': 9.0.13
'@types/lodash-es': 4.17.6
@ -115,6 +119,10 @@ packages:
'@types/responselike': 1.0.0
dev: false
/@types/cross-zip/4.0.0:
resolution: {integrity: sha512-jZRaaM3aib7qgVhCWeo76vVNJW+8+ujJzP/JHPPA4WUp5YBNMsqiyu5uYWg2eS05+jgSfb5NN8eqrAG72Ab2kA==}
dev: true
/@types/dedent/0.7.0:
resolution: {integrity: sha512-EGlKlgMhnLt/cM4DbUSafFdrkeJoC9Mvnj0PUCU7tFmTjMjNRT957kXCx0wYm3JuEq4o4ZsS5vG+NlkM2DMd2A==}
dev: true
@ -263,6 +271,11 @@ packages:
json-buffer: 3.0.1
dev: false
/cross-zip/4.0.0:
resolution: {integrity: sha512-MEzGfZo0rqE10O/B+AEcCSJLZsrWuRUvmqJTqHNqBtALhaJc3E3ixLGLJNTRzEA2K34wbmOHC4fwYs9sVsdcCA==}
engines: {node: '>=12.10'}
dev: false
/currently-unhandled/0.4.1:
resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==}
engines: {node: '>=0.10.0'}

View file

@ -2,23 +2,21 @@ import { Command } from "commander"
import { take } from "lodash-es"
import { usePack } from "../pack.js"
import kleur from "kleur"
import { optionParsePositiveInteger, truncateWithEllipsis } from "../utils.js"
import { optionParsePositiveInteger, truncateWithEllipsis, zip } from "../utils.js"
import { default as wrapAnsi } from "wrap-ansi"
import figures from "figures"
import {
addModrinthMod,
findModForModrinthMod,
getModFileDataForModrinthVersion, isModrinthVersionCompatible,
modrinthApi,
ModrinthMod,
ModrinthVersion,
ModrinthVersionRelation,
sortModrinthVersionsByPreference
} from "../modrinth.js"
} from "../modrinth/api.js"
import dedent from "dedent"
import ago from "s-ago"
import semver from "semver"
import { output } from "../output.js"
import fs from "fs-extra"
import { addModrinthMod, findModForModrinthMod, getModFileDataForModrinthVersion, isModrinthVersionCompatible, sortModrinthVersionsByPreference } from "../modrinth/utils.js"
const modrinthCommand = new Command("modrinth")
.alias("mr")
@ -268,10 +266,60 @@ modrinthVersionCommand.command("activate <id>")
await handleActivate(modrinthMod, modrinthVersion, options.force)
})
modrinthVersionCommand.command("export")
.description("Generate a Modrinth pack file suitable for uploading")
.action(async () => {
// TODO: Implement export
modrinthCommand.command("export")
.description("Generate a Modrinth pack file suitable for uploading.")
.option("-z, --no-zip", "Skip the creation of a zipped .mrpack file.")
.option("-c, --clear", "Remove the output directory afterwards.")
.action(async options => {
const pack = await usePack()
const loader = output.startLoading("Generating")
const outputDirectory = pack.rootDirectoryPath.resolve("modrinth-pack")
await fs.remove(outputDirectory.toString())
await fs.mkdirp(outputDirectory.toString())
await fs.writeJson(outputDirectory.resolve("modrinth.index.json").toString(), {
formatVersion: 1,
game: "minecraft",
versionId: pack.horizrFile.meta.version,
name: pack.horizrFile.meta.name,
summary: pack.horizrFile.meta.description,
dependencies: {
minecraft: pack.horizrFile.versions.minecraft,
[`${pack.horizrFile.loader}-loader`]: pack.horizrFile.versions.loader
},
files: pack.mods.map(mod => ({
path: `mods/${mod.modFile.file.name}`,
hashes: {
sha1: mod.modFile.file.hashes.sha1,
sha512: mod.modFile.file.hashes.sha512
},
env: {
client: mod.modFile.side === "client" || mod.modFile.side === "client+server" ? "required" : "unsupported",
server: mod.modFile.side === "server" || mod.modFile.side === "client+server" ? "required" : "unsupported"
},
downloads: [
mod.modFile.file.downloadUrl
],
fileSize: mod.modFile.file.size
}))
})
if (!options.clear) output.println(kleur.green(`Generated Modrinth pack in ${kleur.yellow("modrinth-pack")}`))
if (options.zip) {
loader.setText(`Creating ${kleur.yellow(".mrpack")} file`)
await zip(outputDirectory.toString(), pack.rootDirectoryPath.resolve("pack.mrpack").toString())
output.println(kleur.green(`Created ${kleur.yellow("pack.mrpack")}`))
}
if (options.clear) {
loader.setText("Removing the output directory")
await fs.remove(outputDirectory.toString())
output.println(kleur.green(`Removed the ${kleur.yellow("modrinth-pack")} directory`))
}
loader.stop()
})
async function handleActivate(modrinthMod: ModrinthMod, modrinthVersion: ModrinthVersion, force: boolean) {

View file

@ -3,7 +3,6 @@ import { usePack } from "../pack.js"
import fs from "fs-extra"
import dedent from "dedent"
import kleur from "kleur"
import { relative } from "path"
import { getSha512HexHash } from "../utils.js"
import { output } from "../output.js"
@ -25,9 +24,10 @@ packwizCommand.command("export")
const loader = output.startLoading("Generating")
const rootDirectoryPath = pack.resolvePath("packwiz")
await fs.remove(rootDirectoryPath)
await fs.mkdirp(pack.resolvePath("packwiz/mods"))
const outputDirectoryPath = pack.rootDirectoryPath.resolve("packwiz")
const modsDirectoryPath = outputDirectoryPath.resolve("mods")
await fs.remove(outputDirectoryPath.toString())
await fs.mkdirp(modsDirectoryPath.toString())
const indexedFiles: IndexedFile[] = []
for (const mod of pack.mods) {
@ -40,16 +40,16 @@ packwizCommand.command("export")
side = "${mod.modFile.side.replace("client+server", "both")}"
[download]
hash-format = "${mod.modFile.file.hashAlgorithm}"
hash = ${JSON.stringify(mod.modFile.file.hash)}
hash-format = "sha512"
hash = ${JSON.stringify(mod.modFile.file.hashes.sha512)}
url = ${JSON.stringify(mod.modFile.file.downloadUrl)}
`
const path = pack.resolvePath("packwiz/mods", mod.id + ".toml")
await fs.writeFile(path, content)
const path = modsDirectoryPath.resolve(mod.id + ".toml")
await fs.writeFile(path.toString(), content)
indexedFiles.push({
path: relative(rootDirectoryPath, path),
path: outputDirectoryPath.relative(path).toString(),
isMeta: true,
sha512HashHex: await getSha512HexHash(content)
})
@ -68,10 +68,10 @@ packwizCommand.command("export")
`).join("\n\n")}
`
await fs.writeFile(pack.resolvePath("packwiz/index.toml"), index)
await fs.writeFile(outputDirectoryPath.resolve("index.toml").toString(), index)
const indexHash = await getSha512HexHash(index)
await fs.writeFile(pack.resolvePath("packwiz/pack.toml"), dedent`
await fs.writeFile(outputDirectoryPath.resolve("pack.toml").toString(), dedent`
name = ${JSON.stringify(pack.horizrFile.meta.name)}
authors = ${JSON.stringify(pack.horizrFile.meta.authors.join(", "))}\
${pack.horizrFile.meta.description === undefined ? "" : "\n" + `description = ${JSON.stringify(pack.horizrFile.meta.description)}`}

View file

@ -2,49 +2,50 @@ import { SafeParseError, z, ZodRawShape } from "zod"
import kleur from "kleur"
import fs from "fs-extra"
import * as process from "process"
import { resolve, dirname } from "path"
import { dirname } from "path"
import { findUp } from "find-up"
import { output } from "./output.js"
import { Path } from "./path.js"
export async function findPackDirectoryPath() {
export async function findPackDirectoryPath(): Promise<Path> {
if (process.argv0.endsWith("/node")) { // run using pnpm
return resolve(process.cwd(), "./test-pack")
return Path.createAbsolute("./test-pack")
} else {
const parent = await findUp("horizr.json")
if (parent === undefined) return output.failAndExit(`${kleur.yellow("horizr.json")} could not be found in the current working directory or any parent.`)
return dirname(parent)
return Path.createAbsolute(dirname(parent))
}
}
export async function readJsonFileInPack<S extends z.ZodObject<ZodRawShape>>(
packPath: string,
filePath: string,
packPath: Path,
filePath: Path,
schema: S
): Promise<z.output<S> | null> {
let data
try {
data = await fs.readJson(resolve(packPath, filePath))
data = await fs.readJson(packPath.resolve(filePath).toString())
} catch (e: unknown) {
if (e instanceof SyntaxError) return output.failAndExit(`${kleur.yellow(filePath)} does not contain valid JSON.`)
if (e instanceof SyntaxError) return output.failAndExit(`${kleur.yellow(filePath.toString())} does not contain valid JSON.`)
else return null
}
const result = await schema.safeParseAsync(data)
if (!result.success) {
const error = (result as SafeParseError<unknown>).error
return output.failAndExit(`${kleur.yellow(filePath)} is invalid:\n${error.issues.map(issue => `- ${kleur.yellow(issue.path.join("/"))}${kleur.red(issue.message)}`).join("\n")}`)
return output.failAndExit(`${kleur.yellow(filePath.toString())} is invalid:\n${error.issues.map(issue => `- ${kleur.yellow(issue.path.join("/"))}${kleur.red(issue.message)}`).join("\n")}`)
}
return result.data
}
export async function writeJsonFileInPack<S extends z.ZodObject<ZodRawShape>>(packPath: string, filePath: string, schema: S, data: z.input<S>) {
const absolutePath = resolve(packPath, filePath)
await fs.mkdirp(dirname(absolutePath))
export async function writeJsonFileInPack<S extends z.ZodObject<ZodRawShape>>(packPath: Path, filePath: Path, schema: S, data: z.input<S>) {
const absolutePath = packPath.resolve(filePath)
await fs.mkdirp(absolutePath.getParent().toString())
await fs.writeJson(absolutePath, schema.parse(data), { spaces: 2 })
await fs.writeJson(absolutePath.toString(), schema.parse(data), { spaces: 2 })
}
const horizrFileSchema = z.object({
@ -65,8 +66,8 @@ const horizrFileSchema = z.object({
export type HorizrFile = z.output<typeof horizrFileSchema>
export async function readHorizrFile(packPath: string) {
const data = await readJsonFileInPack(packPath, "horizr.json", horizrFileSchema)
export async function readHorizrFile(packPath: Path) {
const data = await readJsonFileInPack(packPath, Path.create("horizr.json"), horizrFileSchema)
if (data === null) return output.failAndExit(`${kleur.yellow("horizr.json")} does not exist.`)
if (data.formatVersion !== 1) return output.failAndExit(`${kleur.yellow("horizr.json")} has unsupported format version: ${kleur.yellow(data.formatVersion)}`)
@ -86,8 +87,10 @@ const modFileDataSchema = z.object({
name: z.string(),
size: z.number().int().min(0).optional(),
downloadUrl: z.string().url(),
hashAlgorithm: z.enum(["sha1", "sha256", "sha512"]),
hash: z.string()
hashes: z.object({ // Adopted from Modrinth
sha1: z.string(),
sha512: z.string()
})
})
export type ModFileData = z.output<typeof modFileDataSchema>
@ -107,22 +110,22 @@ const modFileSchema = z.object({
export type ModFile = z.output<typeof modFileSchema>
export async function readModFile(packPath: string, modId: string): Promise<ModFile | null> {
return await readJsonFileInPack(packPath, `mods/${modId}.json`, modFileSchema)
export async function readModFile(packPath: Path, modId: string): Promise<ModFile | null> {
return await readJsonFileInPack(packPath, Path.create("mods", `${modId}.json`), modFileSchema)
}
export async function writeModFile(packPath: string, modId: string, data: z.input<typeof modFileSchema>): Promise<void> {
await writeJsonFileInPack(packPath, `mods/${modId}.json`, modFileSchema, data)
export async function writeModFile(packPath: Path, modId: string, data: z.input<typeof modFileSchema>): Promise<void> {
await writeJsonFileInPack(packPath, Path.create("mods", `${modId}.json`), modFileSchema, data)
}
export async function removeModFile(packPath: string, modId: string): Promise<void> {
await fs.remove(resolve(packPath, `mods/${modId}.json`))
export async function removeModFile(packPath: Path, modId: string): Promise<void> {
await fs.remove(packPath.resolve("mods", `${modId}.json`).toString())
}
export async function readModIds(packPath: string) {
const modsPath = resolve(packPath, "./mods")
await fs.mkdirp(modsPath)
const files = await fs.readdir(modsPath, { withFileTypes: true })
export async function readModIds(packPath: Path) {
const modsPath = packPath.resolve("mods")
await fs.mkdirp(modsPath.toString())
const files = await fs.readdir(modsPath.toString(), { withFileTypes: true })
return files.filter(file => file.isFile() && file.name.endsWith(".json")).map(file => file.name.slice(0, -5))
}

View file

@ -9,7 +9,7 @@ import { default as wrapAnsi } from "wrap-ansi"
import { removeModFile } from "./files.js"
import { output } from "./output.js"
import figures from "figures"
import { releaseChannelOrder } from "./modrinth.js"
import { releaseChannelOrder } from "./shared.js"
const program = new Command("horizr")
@ -39,7 +39,7 @@ program.command("remove <code>")
const pack = await usePack()
const mod = pack.findModByCodeOrFail(code)
await removeModFile(pack.path, mod.id)
await removeModFile(pack.rootDirectoryPath, mod.id)
output.println(`${mod.modFile.name} ${kleur.green("was removed from the pack.")}`)
})

View file

@ -1,18 +1,14 @@
import { IterableElement } from "type-fest"
import originalGot, { HTTPError, Response } from "got"
import { sortBy } from "lodash-es"
import { Loader, Mod, Pack, usePack } from "./pack.js"
import { ModFile, ModFileData, ModFileModrinthSource } from "./files.js"
import { pathExists } from "fs-extra"
import kleur from "kleur"
import { nanoid } from "nanoid/non-secure"
import { KeyvFile } from "keyv-file"
import { resolve } from "path"
import { delay, paths } from "./utils.js"
import { output } from "./output.js"
import { delay } from "../utils.js"
import { output } from "../output.js"
import { paths } from "../path.js"
import { dependencyToRelatedVersionType } from "./utils.js"
import { ModLoader, ReleaseChannel } from "../shared.js"
const keyvCache = new KeyvFile({
filename: resolve(paths.cache, "http.json"),
filename: paths.cache.resolve("http.json").toString(),
writeDelay: 50,
expiredCheckDelay: 24 * 3600 * 1000,
encode: JSON.stringify,
@ -58,84 +54,6 @@ async function getModrinthApi(url: string): Promise<any> {
return response
}
const dependencyToRelatedVersionType: Record<string, IterableElement<ModrinthVersion["relations"]>["type"]> = {
required: "hard_dependency",
optional: "soft_dependency",
embedded: "embedded_dependency",
incompatible: "incompatible"
}
export type ReleaseChannel = "alpha" | "beta" | "release"
export const releaseChannelOrder: ReleaseChannel[] = ["alpha", "beta", "release"]
export const sortModrinthVersionsByPreference = (versions: ModrinthVersion[]) => sortBy(versions, [v => releaseChannelOrder.indexOf(v.releaseChannel), "isFeatured", "publicationDate"]).reverse()
export async function findModForModrinthMod(modrinthMod: ModrinthMod): Promise<(Mod & { modFile: ModFile & { source: ModFileModrinthSource } }) | null> {
const pack = await usePack()
return (
pack.mods.find(
mod => mod.modFile.source.type === "modrinth" && mod.modFile.source.modId === modrinthMod.id
) as (Mod & { modFile: Mod & { source: ModFileModrinthSource } }) | undefined
) ?? null
}
export const isModrinthVersionCompatible = (modrinthVersion: ModrinthVersion, pack: Pack) =>
modrinthVersion.supportedMinecraftVersions.includes(pack.horizrFile.versions.minecraft) && modrinthVersion.supportedLoaders.includes(pack.horizrFile.loader)
export function getModFileDataForModrinthVersion(modrinthMod: ModrinthMod, modrinthModVersion: ModrinthVersion): ModFileData {
const modrinthVersionFile = findCorrectModVersionFile(modrinthModVersion.files)
return {
version: modrinthModVersion.versionString,
hash: modrinthVersionFile.hashes.sha512,
hashAlgorithm: "sha512",
downloadUrl: modrinthVersionFile.url,
name: modrinthVersionFile.fileName,
size: modrinthVersionFile.sizeInBytes,
}
}
export async function addModrinthMod(modrinthMod: ModrinthMod, modrinthVersion: ModrinthVersion) {
const pack = await usePack()
let id = modrinthMod.slug
if (await pathExists(pack.resolvePath("mods", `${id}.json`))) {
const oldId = id
id = `${id}-${nanoid(5)}`
output.warn(
`There is already a mod file named ${kleur.yellow(`${oldId}.json`)} specifying a non-Modrinth mod.\n` +
`The file for this mod will therefore be named ${kleur.yellow(`${id}.json`)}`
)
}
const isClientSupported = modrinthMod.clientSide !== "unsupported"
const isServerSupported = modrinthMod.serverSide !== "unsupported"
await pack.addMod(id, {
name: modrinthMod.title,
enabled: true,
ignoreUpdates: false,
side: isClientSupported && isServerSupported ? "client+server" : isClientSupported ? "client" : "server",
file: getModFileDataForModrinthVersion(modrinthMod, modrinthVersion),
source: {
type: "modrinth",
modId: modrinthMod.id,
versionId: modrinthVersion.id
}
})
}
export function findCorrectModVersionFile(files: ModrinthVersionFile[]) {
const primary = files.find(file => file.isPrimary)
if (primary !== undefined) return primary
// shortest file name
return files.sort((a, b) => a.fileName.length - b.fileName.length)[0]
}
function transformApiModVersion(raw: any): ModrinthVersion {
return {
id: raw.id,
@ -222,7 +140,7 @@ export interface ModrinthVersionFile {
export const modrinthApi = {
clearCache: () => keyvCache.clear(),
async searchMods(
loader: Loader,
loader: ModLoader,
minecraftVersion: string,
query: string,
pagination: PaginationOptions
@ -266,7 +184,7 @@ export const modrinthApi = {
updateDate: new Date(response.updated)
}
},
async listVersions(idOrSlug: string, loader: Loader, minecraftVersion: string): Promise<ModrinthVersion[]> {
async listVersions(idOrSlug: string, loader: ModLoader, minecraftVersion: string): Promise<ModrinthVersion[]> {
const response = await getModrinthApi(`v2/project/${idOrSlug}/version?loaders=["${loader}"]&game_versions=["${minecraftVersion}"]`)
return response.map(transformApiModVersion)

87
src/modrinth/utils.ts Normal file
View file

@ -0,0 +1,87 @@
import { IterableElement } from "type-fest"
import { sortBy } from "lodash-es"
import { Mod, Pack, usePack } from "../pack.js"
import { ModFile, ModFileData, ModFileModrinthSource } from "../files.js"
import { pathExists } from "fs-extra"
import { nanoid } from "nanoid/non-secure"
import { output } from "../output.js"
import kleur from "kleur"
import { ModrinthMod, ModrinthVersion, ModrinthVersionFile } from "./api.js"
import { releaseChannelOrder } from "../shared.js"
export const dependencyToRelatedVersionType: Record<string, IterableElement<ModrinthVersion["relations"]>["type"]> = {
required: "hard_dependency",
optional: "soft_dependency",
embedded: "embedded_dependency",
incompatible: "incompatible"
}
export const sortModrinthVersionsByPreference = (versions: ModrinthVersion[]) => sortBy(versions, [v => releaseChannelOrder.indexOf(v.releaseChannel), "isFeatured", "publicationDate"]).reverse()
export async function findModForModrinthMod(modrinthMod: ModrinthMod): Promise<(Mod & { modFile: ModFile & { source: ModFileModrinthSource } }) | null> {
const pack = await usePack()
return (
pack.mods.find(
mod => mod.modFile.source.type === "modrinth" && mod.modFile.source.modId === modrinthMod.id
) as (Mod & { modFile: Mod & { source: ModFileModrinthSource } }) | undefined
) ?? null
}
export const isModrinthVersionCompatible = (modrinthVersion: ModrinthVersion, pack: Pack) =>
modrinthVersion.supportedMinecraftVersions.includes(pack.horizrFile.versions.minecraft) && modrinthVersion.supportedLoaders.includes(pack.horizrFile.loader)
export function getModFileDataForModrinthVersion(modrinthMod: ModrinthMod, modrinthModVersion: ModrinthVersion): ModFileData {
const modrinthVersionFile = findCorrectModVersionFile(modrinthModVersion.files)
return {
version: modrinthModVersion.versionString,
hashes: {
sha1: modrinthVersionFile.hashes.sha1,
sha512: modrinthVersionFile.hashes.sha512
},
downloadUrl: modrinthVersionFile.url,
name: modrinthVersionFile.fileName,
size: modrinthVersionFile.sizeInBytes,
}
}
export async function addModrinthMod(modrinthMod: ModrinthMod, modrinthVersion: ModrinthVersion) {
const pack = await usePack()
let id = modrinthMod.slug
if (await pathExists(pack.rootDirectoryPath.resolve("mods", `${id}.json`).toString())) {
const oldId = id
id = `${id}-${nanoid(5)}`
output.warn(
`There is already a mod file named ${kleur.yellow(`${oldId}.json`)} specifying a non-Modrinth mod.\n` +
`The file for this mod will therefore be named ${kleur.yellow(`${id}.json`)}`
)
}
const isClientSupported = modrinthMod.clientSide !== "unsupported"
const isServerSupported = modrinthMod.serverSide !== "unsupported"
await pack.addMod(id, {
name: modrinthMod.title,
enabled: true,
ignoreUpdates: false,
side: isClientSupported && isServerSupported ? "client+server" : isClientSupported ? "client" : "server",
file: getModFileDataForModrinthVersion(modrinthMod, modrinthVersion),
source: {
type: "modrinth",
modId: modrinthMod.id,
versionId: modrinthVersion.id
}
})
}
export function findCorrectModVersionFile(files: ModrinthVersionFile[]) {
const primary = files.find(file => file.isPrimary)
if (primary !== undefined) return primary
// shortest file name
return files.sort((a, b) => a.fileName.length - b.fileName.length)[0]
}

View file

@ -1,12 +1,12 @@
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 { modrinthApi } from "./modrinth/api.js"
import semver from "semver"
export type Loader = "fabric" | "quilt"
import { Path } from "./path.js"
import { ReleaseChannel } from "./shared.js"
import { getModFileDataForModrinthVersion, sortModrinthVersionsByPreference } from "./modrinth/utils.js"
export interface Update {
mod: Mod
@ -16,19 +16,15 @@ export interface Update {
}
export interface Pack {
path: string
rootDirectoryPath: Path
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 {
@ -44,17 +40,17 @@ let pack: Pack
export async function usePack(): Promise<Pack> {
if (pack === undefined) {
const path = await findPackDirectoryPath()
const rootDirectoryPath = await findPackDirectoryPath()
pack = {
path,
horizrFile: await readHorizrFile(path),
mods: await Promise.all((await readModIds(path)).map(async id => {
rootDirectoryPath,
horizrFile: await readHorizrFile(rootDirectoryPath),
mods: await Promise.all((await readModIds(rootDirectoryPath)).map(async id => {
const mod: Mod = {
id,
modFile: (await readModFile(path, id))!,
modFile: (await readModFile(rootDirectoryPath, id))!,
async saveModFile() {
await writeModFile(path, id, this.modFile)
await writeModFile(rootDirectoryPath, id, this.modFile)
},
async checkForUpdate(allowedReleaseChannels: ReleaseChannel[]): Promise<Update | null> {
if (mod.modFile.ignoreUpdates) return null
@ -106,7 +102,7 @@ export async function usePack(): Promise<Pack> {
return mod
})),
async addMod(id: string, file: ModFile) {
await writeModFile(path, id, file)
await writeModFile(rootDirectoryPath, id, file)
},
findModByCode(code: string): Mod | null {
if (code.startsWith("mrv:")) {
@ -138,9 +134,6 @@ export async function usePack(): Promise<Pack> {
loader.stop()
return updates.filter(info => info !== null) as Update[]
},
resolvePath(...segments): string {
return resolve(path, ...segments)
}
}
}

62
src/path.ts Normal file
View file

@ -0,0 +1,62 @@
import pathModule from "path"
import envPaths from "env-paths"
export class Path {
constructor(private readonly value: string) {
}
/**
* Returns an absolute path by resolving the last segment against the other segments, this path and the current working directory.
*/
resolve(...segments: (string | Path)[]) {
if (this.isAbsolute()) return this
else return new Path(pathModule.resolve(this.value, ...segments.map(s => s.toString())))
}
/**
* Returns a new path with this path and the segments joined together.
*/
join(...segments: (string | Path)[]) {
return new Path(pathModule.join(this.value, ...segments.map(s => s.toString())))
}
/**
* Returns the relative path from this path to the other path.
*/
relative(other: Path) {
return new Path(pathModule.relative(this.value, other.value))
}
getParent() {
return new Path(pathModule.dirname(this.value))
}
isAbsolute() {
return pathModule.isAbsolute(this.value)
}
toString() {
return this.value
}
static create(...segments: string[]) {
if (segments.length === 0) throw new Error("At least one segment is required")
return new Path(pathModule.join(...segments))
}
static createAbsolute(...segments: string[]) {
if (segments.length === 0) throw new Error("At least one segment is required")
return new Path(pathModule.resolve(...segments))
}
}
const rawPaths = envPaths("horizr", { suffix: "" })
export const paths = {
cache: new Path(rawPaths.cache),
config: new Path(rawPaths.config),
data: new Path(rawPaths.data),
log: new Path(rawPaths.log),
temp: new Path(rawPaths.temp)
}

3
src/shared.ts Normal file
View file

@ -0,0 +1,3 @@
export type ModLoader = "fabric" | "quilt"
export type ReleaseChannel = "alpha" | "beta" | "release"
export const releaseChannelOrder: ReleaseChannel[] = ["alpha", "beta", "release"]

View file

@ -1,8 +1,9 @@
import envPaths from "env-paths"
import { InvalidArgumentError } from "commander"
import hash, { HashaInput } from "hasha"
import { zip as zipWithCallback } from "cross-zip"
import { promisify } from "util"
export const paths = envPaths("horizr", { suffix: "" })
export const zip = promisify(zipWithCallback)
export const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))