diff --git a/src/dependencies/dependency.legacy.ts b/src/dependencies/dependency.legacy.ts new file mode 100644 index 0000000..a029959 --- /dev/null +++ b/src/dependencies/dependency.legacy.ts @@ -0,0 +1,57 @@ +// TODO: Drop support for the legacy format completely. + +import { FabricDependencyType } from "@/loaders/fabric/fabric-dependency-type"; +import { deprecate } from "node:util"; +import { DependencyInfo } from "./dependency"; + +/** + * Checks if the provided dependency string is in the legacy format. + * + * @param dependency - The dependency string to check. + * + * @returns A boolean indicating if the string is in the legacy format. + */ +export function isLegacyDependencyFormat(dependency: string): boolean { + return !!dependency?.includes("|") && !dependency.includes("@"); +} + +/** + * Parses the legacy dependency format. + * + * @param dependencyFormat - The dependency string in the legacy format. + * + * @returns An object containing the parsed dependency info. + * + * @remarks + * + * The legacy format is: `[dependency-id] | [type]? | [version-range]?` + */ +function _parseLegacyDependencyFormat(dependencyFormat: string): DependencyInfo { + const [id, fabricType, versions] = dependencyFormat.split("|").map(x => x.trim()); + const type = fabricType && FabricDependencyType.toDependencyType(FabricDependencyType.parse(fabricType)); + return { id, type, versions }; +} + +/** + * Parses the legacy dependency format with a deprecation warning. + * + * @param dependencyFormat - The dependency string in the legacy format. + * + * @returns An object containing the parsed dependency info. + * + * @remarks + * + * The legacy format is: `[dependency-id] | [type]? | [version-range]?` + * + * @deprecated + * + * The old dependency string format is deprecated. Please use the new format. + * + * Example: `foo@1.0.0-2.0.0(required){modrinth:foo-fabric}#(ignore:curseforge)`. + */ +export const parseLegacyDependencyFormat = deprecate( + _parseLegacyDependencyFormat, + "The old dependency string format is deprecated. " + + "Please use the new format. " + + "Example: foo@1.0.0-2.0.0(required){modrinth:foo-fabric}#(ignore:curseforge)", +); diff --git a/src/dependencies/dependency.ts b/src/dependencies/dependency.ts new file mode 100644 index 0000000..c442216 --- /dev/null +++ b/src/dependencies/dependency.ts @@ -0,0 +1,225 @@ +import { PlatformType } from "@/platforms/platform-type"; +import { $i, isIterable } from "@/utils/collections"; +import { VersionRange, anyVersionRange } from "@/utils/versioning"; +import { DependencyType } from "./dependency-type"; +import { isLegacyDependencyFormat, parseLegacyDependencyFormat } from "./dependency.legacy"; + +/** + * Represents a dependency. + */ +export interface Dependency { + /** + * The unique identifier of the dependency. + */ + get id(): string; + + /** + * The importance of the dependency for the project. + */ + get type(): DependencyType; + + /** + * The range of allowed versions for the dependency. + */ + get versions(): string[]; + + /** + * Checks if the dependency is ignored for a specific platform or globally + * if no platform is specified. + * + * @param platform - The platform to check for (optional). + * + * @returns A boolean indicating if the dependency is ignored. + */ + isIgnored(platform?: PlatformType): boolean; + + /** + * Retrieves the project ID for the dependency on a specific platform. + * + * Useful when a dependency has different identifiers across platforms. + * + * @param platform - The platform to get the project ID for. + * + * @returns The project ID associated with the dependency on the specified platform. + */ + getProjectId(platform: PlatformType): string; +} + +/** + * Represents an intermediate representation of a dependency + * when parsing and creating {@link Dependency} objects from various formats. + */ +export interface DependencyInfo { + /** + * The unique identifier of the dependency. + */ + id: string; + + /** + * The importance of the dependency for the project. + */ + type?: string | DependencyType; + + /** + * The range of allowed versions for the dependency. + */ + versions?: string | string[] | VersionRange; + + /** + * A boolean indicating if the dependency is ignored globally. + */ + ignore?: boolean; + + /** + * A list of platforms the dependency is ignored on. + */ + ignoredPlatforms?: Iterable; + + /** + * A list of aliases for the dependency on different platforms. + */ + aliases?: Iterable; +} + +/** + * Representing different ways a dependency can be expressed. + */ +export type DependencyLike = Dependency | DependencyInfo | string; + +/** + * Parses a dependency string and returns a Dependency object. + * + * @param dependency - The dependency string to parse. + * + * @returns A {@link Dependency} object, or `undefined` if the string is invalid. + */ +export function parseDependency(dependency: string): Dependency | undefined { + const dependencyInfo = isLegacyDependencyFormat(dependency) + ? parseLegacyDependencyFormat(dependency) + : parseDependencyFormat(dependency); + + return dependencyInfo && createDependency(dependencyInfo); +} + +/** + * A regex pattern for matching formatted dependency strings. + */ +const DEPENDENCY_REGEX = /^\s*(?[^@{(#]+)(@(?[^@{(#]*))?(?:\((?[^@{(#]*)\))?(?(?:\{[^:=]+(?:=|:)[^}]*\})+)?(?#\(ignore(?::(?[^\)]*))?\))?\s*$/; + +/** + * A regex pattern for matching dependency aliases in dependency strings. + */ +const DEPENDENCY_ALIASES_REGEX = /\{(?[^:=]+)(?:=|:)(?[^}]*)\}/g; + +/** + * Parses a dependency string and returns an intermediate representation of a dependency. + * + * @param dependencyFormat - The dependency string to parse. + * + * @returns A dependency info, or `undefined` if the string is invalid. + * + * @remarks + * + * The format is `[dependency-id]@[version-range]?([type])?{[platform]:[dependency-id]}?#(ignore:[platform1,platform2])?`. + */ +function parseDependencyFormat(dependencyFormat: string): DependencyInfo | undefined { + const match = dependencyFormat?.match(DEPENDENCY_REGEX); + if (!match) { + return undefined; + } + + const id = match.groups.id.trim(); + const versions = match.groups.versionRange?.trim(); + const type = match.groups.type?.trim(); + const aliases = $i(match.groups.aliases?.matchAll(DEPENDENCY_ALIASES_REGEX) || []).map(x => [x.groups.platform.trim(), x.groups.id.trim()] as const); + const ignoredPlatforms = match.groups.ignoredPlatforms?.split(",").map(x => x.trim()); + const ignore = ignoredPlatforms?.length ? undefined : !!match.groups.ignore; + + return { id, versions, type, aliases, ignore, ignoredPlatforms }; +} + +/** + * Creates a dependency from the given dependency-like value. + * + * @param dependency - A dependency-like value to create a dependency from. + * + * @returns A {@link Dependency}, or `undefined` if the input is invalid. + */ +export function createDependency(dependency: DependencyLike): Dependency | undefined { + if (typeof dependency === "string") { + return parseDependency(dependency); + } + + if (isDependency(dependency)) { + return dependency; + } + + if (!dependency?.id) { + return undefined; + } + + const id = dependency.id || ""; + const type = dependency.type && DependencyType.parse(dependency.type) || DependencyType.REQUIRED; + + const versionRanges = typeof dependency.versions === "string" + ? [dependency.versions] + : isIterable(dependency.versions) + ? [...dependency.versions] + : [(dependency.versions || anyVersionRange()).toString()]; + + const versions = versionRanges.filter(x => x && x !== anyVersionRange().toString()); + if (!versions.length) { + versions.push(anyVersionRange().toString()); + } + + const ignoredPlatforms = $i(dependency.ignoredPlatforms || []).map(x => PlatformType.parse(x)).filter(x => x).toSet(); + const isIgnored = dependency.ignore + ? () => true + : (p: PlatformType) => p ? ignoredPlatforms.has(p) : ignoredPlatforms.size === PlatformType.size; + + const aliases = $i(dependency.aliases || []).map(([key, value]) => [PlatformType.parse(key), value] as const).filter(([key]) => key).toMap(); + const getProjectId = (p: PlatformType) => aliases.get(p) ?? id; + + return { id, versions, type, isIgnored, getProjectId }; +} + +/** + * Formats a dependency as a string. + * + * @param dependency - The dependency to format. + * + * @returns A string representation of the dependency. + */ +export function formatDependency(dependency: Dependency): string { + if (!dependency) { + return ""; + } + + const versionRange = dependency.versions.join(" || "); + const version = versionRange && versionRange !== anyVersionRange().toString() ? `@${versionRange}` : ""; + + const ignoredBy = $i(PlatformType.values()).filter(x => dependency.isIgnored(x)).join(","); + const ignore = ignoredBy && `#(ignore:${ignoredBy})`; + + const aliases = $i(PlatformType.values()).filter(x => dependency.getProjectId(x) !== dependency.id).map(x => `{${x}:${dependency.getProjectId(x)}}`).join(""); + + return `${dependency.id}${version}(${dependency.type})${aliases}${ignore}`; +} + +/** + * Determines if the given value is a {@link Dependency}. + * + * @param dependency - The value to check. + * + * @returns A boolean indicating if the value is a {@link Dependency}. + */ +export function isDependency(dependency: unknown): dependency is Dependency { + const d = dependency as Dependency; + return ( + typeof d?.id === "string" && + typeof d.type === DependencyType.underlyingType && + Array.isArray(d.versions) && + typeof d.getProjectId === "function" && + typeof d.isIgnored === "function" + ); +} diff --git a/tests/unit/dependencies/dependency.spec.ts b/tests/unit/dependencies/dependency.spec.ts new file mode 100644 index 0000000..74a438a --- /dev/null +++ b/tests/unit/dependencies/dependency.spec.ts @@ -0,0 +1,274 @@ +import { DependencyType } from "@/dependencies/dependency-type"; +import { PlatformType } from "@/platforms/platform-type"; +import { parseVersionRange } from "@/utils/versioning/version-range"; +import { createDependency, formatDependency, isDependency, parseDependency } from "@/dependencies/dependency"; + +describe("isDependency", () => { + test("returns true for Dependency-like objects", () => { + expect(isDependency(parseDependency("id"))).toBe(true); + }); + + test("returns false for non-Dependency-like objects", () => { + expect(isDependency({})).toBe(false); + }); + + test("returns false for null and undefined", () => { + expect(isDependency(null)).toBe(false); + expect(isDependency(undefined)).toBe(false); + }); +}); + +describe("parseDependency", () => { + test("parses a fully-formed dependency string", () => { + const dependency = parseDependency("id@1.0.0-2.0.0-alpha.1(optional){modrinth:modrinth-slug}{curseforge:curseforge-slug}#(ignore:curseforge,github)"); + + expect(dependency).toBeDefined(); + expect(dependency.id).toBe("id"); + expect(dependency.versions).toEqual(["1.0.0-2.0.0-alpha.1"]); + expect(dependency.type).toBe(DependencyType.OPTIONAL); + expect(dependency.getProjectId(PlatformType.MODRINTH)).toBe("modrinth-slug"); + expect(dependency.getProjectId(PlatformType.CURSEFORGE)).toBe("curseforge-slug"); + expect(dependency.getProjectId(PlatformType.GITHUB)).toBe("id"); + expect(dependency.isIgnored(PlatformType.MODRINTH)).toBe(false); + expect(dependency.isIgnored(PlatformType.CURSEFORGE)).toBe(true); + expect(dependency.isIgnored(PlatformType.GITHUB)).toBe(true); + }); + + test("parses a dependency string with omitted 'ignore' part", () => { + const dependency = parseDependency("id@1.0.0-2.0.0-alpha.1(optional){modrinth:modrinth-slug}{curseforge:curseforge-slug}"); + + expect(dependency).toBeDefined(); + expect(dependency.id).toBe("id"); + expect(dependency.versions).toEqual(["1.0.0-2.0.0-alpha.1"]); + expect(dependency.type).toBe(DependencyType.OPTIONAL); + expect(dependency.getProjectId(PlatformType.MODRINTH)).toBe("modrinth-slug"); + expect(dependency.getProjectId(PlatformType.CURSEFORGE)).toBe("curseforge-slug"); + expect(dependency.getProjectId(PlatformType.GITHUB)).toBe("id"); + expect(dependency.isIgnored(PlatformType.MODRINTH)).toBe(false); + expect(dependency.isIgnored(PlatformType.CURSEFORGE)).toBe(false); + expect(dependency.isIgnored(PlatformType.GITHUB)).toBe(false); + }); + + test("parses a dependency string with omitted 'aliases' part", () => { + const dependency = parseDependency("id@1.0.0-2.0.0-alpha.1(optional)#(ignore:curseforge,github)"); + + expect(dependency).toBeDefined(); + expect(dependency.id).toBe("id"); + expect(dependency.versions).toEqual(["1.0.0-2.0.0-alpha.1"]); + expect(dependency.type).toBe(DependencyType.OPTIONAL); + expect(dependency.getProjectId(PlatformType.MODRINTH)).toBe("id"); + expect(dependency.getProjectId(PlatformType.CURSEFORGE)).toBe("id"); + expect(dependency.getProjectId(PlatformType.GITHUB)).toBe("id"); + expect(dependency.isIgnored(PlatformType.MODRINTH)).toBe(false); + expect(dependency.isIgnored(PlatformType.CURSEFORGE)).toBe(true); + expect(dependency.isIgnored(PlatformType.GITHUB)).toBe(true); + }); + + test("parses a dependency string with omitted 'type' part", () => { + const dependency = parseDependency("id@1.0.0-2.0.0-alpha.1{modrinth:modrinth-slug}{curseforge:curseforge-slug}#(ignore:curseforge,github)"); + + expect(dependency).toBeDefined(); + expect(dependency.id).toBe("id"); + expect(dependency.versions).toEqual(["1.0.0-2.0.0-alpha.1"]); + expect(dependency.type).toBe(DependencyType.REQUIRED); + expect(dependency.getProjectId(PlatformType.MODRINTH)).toBe("modrinth-slug"); + expect(dependency.getProjectId(PlatformType.CURSEFORGE)).toBe("curseforge-slug"); + expect(dependency.getProjectId(PlatformType.GITHUB)).toBe("id"); + expect(dependency.isIgnored(PlatformType.MODRINTH)).toBe(false); + expect(dependency.isIgnored(PlatformType.CURSEFORGE)).toBe(true); + expect(dependency.isIgnored(PlatformType.GITHUB)).toBe(true); + }); + + test("parses a dependency string with omitted 'version' part", () => { + const dependency = parseDependency("id(optional){modrinth:modrinth-slug}{curseforge:curseforge-slug}#(ignore:curseforge,github)"); + + expect(dependency).toBeDefined(); + expect(dependency.id).toBe("id"); + expect(dependency.versions).toEqual(["*"]); + expect(dependency.type).toBe(DependencyType.OPTIONAL); + expect(dependency.getProjectId(PlatformType.MODRINTH)).toBe("modrinth-slug"); + expect(dependency.getProjectId(PlatformType.CURSEFORGE)).toBe("curseforge-slug"); + expect(dependency.getProjectId(PlatformType.GITHUB)).toBe("id"); + expect(dependency.isIgnored(PlatformType.MODRINTH)).toBe(false); + expect(dependency.isIgnored(PlatformType.CURSEFORGE)).toBe(true); + expect(dependency.isIgnored(PlatformType.GITHUB)).toBe(true); + }); + + test("parses a dependency string that only consists of id", () => { + const dependency = parseDependency("id"); + + expect(dependency).toBeDefined(); + expect(dependency.id).toBe("id"); + expect(dependency.versions).toEqual(["*"]); + expect(dependency.type).toBe(DependencyType.REQUIRED); + expect(dependency.getProjectId(PlatformType.MODRINTH)).toBe("id"); + expect(dependency.getProjectId(PlatformType.CURSEFORGE)).toBe("id"); + expect(dependency.getProjectId(PlatformType.GITHUB)).toBe("id"); + expect(dependency.isIgnored(PlatformType.MODRINTH)).toBe(false); + expect(dependency.isIgnored(PlatformType.CURSEFORGE)).toBe(false); + expect(dependency.isIgnored(PlatformType.GITHUB)).toBe(false); + }); + + test("parses a dependency string that consists of id and version", () => { + const dependency = parseDependency("id@1.0.0"); + + expect(dependency).toBeDefined(); + expect(dependency.id).toBe("id"); + expect(dependency.versions).toEqual(["1.0.0"]); + expect(dependency.type).toBe(DependencyType.REQUIRED); + expect(dependency.getProjectId(PlatformType.MODRINTH)).toBe("id"); + expect(dependency.getProjectId(PlatformType.CURSEFORGE)).toBe("id"); + expect(dependency.getProjectId(PlatformType.GITHUB)).toBe("id"); + expect(dependency.isIgnored(PlatformType.MODRINTH)).toBe(false); + expect(dependency.isIgnored(PlatformType.CURSEFORGE)).toBe(false); + expect(dependency.isIgnored(PlatformType.GITHUB)).toBe(false); + }); + + test("parses a dependency string that consists of id, version, and type", () => { + const dependency = parseDependency("id@1.0.0(optional)"); + + expect(dependency).toBeDefined(); + expect(dependency.id).toBe("id"); + expect(dependency.versions).toEqual(["1.0.0"]); + expect(dependency.type).toBe(DependencyType.OPTIONAL); + expect(dependency.getProjectId(PlatformType.MODRINTH)).toBe("id"); + expect(dependency.getProjectId(PlatformType.CURSEFORGE)).toBe("id"); + expect(dependency.getProjectId(PlatformType.GITHUB)).toBe("id"); + expect(dependency.isIgnored(PlatformType.MODRINTH)).toBe(false); + expect(dependency.isIgnored(PlatformType.CURSEFORGE)).toBe(false); + expect(dependency.isIgnored(PlatformType.GITHUB)).toBe(false); + }); + + test("parses a dependency string that consists of id and type", () => { + const dependency = parseDependency("id(optional)"); + + expect(dependency).toBeDefined(); + expect(dependency.id).toBe("id"); + expect(dependency.versions).toEqual(["*"]); + expect(dependency.type).toBe(DependencyType.OPTIONAL); + expect(dependency.getProjectId(PlatformType.MODRINTH)).toBe("id"); + expect(dependency.getProjectId(PlatformType.CURSEFORGE)).toBe("id"); + expect(dependency.getProjectId(PlatformType.GITHUB)).toBe("id"); + expect(dependency.isIgnored(PlatformType.MODRINTH)).toBe(false); + expect(dependency.isIgnored(PlatformType.CURSEFORGE)).toBe(false); + expect(dependency.isIgnored(PlatformType.GITHUB)).toBe(false); + }); + + test("dependency type parsing is case-insensitive", () => { + for (const type of DependencyType.values()) { + expect(parseDependency(`id(${type.toLowerCase()})`)?.type).toBe(type); + expect(parseDependency(`id(${type.toUpperCase()})`)?.type).toBe(type); + expect(parseDependency(`id(${DependencyType.friendlyNameOf(type)})`)?.type).toBe(type); + } + }); + + test("platform type parsing is case-insensitive", () => { + for (const platform of PlatformType.values()) { + expect(parseDependency(`id{${platform.toLowerCase()}:another-id}`)?.getProjectId(platform)).toBe("another-id"); + expect(parseDependency(`id{${platform.toUpperCase()}:another-id}`)?.getProjectId(platform)).toBe("another-id"); + expect(parseDependency(`id{${PlatformType.friendlyNameOf(platform)}:another-id}`)?.getProjectId(platform)).toBe("another-id"); + + expect(parseDependency(`id#(ignore:${platform.toLowerCase()})`)?.isIgnored(platform)).toBe(true); + expect(parseDependency(`id#(ignore:${platform.toUpperCase()})`)?.isIgnored(platform)).toBe(true); + expect(parseDependency(`id#(ignore:${PlatformType.friendlyNameOf(platform)})`)?.isIgnored(platform)).toBe(true); + } + }); + + test("returns undefined if the input string is null, undefined, or empty", () => { + expect(parseDependency(undefined)).toBeUndefined(); + expect(parseDependency(null)).toBeUndefined(); + expect(parseDependency("")).toBeUndefined(); + }); +}); + +describe("cerateDependency", () => { + test("parses dependency strings", () => { + expect(createDependency("id")).toBeDefined(); + expect(createDependency("id@1.0.0")).toBeDefined(); + expect(createDependency("id@1.0.0(optional)")).toBeDefined(); + expect(createDependency("id(optional)")).toBeDefined(); + expect(createDependency("id@1.0.0-2.0.0-alpha.1(optional){modrinth:modrinth-slug}{curseforge:curseforge-slug}#(ignore:curseforge,github)")).toBeDefined(); + expect(createDependency("id@1.0.0-2.0.0-alpha.1(optional){modrinth:modrinth-slug}{curseforge:curseforge-slug}")).toBeDefined(); + expect(createDependency("id@1.0.0-2.0.0-alpha.1(optional)#(ignore:curseforge,github)")).toBeDefined(); + expect(createDependency("id@1.0.0-2.0.0-alpha.1{modrinth:modrinth-slug}{curseforge:curseforge-slug}#(ignore:curseforge,github)")).toBeDefined(); + expect(createDependency("id(optional){modrinth:modrinth-slug}{curseforge:curseforge-slug}#(ignore:curseforge,github)")).toBeDefined(); + }); + + test("converts DependencyInfo-like objects to Dependency objects", () => { + expect(createDependency({ id: "id" })?.id).toBe("id"); + expect(createDependency({ id: "id", type: DependencyType.EMBEDDED })?.type).toBe(DependencyType.EMBEDDED); + expect(createDependency({ id: "id", versions: "1.0.0" })?.versions).toEqual(["1.0.0"]); + expect(createDependency({ id: "id", versions: ["1.0.0", "2.0.0"] })?.versions).toEqual(["1.0.0", "2.0.0"]); + expect(createDependency({ id: "id", versions: parseVersionRange("1.0.0") })?.versions).toEqual(["1.0.0"]); + expect(createDependency({ id: "id", ignore: true })?.isIgnored()).toBe(true); + expect(createDependency({ id: "id", ignoredPlatforms: [PlatformType.CURSEFORGE] })?.isIgnored(PlatformType.CURSEFORGE)).toBe(true); + expect(createDependency({ id: "id", aliases: [[PlatformType.CURSEFORGE, "curseforge-slug"]] })?.getProjectId(PlatformType.CURSEFORGE)).toBe("curseforge-slug"); + }); + + test("returns Dependency-like objects as is", () => { + const dependency = parseDependency("id"); + + expect(createDependency(dependency)).toBe(dependency); + }); + + test("returns undefined if the input is null, undefined, or an empty string", () => { + expect(createDependency(null)).toBeUndefined(); + expect(createDependency(undefined)).toBeUndefined(); + expect(createDependency("")).toBeUndefined(); + }); +}); + +describe("formatDependency", () => { + test("formats fine-tuned dependencies as fully qualified dependency strings", () => { + const dependencies = [ + "id@1.0.0-2.0.0-alpha.1(optional){curseforge:curseforge-slug}{modrinth:modrinth-slug}#(ignore:curseforge,github)", + "id@1.0.0(embedded){modrinth:modrinth-slug}#(ignore:curseforge)", + "id@1.0.0 || 2.0.0-alpha.1(conflicting){curseforge:curseforge-slug}#(ignore:github)", + ]; + + for (const dependency of dependencies) { + expect(formatDependency(parseDependency(dependency))).toBe(dependency); + } + }); + + test("formats dependencies and omits unused 'ignore' section", () => { + const dependencies = [ + "id@1.0.0-2.0.0-alpha.1(optional){curseforge:curseforge-slug}{modrinth:modrinth-slug}", + "id@1.0.0(embedded){modrinth:modrinth-slug}", + "id@1.0.0 || 2.0.0-alpha.1(conflicting){curseforge:curseforge-slug}", + ]; + + for (const dependency of dependencies) { + expect(formatDependency(parseDependency(dependency))).toBe(dependency); + } + }); + + test("formats dependencies and omits unused 'aliases' section", () => { + const dependencies = [ + "id@1.0.0-2.0.0-alpha.1(optional)#(ignore:curseforge,github)", + "id@1.0.0(embedded)#(ignore:curseforge)", + "id@1.0.0 || 2.0.0-alpha.1(conflicting)#(ignore:github)", + ]; + + for (const dependency of dependencies) { + expect(formatDependency(parseDependency(dependency))).toBe(dependency); + } + }); + + test("formats dependencies and omits unused 'version' section", () => { + const dependencies = [ + "id(optional){curseforge:curseforge-slug}{modrinth:modrinth-slug}#(ignore:curseforge,github)", + "id(embedded){modrinth:modrinth-slug}#(ignore:curseforge)", + "id(conflicting){curseforge:curseforge-slug}#(ignore:github)", + ]; + + for (const dependency of dependencies) { + expect(formatDependency(parseDependency(dependency))).toBe(dependency); + } + }); + + test("returns an empty string for invalid dependencies", () => { + expect(formatDependency(null)).toBe(""); + expect(formatDependency(undefined)).toBe(""); + }); +});