diff options
| -rw-r--r-- | .github/workflows/ci.yaml | 3 | ||||
| -rw-r--r-- | website/.vitepress/theme/components/DownloadCard.vue | 14 | ||||
| -rw-r--r-- | website/.vitepress/theme/components/releaseAssets.test.ts | 144 | ||||
| -rw-r--r-- | website/.vitepress/theme/components/releaseAssets.ts | 78 | ||||
| -rw-r--r-- | website/.vitepress/theme/components/useCompatibilityData.ts | 60 | ||||
| -rw-r--r-- | website/.vitepress/theme/components/useDownloadData.ts | 70 | ||||
| -rw-r--r-- | website/package.json | 1 |
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 .", |
