src/peer_dependencies.ts (213 lines of code) (raw):

import { colour, format } from "./colours.ts"; import { Graph } from "./graph.ts"; import { KnownIssues, Package, registry_package_parser } from "./parser.ts"; import { testRange } from "https://deno.land/std@0.198.0/semver/test_range.ts"; import { assertEquals } from "https://deno.land/std@0.198.0/assert/mod.ts"; import { get_identifier, Issues } from "./utils.ts"; import { parseRange } from "https://deno.land/std@0.198.0/semver/mod.ts"; import { tryParse } from "https://deno.land/std@0.198.0/semver/try_parse.ts"; export const get_unsatisfied_peer_dependencies = ( { dependencies, devDependencies }: Pick< Package, "dependencies" | "devDependencies" >, package_graph: Graph, { known_issues = {} }: { known_issues?: KnownIssues } = {}, ): Issues => { const all_dependencies = { ...dependencies, ...devDependencies }; const unsatisfied: Issues = []; for ( const { name: from, peerDependencies, peerDependenciesMeta } of package_graph .values() ) { for (const [name, version] of Object.entries(peerDependencies ?? {})) { const is_optional = !!peerDependenciesMeta?.[name]?.optional; const local = tryParse(all_dependencies[name]); if (!local) { if (!is_optional) { const severity = known_issues[name] ? "warn" : "error"; unsatisfied.push({ severity, name, version, from }); } continue; } const satisfied = testRange(local, parseRange(version)); if (!satisfied) { const severity = known_issues[get_identifier({ name, version })] ? "warn" : "error"; unsatisfied.push({ severity, name, version, from, message: format(name, local), }); } } } return unsatisfied; }; Deno.test("get_unsatisfied_peer_dependencies", async (test) => { await test.step("works when all dependencies are matched", () => { const unsatisfied_peer_dependencies = get_unsatisfied_peer_dependencies( { dependencies: { "one": "1.2.3", "two": "2.4.6", }, devDependencies: { "three": "3.6.9" }, }, new Map([ [ "one@1.2.3", registry_package_parser.parse({ name: "one", version: "1.2.3", private: false, peerDependencies: { "one": "^1", two: "~2.4.4" }, }), ], ]), ); assertEquals(unsatisfied_peer_dependencies, []); }); await test.step("handles optional dependencies gracefully", () => { const unsatisfied_peer_dependencies = get_unsatisfied_peer_dependencies( { dependencies: { "one": "1.2.3" }, devDependencies: {}, }, new Map([ [ "one@1.2.3", registry_package_parser.parse({ name: "one", version: "1.2.3", peerDependencies: { "two": "^2", }, peerDependenciesMeta: { "two": { optional: true }, }, }), ], ]), ); assertEquals(unsatisfied_peer_dependencies, []); }); await test.step("fails on missing dependency", () => { const unsatisfied_peer_dependencies = get_unsatisfied_peer_dependencies( { dependencies: { "one": "1.2.3", }, devDependencies: {}, }, new Map([ [ "one@1.2.3", registry_package_parser.parse({ name: "one", version: "1.2.3", private: false, peerDependencies: { "peer": "0.0.1", }, }), ], ]), ); assertEquals( unsatisfied_peer_dependencies, [ { severity: "error", name: "peer", version: "0.0.1", from: "one" }, ], ); }); await test.step("fails on invalid range", () => { const unsatisfied_peer_dependencies = get_unsatisfied_peer_dependencies( { dependencies: { "one": "1.2.3", "two": "2.4.6", }, devDependencies: { "three": "3.6.9" }, }, new Map([ [ "peer@0.1.1", registry_package_parser.parse({ name: "mock", version: "0.0.0", private: false, peerDependencies: { "one": "~1.1.1", "two": "^1.2.2", "three": "^3.6.10", }, }), ], ]), ); assertEquals( unsatisfied_peer_dependencies, [ { severity: "error", name: "one", version: "~1.1.1", from: "mock", message: format("one", "1.2.3"), }, { severity: "error", name: "two", version: "^1.2.2", from: "mock", message: format("two", "2.4.6"), }, { severity: "error", name: "three", version: "^3.6.10", from: "mock", message: format("three", "3.6.9"), }, ], ); }); }); export const format_dependencies_issues = ( unsatisfied: Issues, verbose = true, ): void => { const grouped = unsatisfied.reduce( (map, issue) => { const found = map.get(issue.name) ?? []; found.push(issue); map.set(issue.name, found); return map; }, new Map< string, Issues >(), ); for (const [name, issues] of grouped.entries()) { const [first_issue] = issues; verbose && console.info(`║`); verbose && console.info( `╠╤═ local dependency ${first_issue?.message ?? colour.dependency(name)}`, ); let count = issues.length; for (const { from, version } of issues) { const angle = --count === 0 ? "╰" : "├"; verbose && console.warn( `║${angle}─ ${colour.invalid("✕")} ${ format(name, version) } required from ${colour.dependency(from ?? "--")}`, ); } } };