Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@sveltejs/kit": "^2.5.27",
"@sveltejs/vite-plugin-svelte": "^6.2.0",
"@types/node": "^24.10.0",
"@types/semver": "^7.7.1",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.6",
"sass": "^1.75.0",
Expand All @@ -34,6 +35,7 @@
"dependencies": {
"dayjs": "^1.11.11",
"redis": "^5.9.0",
"semver": "^7.7.4",
"svelte-exmarkdown": "^5.0.2"
}
}
2 changes: 1 addition & 1 deletion src/lib/components/GeodeMarkdown.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
return;
}

return `#${parsedColor.toString(16)}`;
return `#${parsedColor.toString(16).padStart(color.length, '0')}`;
};

// this is basically a direct port of the geode function lol
Expand Down
126 changes: 124 additions & 2 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";

import type { ServerDependency } from "./api/models/mod-version";
import semver from "semver";

dayjs.extend(relativeTime);

export const icons = {
Expand All @@ -19,7 +22,7 @@ export const icons = {
windows: "mdi:microsoft-windows",
mac: "mdi:apple-finder", // note: this used to be "mdi:apple". ios will use "mdi:apple" eventually
android: "mdi:android",
ios: "mdi:ipod", // note: change to "mdi:apple" after some time
ios: "mdi:apple", // note: change to "mdi:apple" after some time
linux: "mdi:linux",
copyright: "mdi:copyright",
help: "mdi:help-circle",
Expand Down Expand Up @@ -149,6 +152,125 @@ export function getNewGDUpdateWasReleased(): NewGDUpdateInfo | undefined {
// If no new update, change to `undefined`, if Geode fixed, change `geodeStatus`
return {
newGDVersion: "2.2081",
geodeStatus: "fully-broken",
geodeStatus: "just-updated",
}
}


export type SemverUpperBound =
| {
kind: "finite";
version: string;
inclusive: boolean;
}
| { kind: "infinite"; infinity: true };

/**
* Compare two finite or infinite upper bounds.
* Returns negative if a < b, positive if a > b, zero if equal.
*
* @param {SemverUpperBound} a
* @param {SemverUpperBound} b
* @returns {number}
*/
function compareBounds(a: SemverUpperBound, b: SemverUpperBound): number {
if (a.kind == "infinite" && b.kind == "infinite") return 0;
if (a.kind == "infinite") return 1;
if (b.kind == "infinite") return -1;

const cmp = semver.compare(a.version, b.version);
if (cmp !== 0) return cmp;

// versions are equal, inclusive > exclusive
if (a.inclusive === b.inclusive) return 0;
return a.inclusive ? 1 : -1;
}

/**
* Compares two dependencies by their importance
*
* @param {ServerDependency} a
* @param {ServerDependency} b
* @returns {number}
*/
const compareImportance = (a: ServerDependency, b: ServerDependency): number => {
const importanceRank: Record<string, number> = {
"suggested": 0,
"required": 1
};

const rankA = importanceRank[a.importance]!;
const rankB = importanceRank[b.importance]!;

if (rankA < rankB) return -1;
if (rankA > rankB) return 1;
return 0;
};

/**
* Given a semver range string, return the upper bound version and whether it's inclusive.
*
* @param {string} rangeStr A semver range string
* @returns
*/
export function getUpperBound(rangeStr: string): SemverUpperBound {
try {
const range = new semver.Range(rangeStr);
const comparators = range.set[0];
let upperBound: SemverUpperBound | null = null;

for (const comp of comparators) {
const op = comp.operator;
const version = comp.semver.version;

// other operators like '>', '>=', etc. do not affect the upper bound
if (op === '<' || op === '<=' || op === '=' || !op) {
const inclusive = (op === '<=' || op === '=' || !op);
const candidate: SemverUpperBound = { kind: "finite", version, inclusive };

if (upperBound === null) {
upperBound = candidate;
} else {
// keep the smaller upper bound (intersection)
if (compareBounds(candidate, upperBound) < 0) {
upperBound = candidate;
}
}
}
}

return upperBound ?? { kind: "infinite", infinity: true };
} catch (e) {
console.warn(`Invalid range "${rangeStr}", treating as unbounded`);
return { kind: "infinite", infinity: true };
}
}

/**
* Deduplicate dependencies by mod_id, keeping the entry with the highest upper bound.
*
* @param {ServerDependency[]} deps Array of dependencies
* @returns {ServerDependency[]}
*/
export function deduplicateDependencies(deps: ServerDependency[]): ServerDependency[] {
const map = new Map();

for (const dep of deps) {
const { mod_id, version } = dep;
if (!map.has(mod_id)) {
map.set(mod_id, dep);
continue;
}

const current = map.get(mod_id);
const currentBound = getUpperBound(current.version);
const newBound = getUpperBound(version);

// keep the bound with the larger upper bound, or the one with higher importance
if (compareBounds(newBound, currentBound) < 0 || compareImportance(dep, current) > 0) {
map.set(mod_id, dep);
}
}

return Array.from(map.values());
}
21 changes: 12 additions & 9 deletions src/routes/mods/[id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
import Link from "$lib/components/Link.svelte";
import Icon from "$lib/components/Icon.svelte";
import Gap from "$lib/components/Gap.svelte";
import { serverTimestampToAgoString, serverTimestampToDateString, formatNumber, iconForTag } from "$lib";
import {
serverTimestampToAgoString, serverTimestampToDateString,
formatNumber, iconForTag, deduplicateDependencies
} from "$lib";
import Waves from "$lib/components/Waves.svelte";
import Label from "$lib/components/Label.svelte";
import InfoBox from "$lib/components/InfoBox.svelte";
Expand Down Expand Up @@ -98,6 +101,9 @@
? !!data.mod.links?.homepage || !!data.mod.links?.community
: !!data.mod.links?.homepage && !!data.mod.links?.community,
);

let dedupedDependencies = $derived(deduplicateDependencies(data.version?.dependencies || []));

</script>

<svelte:head>
Expand Down Expand Up @@ -365,25 +371,22 @@
</code>
</p>

{#if data.version.early_load || data.version.api || data.version.gd.ios}
{#if data.version.early_load || data.version.api}
<Row align="center" justify="top" gap="small">
{#if data.version.early_load}
<Label icon="time" design="accent-alt">Early Load</Label>
{/if}
{#if data.version.api}
<Label icon="tag-enhancement" design="accent">API</Label>
{/if}
{#if data.version.gd.ios}
<Label icon="ios" design="gray">{data.version.gd.ios}</Label>
{/if}
</Row>
{/if}
</Column>

<h2>Dependencies</h2>
{#if data.version.dependencies?.length}
{#if dedupedDependencies.length}
<ul class="color-link">
{#each data.version.dependencies as dependency}
{#each dedupedDependencies as dependency}
<li>
{dependency.importance} -
<Link href={`/mods/${dependency.mod_id}`}>
Expand Down Expand Up @@ -497,10 +500,10 @@
</Column>
</section>
<section>
{#if data.version.dependencies?.length}
{#if dedupedDependencies.length}
<p>Dependencies:</p>
<ul>
{#each data.version.dependencies as dependency}
{#each dedupedDependencies as dependency}
<div class="color-link">
<li>
<Link href={`/mods/${dependency.mod_id}`}>
Expand Down