@aravindc26/velu 0.11.0 → 0.11.3

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.
Files changed (60) hide show
  1. package/package.json +15 -6
  2. package/schema/velu.schema.json +1251 -115
  3. package/src/build.ts +1121 -304
  4. package/src/cli.ts +90 -26
  5. package/src/engine/_server.mjs +1684 -277
  6. package/src/engine/app/(docs)/[...slug]/layout.tsx +371 -0
  7. package/src/engine/app/(docs)/[...slug]/page.tsx +926 -0
  8. package/src/engine/app/api/proxy/route.ts +23 -0
  9. package/src/engine/app/copy-page.css +59 -1
  10. package/src/engine/app/global.css +3157 -3
  11. package/src/engine/app/layout.tsx +56 -1
  12. package/src/engine/app/llms-file/route.ts +87 -0
  13. package/src/engine/app/llms-full-file/route.ts +62 -0
  14. package/src/engine/app/md-file/[...slug]/route.ts +409 -0
  15. package/src/engine/app/page.tsx +45 -0
  16. package/src/engine/app/robots.txt/route.ts +63 -0
  17. package/src/engine/app/rss-file/[...slug]/route.ts +169 -0
  18. package/src/engine/app/sitemap.xml/route.ts +82 -0
  19. package/src/engine/components/assistant.tsx +16 -5
  20. package/src/engine/components/changelog-filters.tsx +114 -0
  21. package/src/engine/components/code-group.tsx +383 -0
  22. package/src/engine/components/color.tsx +118 -0
  23. package/src/engine/components/expandable.tsx +77 -0
  24. package/src/engine/components/icon.tsx +136 -0
  25. package/src/engine/components/image-zoom-fallback.tsx +147 -0
  26. package/src/engine/components/image.tsx +111 -0
  27. package/src/engine/components/manual-api-playground.tsx +154 -0
  28. package/src/engine/components/mermaid.tsx +142 -0
  29. package/src/engine/components/openapi-toc-sync.tsx +59 -0
  30. package/src/engine/components/openapi.tsx +1682 -0
  31. package/src/engine/components/page-feedback.tsx +153 -0
  32. package/src/engine/components/product-switcher.tsx +27 -3
  33. package/src/engine/components/prompt.tsx +90 -0
  34. package/src/engine/components/providers.tsx +1 -6
  35. package/src/engine/components/search.tsx +4 -0
  36. package/src/engine/components/sidebar-links.tsx +13 -15
  37. package/src/engine/components/synced-tabs.tsx +57 -0
  38. package/src/engine/components/toc-examples.tsx +110 -0
  39. package/src/engine/components/view.tsx +344 -0
  40. package/src/engine/generated/redirects.ts +3 -0
  41. package/src/engine/lib/changelog.ts +246 -0
  42. package/src/engine/lib/layout.shared.ts +30 -2
  43. package/src/engine/lib/llms.ts +444 -0
  44. package/src/engine/lib/navigation-normalize.mjs +481 -412
  45. package/src/engine/lib/navigation-normalize.ts +261 -54
  46. package/src/engine/lib/redirects.ts +194 -0
  47. package/src/engine/lib/source.ts +107 -4
  48. package/src/engine/lib/velu.ts +368 -2
  49. package/src/engine/mdx-components.tsx +648 -0
  50. package/src/engine/middleware.ts +66 -0
  51. package/src/engine/public/icons/cursor-dark.svg +12 -0
  52. package/src/engine/public/icons/cursor-light.svg +12 -0
  53. package/src/engine/source.config.ts +98 -1
  54. package/src/engine/src/components/PageTitle.astro +16 -5
  55. package/src/engine/src/lib/velu.ts +11 -3
  56. package/src/navigation-normalize.ts +252 -54
  57. package/src/themes.ts +6 -6
  58. package/src/validate.ts +119 -6
  59. package/src/engine/app/(docs)/[[...slug]]/layout.tsx +0 -87
  60. package/src/engine/app/(docs)/[[...slug]]/page.tsx +0 -146
@@ -1,6 +1,14 @@
1
- import { readFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
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
+ }
4
12
 
5
13
  interface VeluTab {
6
14
  tab: string;
@@ -10,6 +18,11 @@ interface VeluTab {
10
18
  groups?: VeluGroup[];
11
19
  }
12
20
 
21
+ interface VeluTabMenuItem {
22
+ item: string;
23
+ pages?: Array<string | VeluSeparator | VeluLink>;
24
+ }
25
+
13
26
  interface VeluSeparator {
14
27
  separator: string;
15
28
  }
@@ -29,6 +42,7 @@ interface VeluAnchor {
29
42
  anchor: string;
30
43
  href?: string;
31
44
  icon?: string;
45
+ iconType?: string;
32
46
  color?: {
33
47
  light: string;
34
48
  dark: string;
@@ -52,6 +66,7 @@ interface VeluProductNav {
52
66
  product: string;
53
67
  description?: string;
54
68
  icon?: string;
69
+ iconType?: string;
55
70
  hidden?: boolean;
56
71
  href?: string;
57
72
  }
@@ -63,11 +78,47 @@ interface VeluVersionNav {
63
78
  href?: string;
64
79
  }
65
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
+
66
116
  export interface VeluProductOption {
67
117
  product: string;
68
118
  slug: string;
69
119
  description?: string;
70
120
  icon?: string;
121
+ iconType?: string;
71
122
  tabSlugs: string[];
72
123
  defaultPath: string;
73
124
  }
@@ -81,8 +132,24 @@ export interface VeluVersionOption {
81
132
  }
82
133
 
83
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
+ };
84
147
  appearance?: 'system' | 'light' | 'dark';
85
148
  languages?: string[];
149
+ openapi?: string | string[] | Record<string, unknown>;
150
+ asyncapi?: string | string[] | Record<string, unknown>;
151
+ api?: VeluApiConfig;
152
+ seo?: VeluSeoConfig;
86
153
  navigation: {
87
154
  tabs?: VeluTab[];
88
155
  languages?: VeluLanguageNav[];
@@ -97,19 +164,39 @@ interface VeluConfig {
97
164
  }
98
165
 
99
166
  let cachedConfig: VeluConfig | null = null;
167
+ let cachedRawConfig: Record<string, unknown> | null = null;
100
168
 
101
169
  function loadVeluConfig(): VeluConfig {
102
170
  if (cachedConfig) return cachedConfig;
103
- const configPath = resolve(process.cwd(), 'velu.json');
171
+ const configPath = resolveConfigPath(process.cwd());
104
172
  const raw = readFileSync(configPath, 'utf-8');
105
173
  cachedConfig = normalizeConfigNavigation(JSON.parse(raw)) as VeluConfig;
106
174
  return cachedConfig;
107
175
  }
108
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
+
109
188
  function isGroup(item: unknown): item is VeluGroup {
110
189
  return typeof item === 'object' && item !== null && 'group' in item;
111
190
  }
112
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
+
113
200
  function slugify(input: string, fallback: string): string {
114
201
  const slug = input
115
202
  .toLowerCase()
@@ -201,6 +288,60 @@ export function getGlobalAnchors(): VeluAnchor[] {
201
288
  );
202
289
  }
203
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
+
204
345
  export function getLanguages(): string[] {
205
346
  const config = loadVeluConfig();
206
347
  // Prefer navigation.languages codes, fall back to top-level languages
@@ -239,6 +380,7 @@ export function getProductOptions(): VeluProductOption[] {
239
380
  slug: prefix,
240
381
  description: product.description,
241
382
  icon: product.icon,
383
+ iconType: product.iconType,
242
384
  tabSlugs,
243
385
  defaultPath,
244
386
  };
@@ -304,3 +446,227 @@ export function getAppearance(): 'system' | 'light' | 'dark' {
304
446
  if (appearance === 'light' || appearance === 'dark') return appearance;
305
447
  return 'system';
306
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
+ }