@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
package/src/validate.ts CHANGED
@@ -3,6 +3,16 @@ import addFormats from "ajv-formats";
3
3
  import { readFileSync, existsSync } from "node:fs";
4
4
  import { resolve, join } from "node:path";
5
5
  import { normalizeConfigNavigation } from "./navigation-normalize.js";
6
+ const PRIMARY_CONFIG_NAME = "docs.json";
7
+ const LEGACY_CONFIG_NAME = "velu.json";
8
+
9
+ function resolveConfigPath(docsDir: string): string | null {
10
+ const primary = join(docsDir, PRIMARY_CONFIG_NAME);
11
+ if (existsSync(primary)) return primary;
12
+ const legacy = join(docsDir, LEGACY_CONFIG_NAME);
13
+ if (existsSync(legacy)) return legacy;
14
+ return null;
15
+ }
6
16
 
7
17
  interface VeluSeparator {
8
18
  separator: string;
@@ -12,12 +22,17 @@ interface VeluLink {
12
22
  href: string;
13
23
  label: string;
14
24
  icon?: string;
25
+ iconType?: string;
15
26
  }
16
27
 
17
28
  interface VeluAnchor {
18
29
  anchor: string;
19
30
  href?: string;
20
31
  icon?: string;
32
+ iconType?: string;
33
+ version?: string;
34
+ openapi?: VeluOpenApiSource;
35
+ asyncapi?: VeluOpenApiSource;
21
36
  color?: {
22
37
  light: string;
23
38
  dark: string;
@@ -30,13 +45,18 @@ interface VeluGlobalTab {
30
45
  tab: string;
31
46
  href: string;
32
47
  icon?: string;
48
+ iconType?: string;
33
49
  }
34
50
 
35
51
  interface VeluGroup {
36
52
  group: string;
37
53
  slug?: string;
38
54
  icon?: string;
55
+ iconType?: string;
39
56
  tag?: string;
57
+ version?: string;
58
+ openapi?: VeluOpenApiSource;
59
+ asyncapi?: VeluOpenApiSource;
40
60
  expanded?: boolean;
41
61
  description?: string;
42
62
  hidden?: boolean;
@@ -46,6 +66,9 @@ interface VeluGroup {
46
66
  interface VeluMenuItem {
47
67
  item: string;
48
68
  icon?: string;
69
+ iconType?: string;
70
+ openapi?: VeluOpenApiSource;
71
+ asyncapi?: VeluOpenApiSource;
49
72
  groups?: VeluGroup[];
50
73
  pages?: (string | VeluSeparator | VeluLink)[];
51
74
  }
@@ -54,7 +77,11 @@ interface VeluTab {
54
77
  tab: string;
55
78
  slug?: string;
56
79
  icon?: string;
80
+ iconType?: string;
81
+ version?: string;
57
82
  href?: string;
83
+ openapi?: VeluOpenApiSource;
84
+ asyncapi?: VeluOpenApiSource;
58
85
  pages?: (string | VeluSeparator | VeluLink)[];
59
86
  groups?: VeluGroup[];
60
87
  menu?: VeluMenuItem[];
@@ -62,28 +89,65 @@ interface VeluTab {
62
89
 
63
90
  interface VeluLanguageNav {
64
91
  language: string;
92
+ openapi?: VeluOpenApiSource;
93
+ asyncapi?: VeluOpenApiSource;
65
94
  tabs: VeluTab[];
66
95
  }
67
96
 
68
97
  interface VeluProductNav {
69
98
  product: string;
70
99
  icon?: string;
100
+ iconType?: string;
101
+ openapi?: VeluOpenApiSource;
102
+ asyncapi?: VeluOpenApiSource;
71
103
  tabs?: VeluTab[];
72
104
  pages?: (string | VeluSeparator | VeluLink)[];
73
105
  }
74
106
 
75
107
  interface VeluVersionNav {
76
108
  version: string;
109
+ openapi?: VeluOpenApiSource;
110
+ asyncapi?: VeluOpenApiSource;
77
111
  tabs: VeluTab[];
78
112
  }
79
113
 
114
+ type VeluOpenApiSource = string | string[] | Record<string, unknown>;
115
+
80
116
  interface VeluConfig {
81
117
  $schema?: string;
118
+ icons?: {
119
+ library?: "fontawesome" | "lucide" | "tabler";
120
+ };
82
121
  theme?: string;
83
122
  colors?: { primary?: string; light?: string; dark?: string };
84
123
  appearance?: "system" | "light" | "dark";
85
124
  styling?: { codeblocks?: { theme?: string | { light: string; dark: string } } };
125
+ openapi?: VeluOpenApiSource;
126
+ asyncapi?: VeluOpenApiSource;
127
+ api?: {
128
+ baseUrl?: string;
129
+ playground?: {
130
+ mode?: string;
131
+ display?: string;
132
+ proxy?: boolean;
133
+ };
134
+ examples?: {
135
+ languages?: string[];
136
+ defaults?: "required" | "all";
137
+ prefill?: boolean;
138
+ autogenerate?: boolean;
139
+ };
140
+ mdx?: {
141
+ server?: string;
142
+ auth?: {
143
+ method?: "bearer" | "basic" | "key" | "none";
144
+ name?: string;
145
+ };
146
+ };
147
+ };
86
148
  navigation: {
149
+ openapi?: VeluOpenApiSource;
150
+ asyncapi?: VeluOpenApiSource;
87
151
  tabs?: VeluTab[];
88
152
  languages?: VeluLanguageNav[];
89
153
  products?: VeluProductNav[];
@@ -96,6 +160,49 @@ interface VeluConfig {
96
160
  };
97
161
  }
98
162
 
163
+ const HTTP_METHODS = new Set([
164
+ "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", "TRACE", "CONNECT", "WEBHOOK",
165
+ ]);
166
+
167
+ function isOpenApiOperationReference(value: string): boolean {
168
+ const trimmed = value.trim();
169
+ if (!trimmed) return false;
170
+ const withSpec = trimmed.match(/^(\S+)\s+([A-Za-z]+)\s+(.+)$/);
171
+ if (withSpec) {
172
+ const method = withSpec[2].toUpperCase();
173
+ const endpoint = withSpec[3].trim();
174
+ if (!HTTP_METHODS.has(method)) return false;
175
+ if (method === "WEBHOOK") return endpoint.length > 0;
176
+ return endpoint.startsWith("/");
177
+ }
178
+ const noSpec = trimmed.match(/^([A-Za-z]+)\s+(.+)$/);
179
+ if (!noSpec) return false;
180
+ const method = noSpec[1].toUpperCase();
181
+ const endpoint = noSpec[2].trim();
182
+ if (!HTTP_METHODS.has(method)) return false;
183
+ if (method === "WEBHOOK") return endpoint.length > 0;
184
+ return endpoint.startsWith("/");
185
+ }
186
+
187
+ function isAsyncApiChannelReference(value: string): boolean {
188
+ const trimmed = value.trim();
189
+ if (!trimmed) return false;
190
+ const withSpec = trimmed.match(/^(\S+)\s+(.+)$/);
191
+ if (!withSpec) return false;
192
+ const first = withSpec[1].trim();
193
+ const maybeMethod = first.toUpperCase();
194
+ if (HTTP_METHODS.has(maybeMethod)) return false;
195
+ const looksLikeSpec =
196
+ first.startsWith('/') ||
197
+ first.startsWith('./') ||
198
+ first.startsWith('../') ||
199
+ /^https?:\/\//i.test(first) ||
200
+ first.endsWith('.json') ||
201
+ first.endsWith('.yaml') ||
202
+ first.endsWith('.yml');
203
+ return looksLikeSpec && withSpec[2].trim().length > 0;
204
+ }
205
+
99
206
  function loadJson(filePath: string): unknown {
100
207
  const raw = readFileSync(filePath, "utf-8");
101
208
  return JSON.parse(raw);
@@ -150,9 +257,12 @@ function collectPages(config: VeluConfig): string[] {
150
257
  function validateVeluConfig(docsDir: string, schemaPath: string): { valid: boolean; errors: string[] } {
151
258
  const errors: string[] = [];
152
259
 
153
- const configPath = join(docsDir, "velu.json");
154
- if (!existsSync(configPath)) {
155
- return { valid: false, errors: [`velu.json not found at ${configPath}`] };
260
+ const configPath = resolveConfigPath(docsDir);
261
+ if (!configPath) {
262
+ return {
263
+ valid: false,
264
+ errors: [`docs.json or velu.json not found at ${join(docsDir, PRIMARY_CONFIG_NAME)}`],
265
+ };
156
266
  }
157
267
 
158
268
  if (!existsSync(schemaPath)) {
@@ -176,12 +286,15 @@ function validateVeluConfig(docsDir: string, schemaPath: string): { valid: boole
176
286
 
177
287
  const config = normalizeConfigNavigation(rawConfig);
178
288
 
179
- // Validate that all referenced .md files exist
289
+ // Validate that all referenced page files exist (.mdx or .md)
180
290
  const pages = collectPages(config);
181
291
  for (const page of pages) {
292
+ if (isOpenApiOperationReference(page)) continue;
293
+ if (isAsyncApiChannelReference(page)) continue;
294
+ const mdxPath = join(docsDir, `${page}.mdx`);
182
295
  const mdPath = join(docsDir, `${page}.md`);
183
- if (!existsSync(mdPath)) {
184
- errors.push(`Missing page: ${page}.md (expected at ${mdPath})`);
296
+ if (!existsSync(mdxPath) && !existsSync(mdPath)) {
297
+ errors.push(`Missing page: ${page}.md or ${page}.mdx (expected at ${mdPath})`);
185
298
  }
186
299
  }
187
300
 
@@ -1,87 +0,0 @@
1
- import type { ReactNode } from 'react';
2
- import { DocsLayout } from 'fumadocs-ui/layouts/docs';
3
- import { baseOptions } from '@/lib/layout.shared';
4
- import { source } from '@/lib/source';
5
- import { getLanguages, getVersionOptions, getProductOptions, type VeluVersionOption, type VeluProductOption } from '@/lib/velu';
6
- import { SidebarLinks } from '@/components/sidebar-links';
7
- import { ProductSwitcher } from '@/components/product-switcher';
8
-
9
- interface LayoutParams {
10
- slug?: string[];
11
- }
12
-
13
- interface SlugLayoutProps {
14
- children: ReactNode;
15
- params: Promise<LayoutParams>;
16
- }
17
-
18
- function resolveLocale(slugInput: string[] | undefined): string {
19
- const languages = getLanguages();
20
- const defaultLanguage = languages[0] ?? 'en';
21
- const slug = slugInput ?? [];
22
- const firstSeg = slug[0];
23
-
24
- return languages.includes(firstSeg ?? '') ? firstSeg! : defaultLanguage;
25
- }
26
-
27
- function resolveCurrentVersion(slugInput: string[] | undefined, versions: VeluVersionOption[]): VeluVersionOption | undefined {
28
- if (versions.length === 0) return undefined;
29
- const firstSeg = (slugInput ?? [])[0] ?? '';
30
- return versions.find((v) => v.slug === firstSeg) ?? versions.find((v) => v.isDefault) ?? versions[0];
31
- }
32
-
33
- function filterTreeBySlugPrefix<T extends { children?: unknown[] }>(tree: T, prefix?: string): T {
34
- if (!prefix) return tree;
35
-
36
- const children = Array.isArray(tree.children) ? tree.children : [];
37
-
38
- const filtered = children.filter((node) => {
39
- if (typeof node !== 'object' || node === null) return false;
40
- const entry = node as { url?: unknown; path?: unknown; $ref?: { metaFile?: unknown; file?: unknown } };
41
- const candidates = [entry.url, entry.path, entry.$ref?.metaFile, entry.$ref?.file]
42
- .filter((value): value is string => typeof value === 'string');
43
- return candidates.some((value) => value.includes(`${prefix}/`));
44
- });
45
-
46
- if (filtered.length === 0) return tree;
47
- return { ...tree, children: filtered } as T;
48
- }
49
-
50
- function resolveCurrentProduct(slugInput: string[] | undefined, products: VeluProductOption[]): VeluProductOption | undefined {
51
- if (products.length === 0) return undefined;
52
- const firstSeg = (slugInput ?? [])[0] ?? '';
53
- return products.find((p) => p.slug === firstSeg) ?? products[0];
54
- }
55
-
56
- export default async function SlugLayout({ children, params }: SlugLayoutProps) {
57
- const resolvedParams = await params;
58
- const locale = resolveLocale(resolvedParams.slug);
59
- const versions = getVersionOptions();
60
- const products = getProductOptions();
61
- const currentVersion = resolveCurrentVersion(resolvedParams.slug, versions);
62
- const currentProduct = resolveCurrentProduct(resolvedParams.slug, products);
63
-
64
- const activePrefix = currentVersion?.slug ?? currentProduct?.slug;
65
- const tree = filterTreeBySlugPrefix(source.getPageTree(locale), activePrefix);
66
-
67
- return (
68
- <DocsLayout
69
- tree={tree}
70
- sidebar={{
71
- collapsible: false,
72
- banner: products.length > 1 ? (
73
- <div className="velu-sidebar-banner">
74
- <ProductSwitcher products={products} />
75
- </div>
76
- ) : undefined,
77
- footer: <SidebarLinks />,
78
- }}
79
- {...baseOptions()}
80
- themeSwitch={{ enabled: false }}
81
- >
82
- {children}
83
- </DocsLayout>
84
- );
85
- }
86
-
87
- export { generateStaticParams } from './page';
@@ -1,146 +0,0 @@
1
- import type { Metadata } from 'next';
2
- import { notFound } from 'next/navigation';
3
- import { createRelativeLink } from 'fumadocs-ui/mdx';
4
- import {
5
- DocsBody,
6
- DocsDescription,
7
- DocsPage,
8
- DocsTitle,
9
- } from 'fumadocs-ui/layouts/docs/page';
10
- import { getMDXComponents } from '@/mdx-components';
11
- import { source } from '@/lib/source';
12
- import { getLanguages, getVersionOptions, getProductOptions } from '@/lib/velu';
13
- import { CopyPageButton } from '@/components/copy-page';
14
-
15
- interface RouteParams {
16
- slug?: string[];
17
- }
18
-
19
- interface PageProps {
20
- params: Promise<RouteParams>;
21
- }
22
-
23
- function resolveLocaleSlug(slugInput: string[] | undefined) {
24
- const languages = getLanguages();
25
- const defaultLanguage = languages[0] ?? 'en';
26
- const slug = slugInput ?? [];
27
- const firstSeg = slug[0];
28
- const hasLocalePrefix = languages.includes(firstSeg ?? '');
29
-
30
- return {
31
- defaultLanguage,
32
- locale: hasLocalePrefix ? firstSeg! : defaultLanguage,
33
- pageSlug: hasLocalePrefix ? slug.slice(1) : slug,
34
- };
35
- }
36
-
37
- function resolveContextFromSlug(slugInput: string[] | undefined) {
38
- const languages = getLanguages();
39
- const versions = getVersionOptions();
40
- const products = getProductOptions();
41
- const slug = slugInput ?? [];
42
-
43
- // Check for language prefix
44
- const firstSeg = slug[0];
45
- const hasLocalePrefix = languages.includes(firstSeg ?? '');
46
- const locale = hasLocalePrefix ? firstSeg! : (languages[0] ?? 'en');
47
- const remainingSlug = hasLocalePrefix ? slug.slice(1) : slug;
48
-
49
- // Check for version/product in remaining slug
50
- const contextSeg = remainingSlug[0] ?? '';
51
- const version = versions.find((v) => v.slug === contextSeg);
52
- const product = products.find((p) => p.slug === contextSeg);
53
-
54
- return {
55
- locale,
56
- version: version?.slug,
57
- product: product?.slug,
58
- };
59
- }
60
-
61
- export default async function Page({ params }: PageProps) {
62
- const resolvedParams = await params;
63
- const { locale, pageSlug } = resolveLocaleSlug(resolvedParams.slug);
64
- const { locale: filterLocale, version, product } = resolveContextFromSlug(resolvedParams.slug);
65
- const hasI18n = getLanguages().length > 1;
66
-
67
- const page = hasI18n ? source.getPage(pageSlug, locale) : source.getPage(pageSlug);
68
-
69
- if (!page) notFound();
70
-
71
- const MDX = page.data.body;
72
-
73
- // Build pagefind filter attributes
74
- const metaAttrs: string[] = [`title:${page.data.title}`];
75
- const filterAttrs: string[] = [];
76
- if (hasI18n) {
77
- metaAttrs.push(`language:${filterLocale}`);
78
- filterAttrs.push(`language:${filterLocale}`);
79
- }
80
- if (version) {
81
- metaAttrs.push(`version:${version}`);
82
- filterAttrs.push(`version:${version}`);
83
- }
84
- if (product) {
85
- metaAttrs.push(`product:${product}`);
86
- filterAttrs.push(`product:${product}`);
87
- }
88
-
89
- return (
90
- <DocsPage toc={page.data.toc} full={page.data.full}>
91
- <div
92
- data-pagefind-body
93
- data-pagefind-meta={metaAttrs.join(',')}
94
- data-pagefind-filter={filterAttrs.length > 0 ? filterAttrs.join(',') : undefined}
95
- >
96
- <div className="velu-title-row">
97
- <DocsTitle>{page.data.title}</DocsTitle>
98
- <CopyPageButton />
99
- </div>
100
- {page.data.description ? <DocsDescription>{page.data.description}</DocsDescription> : null}
101
- <DocsBody>
102
- <MDX
103
- components={getMDXComponents({
104
- a: createRelativeLink(source, page),
105
- })}
106
- />
107
- </DocsBody>
108
- </div>
109
- <footer className="velu-footer">
110
- Powered by <a href="https://getvelu.com" target="_blank" rel="noopener noreferrer">Velu</a>
111
- </footer>
112
- </DocsPage>
113
- );
114
- }
115
-
116
- export async function generateStaticParams() {
117
- const generated = source.generateParams('slug') as Array<{ slug?: string[] }>;
118
- const seen = new Set<string>();
119
-
120
- const nonRoot = generated.filter((entry) => {
121
- const slug = entry.slug ?? [];
122
- if (slug.length === 0) return false;
123
- const key = slug.join('/');
124
- if (seen.has(key)) return false;
125
- seen.add(key);
126
- return true;
127
- });
128
-
129
- // Include root variants for optional catch-all [[...slug]] in export mode.
130
- return [{}, { slug: undefined }, { slug: [] }, ...nonRoot];
131
- }
132
-
133
- export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
134
- const resolvedParams = await params;
135
- const { locale, pageSlug } = resolveLocaleSlug(resolvedParams.slug);
136
- const hasI18n = getLanguages().length > 1;
137
-
138
- const page = hasI18n ? source.getPage(pageSlug, locale) : source.getPage(pageSlug);
139
-
140
- if (!page) notFound();
141
-
142
- return {
143
- title: page.data.title,
144
- description: page.data.description,
145
- };
146
- }