diff --git a/src/loaders/forge/forge-dependency.ts b/src/loaders/forge/forge-dependency.ts new file mode 100644 index 0000000..340094a --- /dev/null +++ b/src/loaders/forge/forge-dependency.ts @@ -0,0 +1,206 @@ +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 { deprecate } from "node:util"; +import { ForgeEnvironmentType } from "./forge-environment-type"; +import { RawForgeMetadata } from "./raw-forge-metadata"; + +// TODO: Remove the deprecated stuff in v4.0. + +/** + * A dependency configuration for a Forge mod. + */ +export interface ForgeDependency { + /** + * The mod id of the dependency. + */ + modId: string; + + /** + * Whether to crash if this dependency is not met. + */ + mandatory: boolean; + + /** + * Whether this dependency is embedded or not. + * + * @custom + */ + embedded?: boolean; + + /** + * Whether this dependency is incompatible with the project or not. + * + * @custom + */ + incompatible?: boolean; + + /** + * The acceptable version range of the dependency, expressed as a Maven version spec. + * + * An empty string is an unbounded version range, which matches any version. + */ + versionRange?: string; + + /** + * Defines if the mod must load before or after this dependency. + * + * The valid values are `BEFORE` (must load before), `AFTER` (must load after), and `NONE` (does not care about order). + * + * Defaults to `NONE`. + */ + ordering?: "BEFORE" | "AFTER" | "NONE"; + + /** + * The physical side. The valid values are: + * + * - `CLIENT` (present on the client). + * - `SERVER` (present on the dedicated server). + * - `BOTH`, the default one, (present on both sides). + */ + side?: ForgeEnvironmentType; + + /** + * Custom action payload. + */ + [ACTION_NAME]?: ForgeDependencyCustomPayload; + + /** + * Custom action payload (legacy). + * + * @deprecated + * + * Use [{@link ACTION_NAME}] instead. + */ + custom?: { + /** + * Custom action payload. + */ + [ACTION_NAME]?: ForgeDependencyCustomPayload; + } +} + +/** + * Custom payload attached to a Forge dependency. + */ +type ForgeDependencyCustomPayload = { + /** + * 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", + "forge", +]; + +/** + * Retrieves Forge dependencies from the metadata. + * + * @param metadata - The raw Forge metadata. + * + * @returns An array of Forge dependencies. + */ +export function getForgeDependencies(metadata: RawForgeMetadata): ForgeDependency[] { + const dependencyMap = $i(Object.values(metadata?.dependencies || {})) + .filter(x => Array.isArray(x)) + .flatMap(x => x) + .filter(x => x?.modId) + .map(x => [x.modId, x] as const) + .reverse() + .toMap(); + + return [...dependencyMap.values()]; +} + +/** +* Converts {@link FabricDependency} to a {@link Dependency} object. +* +* @returns A Dependency object representing the given Fabric dependency, or `undefined` if the input is invalid.. +*/ +export function normalizeForgeDependency(dependency: ForgeDependency): Dependency | undefined { + const payload = getForgeDependencyCustomPayload(dependency); + + const id = dependency?.modId; + const versions = dependency?.versionRange; + const ignore = IGNORED_DEPENDENCIES.includes(dependency?.modId) || typeof payload.ignore === "boolean" && payload.ignore; + const ignoredPlatforms = typeof payload.ignore === "boolean" ? undefined : payload.ignore; + const aliases = $i(PlatformType.values()).map(type => [type, payload[type] ? asString(payload[type]) : undefined] as const).filter(([, id]) => id).toMap(); + const type = ( + dependency?.incompatible && DependencyType.INCOMPATIBLE || + dependency?.embedded && DependencyType.EMBEDDED || + dependency?.mandatory && DependencyType.REQUIRED || + DependencyType.OPTIONAL + ); + + return createDependency({ + id, + versions, + type, + ignore, + ignoredPlatforms, + aliases, + }); +} + +/** + * Gets the custom payload from the Forge dependency. + * + * @param dependency - The Forge dependency. + * + * @returns The custom payload object. + */ +function getForgeDependencyCustomPayload(dependency: ForgeDependency): ForgeDependencyCustomPayload { + return containsLegacyForgeDependencyCustomPayload(dependency) + ? getLegacyForgeDependencyCustomPayload(dependency) + : (dependency?.[ACTION_NAME] || {}); +} + +/** + * Checks if the dependency contains a legacy custom payload definition. + * + * @param dependency - The dependency to check. + * + * @returns A boolean indicating if the legacy custom payload definition is present. + */ +function containsLegacyForgeDependencyCustomPayload(dependency: ForgeDependency): boolean { + return !!dependency?.custom?.[ACTION_NAME]; +} + +/** + * Gets the legacy custom payload from the Forge dependency. + * + * @param dependency - The Forge dependency. + * + * @returns The custom payload object. + */ +function _getLegacyForgeDependencyCustomPayload(dependency: ForgeDependency): ForgeDependencyCustomPayload { + const legacyPayload = dependency?.custom?.[ACTION_NAME]; + const basePayload = dependency?.[ACTION_NAME]; + return { ...legacyPayload, ...basePayload }; +} + +/** + * Gets the legacy custom payload from the Forge dependency. + * + * @param dependency - The Forge dependency. + * + * @returns The custom payload object. + * + * @deprecated + * + * Define `mc-publish` property directly on your Forge dependency object instead of using nested `custom.mc-publish`. + */ +const getLegacyForgeDependencyCustomPayload = deprecate( + _getLegacyForgeDependencyCustomPayload, + "Define `mc-publish` property directly on your Forge dependency object instead of using nested `custom.mc-publish`.", +); diff --git a/tests/unit/loaders/forge/forge-dependency.spec.ts b/tests/unit/loaders/forge/forge-dependency.spec.ts new file mode 100644 index 0000000..cbd4215 --- /dev/null +++ b/tests/unit/loaders/forge/forge-dependency.spec.ts @@ -0,0 +1,63 @@ +import { DependencyType } from "@/dependencies/dependency-type"; +import { RawForgeMetadata } from "@/loaders/forge/raw-forge-metadata"; +import { getForgeDependencies, normalizeForgeDependency } from "@/loaders/forge/forge-dependency"; + +describe("getForgeDependencies", () => { + test("returns an array of dependencies specified in the given metadata", () => { + const metadata = { + dependencies: { + "example-mod": [ + { + modId: "depends-id", + versionRange: "[1.0.0,)", + mandatory: true, + }, + { + modId: "suggests-id", + versionRange: "[2.0.0,)", + mandatory: false, + }, + ], + + "example-mod-2": [{ + modId: "breaks-id", + versionRange: "[4.0.0,5.0.0]", + mandatory: false, + incompatible: true, + }], + }, + } as unknown as RawForgeMetadata; + + const dependencies = getForgeDependencies(metadata); + + expect(dependencies).toEqual([ + { modId: "breaks-id", versionRange: "[4.0.0,5.0.0]", mandatory: false, incompatible: true }, + { modId: "suggests-id", versionRange: "[2.0.0,)", mandatory: false }, + { modId: "depends-id", versionRange: "[1.0.0,)", mandatory: true }, + ]); + }); + + test("returns an empty array if no dependencies were specified", () => { + expect(getForgeDependencies({} as RawForgeMetadata)).toEqual([]); + }); + + test("returns an empty array if metadata was null or undefined", () => { + expect(getForgeDependencies(null)).toEqual([]); + expect(getForgeDependencies(undefined)).toEqual([]); + }); +}); + +describe("normalizeForgeDependency", () => { + test("converts Forge dependency to a more abstract Dependency object", () => { + const forgeDependency = { modId: "suggests-id", mandatory: false, versionRange: "[2.0.0,)" }; + + const dependency = normalizeForgeDependency(forgeDependency); + + expect(dependency).toMatchObject({ id: "suggests-id", versions: ["[2.0.0,)"], type: DependencyType.OPTIONAL }); + }); + + test("returns undefined if dependency was null or undefined", () => { + expect(normalizeForgeDependency(null)).toBeUndefined(); + expect(normalizeForgeDependency(undefined)).toBeUndefined(); + }); +});