QuiltModMetadata refactoring

This commit is contained in:
Kir_Antipov 2022-11-25 01:37:53 +03:00
parent 2fc40453de
commit b7fa4fab67
6 changed files with 211 additions and 113 deletions

View file

@ -201,7 +201,7 @@ Can be automatically retrieved from the config file of your mod:
- `quilt.mod.json` (Quilt) - `quilt.mod.json` (Quilt)
- `mc-publish` field *(recommended)*: - Custom `mc-publish` field:
```json ```json
{ {
// ... // ...
@ -211,16 +211,6 @@ Can be automatically retrieved from the config file of your mod:
} }
``` ```
- `projects` field:
```json
{
// ...
"projects": {
"modrinth": "AANobbMI"
},
}
```
#### modrinth-token #### modrinth-token
A valid token for the Modrinth API. It's required if you want to publish your assets to Modrinth. A valid token for the Modrinth API. It's required if you want to publish your assets to Modrinth.
@ -307,7 +297,7 @@ Can be automatically retrieved from the config file of your mod:
- `quilt.mod.json` (Quilt) - `quilt.mod.json` (Quilt)
- `mc-publish` field *(recommended)*: - Custom `mc-publish` field:
```json ```json
{ {
// ... // ...
@ -317,16 +307,6 @@ Can be automatically retrieved from the config file of your mod:
} }
``` ```
- `projects` field:
```json
{
// ...
"projects": {
"curseforge": 394468
},
}
```
#### curseforge-token #### curseforge-token
A valid token for the CurseForge API. It's required if you want to publish your assets to CurseForge. A valid token for the CurseForge API. It's required if you want to publish your assets to CurseForge.
@ -613,7 +593,7 @@ Can be automatically retrieved from the config file of your mod:
"required-dependency", "required-dependency",
{ {
"id": "optional-dependency", "id": "optional-dependency",
"version": "0.1.0", "versions": "0.1.0",
"optional": true "optional": true
} }
], ],
@ -631,11 +611,11 @@ Can be automatically retrieved from the config file of your mod:
"breaks": [ "breaks": [
{ {
"id": "incompatible-dependency", "id": "incompatible-dependency",
"version": "*" "versions": "*"
}, },
{ {
"id": "conflicting-dependency", "id": "conflicting-dependency",
"version": "*", "versions": "*",
"unless": "some-mod-that-fixes-conflict" "unless": "some-mod-that-fixes-conflict"
} }
], ],

View file

@ -0,0 +1,66 @@
type Plugin = string | { adapter?: string, value: string };
type Entrypoint = Plugin;
type License = string | {
name: string;
id: string;
url: string;
description?: string;
};
type Dependency = string | {
id: string;
version?: string;
versions?: string | string[];
reason?: string;
optional?: boolean;
unless?: Dependency | Dependency[];
};
// https://github.com/QuiltMC/rfcs/blob/main/specification/0002-quilt.mod.json.md
type QuiltModConfig = {
schema_version: 1;
quilt_loader: {
group: string;
id: string;
provides?: Dependency[];
version: string;
entrypoints?: Record<string, Entrypoint | Entrypoint[]>;
plugins?: Plugin[];
jars?: string[];
language_adapters?: Record<string, string>;
depends?: Dependency[];
breaks?: Dependency[];
load_type?: "always" | "if_possible" | "if_required";
repositories?: string[];
intermediate_mappings?: string;
metadata?: Record<string, unknown>;
name?: string;
description?: string;
contributors?: Record<string, string>;
contact?: {
email?: string;
homepage?: string;
issues?: string;
sources?: string;
[key: string]: string;
};
license?: License | License[];
icon?: string | Record<string, string>;
};
mixin?: string | string[];
access_widener?: string | string[];
minecraft?: {
environment?: "client" | "dedicated_server" | "*";
};
} & Record<string, any>;
namespace QuiltModConfig {
export const FILENAME = "quilt.mod.json";
}
export default QuiltModConfig;

View file

@ -1,17 +1,20 @@
import ModMetadata from "../../metadata/mod-metadata"; import ModMetadata from "../mod-metadata";
import ZippedModMetadataReader from "../../metadata/zipped-mod-metadata-reader"; import ZippedModMetadataReader from "../zipped-mod-metadata-reader";
import QuiltModConfig from "./quilt-mod-config";
import QuiltModMetadata from "./quilt-mod-metadata"; import QuiltModMetadata from "./quilt-mod-metadata";
export default class QuiltModMetadataReader extends ZippedModMetadataReader { class QuiltModMetadataReader extends ZippedModMetadataReader<QuiltModConfig> {
constructor() { constructor() {
super("quilt.mod.json"); super(QuiltModConfig.FILENAME);
} }
protected loadConfig(buffer: Buffer): Record<string, unknown> { protected loadConfig(buffer: Buffer): QuiltModConfig {
return JSON.parse(buffer.toString("utf8")); return JSON.parse(buffer.toString("utf8"));
} }
protected createMetadataFromConfig(config: Record<string, unknown>): ModMetadata { protected createMetadataFromConfig(config: QuiltModConfig): ModMetadata {
return new QuiltModMetadata(config); return new QuiltModMetadata(config);
} }
} }
export default QuiltModMetadataReader;

View file

@ -1,24 +1,41 @@
import action from "../../../package.json"; import action from "../../../package.json";
import Dependency from "../../metadata/dependency"; import Dependency from "../dependency";
import DependencyKind from "../../metadata/dependency-kind"; import DependencyKind from "../dependency-kind";
import ModConfig from "../../metadata/mod-config";
import ModConfigDependency from "../../metadata/mod-config-dependency";
import PublisherTarget from "../../publishing/publisher-target"; import PublisherTarget from "../../publishing/publisher-target";
import ModMetadata from "../mod-metadata";
import QuiltModConfig from "./quilt-mod-config";
function extractId(id?: string): string | null { type Aliases = Map<PublisherTarget, string>;
type QuiltDependency = QuiltModConfig["quilt_loader"]["breaks"][number];
type ExtendedQuiltDependency = QuiltDependency & {
embedded?: boolean;
incompatible?: boolean;
};
function getDependencies(config: QuiltModConfig): Dependency[] {
const root = config.quilt_loader;
return getExtendedDependencyEntries(root.depends)
.concat(getExtendedDependencyEntries(root.provides, x => x.embedded = true))
.concat(getExtendedDependencyEntries(root.breaks, x => x.incompatible = true))
.map(parseDependency)
.filter((x, i, self) => self.findIndex(y => x.id === y.id && x.kind === y.kind) === i);
}
function parseId(id?: string): string | null {
if (!id) { if (!id) {
return id ?? null; return id ?? null;
} }
const separatorIndex = id.indexOf(":"); const separatorIndex = id.indexOf(":");
if (separatorIndex !== -1) { if (separatorIndex === -1) {
id = id.substring(separatorIndex + 1); return id;
} }
return id.substring(separatorIndex + 1);
return id;
} }
function getDependencyEntries(container: any, transformer?: (x: any) => void): any[] { function getExtendedDependencyEntries(container: QuiltDependency[], transformer?: (x: ExtendedQuiltDependency) => void): ExtendedQuiltDependency[] {
if (!Array.isArray(container)) { if (!Array.isArray(container)) {
return []; return [];
} }
@ -30,68 +47,101 @@ function getDependencyEntries(container: any, transformer?: (x: any) => void): a
return container; return container;
} }
const ignoredByDefault = ["minecraft", "java", "quilt_loader"]; function parseDependency(body: ExtendedQuiltDependency): Dependency {
const aliases = new Map([ const id = parseId(typeof body === "string" ? body : String(body.id ?? ""));
const ignoredByDefault = isDependencyIgnoredByDefault(id);
const defaultAliases = getDefaultDependencyAliases(id);
if (typeof body === "string") {
return Dependency.create({ id, ignore: ignoredByDefault, aliases: defaultAliases });
}
const version = body.version ?? (Array.isArray(body.versions) ? body.versions.join(" || ") : body.versions || "*");
const kind = (
body.incompatible && body.unless && DependencyKind.Conflicts ||
body.incompatible && DependencyKind.Breaks ||
body.embedded && DependencyKind.Includes ||
body.optional && DependencyKind.Recommends ||
DependencyKind.Depends
);
const ignore = body[action.name]?.ignore ?? ignoredByDefault;
const aliases = new Map([...(defaultAliases || [])]);
for (const target of PublisherTarget.getValues()) {
const targetName = PublisherTarget.toString(target).toLowerCase();
const alias = body[action.name]?.[targetName];
if (alias) {
aliases.set(target, String(alias));
}
}
return Dependency.create({ id, version, kind, ignore, aliases });
}
const ignoredByDefault = [
"minecraft",
"java",
"quilt_loader",
];
function isDependencyIgnoredByDefault(id: string): boolean {
return ignoredByDefault.includes(id);
}
const defaultAliases = new Map<string, string | Aliases>([
["fabric", "fabric-api"], ["fabric", "fabric-api"],
["quilted_fabric_api", "qsl"], ["quilted_fabric_api", "qsl"],
]); ]);
function createDependency(body: any): Dependency { function getDefaultDependencyAliases(id: string): Aliases | null {
const id = extractId(typeof body === "string" ? body : String(body.id ?? "")); if (id.startsWith("quilted_")) {
const ignore = ignoredByDefault.includes(id); id = "quilted_fabric_api";
if (id.startsWith("quilted_") || id.startsWith("quilt_")) {
aliases.set(id, "qsl");
} }
if (typeof body === "string") { if (!defaultAliases.has(id)) {
const dependencyAliases = aliases.has(id) ? new Map(PublisherTarget.getValues().map(x => [x, aliases.get(id)])) : null; return null;
return Dependency.create({ id, ignore, aliases: dependencyAliases });
} }
const dependencyMetadata = { const aliases = defaultAliases.get(id);
ignore, if (typeof aliases !== "string") {
...body, return new Map([...aliases]);
id,
version: body.version ?? String(Array.isArray(body.versions) ? body.versions[0] : body.versions || "*"),
kind: (
body.incompatible && body.unless && DependencyKind.Conflicts ||
body.incompatible && DependencyKind.Breaks ||
body.embedded && DependencyKind.Includes ||
body.optional && DependencyKind.Recommends ||
DependencyKind.Depends
)
};
if (aliases.has(id)) {
if (!dependencyMetadata[action.name]) {
dependencyMetadata[action.name] = {};
}
for (const target of PublisherTarget.getValues()) {
const targetName = PublisherTarget.toString(target).toLowerCase();
if (typeof dependencyMetadata[action.name][targetName] !== "string") {
dependencyMetadata[action.name][targetName] = aliases.get(id);
}
}
} }
return new ModConfigDependency(dependencyMetadata);
return new Map(PublisherTarget.getValues().map(x => [x, aliases]));
} }
export default class QuiltModMetadata extends ModConfig { function getProjects(config: QuiltModConfig): Map<PublisherTarget, string> {
public readonly id: string; const projects = new Map();
public readonly name: string; for (const target of PublisherTarget.getValues()) {
public readonly version: string; const targetName = PublisherTarget.toString(target).toLowerCase();
public readonly loaders: string[]; const projectId = config[action.name]?.[targetName] ?? config.projects?.[targetName];
public readonly dependencies: Dependency[];
constructor(config: Record<string, unknown>) { if (projectId) {
super(config); projects.set(target, String(projectId));
const root = <Record<string, unknown>>this.config.quilt_loader ?? {}; }
this.id = String(root.id ?? ""); }
this.name = String(root.name ?? this.id); return projects;
this.version = String(root.version ?? "*"); }
class QuiltModMetadata implements ModMetadata {
readonly id: string;
readonly name: string;
readonly version: string;
readonly loaders: string[];
readonly dependencies: Dependency[];
private readonly _projects: Map<PublisherTarget, string>;
constructor(config: QuiltModConfig) {
this.id = String(config.quilt_loader.id ?? "");
this.name = String(config.quilt_loader.name ?? this.id);
this.version = String(config.quilt_loader.version ?? "*");
this.loaders = ["quilt"]; this.loaders = ["quilt"];
this.dependencies = getDependencyEntries(root.depends) this.dependencies = getDependencies(config);
.concat(getDependencyEntries(root.provides, x => x.embedded = true)) this._projects = getProjects(config);
.concat(getDependencyEntries(root.breaks, x => x.incompatible = true)) }
.map(createDependency)
.filter((x, i, self) => self.findIndex(y => x.id === y.id && x.kind === y.kind) === i); getProjectId(project: PublisherTarget): string | undefined {
return this._projects.get(project);
} }
} }
export default QuiltModMetadata;

View file

@ -27,35 +27,32 @@
"depends": [ "depends": [
{ {
"id": "quilt_loader", "id": "quilt_loader",
"version": ">=0.11.3" "versions": ">=0.11.3"
}, },
{ {
"id": "quilt_base", "id": "quilt_base",
"version": ">=0.40.0" "versions": ">=0.40.0"
}, },
{ {
"id": "minecraft", "id": "minecraft",
"version": "1.17.x" "versions": [
"1.17",
"1.17.1"
]
}, },
{ {
"id": "java", "id": "java",
"version": ">=16" "versions": ">=16"
}, },
{ {
"id": "recommended-mod", "id": "recommended-mod",
"version": "0.2.0", "versions": "0.2.0",
"optional": true, "optional": true,
"mc-publish": { "mc-publish": {
"curseforge": 42,
"github": "v0.2.0",
"modrinth": "AAAA", "modrinth": "AAAA",
"ignore": true "ignore": true
},
"projects": {
"curseforge": 42
},
"custom": {
"projects": {
"github": "v0.2.0"
}
} }
} }
], ],
@ -66,22 +63,18 @@
"breaking-mod", "breaking-mod",
{ {
"id": "conflicting:conflicting-mod", "id": "conflicting:conflicting-mod",
"version": "<0.40.0", "versions": "<0.40.0",
"unless": "fix-conflicting-mod" "unless": "fix-conflicting-mod"
} }
] ]
}, },
"mc-publish": { "mc-publish": {
"github": "mc1.18-0.4.0-alpha5",
"modrinth": "AANobbMI" "modrinth": "AANobbMI"
}, },
"projects": { "projects": {
"curseforge": 394468 "curseforge": 394468
}, },
"custom": {
"projects": {
"github": "mc1.18-0.4.0-alpha5"
}
},
"mixins": [ "mixins": [
"example-mod.mixins.json" "example-mod.mixins.json"
], ],

View file

@ -233,6 +233,12 @@ describe("ModMetadataReader.readMetadata", () => {
} }
}); });
test("version array is supported", async () => {
const metadata = (await ModMetadataReader.readMetadata("example-mod.quilt.jar"))!;
const minecraft = metadata.dependencies.find(x => x.id === "minecraft");
expect(minecraft.version).toStrictEqual("1.17 || 1.17.1");
});
test("custom metadata can be attached to dependency entry", async () => { test("custom metadata can be attached to dependency entry", async () => {
const metadata = (await ModMetadataReader.readMetadata("example-mod.quilt.jar"))!; const metadata = (await ModMetadataReader.readMetadata("example-mod.quilt.jar"))!;
const recommended = metadata.dependencies.find(x => x.id === "recommended-mod")!; const recommended = metadata.dependencies.find(x => x.id === "recommended-mod")!;