Implement modrinth export

This commit is contained in:
Moritz Ruth 2022-08-18 14:15:31 +02:00
parent 5a1c6f47e4
commit d40c3b82e3
11 changed files with 139 additions and 4 deletions

View file

@ -20,6 +20,7 @@
], ],
"dependencies": { "dependencies": {
"@root/walk": "^1.1.0", "@root/walk": "^1.1.0",
"@sindresorhus/slugify": "^2.1.0",
"address": "^1.2.0", "address": "^1.2.0",
"commander": "^9.4.0", "commander": "^9.4.0",
"dedent": "^0.7.0", "dedent": "^0.7.0",

22
pnpm-lock.yaml generated
View file

@ -2,6 +2,7 @@ lockfileVersion: 5.4
specifiers: specifiers:
'@root/walk': ^1.1.0 '@root/walk': ^1.1.0
'@sindresorhus/slugify': ^2.1.0
'@types/dedent': ^0.7.0 '@types/dedent': ^0.7.0
'@types/fs-extra': ^9.0.13 '@types/fs-extra': ^9.0.13
'@types/lodash-es': ^4.17.6 '@types/lodash-es': ^4.17.6
@ -44,6 +45,7 @@ specifiers:
dependencies: dependencies:
'@root/walk': 1.1.0 '@root/walk': 1.1.0
'@sindresorhus/slugify': 2.1.0
address: 1.2.0 address: 1.2.0
commander: 9.4.0 commander: 9.4.0
dedent: 0.7.0 dedent: 0.7.0
@ -166,6 +168,22 @@ packages:
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
dev: false dev: false
/@sindresorhus/slugify/2.1.0:
resolution: {integrity: sha512-gU3Gdm/V167BmUwIn8APHZ3SeeRVRUSOdXxnt7Q/JkUHLXaaTA/prYmoRumwsSitJZWUDYMzDWdWgrOdvE8IRQ==}
engines: {node: '>=12'}
dependencies:
'@sindresorhus/transliterate': 1.5.0
escape-string-regexp: 5.0.0
dev: false
/@sindresorhus/transliterate/1.5.0:
resolution: {integrity: sha512-/sfSkoNelLq5riqNRp5uBjHIKBi1MWZk9ubRT1WiBQuTfmDf7BeQkph2DJzRB83QagMPHk2VDjuvpy0VuwyzdA==}
engines: {node: '>=12'}
dependencies:
escape-string-regexp: 5.0.0
lodash.deburr: 4.1.0
dev: false
/@szmarczak/http-timer/5.0.1: /@szmarczak/http-timer/5.0.1:
resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
@ -1162,6 +1180,10 @@ packages:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
dev: false dev: false
/lodash.deburr/4.1.0:
resolution: {integrity: sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==}
dev: false
/log-symbols/5.1.0: /log-symbols/5.1.0:
resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==}
engines: {node: '>=12'} engines: {node: '>=12'}

View file

@ -7,6 +7,8 @@ import { fetchFabricMinecraftVersions, fetchFabricVersions } from "../fabricApi.
import enquirer from "enquirer" import enquirer from "enquirer"
import { PACK_MANIFEST_FILE_NAME, PACK_MANIFEST_FORMAT_VERSION, PackManifest } from "../files.js" import { PACK_MANIFEST_FILE_NAME, PACK_MANIFEST_FORMAT_VERSION, PackManifest } from "../files.js"
import pathModule from "path" import pathModule from "path"
import { EXPORTS_DIRECTORY_NAME } from "../pack.js"
import slugify from "@sindresorhus/slugify"
export const initCommand = new Command("init") export const initCommand = new Command("init")
.argument("<path>") .argument("<path>")
@ -62,6 +64,7 @@ export const initCommand = new Command("init")
const file: PackManifest = { const file: PackManifest = {
formatVersion: PACK_MANIFEST_FORMAT_VERSION, formatVersion: PACK_MANIFEST_FORMAT_VERSION,
slug: slugify(answers.name),
meta: { meta: {
name: answers.name, name: answers.name,
version: "1.0.0", version: "1.0.0",
@ -76,7 +79,7 @@ export const initCommand = new Command("init")
} }
await fs.writeJson(manifestFilePath.toString(), file, { spaces: 2 }) await fs.writeJson(manifestFilePath.toString(), file, { spaces: 2 })
await fs.writeFile(path.resolve(".gitignore").toString(), "/generated/") await fs.writeFile(path.resolve(".gitignore").toString(), `/${EXPORTS_DIRECTORY_NAME}/`)
output.println(kleur.green(`Successfully initialized pack in ${kleur.yellow(pathModule.normalize(pathString))}`)) output.println(kleur.green(`Successfully initialized pack in ${kleur.yellow(pathModule.normalize(pathString))}`))
}) })

View file

@ -0,0 +1,41 @@
import { Command } from "commander"
import { usePack } from "../../pack.js"
import { output } from "../../utils/output.js"
import fs from "fs-extra"
import { generateOutputDirectory } from "../../modrinth/exporting.js"
import kleur from "kleur"
import { zipDirectory } from "../../utils/zip.js"
const EXPORT_OUTPUT_DIRECTORY_NAME = "modrinth"
export const exportCommand = new Command("export")
.option("-s, --no-generate", "Skip regenerating the output directory.")
.option("-z, --no-zip", "Skip creating a zipped .mrpack file.")
.option("-c, --clean", "Remove the output directory afterwards.")
.action(async options => {
const pack = await usePack()
const outputDirectoryPath = pack.paths.exports.resolve(EXPORT_OUTPUT_DIRECTORY_NAME)
const loader = output.startLoading("Exporting")
if (options.generate) {
await output.withLoading(generateOutputDirectory(outputDirectoryPath), "Generating the output directory")
output.println(kleur.green(`Generated Modrinth pack directory.`))
}
if (options.zip) {
const fileName = `${pack.manifest.slug}-${pack.manifest.meta.version}.mrpack`
if (!(await fs.pathExists(outputDirectoryPath.toString())))
output.failAndExit(`The ${kleur.yellow(EXPORT_OUTPUT_DIRECTORY_NAME)} export directory does not exist.\nRun the command without ${kleur.yellow("--no-generate")} to create it.`)
await output.withLoading(zipDirectory(outputDirectoryPath, pack.paths.exports.resolve(fileName)), `Creating ${kleur.yellow(".mrpack")} file`)
output.println(kleur.green(`Created ${kleur.yellow(fileName)}`))
}
if (options.clean) {
await fs.remove(outputDirectoryPath.toString())
output.println(kleur.green(`Removed the ${kleur.yellow(EXPORT_OUTPUT_DIRECTORY_NAME)} directory.`))
}
loader.stop()
})

View file

@ -3,10 +3,12 @@ import { activateCommand } from "./activate.js"
import dedent from "dedent" import dedent from "dedent"
import kleur from "kleur" import kleur from "kleur"
import { openCommand } from "./open.js" import { openCommand } from "./open.js"
import { exportCommand } from "./export.js"
export const modrinthCommand = new Command("modrinth") export const modrinthCommand = new Command("modrinth")
.alias("mr") .alias("mr")
.addCommand(activateCommand) .addCommand(activateCommand)
.addCommand(exportCommand)
.addCommand(openCommand) .addCommand(openCommand)
.addHelpText("afterAll", dedent` .addHelpText("afterAll", dedent`
\n${kleur.yellow("<code>")} may be one of the following: \n${kleur.yellow("<code>")} may be one of the following:

View file

@ -49,6 +49,7 @@ export const PACK_MANIFEST_FILE_NAME = "horizr.json"
export const horizrFileSchema = z.object({ export const horizrFileSchema = z.object({
formatVersion: z.literal(PACK_MANIFEST_FORMAT_VERSION), formatVersion: z.literal(PACK_MANIFEST_FORMAT_VERSION),
slug: z.string(),
meta: z.object({ meta: z.object({
name: z.string(), name: z.string(),
version: z.string(), version: z.string(),

60
src/modrinth/exporting.ts Normal file
View file

@ -0,0 +1,60 @@
import { AbsolutePath } from "../utils/path.js"
import { output } from "../utils/output.js"
import fs from "fs-extra"
import { Side, usePack } from "../pack.js"
import kleur from "kleur"
const overridesDirectoryNameBySide: Record<Side, string> = {
client: "client-overrides",
server: "server-overrides",
universal: "overrides"
}
export async function generateOutputDirectory(outputDirectoryPath: AbsolutePath) {
const pack = await usePack()
await fs.remove(outputDirectoryPath.toString())
await fs.mkdirp(outputDirectoryPath.toString())
await fs.writeJson(outputDirectoryPath.resolve("modrinth.index.json").toString(), {
formatVersion: 1,
game: "minecraft",
versionId: pack.manifest.meta.version,
name: pack.manifest.meta.name,
summary: pack.manifest.meta.description,
dependencies: {
minecraft: pack.manifest.versions.minecraft,
"fabric-loader": pack.manifest.versions.fabric
},
files: pack.metaFiles.map(metaFile => ({
path: metaFile.effectivePath.toString(),
hashes: {
sha1: metaFile.content.version.hashes.sha1,
sha512: metaFile.content.version.hashes.sha512
},
env: {
client: metaFile.side === "client" || metaFile.side === "universal" ? "required" : "unsupported",
server: metaFile.side === "server" || metaFile.side === "universal" ? "required" : "unsupported"
},
downloads: [
metaFile.content.version.downloadUrl
],
fileSize: metaFile.content.version.size
}))
}, { spaces: 2 })
let i = 0
for (const staticSourceFile of pack.staticSourceFiles) {
i++
const loader = output.startLoading(`Exporting static source file (${i}/${pack.staticSourceFiles.length}): ${kleur.yellow(staticSourceFile.relativePath.toString())}`)
const outputPath = outputDirectoryPath.resolve(overridesDirectoryNameBySide[staticSourceFile.side], staticSourceFile.effectivePath)
await fs.mkdirp(outputPath.parent().toString())
await fs.copy(staticSourceFile.absolutePath.toString(), outputPath.toString())
// Workaround for https://github.com/PolyMC/PolyMC/issues/1060
if (staticSourceFile.side === "client") {
await fs.mkdirp(outputDirectoryPath.resolve(overridesDirectoryNameBySide.universal, staticSourceFile.effectivePath).parent().toString())
}
loader.stop()
}
}

View file

@ -73,6 +73,8 @@ export interface Update {
apply(): Promise<void> apply(): Promise<void>
} }
export const EXPORTS_DIRECTORY_NAME = "exports"
let pack: Pack let pack: Pack
export async function usePack(): Promise<Pack> { export async function usePack(): Promise<Pack> {
if (pack === undefined) { if (pack === undefined) {
@ -97,7 +99,7 @@ export async function usePack(): Promise<Pack> {
side: pathSegments[0] as Side, side: pathSegments[0] as Side,
relativePath: relativePath, relativePath: relativePath,
absolutePath: sourceDirectoryPath.resolve(relativePath), absolutePath: sourceDirectoryPath.resolve(relativePath),
effectivePath: RelativePath._createDirect(pathSegments.slice(1).join("/")), effectivePath: RelativePath._createDirect(pathSegments.slice(1).join("/"))
} }
if (relativePath.toString().endsWith("." + META_FILE_EXTENSION)) { if (relativePath.toString().endsWith("." + META_FILE_EXTENSION)) {
@ -107,6 +109,7 @@ export async function usePack(): Promise<Pack> {
const metaFile: MetaFile = { const metaFile: MetaFile = {
...sourceFile, ...sourceFile,
isStatic: false, isStatic: false,
effectivePath: sourceFile.effectivePath.parent().joinedWith(content.version.fileName),
content, content,
fetchUpdates: source?.type === "modrinth" fetchUpdates: source?.type === "modrinth"
? allowedReleaseChannels => fetchModrinthModUpdates(metaFile, source, allowedReleaseChannels, manifest.versions.minecraft) ? allowedReleaseChannels => fetchModrinthModUpdates(metaFile, source, allowedReleaseChannels, manifest.versions.minecraft)
@ -134,7 +137,7 @@ export async function usePack(): Promise<Pack> {
paths: { paths: {
root: rootDirectoryPath, root: rootDirectoryPath,
source: sourceDirectoryPath, source: sourceDirectoryPath,
exports: rootDirectoryPath.resolve("exports") exports: rootDirectoryPath.resolve(EXPORTS_DIRECTORY_NAME)
}, },
manifest, manifest,
metaFiles, metaFiles,

View file

@ -1 +1 @@
/generated/ /exports/

View file

@ -1,5 +1,6 @@
{ {
"formatVersion": 1, "formatVersion": 1,
"slug": "test",
"meta": { "meta": {
"name": "Test", "name": "Test",
"version": "1.0.0", "version": "1.0.0",

View file

@ -0,0 +1 @@
content