diff --git a/package-lock.json b/package-lock.json index 93b8002..fe51988 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1788,6 +1788,15 @@ "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", "dev": true }, + "@types/yazl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/yazl/-/yazl-2.4.2.tgz", + "integrity": "sha512-T+9JH8O2guEjXNxqmybzQ92mJUh2oCwDDMSSimZSe1P+pceZiFROZLYmcbqkzV5EUwz6VwcKXCO2S2yUpra6XQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@typescript-eslint/eslint-plugin": { "version": "4.32.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.32.0.tgz", @@ -2201,6 +2210,12 @@ "node-int64": "^0.4.0" } }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -5165,6 +5180,15 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true + }, + "yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3" + } } } } diff --git a/package.json b/package.json index e773351..451da0e 100644 --- a/package.json +++ b/package.json @@ -30,13 +30,15 @@ "@babel/preset-env": "^7.15.6", "@babel/preset-typescript": "^7.15.0", "@types/node": "^16.10.2", + "@types/yazl": "^2.4.2", "@typescript-eslint/eslint-plugin": "^4.32.0", "@typescript-eslint/parser": "^4.32.0", "@vercel/ncc": "^0.31.1", "babel-jest": "^27.2.4", "eslint": "^7.32.0", "jest": "^27.2.4", - "typescript": "^4.4.3" + "typescript": "^4.4.3", + "yazl": "^2.5.1" }, "dependencies": { "@actions/core": "^1.6.0", diff --git a/test/content/fabric.mod.json b/test/content/fabric.mod.json new file mode 100644 index 0000000..f4ed1b2 --- /dev/null +++ b/test/content/fabric.mod.json @@ -0,0 +1,73 @@ +{ + "schemaVersion": 1, + "id": "example-mod", + "version": "0.1.0", + "name": "Example Mod", + "description": "Description", + "authors": [ + "Author" + ], + "contact": { + "homepage": "https://github.com/", + "sources": "https://github.com/" + }, + "license": "MIT", + "icon": "assets/example-mod/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + "example.ExampleMod" + ] + }, + "mixins": [ + "example-mod.mixins.json" + ], + + "projects": { + "modrinth": "AANobbMI" + }, + "custom": { + "projects": { + "curseforge": 394468 + }, + "mc-publish": { + "github": "mc1.18-0.4.0-alpha5" + } + }, + + "depends": { + "fabricloader": ">=0.11.3", + "fabric": "*", + "minecraft": "1.17.x", + "java": ">=16" + }, + "recommends": { + "recommended-mod": { + "version": "0.2.0", + "projects": { + "modrinth": "AAAA" + }, + "custom": { + "projects": { + "curseforge": 42 + }, + "mc-publish": { + "github": "v0.2.0", + "ignore": true + } + } + } + }, + "includes": { + "included-mod": "*" + }, + "suggests": { + "suggested-mod": "*" + }, + "conflicts": { + "conflicting-mod": "*" + }, + "breaks": { + "breaking-mod": "*" + } + } \ No newline at end of file diff --git a/test/content/mods.toml b/test/content/mods.toml new file mode 100644 index 0000000..00141e2 --- /dev/null +++ b/test/content/mods.toml @@ -0,0 +1,65 @@ +modLoader="javafml" +loaderVersion="[34,)" +issueTrackerURL="https://github.com/" +displayURL="https://github.com/" +authors="Author" +license="MIT" + +[[mods]] + modId="example-mod" + version="0.1.0" + displayName="Example Mod" + description=''' + Example mod + ''' + +[projects] + modrinth="AANobbMI" +[custom.projects] + curseforge=394468 +[custom.mc-publish] + github="mc1.18-0.4.0-alpha5" + +[[dependencies.example-mod]] + modId="minecraft" + mandatory=true + versionRange="[1.17, 1.18)" + side="BOTH" + +[[dependencies.example-mod]] + modId="forge" + mandatory=true + versionRange="[34,)" + ordering="NONE" + side="BOTH" + +[[dependencies.example-mod]] + modId="recommended-mod" + mandatory=false + versionRange="0.2.0" + ordering="NONE" + side="BOTH" + [dependencies.example-mod.projects] + modrinth="AAAA" + [dependencies.example-mod.custom.projects] + curseforge=42 + [dependencies.example-mod.custom.mc-publish] + github="v0.2.0" + ignore=true + + +[[dependencies.example-mod]] + modId="included-mod" + mandatory=false + embedded=true + versionRange="*" + ordering="NONE" + side="BOTH" + +[[dependencies.example-mod]] + modId="breaking-mod" + mandatory=false + incompatible=true + versionRange="*" + ordering="NONE" + side="BOTH" diff --git a/test/mod-metadata-reader.test.ts b/test/mod-metadata-reader.test.ts new file mode 100644 index 0000000..fa23b40 --- /dev/null +++ b/test/mod-metadata-reader.test.ts @@ -0,0 +1,156 @@ +import { describe, test, expect, beforeAll, afterAll } from "@jest/globals"; +import Dependency from "../src/metadata/dependency"; +import DependencyKind from "../src/metadata/dependency-kind"; +import ModMetadataReader from "../src/metadata/mod-metadata-reader"; +import PublisherTarget from "../src/publishing/publisher-target"; +import { ZipFile } from "yazl"; +import fs from "fs"; + +describe("ModMetadataReader.readMetadata", () => { + describe("Fabric", () => { + beforeAll(() => new Promise(resolve => { + const zip = new ZipFile(); + zip.addFile("./test/content/fabric.mod.json", "fabric.mod.json"); + zip.end(); + zip.outputStream.pipe(fs.createWriteStream("example-mod.fabric.jar")).on("close", resolve); + })); + + afterAll(() => new Promise(resolve => fs.unlink("example-mod.fabric.jar", resolve))); + + test("the format can be read", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.fabric.jar"); + expect(metadata).toBeTruthy(); + }); + + test("mod info can be read", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.fabric.jar"); + expect(metadata.id).toBe("example-mod"); + expect(metadata.name).toBe("Example Mod"); + expect(metadata.version).toBe("0.1.0"); + expect(metadata.loaders).toMatchObject(["fabric"]); + }); + + test("project ids can be specified in the config file", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.fabric.jar"); + expect(metadata.getProjectId(PublisherTarget.Modrinth)).toBe("AANobbMI"); + expect(metadata.getProjectId(PublisherTarget.CurseForge)).toBe("394468"); + expect(metadata.getProjectId(PublisherTarget.GitHub)).toBe("mc1.18-0.4.0-alpha5"); + }); + + test("all dependencies are read", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.fabric.jar"); + expect(metadata.dependencies).toHaveLength(9); + const dependencies = metadata.dependencies.reduce((agg, x) => { agg[x.id] = x; return agg; }, >{}); + expect(dependencies["fabricloader"]?.kind).toBe(DependencyKind.Depends); + expect(dependencies["fabric"]?.kind).toBe(DependencyKind.Depends); + expect(dependencies["minecraft"]?.kind).toBe(DependencyKind.Depends); + expect(dependencies["java"]?.kind).toBe(DependencyKind.Depends); + expect(dependencies["recommended-mod"]?.kind).toBe(DependencyKind.Recommends); + expect(dependencies["included-mod"]?.kind).toBe(DependencyKind.Includes); + expect(dependencies["suggested-mod"]?.kind).toBe(DependencyKind.Suggests); + expect(dependencies["conflicting-mod"]?.kind).toBe(DependencyKind.Conflicts); + expect(dependencies["breaking-mod"]?.kind).toBe(DependencyKind.Breaks); + }); + + test("dependency info can be read", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.fabric.jar"); + const minecraft = metadata.dependencies.find(x => x.id === "minecraft"); + expect(minecraft).toBeTruthy(); + expect(minecraft.id).toBe("minecraft"); + expect(minecraft.kind).toBe(DependencyKind.Depends); + expect(minecraft.version).toBe("1.17.x"); + expect(minecraft.ignore).toBe(false); + for (const project of PublisherTarget.getValues()) { + expect(minecraft.getProjectSlug(project)).toBe(minecraft.id); + } + }); + + test("custom metadata can be attached to dependency entry", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.fabric.jar"); + const recommended = metadata.dependencies.find(x => x.id === "recommended-mod"); + expect(recommended).toBeTruthy(); + expect(recommended.id).toBe("recommended-mod"); + expect(recommended.kind).toBe(DependencyKind.Recommends); + expect(recommended.version).toBe("0.2.0"); + expect(recommended.ignore).toBe(true); + expect(recommended.getProjectSlug(PublisherTarget.Modrinth)).toBe("AAAA"); + expect(recommended.getProjectSlug(PublisherTarget.CurseForge)).toBe("42"); + expect(recommended.getProjectSlug(PublisherTarget.GitHub)).toBe("v0.2.0"); + }); + }); + + describe("Forge", () => { + beforeAll(() => new Promise(resolve => { + const zip = new ZipFile(); + zip.addFile("./test/content/mods.toml", "META-INF/mods.toml"); + zip.end(); + zip.outputStream.pipe(fs.createWriteStream("example-mod.forge.jar")).on("close", resolve); + })); + + afterAll(() => new Promise(resolve => fs.unlink("example-mod.forge.jar", resolve))); + + test("the format can be read", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.forge.jar"); + expect(metadata).toBeTruthy(); + }); + + test("mod info can be read", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.forge.jar"); + expect(metadata.id).toBe("example-mod"); + expect(metadata.name).toBe("Example Mod"); + expect(metadata.version).toBe("0.1.0"); + expect(metadata.loaders).toMatchObject(["forge"]); + }); + + test("project ids can be specified in the config file", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.forge.jar"); + expect(metadata.getProjectId(PublisherTarget.Modrinth)).toBe("AANobbMI"); + expect(metadata.getProjectId(PublisherTarget.CurseForge)).toBe("394468"); + expect(metadata.getProjectId(PublisherTarget.GitHub)).toBe("mc1.18-0.4.0-alpha5"); + }); + + test("all dependencies are read", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.forge.jar"); + expect(metadata.dependencies).toHaveLength(5); + const dependencies = metadata.dependencies.reduce((agg, x) => { agg[x.id] = x; return agg; }, >{}); + expect(dependencies["forge"]?.kind).toBe(DependencyKind.Depends); + expect(dependencies["minecraft"]?.kind).toBe(DependencyKind.Depends); + expect(dependencies["recommended-mod"]?.kind).toBe(DependencyKind.Recommends); + expect(dependencies["included-mod"]?.kind).toBe(DependencyKind.Includes); + expect(dependencies["breaking-mod"]?.kind).toBe(DependencyKind.Breaks); + }); + + test("dependency info can be read", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.forge.jar"); + const minecraft = metadata.dependencies.find(x => x.id === "minecraft"); + expect(minecraft).toBeTruthy(); + expect(minecraft.id).toBe("minecraft"); + expect(minecraft.kind).toBe(DependencyKind.Depends); + expect(minecraft.version).toBe("[1.17, 1.18)"); + expect(minecraft.ignore).toBe(false); + for (const project of PublisherTarget.getValues()) { + expect(minecraft.getProjectSlug(project)).toBe(minecraft.id); + } + }); + + test("custom metadata can be attached to dependency entry", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.forge.jar"); + const recommended = metadata.dependencies.find(x => x.id === "recommended-mod"); + expect(recommended).toBeTruthy(); + expect(recommended.id).toBe("recommended-mod"); + expect(recommended.kind).toBe(DependencyKind.Recommends); + expect(recommended.version).toBe("0.2.0"); + expect(recommended.ignore).toBe(true); + expect(recommended.getProjectSlug(PublisherTarget.Modrinth)).toBe("AAAA"); + expect(recommended.getProjectSlug(PublisherTarget.CurseForge)).toBe("42"); + expect(recommended.getProjectSlug(PublisherTarget.GitHub)).toBe("v0.2.0"); + }); + }); + + describe("unsupported mod formats", () => { + test("null is returned when the format is not supported or specified file does not exist", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.unknown.jar"); + expect(metadata).toBeNull(); + }); + }); +});