Files

222 lines
5.7 KiB
TypeScript

import {
APP_VERSION,
compareVersions,
getDefaultRepositoryBranch,
getDefaultRepositorySlug,
getReleaseByVersion,
LOCAL_RELEASE_MANIFEST,
} from '@/lib/app-release';
import type {
AppReleaseEntry,
AppReleaseManifest,
AppUpdateResponse,
} from '@/lib/types/app-update';
export const runtime = 'edge';
export const dynamic = 'force-dynamic';
const MANIFEST_PATH = 'app-release.json';
const CHANGELOG_PATH = 'CHANGELOG.md';
function parseRepositoryTarget(repository: string) {
const [owner, name] = repository.split('/', 2);
if (!owner || !name) {
return null;
}
return { owner, name };
}
function buildSourceInfo(repository: string, branch: string) {
const target = parseRepositoryTarget(repository);
if (!target) {
const emptyUrl = 'https://github.com';
return {
repository,
branch,
manifestUrl: emptyUrl,
changelogUrl: emptyUrl,
repositoryUrl: emptyUrl,
};
}
const { owner, name } = target;
return {
repository,
branch,
manifestUrl: `https://raw.githubusercontent.com/${owner}/${name}/${branch}/${MANIFEST_PATH}`,
changelogUrl: `https://github.com/${owner}/${name}/blob/${branch}/${CHANGELOG_PATH}`,
repositoryUrl: `https://github.com/${owner}/${name}`,
};
}
function isValidReleaseEntry(value: unknown): value is AppReleaseEntry {
if (!value || typeof value !== 'object') {
return false;
}
const entry = value as Partial<AppReleaseEntry>;
return (
typeof entry.version === 'string' &&
typeof entry.publishedAt === 'string' &&
typeof entry.title === 'string' &&
Array.isArray(entry.notes) &&
entry.notes.every((note) => typeof note === 'string')
);
}
function normalizeManifest(
value: unknown,
repository: string,
branch: string,
): AppReleaseManifest | null {
if (!value || typeof value !== 'object') {
return null;
}
const manifest = value as Partial<AppReleaseManifest>;
const target = parseRepositoryTarget(repository);
if (!target || typeof manifest.currentVersion !== 'string' || !Array.isArray(manifest.releases)) {
return null;
}
const releases = manifest.releases.filter(isValidReleaseEntry);
if (releases.length === 0) {
return null;
}
return {
repository: {
owner: manifest.repository?.owner || target.owner,
name: manifest.repository?.name || target.name,
branch: manifest.repository?.branch || branch,
},
currentVersion: manifest.currentVersion,
releases,
};
}
async function fetchJson<T>(url: string): Promise<T | null> {
const response = await fetch(url, {
cache: 'no-store',
headers: {
Accept: 'application/json',
},
});
if (!response.ok) {
return null;
}
return response.json() as Promise<T>;
}
function buildResponse(
repository: string,
branch: string,
overrides: Partial<AppUpdateResponse> = {},
): AppUpdateResponse {
const source = buildSourceInfo(repository, branch);
const currentRelease = getReleaseByVersion(APP_VERSION);
return {
currentVersion: APP_VERSION,
currentRelease,
latestVersion: APP_VERSION,
latestRelease: currentRelease,
status: 'up-to-date',
updateAvailable: false,
checkedAt: new Date().toISOString(),
checkedRemotely: false,
usedRemoteManifest: false,
source,
...overrides,
};
}
export async function GET() {
const repository =
process.env.UPDATE_REPOSITORY?.trim() ||
process.env.NEXT_PUBLIC_UPDATE_REPOSITORY?.trim() ||
getDefaultRepositorySlug();
const branch =
process.env.UPDATE_BRANCH?.trim() ||
process.env.NEXT_PUBLIC_UPDATE_BRANCH?.trim() ||
getDefaultRepositoryBranch();
const source = buildSourceInfo(repository, branch);
try {
const remoteManifestJson = await fetchJson<unknown>(source.manifestUrl);
const remoteManifest = normalizeManifest(remoteManifestJson, repository, branch);
if (remoteManifest) {
const latestVersion = remoteManifest.currentVersion;
const latestRelease =
getReleaseByVersion(latestVersion, remoteManifest) ?? remoteManifest.releases[0] ?? null;
const comparison = compareVersions(latestVersion, APP_VERSION);
return Response.json(
buildResponse(repository, branch, {
latestVersion,
latestRelease,
status:
comparison > 0
? 'update-available'
: comparison < 0
? 'ahead-of-remote'
: 'up-to-date',
updateAvailable: comparison > 0,
checkedRemotely: true,
usedRemoteManifest: true,
}),
);
}
const remotePackage = await fetchJson<{ version?: string }>(`${source.manifestUrl.replace(MANIFEST_PATH, 'package.json')}`);
const latestVersion = remotePackage?.version?.trim();
if (latestVersion) {
const comparison = compareVersions(latestVersion, APP_VERSION);
return Response.json(
buildResponse(repository, branch, {
latestVersion,
latestRelease: getReleaseByVersion(latestVersion, LOCAL_RELEASE_MANIFEST),
status:
comparison > 0
? 'update-available'
: comparison < 0
? 'ahead-of-remote'
: 'up-to-date',
updateAvailable: comparison > 0,
checkedRemotely: true,
usedRemoteManifest: false,
}),
);
}
return Response.json(
buildResponse(repository, branch, {
status: 'check-failed',
error: '无法获取远程版本信息。',
}),
{ status: 200 },
);
} catch (error) {
return Response.json(
buildResponse(repository, branch, {
status: 'check-failed',
error: error instanceof Error ? error.message : '未知错误',
}),
{ status: 200 },
);
}
}