From 98007b36a0b4bdfa178812c24cb9a5862e8fff7f Mon Sep 17 00:00:00 2001 From: Moritz Ruth Date: Tue, 16 Aug 2022 15:11:55 +0200 Subject: [PATCH] Implement overrides for Modrinth export --- package.json | 6 +- pnpm-lock.yaml | 53 ++++++++++---- src/commands/modrinth.ts | 96 ++++++++++++++++---------- src/commands/packwiz.ts | 12 +++- src/files.ts | 10 ++- src/main.ts | 13 ++-- src/modrinth/api.ts | 29 ++++++-- src/output.ts | 50 +++++++++++++- src/pack.ts | 36 ++++++++-- src/path.ts | 3 +- src/shared.ts | 3 + src/types.d.ts | 6 ++ src/utils.ts | 23 +++++- test-pack/.gitignore | 1 + test-pack/mods/charm.json | 10 +-- test-pack/mods/fabric-api.json | 21 ++++++ test-pack/mods/sodium.json | 21 ++++++ test-pack/overrides/client/options.txt | 1 + 18 files changed, 313 insertions(+), 81 deletions(-) create mode 100644 src/types.d.ts create mode 100644 test-pack/.gitignore create mode 100644 test-pack/mods/fabric-api.json create mode 100644 test-pack/mods/sodium.json create mode 100644 test-pack/overrides/client/options.txt diff --git a/package.json b/package.json index 9f35892..020e893 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "horizr": "./bin/horizr" }, "dependencies": { + "@root/walk": "^1.1.0", "commander": "^9.4.0", - "cross-zip": "^4.0.0", "dedent": "^0.7.0", "env-paths": "^3.0.0", "figures": "^5.0.0", @@ -37,20 +37,22 @@ "loud-rejection": "^2.2.0", "nanoid": "^4.0.0", "ora": "^6.1.2", + "p-event": "^5.0.1", "p-limit": "^4.0.0", "s-ago": "^2.2.0", "semver": "^7.3.7", "wrap-ansi": "^8.0.1", + "yazl": "^2.5.1", "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", "@types/node": "^18.7.3", "@types/semver": "^7.3.12", "@types/wrap-ansi": "^8.0.1", + "@types/yazl": "^2.4.2", "tsx": "^3.8.2", "type-fest": "^2.18.0", "typescript": "^4.7.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e683024..a5c923d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,15 +1,15 @@ lockfileVersion: 5.4 specifiers: - '@types/cross-zip': ^4.0.0 + '@root/walk': ^1.1.0 '@types/dedent': ^0.7.0 '@types/fs-extra': ^9.0.13 '@types/lodash-es': ^4.17.6 '@types/node': ^18.7.3 '@types/semver': ^7.3.12 '@types/wrap-ansi': ^8.0.1 + '@types/yazl': ^2.4.2 commander: ^9.4.0 - cross-zip: ^4.0.0 dedent: ^0.7.0 env-paths: ^3.0.0 figures: ^5.0.0 @@ -23,6 +23,7 @@ specifiers: loud-rejection: ^2.2.0 nanoid: ^4.0.0 ora: ^6.1.2 + p-event: ^5.0.1 p-limit: ^4.0.0 s-ago: ^2.2.0 semver: ^7.3.7 @@ -30,11 +31,12 @@ specifiers: type-fest: ^2.18.0 typescript: ^4.7.4 wrap-ansi: ^8.0.1 + yazl: ^2.5.1 zod: ^3.18.0 dependencies: + '@root/walk': 1.1.0 commander: 9.4.0 - cross-zip: 4.0.0 dedent: 0.7.0 env-paths: 3.0.0 figures: 5.0.0 @@ -48,20 +50,22 @@ dependencies: loud-rejection: 2.2.0 nanoid: 4.0.0 ora: 6.1.2 + p-event: 5.0.1 p-limit: 4.0.0 s-ago: 2.2.0 semver: 7.3.7 wrap-ansi: 8.0.1 + yazl: 2.5.1 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 '@types/node': 18.7.3 '@types/semver': 7.3.12 '@types/wrap-ansi': 8.0.1 + '@types/yazl': 2.4.2 tsx: 3.8.2 type-fest: 2.18.0 typescript: 4.7.4 @@ -98,6 +102,10 @@ packages: dev: true optional: true + /@root/walk/1.1.0: + resolution: {integrity: sha512-FfXPAta9u2dBuaXhPRawBcijNC9rmKVApmbi6lIZyg36VR/7L02ytxoY5K/14PJlHqiBUoYII73cTlekdKTUOw==} + dev: false + /@sindresorhus/is/5.3.0: resolution: {integrity: sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==} engines: {node: '>=14.16'} @@ -119,10 +127,6 @@ 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 @@ -174,6 +178,12 @@ packages: resolution: {integrity: sha512-cjwgM6WWy9YakrQ36Pq0vg5XoNblVEaNq+/pHngKl4GyyDIxTeskPoG+tp4LsRk0lHrA4LaLJqlvYridi7mzlw==} dev: true + /@types/yazl/2.4.2: + resolution: {integrity: sha512-T+9JH8O2guEjXNxqmybzQ92mJUh2oCwDDMSSimZSe1P+pceZiFROZLYmcbqkzV5EUwz6VwcKXCO2S2yUpra6XQ==} + dependencies: + '@types/node': 18.7.3 + dev: true + /ansi-regex/6.0.1: resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} engines: {node: '>=12'} @@ -201,6 +211,10 @@ packages: readable-stream: 3.6.0 dev: false + /buffer-crc32/0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + dev: false + /buffer-from/1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: true @@ -271,11 +285,6 @@ 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'} @@ -825,6 +834,13 @@ packages: engines: {node: '>=12.20'} dev: false + /p-event/5.0.1: + resolution: {integrity: sha512-dd589iCQ7m1L0bmC5NLlVYfy3TbBEsMUfWx9PyAgPeIcFZ/E2yaTZ4Rz4MiBmmJShviiftHVXOqfnfzJ6kyMrQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-timeout: 5.1.0 + dev: false + /p-limit/4.0.0: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -839,6 +855,11 @@ packages: p-limit: 4.0.0 dev: false + /p-timeout/5.1.0: + resolution: {integrity: sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==} + engines: {node: '>=12'} + dev: false + /path-exists/5.0.0: resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1005,6 +1026,12 @@ packages: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: false + /yazl/2.5.1: + resolution: {integrity: sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==} + dependencies: + buffer-crc32: 0.2.13 + dev: false + /yocto-queue/1.0.0: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} diff --git a/src/commands/modrinth.ts b/src/commands/modrinth.ts index 970d913..a2f62d0 100644 --- a/src/commands/modrinth.ts +++ b/src/commands/modrinth.ts @@ -2,7 +2,7 @@ import { Command } from "commander" import { take } from "lodash-es" import { usePack } from "../pack.js" import kleur from "kleur" -import { optionParsePositiveInteger, truncateWithEllipsis, zip } from "../utils.js" +import { optionParsePositiveInteger, truncateWithEllipsis, zipDirectory } from "../utils.js" import { default as wrapAnsi } from "wrap-ansi" import figures from "figures" import { @@ -267,59 +267,79 @@ modrinthVersionCommand.command("activate ") }) modrinthCommand.command("export") - .description("Generate a Modrinth pack file suitable for uploading.") - .option("-z, --no-zip", "Skip the creation of a zipped .mrpack file.") + .description("Export a Modrinth pack.") + .option("-s, --no-generate", "Skip regenerating the output directory.") + .option("-z, --no-zip", "Skip creating 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()) + const outputDirectory = pack.paths.generated.resolve("modrinth-pack") - 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 + if (options.generate) { + const loader = output.startLoading("Generating") + await pack.validateOverridesDirectories() + 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 }, - 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 - })) - }) + 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 + })) + }, { spaces: 2 }) - if (!options.clear) output.println(kleur.green(`Generated Modrinth pack in ${kleur.yellow("modrinth-pack")}`)) + if (await fs.pathExists(pack.paths.overrides["client-server"].toString())) await output.withLoading( + fs.copy(pack.paths.overrides["client-server"].toString(), outputDirectory.resolve("overrides").toString(), { recursive: true }), + "Copying client-server overrides" + ) + + if (await fs.pathExists(pack.paths.overrides["client"].toString())) await output.withLoading( + fs.copy(pack.paths.overrides["client"].toString(), outputDirectory.resolve("client-overrides").toString(), { recursive: true }), + "Copying client overrides" + ) + + if (await fs.pathExists(pack.paths.overrides["server"].toString())) await output.withLoading( + fs.copy(pack.paths.overrides["server"].toString(), outputDirectory.resolve("server-overrides").toString(), { recursive: true }), + "Copying server overrides" + ) + + output.println(kleur.green(`Generated Modrinth pack`)) + loader.stop() + } if (options.zip) { - loader.setText(`Creating ${kleur.yellow(".mrpack")} file`) - await zip(outputDirectory.toString(), pack.rootDirectoryPath.resolve("pack.mrpack").toString()) + if (!(await fs.pathExists(outputDirectory.toString()))) + output.failAndExit(`The ${kleur.yellow("modrinth-pack")} directory does not exist.\nRun the command without ${kleur.yellow("--no-generate")} to create it.`) + + await output.withLoading(zipDirectory(outputDirectory, pack.paths.generated.resolve("pack.mrpack")), `Creating ${kleur.yellow(".mrpack")} file`) 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) { diff --git a/src/commands/packwiz.ts b/src/commands/packwiz.ts index 60d3a24..04bfe5b 100644 --- a/src/commands/packwiz.ts +++ b/src/commands/packwiz.ts @@ -14,8 +14,14 @@ interface IndexedFile { isMeta: boolean } +packwizCommand.command("import") + .description("Import a packwiz pack.") + .action(async () => { + // TODO: Import packwiz pack + }) + packwizCommand.command("export") - .description("Generates a packwiz pack in the packwiz directory") + .description("Generate a packwiz pack in the packwiz directory.") .action(async () => { const pack = await usePack() @@ -24,7 +30,7 @@ packwizCommand.command("export") const loader = output.startLoading("Generating") - const outputDirectoryPath = pack.rootDirectoryPath.resolve("packwiz") + const outputDirectoryPath = pack.paths.generated.resolve("packwiz") const modsDirectoryPath = outputDirectoryPath.resolve("mods") await fs.remove(outputDirectoryPath.toString()) await fs.mkdirp(modsDirectoryPath.toString()) @@ -88,7 +94,7 @@ packwizCommand.command("export") `) loader.stop() - output.println(kleur.green("Successfully generated packwiz pack.")) + output.println(kleur.green("Generated packwiz pack")) }) export { packwizCommand} diff --git a/src/files.ts b/src/files.ts index f437f5f..a6000fd 100644 --- a/src/files.ts +++ b/src/files.ts @@ -6,6 +6,8 @@ import { dirname } from "path" import { findUp } from "find-up" import { output } from "./output.js" import { Path } from "./path.js" +import { Dirent } from "fs" +import { sides } from "./shared.js" export async function findPackDirectoryPath(): Promise { if (process.argv0.endsWith("/node")) { // run using pnpm @@ -99,7 +101,7 @@ const modFileSchema = z.object({ name: z.string(), enabled: z.boolean().default(true), ignoreUpdates: z.boolean().default(false), - side: z.enum(["client", "server", "client+server"]), + side: z.enum(sides), comment: z.string().optional(), file: modFileDataSchema, source: z.discriminatedUnion("type", [ @@ -129,3 +131,9 @@ export async function readModIds(packPath: Path) { return files.filter(file => file.isFile() && file.name.endsWith(".json")).map(file => file.name.slice(0, -5)) } + +export async function getOverrideDirents(overridesDirectoryPath: Path): Promise { + if (!await fs.pathExists(overridesDirectoryPath.toString())) return [] + + return await fs.readdir(overridesDirectoryPath.toString(), { withFileTypes: true }) +} diff --git a/src/main.ts b/src/main.ts index f37666c..4d7798f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -39,7 +39,7 @@ program.command("remove ") const pack = await usePack() const mod = pack.findModByCodeOrFail(code) - await removeModFile(pack.rootDirectoryPath, mod.id) + await removeModFile(pack.paths.root, mod.id) output.println(`${mod.modFile.name} ${kleur.green("was removed from the pack.")}`) }) @@ -81,15 +81,20 @@ program.command("update [code]") } }) -loudRejection() +loudRejection(stack => { + output.failAndExit(stack) +}) program .addCommand(packwizCommand) .addCommand(modrinthCommand) - .addHelpText("afterAll", "\n" + dedent` + .addHelpText("after", "\n" + dedent` ${kleur.blue("code")} can be one of the following: - The name of a file in the ${kleur.yellow("mods")} directory, optionally without the ${kleur.yellow(".json")} extension - The ID of a Modrinth Project, prefixed with ${kleur.yellow("mr:")} - The ID of a Modrinth Version, prefixed with ${kleur.yellow("mrv:")} `) - .parse(process.argv) + .parseAsync(process.argv) + .catch(error => { + output.failAndExit(error.message) + }) diff --git a/src/modrinth/api.ts b/src/modrinth/api.ts index f0d9b1b..eccf5d7 100644 --- a/src/modrinth/api.ts +++ b/src/modrinth/api.ts @@ -1,4 +1,4 @@ -import originalGot, { HTTPError, Response } from "got" +import got, { HTTPError, Response } from "got" import kleur from "kleur" import { KeyvFile } from "keyv-file" import { delay } from "../utils.js" @@ -15,8 +15,6 @@ const keyvCache = new KeyvFile({ decode: JSON.parse }) -const got = originalGot.extend() - async function getModrinthApiOptional(url: string): Promise { let response: Response @@ -28,7 +26,30 @@ async function getModrinthApiOptional(url: string): Promise { }, cache: keyvCache, responseType: "json", - throwHttpErrors: false + throwHttpErrors: false, + retry: { + limit: 3, + maxRetryAfter: 10, + statusCodes: [ + 408, + 413, + // 429, + 500, + 502, + 503, + 504, + 521, + 522, + 524, + ] + }, + hooks: { + beforeRetry: [ + (error, retryCount) => { + output.warn(`Request to ${kleur.yellow(error.request!.requestUrl!.toString())} failed, retrying ${kleur.gray(`(${retryCount}/3)`)}`) + } + ] + } }) if (response.statusCode.toString().startsWith("2")) { diff --git a/src/output.ts b/src/output.ts index f67f180..59213fe 100644 --- a/src/output.ts +++ b/src/output.ts @@ -23,6 +23,40 @@ export interface InternalLoader extends Loader { } export const output = { + async withLoading(promise: Promise, options: string | { + text: string + exitOnError?: boolean, + getErrorMessage?: (e: Error) => string, + }): Promise { + const actualOptions = typeof options === "string" ? { + text: options, + exitOnError: true + } : { + text: options.text, + exitOnError: options.exitOnError ?? true, + getErrorMessage: options.getErrorMessage ?? (options.exitOnError !== false ? ((e: Error) => e.message) : undefined) + } + + const loader = this.startLoading(actualOptions.text) + + try { + const result = await promise + loader.stop() + return result + } catch (e: unknown) { + const error = e as Error + + if (actualOptions.exitOnError) { + if (actualOptions.getErrorMessage) loader.failAndExit(actualOptions.getErrorMessage(error)) + else loader.failAndExit() + } else { + if (actualOptions.getErrorMessage) loader.fail(actualOptions.getErrorMessage(error)) + else loader.fail() + } + + throw e + } + }, startLoading(text: string): Loader { const loader: InternalLoader = { isActive: false, @@ -30,7 +64,8 @@ export const output = { text, spinner: ora({ spinner: "dots4", - color: "blue" + color: "blue", + prefixText: "\n" }), fail(message?: string) { if (this.state !== "running") throw new Error("state is not 'running'") @@ -45,7 +80,16 @@ export const output = { } }, failAndExit(message?: string): never { - this.fail(message) + if (this.state !== "running") throw new Error("state is not 'running'") + + if (message !== undefined) this.text = this.text + " — " + kleur.red(message) + + if (!this.isActive) { + last(loadersStack)?.deactivate() + } + + this.spinner.fail(this.text) + process.exit(1) }, setText(text: string) { @@ -91,7 +135,7 @@ export const output = { process.stdout.write(text) } else { loader.deactivate() - process.stdout.write(text + "\n" + "\n") + process.stdout.write(text) loader.activate() } }, diff --git a/src/pack.ts b/src/pack.ts index 1c2345d..f26940d 100644 --- a/src/pack.ts +++ b/src/pack.ts @@ -1,11 +1,11 @@ -import { findPackDirectoryPath, HorizrFile, ModFile, ModFileModrinthSource, readHorizrFile, readModFile, readModIds, writeModFile } from "./files.js" +import { findPackDirectoryPath, getOverrideDirents, HorizrFile, ModFile, ModFileModrinthSource, readHorizrFile, readModFile, readModIds, writeModFile } from "./files.js" import { output } from "./output.js" import pLimit from "p-limit" import kleur from "kleur" import { modrinthApi } from "./modrinth/api.js" import semver from "semver" import { Path } from "./path.js" -import { ReleaseChannel } from "./shared.js" +import { ReleaseChannel, Side, sides } from "./shared.js" import { getModFileDataForModrinthVersion, sortModrinthVersionsByPreference } from "./modrinth/utils.js" export interface Update { @@ -16,13 +16,18 @@ export interface Update { } export interface Pack { - rootDirectoryPath: Path + paths: { + root: Path, + generated: Path, + overrides: Record + }, horizrFile: HorizrFile mods: Mod[] addMod(id: string, file: ModFile): Promise findModByCode(code: string): Mod | null findModByCodeOrFail(code: string): Mod + validateOverridesDirectories(): Promise checkForUpdates(allowedReleaseChannels: ReleaseChannel[]): Promise } @@ -41,9 +46,18 @@ let pack: Pack export async function usePack(): Promise { if (pack === undefined) { const rootDirectoryPath = await findPackDirectoryPath() + const overridesDirectoryPath = rootDirectoryPath.resolve("overrides") pack = { - rootDirectoryPath, + paths: { + root: rootDirectoryPath, + generated: rootDirectoryPath.resolve("generated"), + overrides: { + client: overridesDirectoryPath.resolve("client"), + server: overridesDirectoryPath.resolve("server"), + "client-server": overridesDirectoryPath.resolve("client-server") + } + }, horizrFile: await readHorizrFile(rootDirectoryPath), mods: await Promise.all((await readModIds(rootDirectoryPath)).map(async id => { const mod: Mod = { @@ -120,6 +134,20 @@ export async function usePack(): Promise { if (mod === null) return output.failAndExit("The mod could not be found.") return mod }, + async validateOverridesDirectories() { + const dirents = await getOverrideDirents(overridesDirectoryPath) + + const notDirectories = dirents.filter(dirent => !dirent.isDirectory()) + if (notDirectories.length !== 0) + output.failAndExit( + `The ${kleur.yellow("overrides")} directory contains files that are not directories:\n${notDirectories.slice(0, 5).map(e => `- ${e.name}`).join("\n")}` + + (notDirectories.length > 5 ? `\n${kleur.gray(`and ${notDirectories.length - 5} more`)}` : "") + + `\n\nAll files must reside in one of these sub-directories: ${sides.map(kleur.yellow).join(", ")}` + ) + + if (dirents.some(dirent => !(sides as string[]).includes(dirent.name))) + output.failAndExit(`The ${kleur.yellow("overrides")} directory may only contain the following sub-directories:\n${sides.map(side => `- ${side}`).join("\n")}`) + }, async checkForUpdates(allowedReleaseChannels: ReleaseChannel[]): Promise { const limit = pLimit(5) diff --git a/src/path.ts b/src/path.ts index 7009ef9..d6a1b86 100644 --- a/src/path.ts +++ b/src/path.ts @@ -9,8 +9,7 @@ export class Path { * 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()))) + return new Path(pathModule.resolve(this.value, ...segments.map(s => s.toString()))) } /** diff --git a/src/shared.ts b/src/shared.ts index a20c94d..d923840 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -1,3 +1,6 @@ export type ModLoader = "fabric" | "quilt" export type ReleaseChannel = "alpha" | "beta" | "release" export const releaseChannelOrder: ReleaseChannel[] = ["alpha", "beta", "release"] + +export type Side = "client" | "server" | "client-server" +export const sides: [Side, ...Side[]] = ["client", "server", "client-server"] diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..4b63550 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,6 @@ +declare module "@root/walk" { + import { Dirent } from "fs" + export type Visitor = (error: Error, path: string, dirent: Dirent) => Promise + + export function walk(path: string, visitor: Visitor): Promise +} diff --git a/src/utils.ts b/src/utils.ts index 78a1436..d5e639e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,9 +1,26 @@ import { InvalidArgumentError } from "commander" import hash, { HashaInput } from "hasha" -import { zip as zipWithCallback } from "cross-zip" -import { promisify } from "util" +import { Path } from "./path.js" +import { ZipFile } from "yazl" +import { walk } from "@root/walk" +import fs from "fs-extra" +import { pEvent } from "p-event" -export const zip = promisify(zipWithCallback) +export async function zipDirectory(directoryPath: Path, outputFilePath: Path) { + const zipFile = new ZipFile() + zipFile.outputStream.pipe(fs.createWriteStream(outputFilePath.toString())) + + await walk(directoryPath.toString(), async (error, path, dirent) => { + if (error) return + if (directoryPath.toString() === path) return true + if (dirent.name.startsWith(".")) return false + + if (dirent.isFile()) zipFile.addFile(path, directoryPath.relative(Path.create(path)).toString(), { compress: true }) + }) + + zipFile.end() + await pEvent(zipFile.outputStream, "close") +} export const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) diff --git a/test-pack/.gitignore b/test-pack/.gitignore new file mode 100644 index 0000000..9e0adcc --- /dev/null +++ b/test-pack/.gitignore @@ -0,0 +1 @@ +/generated/ diff --git a/test-pack/mods/charm.json b/test-pack/mods/charm.json index 2b1b738..ab801d6 100644 --- a/test-pack/mods/charm.json +++ b/test-pack/mods/charm.json @@ -1,15 +1,17 @@ { "name": "Charm", "enabled": true, - "ignoreUpdates": true, - "side": "client+server", + "ignoreUpdates": false, + "side": "client-server", "file": { "version": "4.2.0+1.18.2", "name": "charm-fabric-1.18.2-4.2.0.jar", "size": 3413876, "downloadUrl": "https://cdn.modrinth.com/data/pOQTcQmj/versions/4.2.0+1.18.2/charm-fabric-1.18.2-4.2.0.jar", - "hashAlgorithm": "sha512", - "hash": "3c8cd08ab1e37dcbf0f5a956cd20d84c98e58ab49fdc13faafb9c2af4dbf7fba7c8328cb5365997fe4414cfc5cb554ed13b3056a22df1c6bd335594f380facb6" + "hashes": { + "sha1": "ebb87cd7fa7935bc30e5ad0b379bb4ede8723a82", + "sha512": "3c8cd08ab1e37dcbf0f5a956cd20d84c98e58ab49fdc13faafb9c2af4dbf7fba7c8328cb5365997fe4414cfc5cb554ed13b3056a22df1c6bd335594f380facb6" + } }, "source": { "type": "modrinth", diff --git a/test-pack/mods/fabric-api.json b/test-pack/mods/fabric-api.json new file mode 100644 index 0000000..75fa0a4 --- /dev/null +++ b/test-pack/mods/fabric-api.json @@ -0,0 +1,21 @@ +{ + "name": "Fabric API", + "enabled": true, + "ignoreUpdates": false, + "side": "client-server", + "file": { + "version": "0.58.0+1.18.2", + "name": "fabric-api-0.58.0+1.18.2.jar", + "size": 1445029, + "downloadUrl": "https://cdn.modrinth.com/data/P7dR8mSH/versions/0.58.0+1.18.2/fabric-api-0.58.0%2B1.18.2.jar", + "hashes": { + "sha1": "b9ab9ab267f8cdff525f9a8edb26435d3e2455f6", + "sha512": "92317b8d48b20d1b370ab67e4954d1db4861b8fb561935edc0c0fc8a525fefbd3c159f3cfbf83ec3455e3179561fab554645138c6d79f5f597abea77dc1a03ed" + } + }, + "source": { + "type": "modrinth", + "modId": "P7dR8mSH", + "versionId": "4XRtXhtL" + } +} diff --git a/test-pack/mods/sodium.json b/test-pack/mods/sodium.json new file mode 100644 index 0000000..55d1e78 --- /dev/null +++ b/test-pack/mods/sodium.json @@ -0,0 +1,21 @@ +{ + "name": "Sodium", + "enabled": true, + "ignoreUpdates": false, + "side": "client", + "file": { + "version": "mc1.18.2-0.4.1", + "name": "sodium-fabric-mc1.18.2-0.4.1+build.15.jar", + "size": 1318645, + "downloadUrl": "https://cdn.modrinth.com/data/AANobbMI/versions/mc1.18.2-0.4.1/sodium-fabric-mc1.18.2-0.4.1%2Bbuild.15.jar", + "hashes": { + "sha1": "f839863a6be7014b8d80058ea1f361521148d049", + "sha512": "86eb4db8fdb9f0bb06274c4f150b55273b5b770ffc89e0ba68011152a231b79ebe0b1adda0dd194f92cdcb386f7a60863d9fee5d15c1c3547ffa22a19083a1ee" + } + }, + "source": { + "type": "modrinth", + "modId": "AANobbMI", + "versionId": "74Y5Z8fo" + } +} diff --git a/test-pack/overrides/client/options.txt b/test-pack/overrides/client/options.txt new file mode 100644 index 0000000..1e3d819 --- /dev/null +++ b/test-pack/overrides/client/options.txt @@ -0,0 +1 @@ +option=1