@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
@@ -0,0 +1,371 @@
1
+ import { isValidElement, type ReactNode } from 'react';
2
+ import { DocsLayout } from 'fumadocs-ui/layouts/notebook';
3
+ import type { LinkItemType } from 'fumadocs-ui/layouts/shared';
4
+ import { baseOptions } from '@/lib/layout.shared';
5
+ import { source } from '@/lib/source';
6
+ import {
7
+ getIconLibrary,
8
+ getLanguages,
9
+ getVersionOptions,
10
+ getProductOptions,
11
+ getTabMenuDefinitions,
12
+ type VeluVersionOption,
13
+ type VeluProductOption,
14
+ } from '@/lib/velu';
15
+ import { SidebarLinks } from '@/components/sidebar-links';
16
+ import { ProductSwitcher } from '@/components/product-switcher';
17
+ import { VeluIcon } from '@/components/icon';
18
+
19
+ interface LayoutParams {
20
+ slug?: string[];
21
+ }
22
+
23
+ interface SlugLayoutProps {
24
+ children: ReactNode;
25
+ params: Promise<LayoutParams>;
26
+ }
27
+
28
+ interface PageTreePageNode {
29
+ type?: string;
30
+ url?: string;
31
+ external?: boolean;
32
+ }
33
+
34
+ interface PageTreeFolderNode {
35
+ type?: string;
36
+ name?: ReactNode;
37
+ icon?: ReactNode;
38
+ description?: ReactNode;
39
+ root?: boolean;
40
+ index?: { url?: string };
41
+ children?: unknown[];
42
+ }
43
+
44
+ function resolveLocale(slugInput: string[] | undefined): string {
45
+ const languages = getLanguages();
46
+ const defaultLanguage = languages[0] ?? 'en';
47
+ const slug = slugInput ?? [];
48
+ const firstSeg = slug[0];
49
+
50
+ return languages.includes(firstSeg ?? '') ? firstSeg! : defaultLanguage;
51
+ }
52
+
53
+ function resolveCurrentVersion(slugInput: string[] | undefined, versions: VeluVersionOption[]): VeluVersionOption | undefined {
54
+ if (versions.length === 0) return undefined;
55
+ const firstSeg = (slugInput ?? [])[0] ?? '';
56
+ return versions.find((v) => v.slug === firstSeg) ?? versions.find((v) => v.isDefault) ?? versions[0];
57
+ }
58
+
59
+ function filterTreeBySlugPrefix<T extends { children?: unknown[] }>(tree: T, prefix?: string): T {
60
+ if (!prefix) return tree;
61
+
62
+ const normPrefix = prefix.replace(/^\/+|\/+$/g, '').toLowerCase();
63
+ if (!normPrefix) return tree;
64
+
65
+ const matchesPrefix = (value: string): boolean => {
66
+ const norm = value.replace(/^\/+|\/+$/g, '').toLowerCase();
67
+ return norm === normPrefix || norm.startsWith(`${normPrefix}/`) || norm.includes(`/${normPrefix}/`) || norm.endsWith(`/${normPrefix}`);
68
+ };
69
+
70
+ const filterNodes = (nodes: unknown[]): unknown[] => {
71
+ const kept: unknown[] = [];
72
+
73
+ for (const node of nodes) {
74
+ if (typeof node !== 'object' || node === null) continue;
75
+ const entry = node as {
76
+ url?: unknown;
77
+ path?: unknown;
78
+ $ref?: { metaFile?: unknown; file?: unknown };
79
+ children?: unknown[];
80
+ };
81
+
82
+ const candidates = [entry.url, entry.path, entry.$ref?.metaFile, entry.$ref?.file]
83
+ .filter((value): value is string => typeof value === 'string');
84
+ const selfMatch = candidates.some(matchesPrefix);
85
+
86
+ const childNodes = Array.isArray(entry.children) ? entry.children : [];
87
+ const filteredChildren = childNodes.length > 0 ? filterNodes(childNodes) : [];
88
+ const childMatch = filteredChildren.length > 0;
89
+
90
+ if (selfMatch || childMatch) {
91
+ kept.push(childMatch ? { ...entry, children: filteredChildren } : entry);
92
+ }
93
+ }
94
+
95
+ return kept;
96
+ };
97
+
98
+ const children = Array.isArray(tree.children) ? tree.children : [];
99
+ const filtered = filterNodes(children);
100
+ if (filtered.length === 0) return tree;
101
+ return { ...tree, children: filtered } as T;
102
+ }
103
+
104
+ function resolveCurrentProduct(slugInput: string[] | undefined, products: VeluProductOption[]): VeluProductOption | undefined {
105
+ if (products.length === 0) return undefined;
106
+ const firstSeg = (slugInput ?? [])[0] ?? '';
107
+ return products.find((p) => p.slug === firstSeg) ?? products[0];
108
+ }
109
+
110
+ function renderIconsInTree<T>(node: T, iconLibrary: 'fontawesome' | 'lucide' | 'tabler'): T {
111
+ if (Array.isArray(node)) return node.map((item) => renderIconsInTree(item, iconLibrary)) as T;
112
+ if (isValidElement(node)) return node;
113
+ if (typeof node !== 'object' || node === null) return node;
114
+
115
+ const out: Record<string, unknown> = {};
116
+ const nodeWithIconType = node as { iconType?: unknown };
117
+ for (const [key, value] of Object.entries(node)) {
118
+ if (key === 'icon' && typeof value === 'string') {
119
+ const iconType = typeof nodeWithIconType.iconType === 'string'
120
+ ? nodeWithIconType.iconType
121
+ : undefined;
122
+ out[key] = <VeluIcon name={value} iconType={iconType} library={iconLibrary} fallback={false} />;
123
+ continue;
124
+ }
125
+ out[key] = renderIconsInTree(value, iconLibrary);
126
+ }
127
+ return out as T;
128
+ }
129
+
130
+ function collectFolderUrls(folder: PageTreeFolderNode, out: Set<string> = new Set<string>()): Set<string> {
131
+ if (typeof folder.index?.url === 'string' && folder.index.url.length > 0) out.add(folder.index.url);
132
+ for (const child of Array.isArray(folder.children) ? folder.children : []) {
133
+ const node = child as PageTreePageNode & PageTreeFolderNode;
134
+ if (node?.type === 'page' && !node.external && typeof node.url === 'string' && node.url.length > 0) {
135
+ out.add(node.url);
136
+ continue;
137
+ }
138
+ if (node?.type === 'folder') collectFolderUrls(node, out);
139
+ }
140
+ return out;
141
+ }
142
+
143
+ function buildNavbarTabs(tree: unknown): Array<{
144
+ url: string;
145
+ title: ReactNode;
146
+ icon?: ReactNode;
147
+ description?: ReactNode;
148
+ urls: Set<string>;
149
+ }> | undefined {
150
+ const rootChildren = Array.isArray((tree as { children?: unknown[] })?.children)
151
+ ? (tree as { children: unknown[] }).children
152
+ : [];
153
+
154
+ const rootFolder = rootChildren.find((child) => {
155
+ const node = child as PageTreeFolderNode;
156
+ return node?.type === 'folder' && node.root === true;
157
+ }) as PageTreeFolderNode | undefined;
158
+
159
+ const tabFolders = Array.isArray(rootFolder?.children)
160
+ ? rootFolder!.children.filter((child) => (child as PageTreeFolderNode)?.type === 'folder') as PageTreeFolderNode[]
161
+ : rootChildren.filter((child) => (child as PageTreeFolderNode)?.type === 'folder') as PageTreeFolderNode[];
162
+
163
+ const tabs = tabFolders
164
+ .map((folder) => {
165
+ const urls = collectFolderUrls(folder);
166
+ const firstUrl = urls.values().next().value as string | undefined;
167
+ if (!firstUrl) return null;
168
+ return {
169
+ url: firstUrl,
170
+ title: folder.name ?? '',
171
+ icon: folder.icon,
172
+ description: folder.description,
173
+ urls,
174
+ };
175
+ })
176
+ .filter((tab): tab is NonNullable<typeof tab> => tab !== null);
177
+
178
+ return tabs.length > 0 ? tabs : undefined;
179
+ }
180
+
181
+ function resolveTabContext(slugInput: string[] | undefined): { containerSlug?: string; tabSlug?: string } {
182
+ const languages = getLanguages();
183
+ const slug = slugInput ?? [];
184
+ const contentSlug = languages.includes(slug[0] ?? '') ? slug.slice(1) : slug;
185
+ if (contentSlug.length === 0) return {};
186
+ if (contentSlug.length > 1) {
187
+ return { containerSlug: contentSlug[0], tabSlug: contentSlug[1] };
188
+ }
189
+ return { tabSlug: contentSlug[0] };
190
+ }
191
+
192
+ function normalizePath(value: string): string {
193
+ return value.replace(/^\/+|\/+$/g, '').toLowerCase();
194
+ }
195
+
196
+ function basename(value: string): string {
197
+ const normalized = normalizePath(value);
198
+ const parts = normalized.split('/').filter(Boolean);
199
+ return parts[parts.length - 1] ?? normalized;
200
+ }
201
+
202
+ function resolveMenuTargetUrl(menuPages: string[], tabUrls: Set<string>): string | undefined {
203
+ const urls = Array.from(tabUrls);
204
+ if (urls.length === 0) return undefined;
205
+
206
+ for (const page of menuPages) {
207
+ const normalizedPage = normalizePath(page);
208
+ if (!normalizedPage) continue;
209
+
210
+ const direct = urls.find((url) => {
211
+ const normalizedUrl = normalizePath(url);
212
+ return normalizedUrl === normalizedPage || normalizedUrl.endsWith(`/${normalizedPage}`);
213
+ });
214
+ if (direct) return direct;
215
+
216
+ const pageBase = basename(normalizedPage);
217
+ const basenameMatches = urls.filter((url) => basename(url) === pageBase);
218
+ if (basenameMatches.length === 1) return basenameMatches[0];
219
+ }
220
+
221
+ return undefined;
222
+ }
223
+
224
+ function resolveMenuLinksForTab(
225
+ tabUrls: Set<string>,
226
+ candidates: ReturnType<typeof getTabMenuDefinitions>,
227
+ ): Array<{ text: string; url: string }> {
228
+ let best: Array<{ text: string; url: string }> = [];
229
+
230
+ for (const candidate of candidates) {
231
+ const resolved = candidate.items
232
+ .map((item) => {
233
+ const target = resolveMenuTargetUrl(item.pages, tabUrls);
234
+ if (!target) return null;
235
+ return { text: item.item, url: target };
236
+ })
237
+ .filter((entry): entry is { text: string; url: string } => entry !== null);
238
+
239
+ if (resolved.length > best.length) best = resolved;
240
+ }
241
+
242
+ return best;
243
+ }
244
+
245
+ function scopeTreeToTab<T extends { children?: unknown[] }>(
246
+ tree: T,
247
+ tabSlug?: string,
248
+ containerSlug?: string,
249
+ ): T {
250
+ const normalizedTab = (tabSlug ?? '').trim().toLowerCase();
251
+ if (!normalizedTab) return tree;
252
+
253
+ const topChildren = Array.isArray(tree.children) ? tree.children : [];
254
+ const rootFolder = topChildren.find((child) => {
255
+ const node = child as PageTreeFolderNode;
256
+ return node?.type === 'folder' && node.root === true;
257
+ }) as PageTreeFolderNode | undefined;
258
+
259
+ if (!rootFolder || !Array.isArray(rootFolder.children)) return tree;
260
+
261
+ const normalizedContainer = (containerSlug ?? '').trim().toLowerCase();
262
+ const matchingChildren = rootFolder.children.filter((child): child is PageTreeFolderNode => {
263
+ const folder = child as PageTreeFolderNode;
264
+ if (folder?.type !== 'folder') return false;
265
+
266
+ const urls = collectFolderUrls(folder);
267
+ for (const url of urls) {
268
+ const segments = url.split('/').filter(Boolean).map((segment) => segment.toLowerCase());
269
+ if (segments.length === 0) continue;
270
+
271
+ const tabCandidate = normalizedContainer && segments[0] === normalizedContainer
272
+ ? segments[1]
273
+ : segments[0];
274
+ if (tabCandidate === normalizedTab) return true;
275
+ }
276
+ return false;
277
+ });
278
+
279
+ if (matchingChildren.length === 0) return tree;
280
+
281
+ const firstMatch = matchingChildren[0];
282
+ const flattenedChildren = matchingChildren.length === 1 && Array.isArray(firstMatch?.children) && firstMatch.children.length > 0
283
+ ? firstMatch.children
284
+ : matchingChildren;
285
+
286
+ const scopedRoot = { ...rootFolder, children: flattenedChildren };
287
+ const scopedChildren = topChildren.map((child) => (child === rootFolder ? scopedRoot : child));
288
+ return { ...tree, children: scopedChildren } as T;
289
+ }
290
+
291
+ export default async function SlugLayout({ children, params }: SlugLayoutProps) {
292
+ const resolvedParams = await params;
293
+ const locale = resolveLocale(resolvedParams.slug);
294
+ const versions = getVersionOptions();
295
+ const products = getProductOptions();
296
+ const iconLibrary = getIconLibrary();
297
+ const currentVersion = resolveCurrentVersion(resolvedParams.slug, versions);
298
+ const currentProduct = resolveCurrentProduct(resolvedParams.slug, products);
299
+ const { containerSlug, tabSlug: currentTabSlug } = resolveTabContext(resolvedParams.slug);
300
+
301
+ const activePrefix = currentVersion?.slug ?? currentProduct?.slug;
302
+ const containerScopedTree = filterTreeBySlugPrefix(source.getPageTree(locale), activePrefix);
303
+ const rawTree = scopeTreeToTab(containerScopedTree, currentTabSlug, containerSlug);
304
+ const navbarTabs = buildNavbarTabs(source.getPageTree(locale)) ?? [];
305
+ const tabMenuDefinitions = getTabMenuDefinitions();
306
+ const tree = renderIconsInTree(rawTree, iconLibrary);
307
+ const base = baseOptions();
308
+ const headerTabLinks: LinkItemType[] = navbarTabs
309
+ .map((tab): LinkItemType | null => {
310
+ const tabText = typeof tab.title === 'string' ? tab.title : '';
311
+ if (tabText.length === 0) return null;
312
+
313
+ const menuCandidates = tabMenuDefinitions.filter(
314
+ (definition) => definition.tab.trim().toLowerCase() === tabText.trim().toLowerCase(),
315
+ );
316
+ const menuLinks = resolveMenuLinksForTab(tab.urls, menuCandidates);
317
+
318
+ if (menuLinks.length > 0) {
319
+ return {
320
+ type: 'menu',
321
+ text: tabText,
322
+ url: tab.url,
323
+ active: 'nested-url',
324
+ secondary: false,
325
+ items: menuLinks.map((item) => ({
326
+ text: item.text,
327
+ url: item.url,
328
+ active: 'nested-url',
329
+ })),
330
+ };
331
+ }
332
+
333
+ return {
334
+ text: tabText,
335
+ url: tab.url,
336
+ active: 'nested-url',
337
+ secondary: false,
338
+ };
339
+ })
340
+ .filter((link): link is LinkItemType => link !== null);
341
+
342
+ return (
343
+ <DocsLayout
344
+ tree={tree}
345
+ sidebar={{
346
+ collapsible: true,
347
+ banner: products.length > 1 ? (
348
+ <div className="velu-sidebar-banner">
349
+ <ProductSwitcher products={products} iconLibrary={iconLibrary} />
350
+ </div>
351
+ ) : undefined,
352
+ footer: ({ className, children, ...props }: any) => (
353
+ <div
354
+ className={['velu-sidebar-footer-shell', className].filter(Boolean).join(' ')}
355
+ {...props}
356
+ >
357
+ {children ? <div className="velu-sidebar-footer-icons">{children}</div> : null}
358
+ <SidebarLinks />
359
+ </div>
360
+ ),
361
+ }}
362
+ {...base}
363
+ links={headerTabLinks.length > 0 ? headerTabLinks : base.links}
364
+ themeSwitch={{ enabled: false }}
365
+ >
366
+ {children}
367
+ </DocsLayout>
368
+ );
369
+ }
370
+
371
+ export { generateStaticParams } from './page';