diff --git a/src/loaders/quilt/quilt-dependency.ts b/src/loaders/quilt/quilt-dependency.ts new file mode 100644 index 0000000..566441f --- /dev/null +++ b/src/loaders/quilt/quilt-dependency.ts @@ -0,0 +1,177 @@ +import { ACTION_NAME } from "@/action"; +import { Dependency, DependencyType, createDependency } from "@/dependencies"; +import { PlatformType } from "@/platforms"; +import { $i } from "@/utils/collections"; +import { asString } from "@/utils/string-utils"; +import { PartialRecord } from "@/utils/types"; +import { RawQuiltMetadata } from "./raw-quilt-metadata"; + +/** + * Represents a single dependency for a Quilt mod project. + */ +export interface QuiltDependency { + /** + * A mod identifier in the form of either `mavenGroup:modId` or `modId`. + */ + id: string; + + /** + * The version of the dependency. + */ + version?: string; + + /** + * The version range for the dependency. + * + * Can be a single version or an array of version ranges. + */ + versions?: string | string[]; + + /** + * A short, human-readable reason for the dependency object to exist. + */ + reason?: string; + + /** + * Dependencies marked as `optional` will only be checked if the mod/plugin specified by the `id` field is present. + */ + optional?: boolean; + + /** + * Indicates whether this dependency is embedded into the mod. + * + * @custom + */ + provided?: boolean; + + /** + * Indicates whether this dependency is incompatible with the mod. + * + * @custom + */ + breaking?: boolean; + + /** + * Describes situations where this dependency can be ignored. + */ + unless?: string | QuiltDependency; + + /** + * Custom action payload. + * + * @custom + */ + [ACTION_NAME]?: QuiltDependencyCustomPayload; +} + +/** + * Custom payload attached to a Quilt dependency. + */ +type QuiltDependencyCustomPayload = { + /** + * Indicates whether the dependency should be ignored globally, + * or by the platforms specified in the given array. + */ + ignore?: boolean | PlatformType[]; +} & PartialRecord; + +/** + * A list of special dependencies that should be ignored. + */ +const IGNORED_DEPENDENCIES: readonly string[] = [ + "minecraft", + "java", + "quilt_loader", +]; + +/** + * A map of aliases for special dependencies for different platforms. + */ +const DEPENDENCY_ALIASES: ReadonlyMap> = $i([ + ["fabric", "fabric-api"], + ["quilt_base", "qsl"], + ["quilted_fabric_api", "qsl"], +].map(([k, v]) => + [k, typeof v === "string" ? $i(PlatformType.values()).map(x => [x, v] as const).toMap() : v] as const, +)).toMap(); + +/** + * Retrieves Quilt dependencies from the metadata. + * + * @param metadata - The raw Quilt metadata. + * + * @returns An array of Quilt dependencies. + */ +export function getQuiltDependencies(metadata: RawQuiltMetadata): QuiltDependency[] { + const dependencyMap = $i(mapQuiltDependencies(metadata?.quilt_loader?.depends)) + .concat(mapQuiltDependencies(metadata?.quilt_loader?.breaks, { breaking: true })) + .concat(mapQuiltDependencies(metadata?.quilt_loader?.provides, { provided: true })) + .filter(x => x.id) + .map(x => [x.id, x] as const) + .toMap(); + + return [...dependencyMap.values()]; +} + +/** + * Maps a dependency field presented in raw Quilt metadata into the array of Quilt dependencies. + * + * @param dependencies - The dependency field to be mapped. + * @param customFields - Custom fields to attach to the dependencies. + * + * @returns The array of Quilt dependencies represented by the given field. + */ +function mapQuiltDependencies(dependencies: (string | QuiltDependency)[], customFields?: Partial): Iterable { + if (!dependencies) { + return []; + } + + return $i(dependencies).map(x => typeof x === "string" ? { id: x, ...customFields } : { ...x, ...customFields }); +} + +/** +* Converts {@link QuiltDependency} to a {@link Dependency} object. +* +* @returns A Dependency object representing the given Quilt dependency. +*/ +export function normalizeQuiltDependency(dependency: QuiltDependency): Dependency { + const payload = getQuiltDependencyCustomPayload(dependency); + + const id = dependency?.id?.includes(":") ? dependency.id.substring(dependency.id.indexOf(":") + 1) : dependency?.id; + const versions = dependency?.version || dependency?.versions; + const ignore = IGNORED_DEPENDENCIES.includes(id) || typeof payload.ignore === "boolean" && payload.ignore; + const ignoredPlatforms = typeof payload.ignore === "boolean" ? undefined : payload.ignore; + const type = ( + dependency?.breaking && dependency?.unless && DependencyType.CONFLICTING || + dependency?.breaking && DependencyType.INCOMPATIBLE || + dependency?.provided && DependencyType.EMBEDDED || + (dependency?.optional || dependency?.unless) && DependencyType.OPTIONAL || + DependencyType.REQUIRED + ); + const aliases = $i(DEPENDENCY_ALIASES.get(id) as Iterable || []) + .concat( + $i(PlatformType.values()).map(type => [type, payload[type] ? asString(payload[type]) : undefined] as const) + ) + .filter(([, id]) => id) + .toMap(); + + return createDependency({ + id, + versions, + type, + ignore, + ignoredPlatforms, + aliases, + }); +} + +/** + * Gets the custom payload from the Quilt dependency. + * + * @param dependency - The Quilt dependency. + * + * @returns The custom payload object. + */ +function getQuiltDependencyCustomPayload(dependency: QuiltDependency): QuiltDependencyCustomPayload { + return dependency?.[ACTION_NAME] || {}; +} diff --git a/tests/unit/loaders/quilt/quilt-dependency.spec.ts b/tests/unit/loaders/quilt/quilt-dependency.spec.ts new file mode 100644 index 0000000..8a95a59 --- /dev/null +++ b/tests/unit/loaders/quilt/quilt-dependency.spec.ts @@ -0,0 +1,73 @@ +import { DependencyType } from "@/dependencies/dependency-type"; +import { RawQuiltMetadata } from "@/loaders/quilt/raw-quilt-metadata"; +import { getQuiltDependencies, normalizeQuiltDependency } from "@/loaders/quilt/quilt-dependency"; + +describe("getQuiltDependencies", () => { + test("returns an array of dependencies specified in the given metadata", () => { + const metadata = { + quilt_loader: { + depends: [ + { + id: "depends-id", + versions: "1.0.0", + }, + { + id: "suggests-id", + versions: "3.0.0", + optional: true, + }, + ], + breaks: [ + { + id: "breaks-id", + versions: ["4.0.0", "5.0.0"], + }, + { + id: "conflicts-id-1", + versions: "6.0.0", + unless: "fixes-conflicts-id-1", + }, + { + id: "conflicts-id-2", + versions: "7.0.0", + unless: "fixes-conflicts-id-2", + }, + ], + } + } as RawQuiltMetadata; + + const dependencies = getQuiltDependencies(metadata); + + expect(dependencies).toEqual([ + { id: "depends-id", versions: "1.0.0" }, + { id: "suggests-id", versions: "3.0.0", optional: true }, + { id: "breaks-id", versions: ["4.0.0", "5.0.0"], breaking: true }, + { id: "conflicts-id-1", versions: "6.0.0", breaking: true, unless: "fixes-conflicts-id-1" }, + { id: "conflicts-id-2", versions: "7.0.0", breaking: true, unless: "fixes-conflicts-id-2" }, + ]); + }); + + test("returns an empty array if no dependencies were specified", () => { + expect(getQuiltDependencies({} as RawQuiltMetadata)).toEqual([]); + }); + + test("returns an empty array if metadata was null or undefined", () => { + expect(getQuiltDependencies(null)).toEqual([]); + expect(getQuiltDependencies(undefined)).toEqual([]); + }); +}); + +describe("normalizeQuiltDependency", () => { + test("converts Quilt dependency to a more abstract Dependency object", () => { + const quiltDependency = { id: "suggested:suggests-id", versions: "2.0.0", optional: true }; + + const dependency = normalizeQuiltDependency(quiltDependency); + + expect(dependency).toMatchObject({ id: "suggests-id", versions: ["2.0.0"], type: DependencyType.OPTIONAL }); + }); + + test("returns undefined if dependency was null or undefined", () => { + expect(normalizeQuiltDependency(null)).toBeUndefined(); + expect(normalizeQuiltDependency(undefined)).toBeUndefined(); + }); +});