@tomehq/theme 0.2.7 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/chunk-3A2LPGUL.js +1991 -0
- package/dist/chunk-3I2QTWTW.js +1948 -0
- package/dist/chunk-45M5UIAB.js +2110 -0
- package/dist/chunk-462AGU3S.js +1959 -0
- package/dist/chunk-DPKZBFQP.js +1777 -0
- package/dist/chunk-GDQIBNX5.js +1962 -0
- package/dist/chunk-INUMUXN5.js +2095 -0
- package/dist/chunk-NOZBIES7.js +1948 -0
- package/dist/chunk-Q7PYTVW3.js +1771 -0
- package/dist/chunk-RDF25WB2.js +2085 -0
- package/dist/chunk-S4ZH5F56.js +1949 -0
- package/dist/chunk-SRD7NJHS.js +1949 -0
- package/dist/chunk-TQDWPSTO.js +2087 -0
- package/dist/chunk-TTRXRPP6.js +1941 -0
- package/dist/chunk-VUT2FMSI.js +1937 -0
- package/dist/chunk-VVCC5JHK.js +1949 -0
- package/dist/chunk-W732TVBK.js +1944 -0
- package/dist/entry.js +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/Shell.test.tsx +72 -0
- package/src/Shell.tsx +204 -16
- package/src/entry-helpers.test.ts +316 -0
- package/src/entry-helpers.ts +103 -0
- package/src/entry.tsx +152 -69
- package/src/routing.test.ts +124 -0
- package/src/routing.ts +45 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
computeEditUrl,
|
|
4
|
+
resolveInitialPageId,
|
|
5
|
+
loadPage,
|
|
6
|
+
detectCurrentVersion,
|
|
7
|
+
} from "./entry-helpers.js";
|
|
8
|
+
import type { MinimalRoute } from "./routing.js";
|
|
9
|
+
|
|
10
|
+
// ── Test fixtures ─────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const routes: MinimalRoute[] = [
|
|
13
|
+
{ id: "index", urlPath: "/" },
|
|
14
|
+
{ id: "quickstart", urlPath: "/quickstart" },
|
|
15
|
+
{ id: "guides/search", urlPath: "/guides/search" },
|
|
16
|
+
{ id: "v1/index", urlPath: "/v1/" },
|
|
17
|
+
{ id: "v1/guides/migration", urlPath: "/v1/guides/migration" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function stubPathnameToPageId(pathname: string, basePath: string, rts: MinimalRoute[]): string | null {
|
|
21
|
+
let relative = pathname;
|
|
22
|
+
if (basePath && relative.startsWith(basePath)) {
|
|
23
|
+
relative = relative.slice(basePath.length);
|
|
24
|
+
}
|
|
25
|
+
const id = relative.replace(/^\//, "").replace(/\/index\.html$/, "").replace(/\.html$/, "").replace(/\/$/, "") || "index";
|
|
26
|
+
return rts.find((r) => r.id === id) ? id : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── computeEditUrl ────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
describe("computeEditUrl", () => {
|
|
32
|
+
it("returns undefined when editLink is undefined", () => {
|
|
33
|
+
expect(computeEditUrl(undefined, "docs/quickstart.md")).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns undefined when filePath is undefined", () => {
|
|
37
|
+
expect(computeEditUrl({ repo: "org/repo" }, undefined)).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns undefined when both are undefined", () => {
|
|
41
|
+
expect(computeEditUrl(undefined, undefined)).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("builds URL with default branch (main)", () => {
|
|
45
|
+
const url = computeEditUrl({ repo: "acme/docs" }, "docs/quickstart.md");
|
|
46
|
+
expect(url).toBe("https://github.com/acme/docs/edit/main/docs/quickstart.md");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("uses custom branch", () => {
|
|
50
|
+
const url = computeEditUrl({ repo: "acme/docs", branch: "develop" }, "docs/quickstart.md");
|
|
51
|
+
expect(url).toBe("https://github.com/acme/docs/edit/develop/docs/quickstart.md");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("uses custom dir prefix", () => {
|
|
55
|
+
const url = computeEditUrl({ repo: "acme/docs", dir: "website" }, "quickstart.md");
|
|
56
|
+
expect(url).toBe("https://github.com/acme/docs/edit/main/website/quickstart.md");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("strips trailing slash from dir", () => {
|
|
60
|
+
const url = computeEditUrl({ repo: "acme/docs", dir: "website/" }, "quickstart.md");
|
|
61
|
+
expect(url).toBe("https://github.com/acme/docs/edit/main/website/quickstart.md");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("uses custom branch and dir together", () => {
|
|
65
|
+
const url = computeEditUrl(
|
|
66
|
+
{ repo: "org/repo", branch: "v2", dir: "packages/docs" },
|
|
67
|
+
"guides/search.md",
|
|
68
|
+
);
|
|
69
|
+
expect(url).toBe("https://github.com/org/repo/edit/v2/packages/docs/guides/search.md");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("handles empty dir string", () => {
|
|
73
|
+
const url = computeEditUrl({ repo: "org/repo", dir: "" }, "index.md");
|
|
74
|
+
expect(url).toBe("https://github.com/org/repo/edit/main/index.md");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("handles nested file paths", () => {
|
|
78
|
+
const url = computeEditUrl({ repo: "org/repo" }, "docs/v1/guides/migration.md");
|
|
79
|
+
expect(url).toBe("https://github.com/org/repo/edit/main/docs/v1/guides/migration.md");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ── resolveInitialPageId ──────────────────────────────────
|
|
84
|
+
|
|
85
|
+
describe("resolveInitialPageId", () => {
|
|
86
|
+
it("resolves from pathname when route exists", () => {
|
|
87
|
+
const id = resolveInitialPageId("/docs/quickstart", "", routes, "/docs", stubPathnameToPageId);
|
|
88
|
+
expect(id).toBe("quickstart");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("resolves index from basePath root", () => {
|
|
92
|
+
const id = resolveInitialPageId("/docs/", "", routes, "/docs", stubPathnameToPageId);
|
|
93
|
+
expect(id).toBe("index");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("resolves nested route from pathname", () => {
|
|
97
|
+
const id = resolveInitialPageId("/docs/guides/search", "", routes, "/docs", stubPathnameToPageId);
|
|
98
|
+
expect(id).toBe("guides/search");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("falls back to hash for legacy URLs when pathname doesn't match", () => {
|
|
102
|
+
// Pathname resolver returns null, so hash fallback triggers
|
|
103
|
+
const noRouteResolver = () => null;
|
|
104
|
+
const id = resolveInitialPageId("/docs/", "#quickstart", routes, "/docs", noRouteResolver);
|
|
105
|
+
expect(id).toBe("quickstart");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("falls back to hash with nested IDs when pathname fails", () => {
|
|
109
|
+
// Pathname resolves to no known route, so hash fallback kicks in
|
|
110
|
+
const noRouteResolver = () => null;
|
|
111
|
+
const id = resolveInitialPageId("/", "#guides/search", routes, "", noRouteResolver);
|
|
112
|
+
expect(id).toBe("guides/search");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("returns first route ID when neither pathname nor hash match", () => {
|
|
116
|
+
const id = resolveInitialPageId("/unknown", "", routes, "/docs", stubPathnameToPageId);
|
|
117
|
+
expect(id).toBe("index");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("returns 'index' when routes array is empty", () => {
|
|
121
|
+
const id = resolveInitialPageId("/unknown", "", [], "/docs", stubPathnameToPageId);
|
|
122
|
+
expect(id).toBe("index");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("prefers pathname over hash when both match", () => {
|
|
126
|
+
const id = resolveInitialPageId("/docs/quickstart", "#guides/search", routes, "/docs", stubPathnameToPageId);
|
|
127
|
+
expect(id).toBe("quickstart");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("handles hash without # prefix", () => {
|
|
131
|
+
// The real hash will always start with #, but the function handles both
|
|
132
|
+
const id = resolveInitialPageId("/unknown", "quickstart", routes, "/docs", stubPathnameToPageId);
|
|
133
|
+
expect(id).toBe("quickstart");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("ignores hash that matches no route", () => {
|
|
137
|
+
const id = resolveInitialPageId("/unknown", "#nonexistent", routes, "/docs", stubPathnameToPageId);
|
|
138
|
+
expect(id).toBe("index");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("works with empty basePath", () => {
|
|
142
|
+
const id = resolveInitialPageId("/quickstart", "", routes, "", stubPathnameToPageId);
|
|
143
|
+
expect(id).toBe("quickstart");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("resolves versioned routes", () => {
|
|
147
|
+
const id = resolveInitialPageId("/docs/v1/guides/migration", "", routes, "/docs", stubPathnameToPageId);
|
|
148
|
+
expect(id).toBe("v1/guides/migration");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ── loadPage ──────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
describe("loadPage", () => {
|
|
155
|
+
const routesWithMeta = [
|
|
156
|
+
{ id: "index", urlPath: "/", isMdx: false },
|
|
157
|
+
{ id: "quickstart", urlPath: "/quickstart", isMdx: false },
|
|
158
|
+
{ id: "about", urlPath: "/about", isMdx: true },
|
|
159
|
+
{ id: "changelog", urlPath: "/changelog", isMdx: false },
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
it("loads a regular markdown page", async () => {
|
|
163
|
+
const mockLoader = vi.fn().mockResolvedValue({
|
|
164
|
+
default: {
|
|
165
|
+
html: "<h1>Hello</h1>",
|
|
166
|
+
frontmatter: { title: "Hello" },
|
|
167
|
+
headings: [{ depth: 1, text: "Hello", id: "hello" }],
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
const page = await loadPage("quickstart", routesWithMeta, mockLoader);
|
|
171
|
+
expect(page).not.toBeNull();
|
|
172
|
+
expect(page!.isMdx).toBe(false);
|
|
173
|
+
if (!page!.isMdx) {
|
|
174
|
+
expect(page!.html).toBe("<h1>Hello</h1>");
|
|
175
|
+
expect(page!.frontmatter.title).toBe("Hello");
|
|
176
|
+
expect(page!.headings).toHaveLength(1);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("loads an MDX page with meta", async () => {
|
|
181
|
+
const MockComponent = () => null;
|
|
182
|
+
const mockLoader = vi.fn().mockResolvedValue({
|
|
183
|
+
default: MockComponent,
|
|
184
|
+
meta: {
|
|
185
|
+
frontmatter: { title: "About Us" },
|
|
186
|
+
headings: [{ depth: 2, text: "Team", id: "team" }],
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
const page = await loadPage("about", routesWithMeta, mockLoader);
|
|
190
|
+
expect(page).not.toBeNull();
|
|
191
|
+
expect(page!.isMdx).toBe(true);
|
|
192
|
+
if (page!.isMdx) {
|
|
193
|
+
expect(page!.component).toBe(MockComponent);
|
|
194
|
+
expect(page!.frontmatter.title).toBe("About Us");
|
|
195
|
+
expect(page!.headings[0].text).toBe("Team");
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("returns null when module has no default export", async () => {
|
|
200
|
+
const mockLoader = vi.fn().mockResolvedValue({ default: null });
|
|
201
|
+
const page = await loadPage("quickstart", routesWithMeta, mockLoader);
|
|
202
|
+
expect(page).toBeNull();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("returns null on loader error", async () => {
|
|
206
|
+
const mockLoader = vi.fn().mockRejectedValue(new Error("Module not found"));
|
|
207
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
208
|
+
const page = await loadPage("quickstart", routesWithMeta, mockLoader);
|
|
209
|
+
expect(page).toBeNull();
|
|
210
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
211
|
+
expect.stringContaining("Failed to load page"),
|
|
212
|
+
expect.any(Error),
|
|
213
|
+
);
|
|
214
|
+
consoleSpy.mockRestore();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("loads a changelog page with entries", async () => {
|
|
218
|
+
const changelogEntries = [
|
|
219
|
+
{ version: "1.0.0", date: "2025-01-01", sections: [{ type: "Added", items: ["Feature A"] }] },
|
|
220
|
+
];
|
|
221
|
+
const mockLoader = vi.fn().mockResolvedValue({
|
|
222
|
+
default: {
|
|
223
|
+
html: "<h1>Changelog</h1>",
|
|
224
|
+
frontmatter: { title: "Changelog", type: "changelog" },
|
|
225
|
+
headings: [],
|
|
226
|
+
},
|
|
227
|
+
isChangelog: true,
|
|
228
|
+
changelogEntries,
|
|
229
|
+
});
|
|
230
|
+
const page = await loadPage("changelog", routesWithMeta, mockLoader);
|
|
231
|
+
expect(page).not.toBeNull();
|
|
232
|
+
expect(page!.isMdx).toBe(false);
|
|
233
|
+
if (!page!.isMdx) {
|
|
234
|
+
expect(page!.changelogEntries).toEqual(changelogEntries);
|
|
235
|
+
expect(page!.frontmatter.title).toBe("Changelog");
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("loads markdown page without changelog entries when isChangelog is false", async () => {
|
|
240
|
+
const mockLoader = vi.fn().mockResolvedValue({
|
|
241
|
+
default: {
|
|
242
|
+
html: "<h1>Regular</h1>",
|
|
243
|
+
frontmatter: { title: "Regular" },
|
|
244
|
+
headings: [],
|
|
245
|
+
},
|
|
246
|
+
isChangelog: false,
|
|
247
|
+
});
|
|
248
|
+
const page = await loadPage("quickstart", routesWithMeta, mockLoader);
|
|
249
|
+
expect(page).not.toBeNull();
|
|
250
|
+
expect(page!.isMdx).toBe(false);
|
|
251
|
+
if (!page!.isMdx) {
|
|
252
|
+
expect(page!.changelogEntries).toBeUndefined();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("calls loadPageModule with the correct ID", async () => {
|
|
257
|
+
const mockLoader = vi.fn().mockResolvedValue({
|
|
258
|
+
default: { html: "", frontmatter: { title: "" }, headings: [] },
|
|
259
|
+
});
|
|
260
|
+
await loadPage("quickstart", routesWithMeta, mockLoader);
|
|
261
|
+
expect(mockLoader).toHaveBeenCalledWith("quickstart");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("handles page ID not in routes array", async () => {
|
|
265
|
+
const mockLoader = vi.fn().mockResolvedValue({
|
|
266
|
+
default: { html: "<p>Found</p>", frontmatter: { title: "Found" }, headings: [] },
|
|
267
|
+
});
|
|
268
|
+
const page = await loadPage("unknown-page", routesWithMeta, mockLoader);
|
|
269
|
+
expect(page).not.toBeNull();
|
|
270
|
+
// Should still return the page data since the module loaded
|
|
271
|
+
expect(page!.isMdx).toBe(false);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("does not treat non-MDX route as MDX even if mod.meta exists", async () => {
|
|
275
|
+
// A markdown route shouldn't be treated as MDX even if the module has meta
|
|
276
|
+
const mockLoader = vi.fn().mockResolvedValue({
|
|
277
|
+
default: { html: "<p>MD</p>", frontmatter: { title: "MD" }, headings: [] },
|
|
278
|
+
meta: { frontmatter: { title: "MDX" }, headings: [] },
|
|
279
|
+
});
|
|
280
|
+
const page = await loadPage("quickstart", routesWithMeta, mockLoader);
|
|
281
|
+
expect(page).not.toBeNull();
|
|
282
|
+
expect(page!.isMdx).toBe(false);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ── detectCurrentVersion ──────────────────────────────────
|
|
287
|
+
|
|
288
|
+
describe("detectCurrentVersion", () => {
|
|
289
|
+
it("returns route version when present", () => {
|
|
290
|
+
expect(detectCurrentVersion({ version: "v1" }, { current: "v2" })).toBe("v1");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("falls back to versions.current when route has no version", () => {
|
|
294
|
+
expect(detectCurrentVersion({}, { current: "v2" })).toBe("v2");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("falls back to versions.current when route is undefined", () => {
|
|
298
|
+
expect(detectCurrentVersion(undefined, { current: "v2" })).toBe("v2");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("returns undefined when neither has a version", () => {
|
|
302
|
+
expect(detectCurrentVersion({}, {})).toBeUndefined();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("returns undefined when both are undefined", () => {
|
|
306
|
+
expect(detectCurrentVersion(undefined, undefined)).toBeUndefined();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("prefers route version over versions.current", () => {
|
|
310
|
+
expect(detectCurrentVersion({ version: "v3" }, { current: "v1" })).toBe("v3");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("returns undefined when route has no version and versions has no current", () => {
|
|
314
|
+
expect(detectCurrentVersion({ version: undefined }, { current: undefined })).toBeUndefined();
|
|
315
|
+
});
|
|
316
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { MinimalRoute } from "./routing.js";
|
|
2
|
+
|
|
3
|
+
// ── PAGE TYPES ────────────────────────────────────────────
|
|
4
|
+
export interface HtmlPage {
|
|
5
|
+
isMdx: false;
|
|
6
|
+
html: string;
|
|
7
|
+
frontmatter: { title: string; description?: string; toc?: boolean; type?: string };
|
|
8
|
+
headings: Array<{ depth: number; text: string; id: string }>;
|
|
9
|
+
changelogEntries?: Array<{ version: string; date?: string; url?: string; sections: Array<{ type: string; items: string[] }> }>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MdxPage {
|
|
13
|
+
isMdx: true;
|
|
14
|
+
component: React.ComponentType<{ components?: Record<string, React.ComponentType> }>;
|
|
15
|
+
frontmatter: { title: string; description?: string; toc?: boolean; type?: string };
|
|
16
|
+
headings: Array<{ depth: number; text: string; id: string }>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type LoadedPage = HtmlPage | MdxPage;
|
|
20
|
+
|
|
21
|
+
// ── EDIT URL COMPUTATION ──────────────────────────────────
|
|
22
|
+
export interface EditLinkConfig {
|
|
23
|
+
repo: string;
|
|
24
|
+
branch?: string;
|
|
25
|
+
dir?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function computeEditUrl(
|
|
29
|
+
editLink: EditLinkConfig | undefined,
|
|
30
|
+
filePath: string | undefined,
|
|
31
|
+
): string | undefined {
|
|
32
|
+
if (!editLink || !filePath) return undefined;
|
|
33
|
+
const { repo, branch = "main", dir = "" } = editLink;
|
|
34
|
+
const dirPrefix = dir ? `${dir.replace(/\/$/, "")}/` : "";
|
|
35
|
+
return `https://github.com/${repo}/edit/${branch}/${dirPrefix}${filePath}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── INITIAL PAGE RESOLUTION ───────────────────────────────
|
|
39
|
+
export function resolveInitialPageId(
|
|
40
|
+
pathname: string,
|
|
41
|
+
hash: string,
|
|
42
|
+
routes: MinimalRoute[],
|
|
43
|
+
basePath: string,
|
|
44
|
+
pathnameToPageIdFn: (pathname: string, basePath: string, routes: MinimalRoute[]) => string | null,
|
|
45
|
+
): string {
|
|
46
|
+
// History API: resolve page from pathname
|
|
47
|
+
const resolved = pathnameToPageIdFn(pathname, basePath, routes);
|
|
48
|
+
if (resolved) return resolved;
|
|
49
|
+
|
|
50
|
+
// Legacy hash fallback: support old /#page-id URLs
|
|
51
|
+
const hashId = hash.startsWith("#") ? hash.slice(1) : hash;
|
|
52
|
+
if (hashId && routes.some((r) => r.id === hashId)) return hashId;
|
|
53
|
+
|
|
54
|
+
return routes[0]?.id || "index";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── PAGE LOADER ──────────────────────────────────────────
|
|
58
|
+
export interface RouteWithMeta extends MinimalRoute {
|
|
59
|
+
isMdx?: boolean;
|
|
60
|
+
frontmatter?: { title: string; description?: string; toc?: boolean; type?: string };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function loadPage(
|
|
64
|
+
id: string,
|
|
65
|
+
routes: RouteWithMeta[],
|
|
66
|
+
loadPageModule: (id: string) => Promise<any>,
|
|
67
|
+
): Promise<LoadedPage | null> {
|
|
68
|
+
try {
|
|
69
|
+
const route = routes.find((r) => r.id === id);
|
|
70
|
+
const mod = await loadPageModule(id);
|
|
71
|
+
|
|
72
|
+
if (route?.isMdx && mod.meta) {
|
|
73
|
+
// MDX page — mod.default is the React component
|
|
74
|
+
return {
|
|
75
|
+
isMdx: true,
|
|
76
|
+
component: mod.default,
|
|
77
|
+
frontmatter: mod.meta.frontmatter,
|
|
78
|
+
headings: mod.meta.headings,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Regular .md page — mod.default is { html, frontmatter, headings }
|
|
83
|
+
if (!mod.default) return null;
|
|
84
|
+
|
|
85
|
+
// Changelog page type
|
|
86
|
+
if (mod.isChangelog && mod.changelogEntries) {
|
|
87
|
+
return { isMdx: false, ...mod.default, changelogEntries: mod.changelogEntries };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { isMdx: false, ...mod.default };
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error(`Failed to load page: ${id}`, err);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── VERSION DETECTION ─────────────────────────────────────
|
|
98
|
+
export function detectCurrentVersion(
|
|
99
|
+
currentRoute: { version?: string } | undefined,
|
|
100
|
+
versions: { current?: string } | undefined,
|
|
101
|
+
): string | undefined {
|
|
102
|
+
return currentRoute?.version || (versions?.current ?? undefined);
|
|
103
|
+
}
|