@aravindc26/velu 0.10.0 → 0.11.1
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/package.json +15 -6
- package/schema/velu.schema.json +1864 -30
- package/src/build.ts +1161 -180
- package/src/cli.ts +121 -16
- package/src/engine/_server.mjs +1708 -192
- package/src/engine/app/(docs)/[...slug]/layout.tsx +377 -0
- package/src/engine/app/(docs)/[...slug]/page.tsx +917 -0
- package/src/engine/app/(docs)/layout.tsx +1 -13
- package/src/engine/app/api/proxy/route.ts +23 -0
- package/src/engine/app/copy-page.css +59 -1
- package/src/engine/app/global.css +3487 -6
- package/src/engine/app/layout.tsx +59 -8
- package/src/engine/app/llms-file/route.ts +87 -0
- package/src/engine/app/llms-full-file/route.ts +62 -0
- package/src/engine/app/md-file/[...slug]/route.ts +409 -0
- package/src/engine/app/page.tsx +45 -0
- package/src/engine/app/robots.txt/route.ts +61 -0
- package/src/engine/app/rss-file/[...slug]/route.ts +176 -0
- package/src/engine/app/search.css +20 -0
- package/src/engine/app/sitemap.xml/route.ts +80 -0
- package/src/engine/components/assistant.tsx +16 -5
- package/src/engine/components/changelog-filters.tsx +114 -0
- package/src/engine/components/code-group.tsx +383 -0
- package/src/engine/components/color.tsx +118 -0
- package/src/engine/components/expandable.tsx +77 -0
- package/src/engine/components/icon.tsx +136 -0
- package/src/engine/components/image-zoom-fallback.tsx +147 -0
- package/src/engine/components/image.tsx +111 -0
- package/src/engine/components/lang-switcher.tsx +95 -0
- package/src/engine/components/manual-api-playground.tsx +154 -0
- package/src/engine/components/mermaid.tsx +142 -0
- package/src/engine/components/openapi-toc-sync.tsx +59 -0
- package/src/engine/components/openapi.tsx +1679 -0
- package/src/engine/components/page-feedback.tsx +153 -0
- package/src/engine/components/product-switcher.tsx +102 -0
- package/src/engine/components/prompt.tsx +90 -0
- package/src/engine/components/providers.tsx +21 -0
- package/src/engine/components/search.tsx +70 -3
- package/src/engine/components/sidebar-links.tsx +49 -0
- package/src/engine/components/synced-tabs.tsx +57 -0
- package/src/engine/components/theme-toggle.tsx +39 -0
- package/src/engine/components/toc-examples.tsx +110 -0
- package/src/engine/components/version-switcher.tsx +89 -0
- package/src/engine/components/view.tsx +344 -0
- package/src/engine/generated/redirects.ts +3 -0
- package/src/engine/lib/changelog.ts +246 -0
- package/src/engine/lib/layout.shared.ts +57 -7
- package/src/engine/lib/llms.ts +444 -0
- package/src/engine/lib/navigation-normalize.mjs +525 -0
- package/src/engine/lib/navigation-normalize.ts +695 -0
- package/src/engine/lib/redirects.ts +194 -0
- package/src/engine/lib/source.ts +121 -4
- package/src/engine/lib/velu.ts +635 -5
- package/src/engine/mdx-components.tsx +648 -0
- package/src/engine/middleware.ts +66 -0
- package/src/engine/next.config.mjs +2 -2
- package/src/engine/public/icons/cursor-dark.svg +12 -0
- package/src/engine/public/icons/cursor-light.svg +12 -0
- package/src/engine/source.config.ts +98 -1
- package/src/engine/src/components/PageTitle.astro +16 -5
- package/src/engine/src/lib/velu.ts +97 -16
- package/src/navigation-normalize.ts +686 -0
- package/src/themes.ts +6 -6
- package/src/validate.ts +235 -24
- package/src/engine/app/(docs)/[[...slug]]/page.tsx +0 -69
package/src/engine/lib/velu.ts
CHANGED
|
@@ -1,38 +1,444 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
+
import { normalizeConfigNavigation } from './navigation-normalize';
|
|
4
|
+
const PRIMARY_CONFIG_NAME = 'docs.json';
|
|
5
|
+
const LEGACY_CONFIG_NAME = 'velu.json';
|
|
6
|
+
|
|
7
|
+
function resolveConfigPath(cwd: string): string {
|
|
8
|
+
const primary = resolve(cwd, PRIMARY_CONFIG_NAME);
|
|
9
|
+
if (existsSync(primary)) return primary;
|
|
10
|
+
return resolve(cwd, LEGACY_CONFIG_NAME);
|
|
11
|
+
}
|
|
3
12
|
|
|
4
13
|
interface VeluTab {
|
|
5
14
|
tab: string;
|
|
15
|
+
slug?: string;
|
|
6
16
|
href?: string;
|
|
17
|
+
pages?: Array<string | VeluSeparator | VeluLink>;
|
|
18
|
+
groups?: VeluGroup[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface VeluTabMenuItem {
|
|
22
|
+
item: string;
|
|
23
|
+
pages?: Array<string | VeluSeparator | VeluLink>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface VeluSeparator {
|
|
27
|
+
separator: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface VeluLink {
|
|
31
|
+
href: string;
|
|
32
|
+
label: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface VeluGroup {
|
|
36
|
+
group: string;
|
|
37
|
+
slug?: string;
|
|
38
|
+
pages: Array<string | VeluGroup | VeluSeparator | VeluLink>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface VeluAnchor {
|
|
42
|
+
anchor: string;
|
|
43
|
+
href?: string;
|
|
44
|
+
icon?: string;
|
|
45
|
+
iconType?: string;
|
|
46
|
+
color?: {
|
|
47
|
+
light: string;
|
|
48
|
+
dark: string;
|
|
49
|
+
};
|
|
50
|
+
tabs?: VeluTab[];
|
|
51
|
+
hidden?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface VeluGlobalTab {
|
|
55
|
+
tab: string;
|
|
56
|
+
href: string;
|
|
57
|
+
icon?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface VeluLanguageNav {
|
|
61
|
+
language: string;
|
|
62
|
+
tabs: VeluTab[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface VeluProductNav {
|
|
66
|
+
product: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
icon?: string;
|
|
69
|
+
iconType?: string;
|
|
70
|
+
hidden?: boolean;
|
|
71
|
+
href?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface VeluVersionNav {
|
|
75
|
+
version: string;
|
|
76
|
+
default?: boolean;
|
|
77
|
+
hidden?: boolean;
|
|
78
|
+
href?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type VeluApiAuthMethod = 'bearer' | 'basic' | 'key' | 'none';
|
|
82
|
+
|
|
83
|
+
interface VeluApiConfig {
|
|
84
|
+
baseUrl?: string;
|
|
85
|
+
playground?: {
|
|
86
|
+
mode?: string;
|
|
87
|
+
display?: string;
|
|
88
|
+
proxy?: boolean;
|
|
89
|
+
};
|
|
90
|
+
examples?: {
|
|
91
|
+
languages?: string[];
|
|
92
|
+
defaults?: 'required' | 'all';
|
|
93
|
+
prefill?: boolean;
|
|
94
|
+
autogenerate?: boolean;
|
|
95
|
+
};
|
|
96
|
+
mdx?: {
|
|
97
|
+
server?: string | string[];
|
|
98
|
+
auth?: {
|
|
99
|
+
method?: VeluApiAuthMethod | string;
|
|
100
|
+
name?: string;
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface VeluSeoConfig {
|
|
106
|
+
metatags?: Record<string, unknown>;
|
|
107
|
+
indexing?: 'navigable' | 'all' | string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface VeluThemeAsset {
|
|
111
|
+
light?: string;
|
|
112
|
+
dark?: string;
|
|
113
|
+
href?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface VeluProductOption {
|
|
117
|
+
product: string;
|
|
118
|
+
slug: string;
|
|
119
|
+
description?: string;
|
|
120
|
+
icon?: string;
|
|
121
|
+
iconType?: string;
|
|
122
|
+
tabSlugs: string[];
|
|
123
|
+
defaultPath: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface VeluVersionOption {
|
|
127
|
+
version: string;
|
|
128
|
+
slug: string;
|
|
129
|
+
isDefault: boolean;
|
|
130
|
+
tabSlugs: string[];
|
|
131
|
+
defaultPath: string;
|
|
7
132
|
}
|
|
8
133
|
|
|
9
134
|
interface VeluConfig {
|
|
135
|
+
name?: string;
|
|
136
|
+
title?: string;
|
|
137
|
+
favicon?: string | VeluThemeAsset;
|
|
138
|
+
logo?: string | VeluThemeAsset;
|
|
139
|
+
colors?: {
|
|
140
|
+
primary?: string;
|
|
141
|
+
light?: string;
|
|
142
|
+
dark?: string;
|
|
143
|
+
};
|
|
144
|
+
icons?: {
|
|
145
|
+
library?: string;
|
|
146
|
+
};
|
|
10
147
|
appearance?: 'system' | 'light' | 'dark';
|
|
148
|
+
languages?: string[];
|
|
149
|
+
openapi?: string | string[] | Record<string, unknown>;
|
|
150
|
+
asyncapi?: string | string[] | Record<string, unknown>;
|
|
151
|
+
api?: VeluApiConfig;
|
|
152
|
+
seo?: VeluSeoConfig;
|
|
11
153
|
navigation: {
|
|
12
|
-
tabs
|
|
154
|
+
tabs?: VeluTab[];
|
|
155
|
+
languages?: VeluLanguageNav[];
|
|
156
|
+
products?: VeluProductNav[];
|
|
157
|
+
versions?: VeluVersionNav[];
|
|
158
|
+
anchors?: VeluAnchor[];
|
|
159
|
+
global?: {
|
|
160
|
+
anchors?: VeluAnchor[];
|
|
161
|
+
tabs?: VeluGlobalTab[];
|
|
162
|
+
};
|
|
13
163
|
};
|
|
14
164
|
}
|
|
15
165
|
|
|
16
166
|
let cachedConfig: VeluConfig | null = null;
|
|
167
|
+
let cachedRawConfig: Record<string, unknown> | null = null;
|
|
17
168
|
|
|
18
169
|
function loadVeluConfig(): VeluConfig {
|
|
19
170
|
if (cachedConfig) return cachedConfig;
|
|
20
|
-
const configPath =
|
|
171
|
+
const configPath = resolveConfigPath(process.cwd());
|
|
21
172
|
const raw = readFileSync(configPath, 'utf-8');
|
|
22
|
-
cachedConfig = JSON.parse(raw) as VeluConfig;
|
|
173
|
+
cachedConfig = normalizeConfigNavigation(JSON.parse(raw)) as VeluConfig;
|
|
23
174
|
return cachedConfig;
|
|
24
175
|
}
|
|
25
176
|
|
|
177
|
+
function loadRawConfig(): Record<string, unknown> {
|
|
178
|
+
if (cachedRawConfig) return cachedRawConfig;
|
|
179
|
+
const configPath = resolveConfigPath(process.cwd());
|
|
180
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
181
|
+
const parsed = JSON.parse(raw);
|
|
182
|
+
cachedRawConfig = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
183
|
+
? parsed as Record<string, unknown>
|
|
184
|
+
: {};
|
|
185
|
+
return cachedRawConfig;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isGroup(item: unknown): item is VeluGroup {
|
|
189
|
+
return typeof item === 'object' && item !== null && 'group' in item;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
193
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isTabMenuItem(value: unknown): value is VeluTabMenuItem {
|
|
197
|
+
return isRecord(value) && typeof value.item === 'string';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function slugify(input: string, fallback: string): string {
|
|
201
|
+
const slug = input
|
|
202
|
+
.toLowerCase()
|
|
203
|
+
.trim()
|
|
204
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
205
|
+
.replace(/^-+|-+$/g, '');
|
|
206
|
+
return slug || fallback;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function pageBasename(page: string): string {
|
|
210
|
+
const parts = page.split('/').filter(Boolean);
|
|
211
|
+
return parts[parts.length - 1] ?? page;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function findFirstPageInGroup(group: VeluGroup): string | undefined {
|
|
215
|
+
for (const item of group.pages) {
|
|
216
|
+
if (typeof item === 'string') return item;
|
|
217
|
+
if (isGroup(item)) {
|
|
218
|
+
const nested = findFirstPageInGroup(item);
|
|
219
|
+
if (nested) return nested;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function findFirstPageInTab(tab: VeluTab): string | undefined {
|
|
226
|
+
if (tab.pages) {
|
|
227
|
+
for (const item of tab.pages) {
|
|
228
|
+
if (typeof item === 'string') return item;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (tab.groups) {
|
|
232
|
+
for (const group of tab.groups) {
|
|
233
|
+
const nested = findFirstPageInGroup(group);
|
|
234
|
+
if (nested) return nested;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function parseVersionParts(version: string): number[] {
|
|
241
|
+
const parts = version.match(/\d+/g);
|
|
242
|
+
return parts ? parts.map((n) => Number(n)) : [];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function compareVersionParts(a: number[], b: number[]): number {
|
|
246
|
+
const len = Math.max(a.length, b.length);
|
|
247
|
+
for (let i = 0; i < len; i += 1) {
|
|
248
|
+
const av = a[i] ?? 0;
|
|
249
|
+
const bv = b[i] ?? 0;
|
|
250
|
+
if (av !== bv) return av - bv;
|
|
251
|
+
}
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
26
255
|
export function getExternalTabs(): Array<{ label: string; href: string }> {
|
|
27
256
|
const config = loadVeluConfig();
|
|
28
257
|
const tabs = config.navigation?.tabs ?? [];
|
|
258
|
+
const globalTabs = config.navigation?.global?.tabs ?? [];
|
|
29
259
|
|
|
30
|
-
|
|
260
|
+
const tabLinks = tabs
|
|
31
261
|
.filter((tab): tab is VeluTab & { href: string } => typeof tab.href === 'string' && tab.href.length > 0)
|
|
32
262
|
.map((tab) => ({
|
|
33
263
|
label: tab.tab,
|
|
34
264
|
href: tab.href,
|
|
35
265
|
}));
|
|
266
|
+
|
|
267
|
+
const globalLinks = globalTabs
|
|
268
|
+
.filter((tab): tab is VeluGlobalTab => typeof tab.href === 'string' && tab.href.length > 0)
|
|
269
|
+
.map((tab) => ({
|
|
270
|
+
label: tab.tab,
|
|
271
|
+
href: tab.href,
|
|
272
|
+
}));
|
|
273
|
+
|
|
274
|
+
return [...tabLinks, ...globalLinks];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function getNavbarAnchors(): VeluAnchor[] {
|
|
278
|
+
const config = loadVeluConfig();
|
|
279
|
+
return (config.navigation.anchors ?? []).filter(
|
|
280
|
+
(a): a is VeluAnchor & { href: string } => typeof a.href === 'string' && a.href.length > 0 && !a.hidden
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function getGlobalAnchors(): VeluAnchor[] {
|
|
285
|
+
const config = loadVeluConfig();
|
|
286
|
+
return (config.navigation.global?.anchors ?? []).filter(
|
|
287
|
+
(a): a is VeluAnchor & { href: string } => typeof a.href === 'string' && a.href.length > 0 && !a.hidden
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface VeluTabMenuDefinition {
|
|
292
|
+
tab: string;
|
|
293
|
+
items: Array<{
|
|
294
|
+
item: string;
|
|
295
|
+
pages: string[];
|
|
296
|
+
}>;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function extractMenuItems(menuValue: unknown): VeluTabMenuDefinition['items'] {
|
|
300
|
+
if (!Array.isArray(menuValue)) return [];
|
|
301
|
+
const out: VeluTabMenuDefinition['items'] = [];
|
|
302
|
+
for (const entry of menuValue) {
|
|
303
|
+
if (!isTabMenuItem(entry)) continue;
|
|
304
|
+
const pages = Array.isArray(entry.pages)
|
|
305
|
+
? entry.pages.filter((page): page is string => typeof page === 'string' && page.trim().length > 0)
|
|
306
|
+
: [];
|
|
307
|
+
out.push({ item: entry.item, pages });
|
|
308
|
+
}
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function collectTabMenus(section: unknown, out: VeluTabMenuDefinition[]): void {
|
|
313
|
+
if (!isRecord(section)) return;
|
|
314
|
+
|
|
315
|
+
const tabs = Array.isArray(section.tabs) ? section.tabs : [];
|
|
316
|
+
for (const tabCandidate of tabs) {
|
|
317
|
+
if (!isRecord(tabCandidate)) continue;
|
|
318
|
+
if (typeof tabCandidate.tab === 'string') {
|
|
319
|
+
const items = extractMenuItems(tabCandidate.menu);
|
|
320
|
+
if (items.length > 0) out.push({ tab: tabCandidate.tab, items });
|
|
321
|
+
}
|
|
322
|
+
collectTabMenus(tabCandidate, out);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const nestedKeys: Array<'dropdowns' | 'products' | 'versions' | 'anchors'> = [
|
|
326
|
+
'dropdowns',
|
|
327
|
+
'products',
|
|
328
|
+
'versions',
|
|
329
|
+
'anchors',
|
|
330
|
+
];
|
|
331
|
+
for (const key of nestedKeys) {
|
|
332
|
+
const list = Array.isArray(section[key]) ? section[key] : [];
|
|
333
|
+
for (const entry of list) collectTabMenus(entry, out);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function getTabMenuDefinitions(): VeluTabMenuDefinition[] {
|
|
338
|
+
const raw = loadRawConfig();
|
|
339
|
+
const navigation = isRecord(raw.navigation) ? raw.navigation : {};
|
|
340
|
+
const out: VeluTabMenuDefinition[] = [];
|
|
341
|
+
collectTabMenus(navigation, out);
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function getLanguages(): string[] {
|
|
346
|
+
const config = loadVeluConfig();
|
|
347
|
+
// Prefer navigation.languages codes, fall back to top-level languages
|
|
348
|
+
if (config.navigation.languages && config.navigation.languages.length > 0) {
|
|
349
|
+
return config.navigation.languages.map((l) => l.language);
|
|
350
|
+
}
|
|
351
|
+
return config.languages ?? [];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function getProductOptions(): VeluProductOption[] {
|
|
355
|
+
const config = loadVeluConfig();
|
|
356
|
+
const products = (config.navigation.products ?? []).filter((p) => !p.hidden);
|
|
357
|
+
if (products.length === 0) return [];
|
|
358
|
+
|
|
359
|
+
const allTabs = config.navigation.tabs ?? [];
|
|
360
|
+
|
|
361
|
+
return products.map((product, index) => {
|
|
362
|
+
const prefix = slugify(product.product, `product-${index + 1}`);
|
|
363
|
+
const productTabs = allTabs.filter((tab) => {
|
|
364
|
+
const slug = tab.slug ?? '';
|
|
365
|
+
return slug === prefix || slug.startsWith(`${prefix}/`);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const tabSlugs = productTabs
|
|
369
|
+
.map((tab) => tab.slug)
|
|
370
|
+
.filter((slug): slug is string => typeof slug === 'string' && slug.length > 0);
|
|
371
|
+
|
|
372
|
+
const firstTab = productTabs[0];
|
|
373
|
+
const firstPage = firstTab ? findFirstPageInTab(firstTab) : undefined;
|
|
374
|
+
const defaultPath = firstTab
|
|
375
|
+
? (firstPage ? `/${firstTab.slug}/${pageBasename(firstPage)}` : `/${firstTab.slug}`)
|
|
376
|
+
: (product.href ?? '/');
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
product: product.product,
|
|
380
|
+
slug: prefix,
|
|
381
|
+
description: product.description,
|
|
382
|
+
icon: product.icon,
|
|
383
|
+
iconType: product.iconType,
|
|
384
|
+
tabSlugs,
|
|
385
|
+
defaultPath,
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function getVersionOptions(): VeluVersionOption[] {
|
|
391
|
+
const config = loadVeluConfig();
|
|
392
|
+
const versions = (config.navigation.versions ?? []).filter((v) => !v.hidden);
|
|
393
|
+
if (versions.length === 0) return [];
|
|
394
|
+
|
|
395
|
+
const allTabs = config.navigation.tabs ?? [];
|
|
396
|
+
|
|
397
|
+
const baseEntries = versions.map((version, index) => {
|
|
398
|
+
const prefix = slugify(version.version, `version-${index + 1}`);
|
|
399
|
+
const versionTabs = allTabs.filter((tab) => {
|
|
400
|
+
const slug = tab.slug ?? '';
|
|
401
|
+
return slug === prefix || slug.startsWith(`${prefix}/`);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const tabSlugs = versionTabs
|
|
405
|
+
.map((tab) => tab.slug)
|
|
406
|
+
.filter((slug): slug is string => typeof slug === 'string' && slug.length > 0);
|
|
407
|
+
|
|
408
|
+
const firstTab = versionTabs[0];
|
|
409
|
+
const firstPage = firstTab ? findFirstPageInTab(firstTab) : undefined;
|
|
410
|
+
const defaultPath = firstTab
|
|
411
|
+
? (firstPage ? `/${firstTab.slug}/${pageBasename(firstPage)}` : `/${firstTab.slug}`)
|
|
412
|
+
: (version.href ?? '/');
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
version: version.version,
|
|
416
|
+
slug: prefix,
|
|
417
|
+
explicitDefault: version.default === true,
|
|
418
|
+
versionParts: parseVersionParts(version.version),
|
|
419
|
+
tabSlugs,
|
|
420
|
+
defaultPath,
|
|
421
|
+
order: index,
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const explicitDefault = baseEntries.find((entry) => entry.explicitDefault);
|
|
426
|
+
const latest = explicitDefault
|
|
427
|
+
?? baseEntries
|
|
428
|
+
.slice()
|
|
429
|
+
.sort((a, b) => {
|
|
430
|
+
const cmp = compareVersionParts(b.versionParts, a.versionParts);
|
|
431
|
+
if (cmp !== 0) return cmp;
|
|
432
|
+
return a.order - b.order;
|
|
433
|
+
})[0];
|
|
434
|
+
|
|
435
|
+
return baseEntries.map((entry) => ({
|
|
436
|
+
version: entry.version,
|
|
437
|
+
slug: entry.slug,
|
|
438
|
+
isDefault: entry.slug === latest?.slug,
|
|
439
|
+
tabSlugs: entry.tabSlugs,
|
|
440
|
+
defaultPath: entry.defaultPath,
|
|
441
|
+
}));
|
|
36
442
|
}
|
|
37
443
|
|
|
38
444
|
export function getAppearance(): 'system' | 'light' | 'dark' {
|
|
@@ -40,3 +446,227 @@ export function getAppearance(): 'system' | 'light' | 'dark' {
|
|
|
40
446
|
if (appearance === 'light' || appearance === 'dark') return appearance;
|
|
41
447
|
return 'system';
|
|
42
448
|
}
|
|
449
|
+
|
|
450
|
+
export type VeluIconLibrary = 'fontawesome' | 'lucide' | 'tabler';
|
|
451
|
+
|
|
452
|
+
export function getIconLibrary(): VeluIconLibrary {
|
|
453
|
+
const raw = loadVeluConfig().icons?.library;
|
|
454
|
+
if (raw === 'lucide' || raw === 'tabler' || raw === 'fontawesome') return raw;
|
|
455
|
+
return 'fontawesome';
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
type PlaygroundDisplayMode = 'interactive' | 'simple' | 'none' | 'auth';
|
|
459
|
+
|
|
460
|
+
export interface VeluResolvedApiConfig {
|
|
461
|
+
baseUrl?: string;
|
|
462
|
+
mdxServer?: string;
|
|
463
|
+
mdxServers?: string[];
|
|
464
|
+
authMethod: VeluApiAuthMethod;
|
|
465
|
+
authName?: string;
|
|
466
|
+
playgroundDisplay: PlaygroundDisplayMode;
|
|
467
|
+
playgroundProxyEnabled: boolean;
|
|
468
|
+
exampleLanguages?: string[];
|
|
469
|
+
exampleDefaults: 'required' | 'all';
|
|
470
|
+
examplePrefill: boolean;
|
|
471
|
+
exampleAutogenerate: boolean;
|
|
472
|
+
defaultOpenApiSpec?: string;
|
|
473
|
+
defaultAsyncApiSpec?: string;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export interface VeluResolvedSeoConfig {
|
|
477
|
+
metatags: Record<string, string>;
|
|
478
|
+
indexing: 'navigable' | 'all';
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function normalizePlaygroundDisplay(api: VeluApiConfig | undefined): PlaygroundDisplayMode {
|
|
482
|
+
const display = api?.playground?.display;
|
|
483
|
+
if (display === 'interactive' || display === 'simple' || display === 'none') return display;
|
|
484
|
+
if (display === 'auth') return 'none';
|
|
485
|
+
|
|
486
|
+
const mode = api?.playground?.mode;
|
|
487
|
+
if (mode === 'hide' || mode === 'none') return 'none';
|
|
488
|
+
return 'interactive';
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function normalizeAuthMethod(method: unknown): VeluApiAuthMethod {
|
|
492
|
+
if (method === 'bearer' || method === 'basic' || method === 'key' || method === 'none') return method;
|
|
493
|
+
return 'none';
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function extractOpenApiSource(openapi: VeluConfig['openapi']): string | string[] | undefined {
|
|
497
|
+
if (typeof openapi === 'string' || Array.isArray(openapi)) return openapi;
|
|
498
|
+
if (openapi && typeof openapi === 'object') {
|
|
499
|
+
const source = (openapi as Record<string, unknown>).source;
|
|
500
|
+
if (typeof source === 'string' || Array.isArray(source)) return source as string | string[];
|
|
501
|
+
}
|
|
502
|
+
return undefined;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function resolveDefaultOpenApiSpec(openapi: VeluConfig['openapi']): string | undefined {
|
|
506
|
+
const source = extractOpenApiSource(openapi);
|
|
507
|
+
if (typeof source === 'string' && source.trim()) return source.trim();
|
|
508
|
+
if (Array.isArray(source)) {
|
|
509
|
+
const first = source.find((entry) => typeof entry === 'string' && entry.trim().length > 0);
|
|
510
|
+
return typeof first === 'string' ? first.trim() : undefined;
|
|
511
|
+
}
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function normalizeExampleLanguages(value: unknown): string[] | undefined {
|
|
516
|
+
if (!Array.isArray(value)) return undefined;
|
|
517
|
+
const normalized = value
|
|
518
|
+
.filter((entry): entry is string => typeof entry === 'string')
|
|
519
|
+
.map((entry) => entry.trim())
|
|
520
|
+
.filter((entry) => entry.length > 0);
|
|
521
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function normalizeMdxServers(value: unknown): string[] | undefined {
|
|
525
|
+
const rawValues = Array.isArray(value) ? value : (typeof value === 'string' ? [value] : []);
|
|
526
|
+
const normalized = rawValues
|
|
527
|
+
.filter((entry): entry is string => typeof entry === 'string')
|
|
528
|
+
.map((entry) => entry.trim())
|
|
529
|
+
.filter((entry) => entry.length > 0);
|
|
530
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function normalizeSeoMetatags(value: unknown): Record<string, string> {
|
|
534
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
535
|
+
const output: Record<string, string> = {};
|
|
536
|
+
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
|
|
537
|
+
const tag = key.trim();
|
|
538
|
+
if (!tag) continue;
|
|
539
|
+
if (typeof raw === 'string') {
|
|
540
|
+
const normalized = raw.trim();
|
|
541
|
+
if (normalized) output[tag] = normalized;
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (typeof raw === 'number' || typeof raw === 'boolean') {
|
|
545
|
+
output[tag] = String(raw);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return output;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function normalizeAssetPath(value: unknown): string | undefined {
|
|
552
|
+
if (typeof value !== 'string') return undefined;
|
|
553
|
+
const trimmed = value.trim();
|
|
554
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function normalizeThemeAsset(value: unknown): VeluThemeAsset {
|
|
558
|
+
if (typeof value === 'string') {
|
|
559
|
+
const asset = normalizeAssetPath(value);
|
|
560
|
+
return asset ? { light: asset, dark: asset } : {};
|
|
561
|
+
}
|
|
562
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
563
|
+
const record = value as Record<string, unknown>;
|
|
564
|
+
const light = normalizeAssetPath(record.light);
|
|
565
|
+
const dark = normalizeAssetPath(record.dark);
|
|
566
|
+
const any = normalizeAssetPath(record.default);
|
|
567
|
+
const href = normalizeAssetPath(record.href);
|
|
568
|
+
return {
|
|
569
|
+
...(light ? { light } : {}),
|
|
570
|
+
...(dark ? { dark } : {}),
|
|
571
|
+
...(!light && any ? { light: any } : {}),
|
|
572
|
+
...(!dark && any ? { dark: any } : {}),
|
|
573
|
+
...(href ? { href } : {}),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function extractOrigin(value: string | undefined): string | undefined {
|
|
578
|
+
if (!value) return undefined;
|
|
579
|
+
const trimmed = value.trim();
|
|
580
|
+
if (!trimmed) return undefined;
|
|
581
|
+
try {
|
|
582
|
+
return new URL(trimmed).origin;
|
|
583
|
+
} catch {
|
|
584
|
+
return undefined;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function getApiConfig(): VeluResolvedApiConfig {
|
|
589
|
+
const config = loadVeluConfig();
|
|
590
|
+
const api = config.api;
|
|
591
|
+
const auth = api?.mdx?.auth;
|
|
592
|
+
const examples = api?.examples;
|
|
593
|
+
const playgroundDisplay = normalizePlaygroundDisplay(api);
|
|
594
|
+
const staticExportBuild = process.env.VELU_STATIC_EXPORT === '1';
|
|
595
|
+
const mdxServers = normalizeMdxServers(api?.mdx?.server);
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
baseUrl: typeof api?.baseUrl === 'string' && api.baseUrl.trim() ? api.baseUrl.trim() : undefined,
|
|
599
|
+
mdxServer: mdxServers?.[0],
|
|
600
|
+
mdxServers,
|
|
601
|
+
authMethod: normalizeAuthMethod(auth?.method),
|
|
602
|
+
authName: typeof auth?.name === 'string' && auth.name.trim() ? auth.name.trim() : undefined,
|
|
603
|
+
playgroundDisplay,
|
|
604
|
+
// Next static export cannot include runtime route handlers such as /api/proxy.
|
|
605
|
+
// Disable proxy automatically for static export builds.
|
|
606
|
+
playgroundProxyEnabled: !staticExportBuild && api?.playground?.proxy !== false,
|
|
607
|
+
exampleLanguages: normalizeExampleLanguages(examples?.languages),
|
|
608
|
+
exampleDefaults: examples?.defaults === 'required' ? 'required' : 'all',
|
|
609
|
+
examplePrefill: examples?.prefill === true,
|
|
610
|
+
exampleAutogenerate: examples?.autogenerate !== false,
|
|
611
|
+
defaultOpenApiSpec: resolveDefaultOpenApiSpec(config.openapi),
|
|
612
|
+
defaultAsyncApiSpec: resolveDefaultOpenApiSpec(config.asyncapi),
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export function getSeoConfig(): VeluResolvedSeoConfig {
|
|
617
|
+
const config = loadVeluConfig();
|
|
618
|
+
const seo = config.seo;
|
|
619
|
+
const indexing: 'navigable' | 'all' = seo?.indexing === 'all' ? 'all' : 'navigable';
|
|
620
|
+
return {
|
|
621
|
+
metatags: normalizeSeoMetatags(seo?.metatags),
|
|
622
|
+
indexing,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export function getSiteName(): string {
|
|
627
|
+
const config = loadVeluConfig();
|
|
628
|
+
const fromName = normalizeAssetPath(config.name);
|
|
629
|
+
if (fromName) return fromName;
|
|
630
|
+
const fromTitle = normalizeAssetPath(config.title);
|
|
631
|
+
if (fromTitle) return fromTitle;
|
|
632
|
+
return 'Velu Docs';
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export function getSiteFavicon(): string | undefined {
|
|
636
|
+
const config = loadVeluConfig();
|
|
637
|
+
const asset = normalizeThemeAsset(config.favicon);
|
|
638
|
+
return asset.light ?? asset.dark;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export function getSiteLogoAsset(): VeluThemeAsset {
|
|
642
|
+
const config = loadVeluConfig();
|
|
643
|
+
return normalizeThemeAsset(config.logo);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function getSitePrimaryColor(): string | undefined {
|
|
647
|
+
const config = loadVeluConfig();
|
|
648
|
+
const colors = config.colors;
|
|
649
|
+
if (!colors) return undefined;
|
|
650
|
+
return normalizeAssetPath(colors.primary) ?? normalizeAssetPath(colors.light) ?? normalizeAssetPath(colors.dark);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
export function getSiteOrigin(): string {
|
|
654
|
+
const seo = getSeoConfig();
|
|
655
|
+
const envCandidates = [
|
|
656
|
+
process.env.VELU_SITE_URL,
|
|
657
|
+
process.env.NEXT_PUBLIC_SITE_URL,
|
|
658
|
+
process.env.SITE_URL,
|
|
659
|
+
];
|
|
660
|
+
for (const candidate of envCandidates) {
|
|
661
|
+
const origin = extractOrigin(candidate);
|
|
662
|
+
if (origin) return origin;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const canonicalOrigin = extractOrigin(seo.metatags.canonical);
|
|
666
|
+
if (canonicalOrigin) return canonicalOrigin;
|
|
667
|
+
|
|
668
|
+
const ogOrigin = extractOrigin(seo.metatags['og:url']);
|
|
669
|
+
if (ogOrigin) return ogOrigin;
|
|
670
|
+
|
|
671
|
+
return 'http://localhost:4321';
|
|
672
|
+
}
|