feat: add windows desktop release support

This commit is contained in:
lucaronin
2026-04-24 18:45:17 +02:00
parent f896c01829
commit d6b3c0aef3
36 changed files with 1335 additions and 452 deletions

View File

@@ -1,87 +1,118 @@
import {
buildStableDownloadRedirectPage,
extractStableDmgUrl,
extractStableDmgUrlFromReleases,
resolveStableDmgUrl,
extractStableDownloadTargets,
extractStableDownloadTargetsFromReleases,
resolveStableDownloadTargets,
} from './releaseDownloadPage'
describe('extractStableDmgUrl', () => {
it('returns the stable dmg url when present', () => {
describe('extractStableDownloadTargets', () => {
it('returns stable downloads for each supported desktop platform when present', () => {
expect(
extractStableDmgUrl({
extractStableDownloadTargets({
platforms: {
'darwin-aarch64': {
dmg_url: 'https://example.com/Tolaria.dmg',
download_url: 'https://example.com/Tolaria.dmg',
},
'linux-x86_64': {
download_url: 'https://example.com/Tolaria.AppImage',
},
'windows-x86_64': {
url: 'https://example.com/Tolaria-setup.exe',
},
},
}),
).toBe('https://example.com/Tolaria.dmg')
})
it('returns null when the stable dmg url is missing', () => {
expect(
extractStableDmgUrl({
platforms: {
'darwin-aarch64': {
url: 'https://example.com/Tolaria.app.tar.gz',
},
},
}),
).toBeNull()
).toMatchObject({
'darwin-aarch64': {
label: 'macOS',
url: 'https://example.com/Tolaria.dmg',
},
'linux-x86_64': {
label: 'Linux',
url: 'https://example.com/Tolaria.AppImage',
},
'windows-x86_64': {
label: 'Windows',
url: 'https://example.com/Tolaria-setup.exe',
},
})
})
})
describe('buildStableDownloadRedirectPage', () => {
it('builds a redirect page when a stable dmg exists', () => {
const html = buildStableDownloadRedirectPage('https://example.com/Tolaria.dmg')
it('builds a redirect page with platform-specific download links', () => {
const html = buildStableDownloadRedirectPage({
'darwin-aarch64': {
buttonLabel: 'Download Tolaria for macOS',
label: 'macOS',
url: 'https://example.com/Tolaria.dmg',
},
'windows-x86_64': {
buttonLabel: 'Download Tolaria for Windows',
label: 'Windows',
url: 'https://example.com/Tolaria-setup.exe',
},
})
expect(html).toContain('Tolaria Stable Download')
expect(html).toContain('window.location.replace("https://example.com/Tolaria.dmg");')
expect(html).toContain('Download latest stable DMG')
expect(html).toContain('meta http-equiv="refresh"')
expect(html).toContain('DOWNLOAD_TARGETS')
expect(html).toContain('Download Tolaria for Windows')
expect(html).toContain('Download Tolaria for macOS')
expect(html).toContain('window.location.replace')
})
it('builds a fallback page when no stable dmg exists yet', () => {
const html = buildStableDownloadRedirectPage(null)
it('builds a fallback page when no stable downloads exist yet', () => {
const html = buildStableDownloadRedirectPage({})
expect(html).toContain('Tolaria Stable Download Unavailable')
expect(html).toContain('View release history')
expect(html).toContain('https://refactoringhq.github.io/tolaria/')
expect(html).not.toContain('window.location.replace(')
expect(html).not.toContain('DOWNLOAD_TARGETS')
})
})
describe('resolveStableDmgUrl', () => {
it('falls back to the latest stable github release dmg when latest.json has no dmg url', () => {
describe('resolveStableDownloadTargets', () => {
it('falls back to stable release assets when latest.json is incomplete', () => {
const latestPayload = {
platforms: {
'darwin-aarch64': {
url: 'https://example.com/Tolaria.app.tar.gz',
download_url: 'https://example.com/Tolaria.dmg',
},
},
}
const releasesPayload = [
{
prerelease: true,
assets: [
{
name: 'Tolaria-alpha.dmg',
browser_download_url: 'https://example.com/alpha.dmg',
},
],
},
{
prerelease: false,
assets: [
{
name: 'Tolaria.dmg',
browser_download_url: 'https://example.com/stable.dmg',
name: 'Tolaria-setup.exe',
browser_download_url: 'https://example.com/Tolaria-setup.exe',
},
{
name: 'Tolaria.AppImage',
browser_download_url: 'https://example.com/Tolaria.AppImage',
},
],
},
]
expect(extractStableDmgUrlFromReleases(releasesPayload)).toBe('https://example.com/stable.dmg')
expect(resolveStableDmgUrl(latestPayload, releasesPayload)).toBe('https://example.com/stable.dmg')
expect(extractStableDownloadTargetsFromReleases(releasesPayload)).toMatchObject({
'linux-x86_64': {
url: 'https://example.com/Tolaria.AppImage',
},
'windows-x86_64': {
url: 'https://example.com/Tolaria-setup.exe',
},
})
expect(resolveStableDownloadTargets(latestPayload, releasesPayload)).toMatchObject({
'darwin-aarch64': {
url: 'https://example.com/Tolaria.dmg',
},
'linux-x86_64': {
url: 'https://example.com/Tolaria.AppImage',
},
'windows-x86_64': {
url: 'https://example.com/Tolaria-setup.exe',
},
})
})
})

View File

@@ -1,7 +1,12 @@
const RELEASE_HISTORY_URL = 'https://refactoringhq.github.io/tolaria/'
type StablePlatformKey = 'darwin-aarch64' | 'linux-x86_64' | 'windows-x86_64'
type PlatformPayload = {
dmg_url?: unknown
download_url?: unknown
installer_url?: unknown
url?: unknown
}
type LatestReleasePayload = {
@@ -19,15 +24,42 @@ type GitHubReleasePayload = {
prerelease?: unknown
}
type DownloadPageContent = {
export type StableDownloadTarget = {
buttonLabel: string
destinationUrl: string
label: string
url: string
}
export type StableDownloadTargets = Partial<Record<StablePlatformKey, StableDownloadTarget>>
type DownloadPageContent = {
helperText: string
message: string
shouldRedirect: boolean
title: string
}
const PLATFORM_METADATA: Record<StablePlatformKey, { buttonLabel: string; label: string }> = {
'darwin-aarch64': {
buttonLabel: 'Download Tolaria for macOS',
label: 'macOS',
},
'linux-x86_64': {
buttonLabel: 'Download Tolaria for Linux',
label: 'Linux',
},
'windows-x86_64': {
buttonLabel: 'Download Tolaria for Windows',
label: 'Windows',
},
}
const PLATFORM_ORDER: StablePlatformKey[] = [
'darwin-aarch64',
'windows-x86_64',
'linux-x86_64',
]
const REDIRECT_PAGE_STYLES = `
:root {
color-scheme: light;
@@ -49,7 +81,7 @@ const REDIRECT_PAGE_STYLES = `
}
main {
width: min(100%, 460px);
width: min(100%, 520px);
background: #ffffff;
border: 1px solid #e9e9e7;
border-radius: 16px;
@@ -68,6 +100,13 @@ const REDIRECT_PAGE_STYLES = `
line-height: 1.5;
}
.button-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 16px;
}
a {
display: inline-flex;
align-items: center;
@@ -81,10 +120,20 @@ const REDIRECT_PAGE_STYLES = `
font-weight: 600;
}
a[data-secondary="true"] {
background: #eef2ff;
color: #1d4ed8;
}
a:hover,
a:focus-visible {
background: #1248cc;
}
a[data-secondary="true"]:hover,
a[data-secondary="true"]:focus-visible {
background: #dbe4ff;
}
`
function escapeHtml(value: string): string {
@@ -95,115 +144,281 @@ function escapeHtml(value: string): string {
.replaceAll('"', '&quot;')
}
export function extractStableDmgUrl(payload: unknown): string | null {
function normalizeUrl(value: unknown): string | null {
if (typeof value !== 'string') return null
const trimmed = value.trim()
return trimmed.length > 0 ? trimmed : null
}
function buildStableDownloadTarget(
platform: StablePlatformKey,
url: string,
): StableDownloadTarget {
return {
...PLATFORM_METADATA[platform],
url,
}
}
function extractPlatformDownloadUrl(
platform: StablePlatformKey,
payload: PlatformPayload | undefined,
): string | null {
if (!payload || typeof payload !== 'object') return null
switch (platform) {
case 'darwin-aarch64':
return (
normalizeUrl(payload.download_url)
?? normalizeUrl(payload.dmg_url)
?? normalizeUrl(payload.url)
)
case 'windows-x86_64':
return (
normalizeUrl(payload.download_url)
?? normalizeUrl(payload.installer_url)
?? normalizeUrl(payload.url)
)
case 'linux-x86_64':
return normalizeUrl(payload.download_url) ?? normalizeUrl(payload.url)
}
}
export function extractStableDownloadTargets(payload: unknown): StableDownloadTargets {
if (!payload || typeof payload !== 'object') return {}
const { platforms } = payload as LatestReleasePayload
if (!platforms || typeof platforms !== 'object') return null
if (!platforms || typeof platforms !== 'object') return {}
const dmgUrl = platforms['darwin-aarch64']?.dmg_url
if (typeof dmgUrl !== 'string') return null
const downloads: StableDownloadTargets = {}
for (const platform of PLATFORM_ORDER) {
const url = extractPlatformDownloadUrl(platform, platforms[platform])
if (url) downloads[platform] = buildStableDownloadTarget(platform, url)
}
const trimmedUrl = dmgUrl.trim()
return trimmedUrl.length > 0 ? trimmedUrl : null
return downloads
}
function isPublicStableRelease(release: GitHubReleasePayload): boolean {
return release.draft !== true && release.prerelease !== true
}
function extractAssetDmgUrl(asset: ReleaseAssetPayload): string | null {
if (typeof asset.name !== 'string' || !asset.name.endsWith('.dmg')) return null
if (typeof asset.browser_download_url !== 'string') return null
const trimmedUrl = asset.browser_download_url.trim()
return trimmedUrl.length > 0 ? trimmedUrl : null
}
function findReleaseDmgUrl(release: GitHubReleasePayload): string | null {
if (!isPublicStableRelease(release)) return null
if (!Array.isArray(release.assets)) return null
for (const asset of release.assets) {
const dmgUrl = extractAssetDmgUrl(asset)
if (dmgUrl !== null) return dmgUrl
function classifyReleaseAsset(name: string): {
platform: StablePlatformKey
preference: number
} | null {
if (name.endsWith('.dmg')) {
return { platform: 'darwin-aarch64', preference: 2 }
}
if (name.endsWith('.app.tar.gz')) {
return { platform: 'darwin-aarch64', preference: 1 }
}
if (name.endsWith('-setup.exe')) {
return { platform: 'windows-x86_64', preference: 2 }
}
if (name.endsWith('.msi')) {
return { platform: 'windows-x86_64', preference: 1 }
}
if (name.endsWith('.AppImage')) {
return { platform: 'linux-x86_64', preference: 2 }
}
if (name.endsWith('.deb')) {
return { platform: 'linux-x86_64', preference: 1 }
}
return null
}
export function extractStableDmgUrlFromReleases(payload: unknown): string | null {
if (!Array.isArray(payload)) return null
type ReleaseAssetSelectionState = {
downloads: StableDownloadTargets
preferences: Partial<Record<StablePlatformKey, number>>
}
type ReleaseAssetSelection = {
platform: StablePlatformKey
preference: number
url: string
}
function updateDownloadPreference(
state: ReleaseAssetSelectionState,
selection: ReleaseAssetSelection,
) {
const currentPreference = state.preferences[selection.platform] ?? Number.NEGATIVE_INFINITY
if (selection.preference < currentPreference) return
state.preferences[selection.platform] = selection.preference
state.downloads[selection.platform] = buildStableDownloadTarget(selection.platform, selection.url)
}
function selectReleaseAsset(asset: ReleaseAssetPayload): ReleaseAssetSelection | null {
const name = typeof asset.name === 'string' ? asset.name.trim() : ''
const url = normalizeUrl(asset.browser_download_url)
const classification = classifyReleaseAsset(name)
if (!classification || !url) return null
return {
...classification,
url,
}
}
function extractStableDownloadTargetsFromAssets(
assets: ReleaseAssetPayload[],
): StableDownloadTargets {
const state: ReleaseAssetSelectionState = {
downloads: {},
preferences: {},
}
for (const asset of assets) {
const selection = selectReleaseAsset(asset)
if (!selection) continue
updateDownloadPreference(state, selection)
}
return state.downloads
}
function findPublicStableRelease(
payload: unknown[],
): GitHubReleasePayload | null {
for (const release of payload) {
if (!release || typeof release !== 'object') continue
const dmgUrl = findReleaseDmgUrl(release as GitHubReleasePayload)
if (dmgUrl !== null) return dmgUrl
const typedRelease = release as GitHubReleasePayload
if (!isPublicStableRelease(typedRelease) || !Array.isArray(typedRelease.assets)) continue
return typedRelease
}
return null
}
export function resolveStableDmgUrl(
latestPayload: unknown,
releasesPayload: unknown,
): string | null {
return extractStableDmgUrl(latestPayload) ?? extractStableDmgUrlFromReleases(releasesPayload)
export function extractStableDownloadTargetsFromReleases(
payload: unknown,
): StableDownloadTargets {
if (!Array.isArray(payload)) return {}
const stableRelease = findPublicStableRelease(payload)
return stableRelease && Array.isArray(stableRelease.assets)
? extractStableDownloadTargetsFromAssets(stableRelease.assets)
: {}
}
function buildStableDownloadPageContent(dmgUrl: string | null): DownloadPageContent {
if (dmgUrl !== null) {
export function resolveStableDownloadTargets(
latestPayload: unknown,
releasesPayload: unknown,
): StableDownloadTargets {
return {
...extractStableDownloadTargetsFromReleases(releasesPayload),
...extractStableDownloadTargets(latestPayload),
}
}
function buildStableDownloadPageContent(
downloads: StableDownloadTargets,
): DownloadPageContent {
if (Object.keys(downloads).length > 0) {
return {
buttonLabel: 'Download latest stable DMG',
destinationUrl: dmgUrl,
helperText: 'If the download does not start automatically, use the button below.',
message: 'Redirecting to the latest stable Tolaria DMG.',
helperText: 'If the download does not start automatically, use one of the platform links below.',
message: 'Preparing the latest stable Tolaria download for your platform.',
shouldRedirect: true,
title: 'Tolaria Stable Download',
}
}
return {
buttonLabel: 'View release history',
destinationUrl: RELEASE_HISTORY_URL,
helperText: 'Use the button below to check the latest release history.',
message: 'No stable Tolaria DMG is available yet.',
message: 'No stable Tolaria downloads are available yet.',
shouldRedirect: false,
title: 'Tolaria Stable Download Unavailable',
}
}
function buildRedirectMarkup(destinationUrl: string, shouldRedirect: boolean): string {
if (!shouldRedirect) return ''
function buildDownloadsMarkup(downloads: StableDownloadTargets): string {
const targets = PLATFORM_ORDER
.map((platform) => downloads[platform])
.filter((target): target is StableDownloadTarget => Boolean(target))
if (targets.length === 0) {
return `<div class="button-list"><a id="download-link" href="${RELEASE_HISTORY_URL}" data-secondary="true">View release history</a></div>`
}
const primaryTarget = targets[0]
const secondaryLinks = targets
.map((target) => (
`<a href="${escapeHtml(target.url)}" data-secondary="true">${escapeHtml(target.label)}</a>`
))
.join('')
return `
<div class="button-list">
<a id="download-link" href="${escapeHtml(primaryTarget.url)}">${escapeHtml(primaryTarget.buttonLabel)}</a>
</div>
<div class="button-list">${secondaryLinks}</div>
<div class="button-list">
<a href="${RELEASE_HISTORY_URL}" data-secondary="true">View release history</a>
</div>`
}
function buildRedirectMarkup(downloads: StableDownloadTargets): string {
if (Object.keys(downloads).length === 0) return ''
const serializedTargets = JSON.stringify(downloads)
const escapedDestinationUrl = escapeHtml(destinationUrl)
return `
<meta http-equiv="refresh" content="0; url=${escapedDestinationUrl}">
<script>
window.location.replace(${JSON.stringify(destinationUrl)});
const DOWNLOAD_TARGETS = ${serializedTargets};
const PLATFORM_ORDER = ${JSON.stringify(PLATFORM_ORDER)};
function detectPlatform(userAgent) {
if (/Windows/i.test(userAgent)) return 'windows-x86_64';
if (/Mac OS X|Macintosh/i.test(userAgent)) return 'darwin-aarch64';
if (/Linux/i.test(userAgent) && !/Android/i.test(userAgent)) return 'linux-x86_64';
return null;
}
const detectedPlatform = detectPlatform(navigator.userAgent);
const resolvedTarget = (
detectedPlatform && DOWNLOAD_TARGETS[detectedPlatform]
) || DOWNLOAD_TARGETS[PLATFORM_ORDER.find((platform) => DOWNLOAD_TARGETS[platform])] || null;
if (resolvedTarget) {
const link = document.getElementById('download-link');
const message = document.getElementById('download-message');
if (link) {
link.href = resolvedTarget.url;
link.textContent = resolvedTarget.buttonLabel;
}
if (message) {
message.textContent = 'Redirecting to the latest stable Tolaria download for ' + resolvedTarget.label + '.';
}
window.location.replace(resolvedTarget.url);
}
</script>`
}
export function buildStableDownloadRedirectPage(dmgUrl: string | null): string {
const page = buildStableDownloadPageContent(dmgUrl)
const escapedDestinationUrl = escapeHtml(page.destinationUrl)
const redirectMarkup = buildRedirectMarkup(page.destinationUrl, page.shouldRedirect)
export function buildStableDownloadRedirectPage(
downloads: StableDownloadTargets,
): string {
const page = buildStableDownloadPageContent(downloads)
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${page.title}</title>${redirectMarkup}
<title>${page.title}</title>${page.shouldRedirect ? buildRedirectMarkup(downloads) : ''}
<style>${REDIRECT_PAGE_STYLES}
</style>
</head>
<body>
<main>
<h1>${page.title}</h1>
<p>${page.message}</p>
<p id="download-message">${page.message}</p>
<p>${page.helperText}</p>
<a id="download-link" href="${escapedDestinationUrl}">${page.buttonLabel}</a>
${buildDownloadsMarkup(downloads)}
</main>
</body>
</html>

View File

@@ -21,8 +21,8 @@ describe('buildReleaseHistoryPage', () => {
{
assets: [
{
browser_download_url: 'https://example.com/Tolaria.app.tar.gz',
name: 'Tolaria.app.tar.gz',
browser_download_url: 'https://example.com/Tolaria-setup.exe',
name: 'Tolaria-setup.exe',
},
],
body: '**Alpha** notes with [details](https://example.com/details).',
@@ -42,7 +42,7 @@ describe('buildReleaseHistoryPage', () => {
expect(html).toContain('<h2>Highlights</h2>')
expect(html).toContain('<li>Faster startup</li>')
expect(html).toContain('<strong>Alpha</strong> notes')
expect(html).toContain('Tolaria.app.tar.gz')
expect(html).toContain('Tolaria-setup.exe')
expect(html).toContain('View on GitHub')
})

View File

@@ -430,7 +430,14 @@ function parsePublishedTimestamp(value: unknown): number {
}
function isDownloadableAsset(name: string): boolean {
return name.endsWith('.dmg') || name.endsWith('.app.tar.gz') || name.endsWith('.zip')
return (
name.endsWith('.dmg')
|| name.endsWith('.app.tar.gz')
|| name.endsWith('-setup.exe')
|| name.endsWith('.msi')
|| name.endsWith('.AppImage')
|| name.endsWith('.deb')
)
}
function normalizeDownloads(assets: ReleaseAssetPayload[] | undefined): ReleaseDownload[] {