diff --git a/scripts/sync-to-d1.js b/scripts/sync-to-d1.js index 976dffdf5..5618a218a 100644 --- a/scripts/sync-to-d1.js +++ b/scripts/sync-to-d1.js @@ -21,12 +21,14 @@ import { fetchWithRetry } from "./lib/fetch-with-retry.js"; const DOCS_DIR = join(process.cwd(), "docs"); const MANUAL_OVERRIDES_FILE = join(DOCS_DIR, "manual-overrides.json"); -// Prerelease version regex - ported from mise +// Fallback for tools that do not expose explicit prerelease metadata in TOML. +// Prefer `prerelease = true` from mise where present; this catches older and +// non-metadata sources so latest_stable_version does not regress. const PRERELEASE_REGEX = /(-src|-dev|-latest|-stm|[-.](rc|pre)|-milestone|-alpha|-beta|-next|([abc])\d+$|snapshot|master)/i; -function isPrerelease(version) { - return PRERELEASE_REGEX.test(version); +function isPrerelease(version, data) { + return data?.prerelease === true || PRERELEASE_REGEX.test(version); } function toISOString(value) { @@ -190,8 +192,8 @@ function processTomlFile(filePath) { let latestStableVersion = null; for (let i = versions.length - 1; i >= 0; i--) { - const [version] = versions[i]; - if (!isPrerelease(version)) { + const [version, data] = versions[i]; + if (!isPrerelease(version, data)) { latestStableVersion = version; break; } diff --git a/scripts/update.sh b/scripts/update.sh index 38716d62a..dece41dd4 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -340,7 +340,9 @@ generate_toml_file() { local error_output error_output=$(mktemp) - # Try to get JSON with timestamps from mise ls-remote --json. + # Try to get JSON with timestamps/release URLs/prerelease flags from + # mise ls-remote --prerelease --json. The TOML path can carry prerelease + # metadata, so collect the superset and let clients filter by that flag. # Pass the rotated per-tool token via GITHUB_API_TOKEN so this call # isn't rate-limited by the workflow's single shared MISE_GITHUB_TOKEN. # Without this, ~58% of tools per run hit GitHub's 5000/hr authenticated @@ -350,7 +352,7 @@ generate_toml_file() { # plain-text path — losing `release_url` and `created_at` for any new # version that wasn't already in the existing TOML. local json_output - if json_output=$(GITHUB_API_TOKEN="$token" mise ls-remote --json "$tool" 2>/dev/null) && [ -n "$json_output" ]; then + if json_output=$(GITHUB_API_TOKEN="$token" mise ls-remote --prerelease --json "$tool" 2>/dev/null) && [ -n "$json_output" ]; then local json_count json_count=$(printf '%s' "$json_output" | jq 'if type == "array" then length else 0 end' 2>/dev/null || echo 0) if [ "${json_count:-0}" -gt 0 ]; then diff --git a/web/src/components/VersionsTable.tsx b/web/src/components/VersionsTable.tsx index b685326ef..9dfaef395 100644 --- a/web/src/components/VersionsTable.tsx +++ b/web/src/components/VersionsTable.tsx @@ -11,6 +11,7 @@ interface Version { version: string; created_at?: string | null; release_url?: string | null; + prerelease?: boolean; } type VersionSortKey = "default" | "downloads" | "released"; @@ -357,7 +358,7 @@ export function VersionsTable({ // Filter by prerelease if (hidePrerelease) { - result = result.filter((v) => !isPrerelease(v.version)); + result = result.filter((v) => !isPrerelease(v)); } // Filter by version prefix @@ -414,7 +415,7 @@ export function VersionsTable({ (v) => getDistribution(v.version, tool) === distribution, ); } - return result.filter((v) => isPrerelease(v.version)).length; + return result.filter((v) => isPrerelease(v)).length; }, [versions, distribution, tool]); // Build timeline from versions filtered by distribution and version prefix only diff --git a/web/src/lib/versions.ts b/web/src/lib/versions.ts index 1ed9b9b03..400df476c 100644 --- a/web/src/lib/versions.ts +++ b/web/src/lib/versions.ts @@ -1,51 +1,20 @@ /** - * Version filtering utilities - ported from mise - * @see https://github.com/jdx/mise/blob/main/src/plugins/mod.rs - */ - -/** - * Regex to identify prerelease/development/unstable versions. - * Versions matching this pattern should be excluded when showing stable versions. - * - * Matches: - * - -src, -dev, -latest, -stm (build markers) - * - -rc, .rc (release candidates) - * - -milestone (milestone releases) - * - -alpha, -beta (pre-releases) - * - -pre, .pre (pre-releases) - * - -next (next version markers) - * - a1, b2, c3 etc. (single letter + digits) - * - snapshot, SNAPSHOT (snapshot builds) - * - master (development branch) + * Regex fallback for backends that do not expose explicit prerelease metadata. + * Prefer stored `prerelease = true` where available; this only covers older + * and non-metadata sources such as Java/Core plugin version lists. */ const PRERELEASE_REGEX = /(-src|-dev|-latest|-stm|[-.](rc|pre)|-milestone|-alpha|-beta|-next|([abc])\d+$|snapshot|master)/i; -/** - * Check if a version string appears to be a prerelease/development version - */ -export function isPrerelease(version: string): boolean { +export function isPrereleaseVersion(version: string): boolean { return PRERELEASE_REGEX.test(version); } -/** - * Filter an array of versions to only include stable releases - */ -export function filterStableVersions( - versions: T[], -): T[] { - return versions.filter((v) => !isPrerelease(v.version)); -} - -/** - * Get the latest stable version from an array of versions - * Assumes versions are ordered oldest to newest - */ -export function getLatestStableVersion( - versions: T[], -): T | undefined { - const stable = filterStableVersions(versions); - return stable[stable.length - 1]; +export function isPrerelease(version: { + version: string; + prerelease?: boolean | null; +}): boolean { + return version.prerelease === true || isPrereleaseVersion(version.version); } /** diff --git a/web/src/pages/[...tool].toml.ts b/web/src/pages/[...tool].toml.ts index 713ceb8a2..b4147d6c7 100644 --- a/web/src/pages/[...tool].toml.ts +++ b/web/src/pages/[...tool].toml.ts @@ -12,6 +12,7 @@ interface VersionRow { version: string; created_at: string | null; release_url: string | null; + prerelease: number; } // Legacy endpoint: GET /:tool.toml - serves TOML version file from D1 @@ -55,7 +56,7 @@ export const GET: APIRoute = async ({ request, params, locals }) => { // Get versions ordered by sort_order (semantic version order from TOML file) // Only include versions from mise ls-remote (not user-tracked installs) const versions = await db.all(sql` - SELECT version, created_at, release_url + SELECT version, created_at, release_url, prerelease FROM versions WHERE tool_id = ${toolId} AND from_mise = 1 ORDER BY sort_order ASC, id ASC @@ -100,6 +101,9 @@ export const GET: APIRoute = async ({ request, params, locals }) => { if (v.release_url) { parts.push(`release_url = "${v.release_url}"`); } + if (v.prerelease === 1) { + parts.push("prerelease = true"); + } if (parts.length > 0) { lines.push(`"${v.version}" = { ${parts.join(", ")} }`); diff --git a/web/src/pages/[...tool].ts b/web/src/pages/[...tool].ts index 84814216b..937eec315 100644 --- a/web/src/pages/[...tool].ts +++ b/web/src/pages/[...tool].ts @@ -57,10 +57,10 @@ export const GET: APIRoute = async ({ params, locals }) => { const toolId = (toolResult[0] as { id: number }).id; - // Get versions ordered by sort_order (semantic version order from TOML file) - // Only include versions from mise ls-remote (not user-tracked installs) + // Get versions ordered by sort_order (semantic version order from TOML file). + // Plain text cannot carry prerelease metadata, so keep it stable-only. const versions = await db.all<{ version: string }>(sql` - SELECT version FROM versions WHERE tool_id = ${toolId} AND from_mise = 1 ORDER BY sort_order ASC, id ASC + SELECT version FROM versions WHERE tool_id = ${toolId} AND from_mise = 1 AND prerelease = 0 ORDER BY sort_order ASC, id ASC `); if (versions.length === 0) { diff --git a/web/src/pages/tools/[...tool].ts b/web/src/pages/tools/[...tool].ts index 1f6b99dfd..c60f2a443 100644 --- a/web/src/pages/tools/[...tool].ts +++ b/web/src/pages/tools/[...tool].ts @@ -43,12 +43,12 @@ export const GET: APIRoute = async ({ params, locals }) => { const toolId = (toolResult[0] as { id: number }).id; - // Get versions ordered by sort_order (semantic version order from TOML file) - // Only include versions from mise ls-remote (not user-tracked installs) + // Get versions ordered by sort_order (semantic version order from TOML file). + // Plain text cannot carry prerelease metadata, so keep it stable-only. const versions = await db.all<{ version: string }>(sql` SELECT version FROM versions - WHERE tool_id = ${toolId} AND from_mise = 1 + WHERE tool_id = ${toolId} AND from_mise = 1 AND prerelease = 0 ORDER BY sort_order ASC, id ASC `); diff --git a/web/src/pages/tools/[tool].astro b/web/src/pages/tools/[tool].astro index 3665982be..05bec8f98 100644 --- a/web/src/pages/tools/[tool].astro +++ b/web/src/pages/tools/[tool].astro @@ -30,7 +30,7 @@ if (!toolMeta) { } // Load versions from D1 database -let versions: Array<{ version: string; created_at?: string | null; release_url?: string | null }> = []; +let versions: Array<{ version: string; created_at?: string | null; release_url?: string | null; prerelease?: boolean }> = []; try { // Get tool_id first const toolResult = await db.all<{ id: number }>(sql` @@ -39,8 +39,8 @@ try { if (toolResult.length > 0) { const toolId = toolResult[0].id; - const versionRows = await db.all<{ version: string; created_at: string | null; release_url: string | null }>(sql` - SELECT version, created_at, release_url + const versionRows = await db.all<{ version: string; created_at: string | null; release_url: string | null; prerelease: number }>(sql` + SELECT version, created_at, release_url, prerelease FROM versions WHERE tool_id = ${toolId} ORDER BY sort_order ASC, id ASC @@ -49,6 +49,7 @@ try { version: row.version, created_at: row.created_at || null, release_url: row.release_url || null, + prerelease: row.prerelease === 1, })); } } catch (e) { diff --git a/web/src/pages/tools/[tool].toml.ts b/web/src/pages/tools/[tool].toml.ts index c278aada8..3100b3e20 100644 --- a/web/src/pages/tools/[tool].toml.ts +++ b/web/src/pages/tools/[tool].toml.ts @@ -12,6 +12,7 @@ interface VersionRow { version: string; created_at: string | null; release_url: string | null; + prerelease: number; } export const GET: APIRoute = async ({ request, params, locals }) => { @@ -53,7 +54,7 @@ export const GET: APIRoute = async ({ request, params, locals }) => { // Get versions ordered by sort_order (semantic version order from TOML file) // Only include versions from mise ls-remote (not user-tracked installs) const versions = await db.all(sql` - SELECT version, created_at, release_url + SELECT version, created_at, release_url, prerelease FROM versions WHERE tool_id = ${toolId} AND from_mise = 1 ORDER BY sort_order ASC, id ASC @@ -98,6 +99,9 @@ export const GET: APIRoute = async ({ request, params, locals }) => { if (v.release_url) { parts.push(`release_url = "${v.release_url}"`); } + if (v.prerelease === 1) { + parts.push("prerelease = true"); + } if (parts.length > 0) { lines.push(`"${v.version}" = { ${parts.join(", ")} }`);