Skip to content

Commit 6c6f801

Browse files
committed
fix: resolve default version via npm alpha dist-tag
The homepage default (no ?tag= query) scanned the GitHub /releases list and returned the first release with matching installer assets, which could pick up a stable release instead of the intended alpha channel. Resolve the default version via npm's `alpha` dist-tag first, then fetch that specific tag from GitHub. On failure (incl. 404 from npm/GitHub being momentarily out of sync) return null so getRelease doesn't poison `release:latest` with a 5-minute negative cache.
1 parent 4acf2b0 commit 6c6f801

2 files changed

Lines changed: 82 additions & 23 deletions

File tree

routes/index.ts

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const ASSET_NAMES: Record<Arch, string> = {
1212
};
1313
const LATEST_CACHE_TTL = 300; // 5 minutes
1414
const TAGGED_CACHE_TTL = 86400; // 24 hours
15+
const DEFAULT_DIST_TAG = "alpha";
1516

1617
type Arch = "x64" | "arm64";
1718

@@ -76,25 +77,19 @@ export async function fetchRelease(
7677
return (await res.json()) as GitHubRelease;
7778
}
7879

79-
// Includes pre-releases, unlike /releases/latest
80+
const version = await fetchNpmDistTagVersion(DEFAULT_DIST_TAG);
81+
if (!version) return null;
8082
const res = await fetchGitHub(
81-
`/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases?per_page=10`,
83+
`/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/tags/v${version}`,
8284
githubToken,
8385
);
8486
if (!res.ok) {
85-
console.error(`GitHub API error: ${res.status} ${res.statusText} for releases list`);
87+
console.error(`GitHub API error: ${res.status} ${res.statusText} for default tag v${version}`);
88+
// Treat all failures (incl. 404) as transient so getRelease doesn't cache a negative
89+
// under `release:latest` when npm/GitHub are momentarily out of sync.
8690
return null;
8791
}
88-
const releases = (await res.json()) as GitHubRelease[];
89-
console.log(
90-
"GitHub releases:",
91-
JSON.stringify(releases.map((r) => ({ tag: r.tag_name, assets: r.assets.map((a) => a.name) }))),
92-
);
93-
return (
94-
releases.find((r) =>
95-
r.assets.some((a) => a.name === ASSET_NAMES.x64 || a.name === ASSET_NAMES.arm64),
96-
) ?? null
97-
);
92+
return (await res.json()) as GitHubRelease;
9893
}
9994

10095
export function buildReleaseFromTag(tag: string): CachedRelease {
@@ -108,12 +103,7 @@ export function buildReleaseFromTag(tag: string): CachedRelease {
108103
};
109104
}
110105

111-
interface NpmDistTags {
112-
alpha?: string;
113-
latest?: string;
114-
}
115-
116-
async function fetchLatestVersionFromNpm(): Promise<string | null> {
106+
async function fetchNpmDistTagVersion(distTag: string): Promise<string | null> {
117107
try {
118108
const res = await fetch("https://registry.npmjs.com/vite-plus", {
119109
headers: { Accept: "application/vnd.npm.install-v1+json" },
@@ -122,9 +112,8 @@ async function fetchLatestVersionFromNpm(): Promise<string | null> {
122112
console.error(`npm registry error: ${res.status} ${res.statusText}`);
123113
return null;
124114
}
125-
const data = (await res.json()) as { "dist-tags"?: NpmDistTags };
126-
// Windows installers are currently published under the alpha dist-tag
127-
return data["dist-tags"]?.alpha ?? data["dist-tags"]?.latest ?? null;
115+
const data = (await res.json()) as { "dist-tags"?: Record<string, string> };
116+
return data["dist-tags"]?.[distTag] ?? null;
128117
} catch (err) {
129118
console.error("Failed to fetch version from npm registry:", err);
130119
return null;
@@ -176,7 +165,7 @@ async function getRelease(
176165

177166
// Fallback 2: construct download URLs from tag or npm registry version
178167
if (tag) return buildReleaseFromTag(tag);
179-
const version = await fetchLatestVersionFromNpm();
168+
const version = await fetchNpmDistTagVersion(DEFAULT_DIST_TAG);
180169
if (version) return buildReleaseFromTag(`v${version}`);
181170

182171
return null;

tests/index.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,74 @@ describe("fetchRelease", () => {
190190
const result = await fetchRelease("v1.0.0", undefined);
191191
expect(result).toEqual(release);
192192
});
193+
194+
it("default path: resolves via npm alpha dist-tag then fetches that GitHub tag", async () => {
195+
const release = {
196+
tag_name: "v0.1.17-alpha.0",
197+
assets: [
198+
{
199+
name: "vp-setup-x86_64-pc-windows-msvc.exe",
200+
browser_download_url:
201+
"https://github.com/voidzero-dev/vite-plus/releases/download/v0.1.17-alpha.0/vp-setup-x86_64-pc-windows-msvc.exe",
202+
},
203+
],
204+
};
205+
const fetchMock = vi
206+
.fn()
207+
.mockResolvedValueOnce(
208+
new Response(JSON.stringify({ "dist-tags": { alpha: "0.1.17-alpha.0" } }), { status: 200 }),
209+
)
210+
.mockResolvedValueOnce(new Response(JSON.stringify(release), { status: 200 }));
211+
vi.stubGlobal("fetch", fetchMock);
212+
213+
const result = await fetchRelease(undefined, undefined);
214+
215+
expect(result).toEqual(release);
216+
expect(fetchMock).toHaveBeenCalledTimes(2);
217+
const secondCallUrl = fetchMock.mock.calls[1][0];
218+
expect(secondCallUrl).toContain("/releases/tags/v0.1.17-alpha.0");
219+
});
220+
221+
it("default path: returns null and skips GitHub when npm registry fails", async () => {
222+
const fetchMock = vi
223+
.fn()
224+
.mockResolvedValueOnce(
225+
new Response("Service Unavailable", { status: 503, statusText: "Service Unavailable" }),
226+
);
227+
vi.stubGlobal("fetch", fetchMock);
228+
229+
const result = await fetchRelease(undefined, undefined);
230+
231+
expect(result).toBeNull();
232+
expect(fetchMock).toHaveBeenCalledTimes(1);
233+
});
234+
235+
it("default path: returns null and skips GitHub when npm has no alpha dist-tag", async () => {
236+
const fetchMock = vi
237+
.fn()
238+
.mockResolvedValueOnce(
239+
new Response(JSON.stringify({ "dist-tags": { latest: "0.1.0" } }), { status: 200 }),
240+
);
241+
vi.stubGlobal("fetch", fetchMock);
242+
243+
const result = await fetchRelease(undefined, undefined);
244+
245+
expect(result).toBeNull();
246+
expect(fetchMock).toHaveBeenCalledTimes(1);
247+
});
248+
249+
it('default path: returns null (not "not-found") when GitHub 404s the resolved tag', async () => {
250+
const fetchMock = vi
251+
.fn()
252+
.mockResolvedValueOnce(
253+
new Response(JSON.stringify({ "dist-tags": { alpha: "0.1.17-alpha.0" } }), { status: 200 }),
254+
)
255+
.mockResolvedValueOnce(new Response("Not Found", { status: 404, statusText: "Not Found" }));
256+
vi.stubGlobal("fetch", fetchMock);
257+
258+
const result = await fetchRelease(undefined, undefined);
259+
260+
expect(result).toBeNull();
261+
expect(fetchMock).toHaveBeenCalledTimes(2);
262+
});
193263
});

0 commit comments

Comments
 (0)