summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yaml3
-rw-r--r--website/.vitepress/theme/components/DownloadCard.vue14
-rw-r--r--website/.vitepress/theme/components/releaseAssets.test.ts144
-rw-r--r--website/.vitepress/theme/components/releaseAssets.ts78
-rw-r--r--website/.vitepress/theme/components/useCompatibilityData.ts60
-rw-r--r--website/.vitepress/theme/components/useDownloadData.ts70
-rw-r--r--website/package.json1
7 files changed, 281 insertions, 89 deletions
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 28e2dfb..16e4b50 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -118,6 +118,9 @@ jobs:
- name: Check lints
run: bun run lint
+ - name: Run tests
+ run: bun run test
+
- name: Build documentation
run: bun run build
diff --git a/website/.vitepress/theme/components/DownloadCard.vue b/website/.vitepress/theme/components/DownloadCard.vue
index add8d45..046de21 100644
--- a/website/.vitepress/theme/components/DownloadCard.vue
+++ b/website/.vitepress/theme/components/DownloadCard.vue
@@ -71,14 +71,12 @@ const t = computed(() =>
},
);
-function formatSize(bytes: number | null): string {
- if (!bytes) return '-';
+function formatSize(bytes: number): string {
const mb = bytes / (1024 * 1024);
return `${mb.toFixed(1)} MB`;
}
-function formatDate(dateStr: string | null): string {
- if (!dateStr) return '-';
+function formatDate(dateStr: string): string {
const locale = isEn.value ? 'en-US' : 'ja-JP';
return new Date(dateStr).toLocaleDateString(locale, {
year: 'numeric',
@@ -138,11 +136,11 @@ function formatDate(dateStr: string | null): string {
<dd>{{ formatSize(data.paper.fileSize) }}</dd>
</div>
<div>
- <dd><code>{{ data.paper.fileName ?? '-' }}</code></dd>
+ <dd><code>{{ data.paper.fileName }}</code></dd>
</div>
</dl>
<div class="download-actions">
- <a v-if="data.paper.downloadUrl" :href="data.paper.downloadUrl" class="download-btn primary">{{ t.download }}</a>
+ <a :href="data.paper.downloadUrl" class="download-btn primary">{{ t.download }}</a>
<a :href="data.paper.releaseUrl" class="download-btn" target="_blank" rel="noopener">{{ t.releaseNotes }}</a>
</div>
</div>
@@ -172,11 +170,11 @@ function formatDate(dateStr: string | null): string {
<dd>{{ formatSize(data.velocity.fileSize) }}</dd>
</div>
<div>
- <dd><code>{{ data.velocity.fileName ?? '-' }}</code></dd>
+ <dd><code>{{ data.velocity.fileName }}</code></dd>
</div>
</dl>
<div class="download-actions">
- <a v-if="data.velocity.downloadUrl" :href="data.velocity.downloadUrl" class="download-btn primary">{{ t.download }}</a>
+ <a :href="data.velocity.downloadUrl" class="download-btn primary">{{ t.download }}</a>
<a :href="data.velocity.releaseUrl" class="download-btn" target="_blank" rel="noopener">{{ t.releaseNotes }}</a>
</div>
</div>
diff --git a/website/.vitepress/theme/components/releaseAssets.test.ts b/website/.vitepress/theme/components/releaseAssets.test.ts
new file mode 100644
index 0000000..32dea7b
--- /dev/null
+++ b/website/.vitepress/theme/components/releaseAssets.test.ts
@@ -0,0 +1,144 @@
+import { describe, expect, test } from 'bun:test';
+import {
+ findPlatformAsset,
+ type GitHubRelease,
+ latestPlatformRelease,
+ platformReleases,
+} from './releaseAssets';
+
+function release(
+ tag: string,
+ publishedAt: string,
+ assetNames: string[],
+): GitHubRelease {
+ return {
+ tag_name: tag,
+ published_at: publishedAt,
+ html_url: `https://github.com/m1sk9/LunaticChat/releases/tag/${tag}`,
+ assets: assetNames.map((name) => ({
+ name,
+ size: 1024,
+ browser_download_url: `https://example.invalid/${name}`,
+ })),
+ };
+}
+
+const UNIFIED_1_3_0 = release('v1.3.0', '2026-08-04T20:05:51Z', [
+ 'LunaticChat-1.2.0-velocity.jar',
+ 'LunaticChat-1.3.0.jar',
+]);
+const PAPER_1_2_2 = release('paper/v1.2.2', '2026-06-12T07:57:55Z', [
+ 'LunaticChat-1.2.2.jar',
+]);
+const VELOCITY_1_1_0 = release('velocity/v1.1.0', '2026-06-12T07:57:33Z', [
+ 'LunaticChat-1.1.0-velocity.jar',
+]);
+
+describe('findPlatformAsset', () => {
+ test('reads each platform version from its own JAR, not from the shared tag', () => {
+ expect(findPlatformAsset(UNIFIED_1_3_0, 'paper')?.version).toBe('1.3.0');
+ expect(findPlatformAsset(UNIFIED_1_3_0, 'velocity')?.version).toBe('1.2.0');
+ });
+
+ test('returns the asset backing the reported version', () => {
+ expect(findPlatformAsset(UNIFIED_1_3_0, 'velocity')?.asset.name).toBe(
+ 'LunaticChat-1.2.0-velocity.jar',
+ );
+ });
+
+ test('does not mistake the Velocity JAR for the Paper JAR', () => {
+ expect(findPlatformAsset(VELOCITY_1_1_0, 'paper')).toBeNull();
+ });
+
+ test('returns null when the release carries no JAR for the platform', () => {
+ expect(findPlatformAsset(PAPER_1_2_2, 'velocity')).toBeNull();
+ });
+});
+
+describe('latestPlatformRelease', () => {
+ const releases = [UNIFIED_1_3_0, VELOCITY_1_1_0, PAPER_1_2_2];
+
+ test('prefers a newer unified release over an older platform-specific tag', () => {
+ expect(latestPlatformRelease(releases, 'paper')?.version).toBe('1.3.0');
+ expect(latestPlatformRelease(releases, 'velocity')?.version).toBe('1.2.0');
+ });
+
+ test('prefers a newer platform-specific tag over an older unified release', () => {
+ const paperOnly = release('paper/v1.4.0', '2026-09-01T00:00:00Z', [
+ 'LunaticChat-1.4.0.jar',
+ ]);
+ const withPaperOnly = [...releases, paperOnly];
+
+ expect(latestPlatformRelease(withPaperOnly, 'paper')?.version).toBe(
+ '1.4.0',
+ );
+ expect(latestPlatformRelease(withPaperOnly, 'velocity')?.version).toBe(
+ '1.2.0',
+ );
+ });
+
+ test('advances one platform without disturbing the other', () => {
+ const velocityOnly = release('velocity/v1.3.0', '2026-09-01T00:00:00Z', [
+ 'LunaticChat-1.3.0-velocity.jar',
+ ]);
+ const withVelocityOnly = [...releases, velocityOnly];
+
+ expect(latestPlatformRelease(withVelocityOnly, 'velocity')?.version).toBe(
+ '1.3.0',
+ );
+ expect(latestPlatformRelease(withVelocityOnly, 'paper')?.version).toBe(
+ '1.3.0',
+ );
+ });
+
+ test('ranks by publication time rather than by response order', () => {
+ const outOfOrder = [PAPER_1_2_2, UNIFIED_1_3_0];
+ expect(latestPlatformRelease(outOfOrder, 'paper')?.release.tag_name).toBe(
+ 'v1.3.0',
+ );
+ });
+
+ test('returns null when no release carries a JAR for the platform', () => {
+ expect(latestPlatformRelease([PAPER_1_2_2], 'velocity')).toBeNull();
+ });
+});
+
+describe('platformReleases', () => {
+ test('lists every release carrying the platform JAR, newest first', () => {
+ expect(
+ platformReleases([PAPER_1_2_2, UNIFIED_1_3_0], 'paper').map(
+ (e) => e.version,
+ ),
+ ).toEqual(['1.3.0', '1.2.2']);
+ });
+
+ test('excludes releases that ship only the other platform', () => {
+ expect(platformReleases([VELOCITY_1_1_0], 'paper')).toEqual([]);
+ });
+
+ test('lists a version once when a later unified release re-attaches its JAR', () => {
+ // v1.4.0 bumps Paper alone, so it re-attaches the unchanged Velocity 1.2.0 JAR.
+ const unified_1_4_0 = release('v1.4.0', '2026-09-01T00:00:00Z', [
+ 'LunaticChat-1.2.0-velocity.jar',
+ 'LunaticChat-1.4.0.jar',
+ ]);
+
+ expect(
+ platformReleases([UNIFIED_1_3_0, unified_1_4_0], 'velocity').map(
+ (e) => e.version,
+ ),
+ ).toEqual(['1.2.0']);
+ });
+
+ test('attributes a re-attached JAR to the release that shipped it last', () => {
+ const unified_1_4_0 = release('v1.4.0', '2026-09-01T00:00:00Z', [
+ 'LunaticChat-1.2.0-velocity.jar',
+ 'LunaticChat-1.4.0.jar',
+ ]);
+
+ expect(
+ platformReleases([UNIFIED_1_3_0, unified_1_4_0], 'velocity')[0]?.release
+ .tag_name,
+ ).toBe('v1.4.0');
+ });
+});
diff --git a/website/.vitepress/theme/components/releaseAssets.ts b/website/.vitepress/theme/components/releaseAssets.ts
new file mode 100644
index 0000000..f05977b
--- /dev/null
+++ b/website/.vitepress/theme/components/releaseAssets.ts
@@ -0,0 +1,78 @@
+export type Platform = 'paper' | 'velocity';
+
+export interface ReleaseAsset {
+ name: string;
+ size: number;
+ browser_download_url: string;
+}
+
+export interface GitHubRelease {
+ tag_name: string;
+ published_at: string;
+ html_url: string;
+ assets: ReleaseAsset[];
+}
+
+export interface PlatformAsset {
+ version: string;
+ asset: ReleaseAsset;
+}
+
+export interface PlatformRelease extends PlatformAsset {
+ release: GitHubRelease;
+}
+
+// A unified `vX.Y.Z` tag ships both platforms and their versions need not agree
+// (v1.3.0 carried Velocity 1.2.0), so a tag name can never stand in for a
+// platform version — only the JAR file name states it.
+const JAR_PATTERN: Record<Platform, RegExp> = {
+ paper: /^LunaticChat-(\d+\.\d+\.\d+)\.jar$/,
+ velocity: /^LunaticChat-(\d+\.\d+\.\d+)-velocity\.jar$/,
+};
+
+export function findPlatformAsset(
+ release: GitHubRelease,
+ platform: Platform,
+): PlatformAsset | null {
+ for (const asset of release.assets) {
+ const version = JAR_PATTERN[platform].exec(asset.name)?.[1];
+ if (version) return { version, asset };
+ }
+ return null;
+}
+
+export function platformReleases(
+ releases: GitHubRelease[],
+ platform: Platform,
+): PlatformRelease[] {
+ // The API orders releases by tag creation, not by publication — velocity/v1.1.0
+ // precedes the later-published paper/v1.2.2 — so response order cannot decide
+ // which release is the newest.
+ const found = releases
+ .flatMap((release) => {
+ const asset = findPlatformAsset(release, platform);
+ return asset ? [{ release, ...asset }] : [];
+ })
+ .sort(
+ (a, b) =>
+ Date.parse(b.release.published_at) - Date.parse(a.release.published_at),
+ );
+
+ // A unified `vX.Y.Z` release always attaches both JARs, so a platform version
+ // reappears under a new tag whenever only the other platform was bumped. The
+ // newest publication is the one that shipped, and keeping both would list the
+ // same version twice — with two protocol versions when the protocol moved.
+ const seen = new Set<string>();
+ return found.filter((entry) => {
+ if (seen.has(entry.version)) return false;
+ seen.add(entry.version);
+ return true;
+ });
+}
+
+export function latestPlatformRelease(
+ releases: GitHubRelease[],
+ platform: Platform,
+): PlatformRelease | null {
+ return platformReleases(releases, platform)[0] ?? null;
+}
diff --git a/website/.vitepress/theme/components/useCompatibilityData.ts b/website/.vitepress/theme/components/useCompatibilityData.ts
index d0de01d..2952355 100644
--- a/website/.vitepress/theme/components/useCompatibilityData.ts
+++ b/website/.vitepress/theme/components/useCompatibilityData.ts
@@ -1,15 +1,14 @@
-import { ref, onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
+import {
+ type GitHubRelease,
+ type Platform,
+ platformReleases,
+} from './releaseAssets';
const REPO = 'm1sk9/LunaticChat';
const PROTOCOL_FILE_PATH =
'engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersion.kt';
-interface GitHubRelease {
- tag_name: string;
- published_at: string;
- html_url: string;
-}
-
export interface ProtocolVersion {
major: number;
minor: number;
@@ -18,7 +17,7 @@ export interface ProtocolVersion {
}
export interface PlatformReleaseEntry {
- platform: 'paper' | 'velocity';
+ platform: Platform;
version: string;
tag: string;
publishedAt: string;
@@ -55,7 +54,9 @@ function parseProtocolVersion(source: string): ProtocolVersion | null {
return { major, minor, patch, minSupportedMinor };
}
-async function fetchProtocolAtTag(tag: string): Promise<ProtocolVersion | null> {
+async function fetchProtocolAtTag(
+ tag: string,
+): Promise<ProtocolVersion | null> {
const url = `https://raw.githubusercontent.com/${REPO}/${encodeURIComponent(tag)}/${PROTOCOL_FILE_PATH}`;
try {
const res = await fetch(url);
@@ -67,19 +68,18 @@ async function fetchProtocolAtTag(tag: string): Promise<ProtocolVersion | null>
}
}
-function buildEntry(
- release: GitHubRelease,
- platform: 'paper' | 'velocity',
-): PlatformReleaseEntry {
- const version = release.tag_name.replace(/^(paper\/|velocity\/)?v/, '');
- return {
+function buildEntries(
+ releases: GitHubRelease[],
+ platform: Platform,
+): PlatformReleaseEntry[] {
+ return platformReleases(releases, platform).map((entry) => ({
platform,
- version,
- tag: release.tag_name,
- publishedAt: release.published_at,
- releaseUrl: release.html_url,
+ version: entry.version,
+ tag: entry.release.tag_name,
+ publishedAt: entry.release.published_at,
+ releaseUrl: entry.release.html_url,
protocol: null,
- };
+ }));
}
export function useCompatibilityData() {
@@ -99,18 +99,8 @@ export function useCompatibilityData() {
const releases: GitHubRelease[] = await res.json();
- const isUnified = (tag: string) => /^v\d/.test(tag);
- const isPaperTag = (tag: string) =>
- tag.startsWith('paper/v') || isUnified(tag);
- const isVelocityTag = (tag: string) =>
- tag.startsWith('velocity/v') || isUnified(tag);
-
- const paper = releases
- .filter((r) => isPaperTag(r.tag_name))
- .map((r) => buildEntry(r, 'paper'));
- const velocity = releases
- .filter((r) => isVelocityTag(r.tag_name))
- .map((r) => buildEntry(r, 'velocity'));
+ const paper = buildEntries(releases, 'paper');
+ const velocity = buildEntries(releases, 'velocity');
const all = [...paper, ...velocity];
const protocols = await Promise.all(
@@ -134,7 +124,11 @@ export function useCompatibilityData() {
return { data, loading, error };
}
-export type CompatibilityResult = 'compatible' | 'major-mismatch' | 'paper-too-new' | 'paper-too-old';
+export type CompatibilityResult =
+ | 'compatible'
+ | 'major-mismatch'
+ | 'paper-too-new'
+ | 'paper-too-old';
// Mirrors the gatekeeping done by Velocity in
// platform-velocity/.../PluginMessageHandler.kt — Paper does not validate.
diff --git a/website/.vitepress/theme/components/useDownloadData.ts b/website/.vitepress/theme/components/useDownloadData.ts
index 0c9e7e0..8ed264a 100644
--- a/website/.vitepress/theme/components/useDownloadData.ts
+++ b/website/.vitepress/theme/components/useDownloadData.ts
@@ -1,27 +1,19 @@
-import { ref, onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
+import {
+ type GitHubRelease,
+ latestPlatformRelease,
+ type Platform,
+} from './releaseAssets';
const REPO = 'm1sk9/LunaticChat';
-interface ReleaseAsset {
- name: string;
- size: number;
- browser_download_url: string;
-}
-
-interface GitHubRelease {
- tag_name: string;
- published_at: string;
- html_url: string;
- assets: ReleaseAsset[];
-}
-
export interface PlatformRelease {
version: string;
publishedAt: string;
releaseUrl: string;
- downloadUrl: string | null;
- fileName: string | null;
- fileSize: number | null;
+ downloadUrl: string;
+ fileName: string;
+ fileSize: number;
}
export interface DownloadData {
@@ -30,22 +22,20 @@ export interface DownloadData {
ci: { url: string };
}
-function parsePlatformRelease(
- release: GitHubRelease | null,
- jarPattern: RegExp,
+function resolvePlatformRelease(
+ releases: GitHubRelease[],
+ platform: Platform,
): PlatformRelease | null {
- if (!release) return null;
-
- const asset = release.assets.find((a) => jarPattern.test(a.name));
- const version = release.tag_name.replace(/^(paper\/|velocity\/)?v/, '');
+ const latest = latestPlatformRelease(releases, platform);
+ if (!latest) return null;
return {
- version,
- publishedAt: release.published_at,
- releaseUrl: release.html_url,
- downloadUrl: asset?.browser_download_url ?? null,
- fileName: asset?.name ?? null,
- fileSize: asset?.size ?? null,
+ version: latest.version,
+ publishedAt: latest.release.published_at,
+ releaseUrl: latest.release.html_url,
+ downloadUrl: latest.asset.browser_download_url,
+ fileName: latest.asset.name,
+ fileSize: latest.asset.size,
};
}
@@ -72,25 +62,9 @@ export function useDownloadData() {
const releases: GitHubRelease[] = await res.json();
- const paperRelease =
- releases.find((r) => r.tag_name.startsWith('paper/v')) ??
- releases.find((r) => /^v\d/.test(r.tag_name)) ??
- null;
-
- const velocityRelease =
- releases.find((r) => r.tag_name.startsWith('velocity/v')) ??
- releases.find((r) => /^v\d/.test(r.tag_name)) ??
- null;
-
data.value = {
- paper: parsePlatformRelease(
- paperRelease,
- /^LunaticChat-[\d.]+\.jar$/,
- ),
- velocity: parsePlatformRelease(
- velocityRelease,
- /^LunaticChat-[\d.]+-velocity\.jar$/,
- ),
+ paper: resolvePlatformRelease(releases, 'paper'),
+ velocity: resolvePlatformRelease(releases, 'velocity'),
ci: data.value.ci,
};
} catch {
diff --git a/website/package.json b/website/package.json
index 48c2a6b..5b18155 100644
--- a/website/package.json
+++ b/website/package.json
@@ -3,6 +3,7 @@
"dev": "vitepress dev",
"build": "vitepress build",
"preview": "vitepress preview",
+ "test": "bun test ./.vitepress/",
"format": "biome format --write .",
"format:check": "biome ci .",
"lint": "biome lint .",