Skip to content

Commit 6cabc7a

Browse files
author
Taras Mankovski
authored
🐛 Fix API reference showing alpha instead of stable release (#1130)
1 parent 65e7bc1 commit 6cabc7a

8 files changed

Lines changed: 148 additions & 34 deletions

File tree

www/components/api/api-page.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,16 @@ export function* ApiPage({
6363
),
6464
linkResolver: createSibling,
6565
versionToggle: yield* (function* () {
66-
const { series: SERIES } = yield* useConfig();
66+
const { series } = yield* useConfig();
67+
// Only show stable series in version toggle (no prereleases)
68+
const stableSeries = series.filter((s) => !s.includePrerelease);
6769
const currentSeries = `v${major(pkg.version)}`;
6870

6971
const links = yield* all(
70-
SERIES.map(function* (s) {
72+
stableSeries.map(function* (s) {
7173
const seriesPkg = yield* usePackage({
7274
type: "worktree",
73-
series: s,
75+
series: s.name,
7476
});
7577
const seriesDocs = yield* seriesPkg.docs();
7678
const hasSymbol = seriesDocs["."].some((node) =>
@@ -81,9 +83,9 @@ export function* ApiPage({
8183

8284
return (
8385
<a
84-
href={yield* createRootUrl(`api/${s}`)(current)}
86+
href={yield* createRootUrl(`api/${s.name}`)(current)}
8587
class={`text-base ${
86-
s === currentSeries
88+
s.name === currentSeries
8789
? "font-bold text-sky-500"
8890
: "text-gray-600 dark:text-gray-400 hover:text-sky-500"
8991
}`}

www/context/config.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,56 @@
11
import { createContext, type Operation } from "effection";
22

3-
export interface SiteConfig<T extends string = string> {
4-
series: T[];
5-
current: NoInfer<T>;
3+
export interface SeriesConfig {
4+
/** Series identifier used in URLs, e.g., "v4", "v4-next" */
5+
name: string;
6+
/** Major version number for semver matching */
7+
major: number;
8+
/** Include prerelease versions when finding latest tag */
9+
includePrerelease?: boolean;
10+
/** Parent series name for grouping (e.g., "v4-next" -> "v4") */
11+
parent?: string;
12+
}
13+
14+
export interface SiteConfig {
15+
series: SeriesConfig[];
16+
current: string;
617
}
718

819
const ConfigContext = createContext<SiteConfig>("site-config", {
9-
series: ["v3", "v4"],
20+
series: [
21+
{ name: "v3", major: 3 },
22+
{ name: "v4", major: 4 },
23+
{ name: "v4-next", major: 4, includePrerelease: true, parent: "v4" },
24+
],
1025
current: "v4",
1126
});
1227

13-
export function* initConfig<T extends string>(
14-
config: SiteConfig<T>,
28+
export function* initConfig(
29+
config: SiteConfig,
1530
): Operation<void> {
1631
yield* ConfigContext.set(config);
1732
}
1833

1934
export function* useConfig(): Operation<SiteConfig> {
2035
return yield* ConfigContext.expect();
2136
}
37+
38+
/**
39+
* Find a series configuration by name
40+
*/
41+
export function findSeries(
42+
config: SiteConfig,
43+
name: string,
44+
): SeriesConfig | undefined {
45+
return config.series.find((s) => s.name === name);
46+
}
47+
48+
/**
49+
* Get series that are children of a parent series
50+
*/
51+
export function getChildSeries(
52+
config: SiteConfig,
53+
parentName: string,
54+
): SeriesConfig[] {
55+
return config.series.filter((s) => s.parent === parentName);
56+
}

www/lib/package.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { relative } from "node:path";
33
import { fileURLToPath } from "node:url";
44

55
import z from "zod";
6-
import { SiteConfig } from "../context/config.ts";
6+
import { findSeries, type SeriesConfig, useConfig } from "../context/config.ts";
77
import { useJSRClient } from "../context/jsr.ts";
88
import { LocalDocsPages, useDocPages } from "../hooks/use-deno-doc.tsx";
99
import { useDescription, useTitle } from "../hooks/use-description-parse.tsx";
@@ -19,7 +19,10 @@ import { useWorktree } from "./worktrees.ts";
1919

2020
export type WorkTreePackageOptions = {
2121
type: "worktree";
22-
series: SiteConfig["series"][number];
22+
/** Series name, e.g., "v4", "v4-next" */
23+
series: string;
24+
/** Override series config (optional, will look up from SiteConfig if not provided) */
25+
seriesConfig?: SeriesConfig;
2326
};
2427

2528
export type ClonePackageOptions = {
@@ -72,9 +75,26 @@ export function* usePackage(options: PackageOptions): Operation<Package> {
7275
if (options.type === "worktree") {
7376
let repo = createRepo({ name: "effection", owner: "thefrontside" });
7477

75-
let tags = yield* repo.tags(new RegExp(`effection-${options.series}.*`));
78+
// Get series config from options or look it up
79+
let seriesConfig = options.seriesConfig;
80+
if (!seriesConfig) {
81+
let config = yield* useConfig();
82+
seriesConfig = findSeries(config, options.series);
83+
if (!seriesConfig) {
84+
throw new Error(`unknown series: ${options.series}`);
85+
}
86+
}
87+
88+
// Fetch all tags for this major version
89+
let tags = yield* repo.tags(
90+
new RegExp(`effection-v${seriesConfig.major}.*`),
91+
);
7692

77-
let ref = findLatestSemverTag(tags);
93+
// Use semver range to find latest (excludes prereleases unless configured)
94+
let range = `${seriesConfig.major}.x`;
95+
let ref = findLatestSemverTag(tags, range, {
96+
includePrerelease: seriesConfig.includePrerelease,
97+
});
7898

7999
if (!ref) {
80100
throw new Error(`unable to find package ref for ${options.series}`);

www/lib/semver.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
export { compare, major, minor, rsort } from "semver";
1+
export { compare, gt, major, maxSatisfying, minor, rsort } from "semver";
22

3-
import { rsort } from "semver";
3+
import { maxSatisfying, rsort } from "semver";
44

55
export function extractVersion(input: string) {
66
let parts = input.match(
@@ -14,14 +14,33 @@ export function extractVersion(input: string) {
1414
}
1515
}
1616

17+
export interface FindLatestSemverTagOptions {
18+
includePrerelease?: boolean;
19+
}
20+
1721
/**
1822
* Find the latest Semver tag from an array of tags
1923
* @param tags - Array of tag objects with name property
24+
* @param range - Optional semver range to filter tags (e.g., "4.x", ">=3.0.0")
25+
* @param options - Optional settings like includePrerelease
2026
* @returns Latest semver tag if found, undefined otherwise
2127
*/
2228
export function findLatestSemverTag<T extends { name: string }>(
2329
tags: T[],
30+
range?: string,
31+
options?: FindLatestSemverTagOptions,
2432
): T | undefined {
25-
let [latest] = rsort(tags.map((tag) => tag.name).map(extractVersion));
33+
let versions = tags.map((tag) => tag.name).map(extractVersion);
34+
35+
let latest: string | undefined;
36+
if (range) {
37+
// Use semver range matching (excludes prereleases by default)
38+
latest = maxSatisfying(versions, range, options) ?? undefined;
39+
} else {
40+
// Fallback to current behavior: sort all and take first
41+
[latest] = rsort(versions);
42+
}
43+
44+
if (!latest) return undefined;
2645
return tags.find((tag) => tag.name.endsWith(latest));
2746
}

www/main.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,16 @@ if (import.meta.main) {
3737
await main(function* () {
3838
let { current, series } = yield* useConfig();
3939

40+
// Get stable series (no prereleases) for guides
41+
let stableSeries = series.filter((s) => !s.includePrerelease);
42+
4043
yield* initClones("build/clones");
4144
yield* initWorktrees("build/worktrees");
4245
yield* initGuides({
4346
current,
44-
worktrees: series.filter((s) => s !== current),
47+
worktrees: stableSeries
48+
.filter((s) => s.name !== current)
49+
.map((s) => s.name),
4550
});
4651

4752
yield* initBlog();
@@ -58,17 +63,22 @@ if (import.meta.main) {
5863
route("/search", searchRoute()),
5964
route("/docs", redirectIndexRoute(firstPage(current))),
6065
route("/docs/:id", redirectDocsRoute(current)),
61-
...series.map((s) =>
62-
route(`/guides/${s}`, redirectIndexRoute(firstPage(s)))
66+
// Guides only for stable series (no prereleases)
67+
...stableSeries.map((s) =>
68+
route(`/guides/${s.name}`, redirectIndexRoute(firstPage(s.name)))
6369
),
6470
route("/guides/:series/:id", guidesRoute({ search: true })),
6571
route("/contrib", xIndexRedirect()),
6672
route("/contrib/:workspacePath", xPackageRedirect()),
6773
route("/x", xIndexRoute({ search: true })),
6874
route("/x/:workspacePath", xPackageRoute({ search: true })),
6975
route("/api", apiIndexRoute({ search: true })),
76+
// API docs for all series including prereleases
7077
...series.map((s) =>
71-
route(`/api/${s}/:symbol`, apiReferenceRoute(s, { search: true }))
78+
route(
79+
`/api/${s.name}/:symbol`,
80+
apiReferenceRoute(s.name, { search: true }),
81+
)
7282
),
7383
route("/blog", blogIndexRoute({ search: true })),
7484
route("/blog/feed.xml", blogFeedRoute()),

www/routes/api-index-route.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Icon } from "../components/type/icon.tsx";
44
import { DocPage } from "../hooks/use-deno-doc.tsx";
55
import { ResolveLinkFunction } from "../hooks/use-markdown.tsx";
66
import { usePackage } from "../lib/package.ts";
7+
import { gt } from "../lib/semver.ts";
78
import { SitemapRoute } from "../plugins/sitemap.ts";
89
import { useAppHtml } from "./app.html.tsx";
910
import { createChildURL } from "../lib/links-resolvers.ts";
@@ -26,6 +27,18 @@ export function apiIndexRoute(
2627
series: "v4",
2728
});
2829

30+
let v4Next = yield* usePackage({
31+
type: "worktree",
32+
series: "v4-next",
33+
});
34+
35+
// Only show prerelease link if it's newer than stable
36+
let showV4Prerelease = gt(v4Next.version, v4.version);
37+
38+
// Get first symbol for prerelease link
39+
let v4NextDocs = showV4Prerelease ? yield* v4Next.docs() : null;
40+
let v4NextFirstSymbol = v4NextDocs?.["."]?.[0]?.name ?? "run";
41+
2942
let docs = {
3043
v3: yield* v3.docs(),
3144
v4: yield* v4.docs(),
@@ -43,6 +56,18 @@ export function apiIndexRoute(
4356
<section>
4457
<h3 id={v4.version} class="group scroll-mt-[200px]">
4558
{v4.version}
59+
{showV4Prerelease && (
60+
<span class="text-sm font-normal text-gray-500 dark:text-gray-400 ml-2">
61+
·{" "}
62+
<a
63+
href={`/api/v4-next/${v4NextFirstSymbol}`}
64+
class="text-blue-600 dark:text-blue-400 hover:underline"
65+
>
66+
{v4Next.version}
67+
</a>{" "}
68+
also available
69+
</span>
70+
)}
4671
<a
4772
href={`#${v4.version}`}
4873
class="opacity-0 group-hover:opacity-100 after:content-['#'] after:ml-1.5 no-underline"

www/routes/api-reference-route.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ import { useAppHtml } from "./app.html.tsx";
66
import { ApiPage } from "../components/api/api-page.tsx";
77
import { usePackage } from "../lib/package.ts";
88
import { createSibling } from "../lib/links-resolvers.ts";
9-
import { SiteConfig } from "../context/config.ts";
109

11-
export function apiReferenceRoute(series: SiteConfig["series"][number], {
10+
export function apiReferenceRoute(series: string, {
1211
search,
1312
}: {
1413
search: boolean;

www/routes/guides-route.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,23 +30,27 @@ export function guidesRoute({
3030
}): SitemapRoute<JSXElement> {
3131
return {
3232
*routemap(pathname) {
33-
let { series: SERIES } = yield* useConfig();
34-
let paths = SERIES.map(function* (series) {
33+
let { series } = yield* useConfig();
34+
// Only stable series have guides (no prereleases)
35+
let stableSeries = series.filter((s) => !s.includePrerelease);
36+
let paths = stableSeries.map(function* (s) {
3537
let paths: RoutePath[] = [];
3638

37-
let pages = yield* useGuides(series);
39+
let pages = yield* useGuides(s.name);
3840

3941
for (let page of yield* pages.all()) {
4042
paths.push({
41-
pathname: pathname({ id: page.id, series }),
43+
pathname: pathname({ id: page.id, series: s.name }),
4244
});
4345
}
4446
return paths;
4547
});
4648
return (yield* all(paths)).flat();
4749
},
4850
*handler(req) {
49-
let { series: SERIES, current } = yield* useConfig();
51+
let { series: allSeries, current } = yield* useConfig();
52+
// Only stable series have guides (no prereleases)
53+
let stableSeries = allSeries.filter((s) => !s.includePrerelease);
5054

5155
let { id, series = current } = yield* useParams<{
5256
id: string | undefined;
@@ -114,21 +118,21 @@ export function guidesRoute({
114118
}
115119

116120
let versionToggle = yield* all(
117-
SERIES.map(function* (s) {
118-
let target = yield* useGuides(s);
121+
stableSeries.map(function* (s) {
122+
let target = yield* useGuides(s.name);
119123
let targetPage = yield* target.get(page.id);
120-
let base = current === s ? "docs" : `guides/${s}`;
124+
let base = current === s.name ? "docs" : `guides/${s.name}`;
121125
let url = yield* createRootUrl(base)(targetPage ? page.id : "/");
122126
return (
123127
<a
124128
href={url}
125129
class={`text-base ${
126-
s === series
130+
s.name === series
127131
? "font-bold text-sky-500"
128132
: "text-gray-600 dark:text-gray-400 hover:text-sky-500"
129133
}`}
130134
>
131-
{s}
135+
{s.name}
132136
</a>
133137
);
134138
}),

0 commit comments

Comments
 (0)