@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.
Files changed (65) hide show
  1. package/package.json +15 -6
  2. package/schema/velu.schema.json +1864 -30
  3. package/src/build.ts +1161 -180
  4. package/src/cli.ts +121 -16
  5. package/src/engine/_server.mjs +1708 -192
  6. package/src/engine/app/(docs)/[...slug]/layout.tsx +377 -0
  7. package/src/engine/app/(docs)/[...slug]/page.tsx +917 -0
  8. package/src/engine/app/(docs)/layout.tsx +1 -13
  9. package/src/engine/app/api/proxy/route.ts +23 -0
  10. package/src/engine/app/copy-page.css +59 -1
  11. package/src/engine/app/global.css +3487 -6
  12. package/src/engine/app/layout.tsx +59 -8
  13. package/src/engine/app/llms-file/route.ts +87 -0
  14. package/src/engine/app/llms-full-file/route.ts +62 -0
  15. package/src/engine/app/md-file/[...slug]/route.ts +409 -0
  16. package/src/engine/app/page.tsx +45 -0
  17. package/src/engine/app/robots.txt/route.ts +61 -0
  18. package/src/engine/app/rss-file/[...slug]/route.ts +176 -0
  19. package/src/engine/app/search.css +20 -0
  20. package/src/engine/app/sitemap.xml/route.ts +80 -0
  21. package/src/engine/components/assistant.tsx +16 -5
  22. package/src/engine/components/changelog-filters.tsx +114 -0
  23. package/src/engine/components/code-group.tsx +383 -0
  24. package/src/engine/components/color.tsx +118 -0
  25. package/src/engine/components/expandable.tsx +77 -0
  26. package/src/engine/components/icon.tsx +136 -0
  27. package/src/engine/components/image-zoom-fallback.tsx +147 -0
  28. package/src/engine/components/image.tsx +111 -0
  29. package/src/engine/components/lang-switcher.tsx +95 -0
  30. package/src/engine/components/manual-api-playground.tsx +154 -0
  31. package/src/engine/components/mermaid.tsx +142 -0
  32. package/src/engine/components/openapi-toc-sync.tsx +59 -0
  33. package/src/engine/components/openapi.tsx +1679 -0
  34. package/src/engine/components/page-feedback.tsx +153 -0
  35. package/src/engine/components/product-switcher.tsx +102 -0
  36. package/src/engine/components/prompt.tsx +90 -0
  37. package/src/engine/components/providers.tsx +21 -0
  38. package/src/engine/components/search.tsx +70 -3
  39. package/src/engine/components/sidebar-links.tsx +49 -0
  40. package/src/engine/components/synced-tabs.tsx +57 -0
  41. package/src/engine/components/theme-toggle.tsx +39 -0
  42. package/src/engine/components/toc-examples.tsx +110 -0
  43. package/src/engine/components/version-switcher.tsx +89 -0
  44. package/src/engine/components/view.tsx +344 -0
  45. package/src/engine/generated/redirects.ts +3 -0
  46. package/src/engine/lib/changelog.ts +246 -0
  47. package/src/engine/lib/layout.shared.ts +57 -7
  48. package/src/engine/lib/llms.ts +444 -0
  49. package/src/engine/lib/navigation-normalize.mjs +525 -0
  50. package/src/engine/lib/navigation-normalize.ts +695 -0
  51. package/src/engine/lib/redirects.ts +194 -0
  52. package/src/engine/lib/source.ts +121 -4
  53. package/src/engine/lib/velu.ts +635 -5
  54. package/src/engine/mdx-components.tsx +648 -0
  55. package/src/engine/middleware.ts +66 -0
  56. package/src/engine/next.config.mjs +2 -2
  57. package/src/engine/public/icons/cursor-dark.svg +12 -0
  58. package/src/engine/public/icons/cursor-light.svg +12 -0
  59. package/src/engine/source.config.ts +98 -1
  60. package/src/engine/src/components/PageTitle.astro +16 -5
  61. package/src/engine/src/lib/velu.ts +97 -16
  62. package/src/navigation-normalize.ts +686 -0
  63. package/src/themes.ts +6 -6
  64. package/src/validate.ts +235 -24
  65. package/src/engine/app/(docs)/[[...slug]]/page.tsx +0 -69
package/src/themes.ts CHANGED
@@ -129,11 +129,11 @@ function generateThemeCss(config: ThemeConfig): string {
129
129
  if (lightAccent) {
130
130
  const palette = deriveAccentPalette(lightAccent);
131
131
  lines.push(":root {");
132
- lines.push(` --color-fd-primary: ${palette.light.accent};`);
133
- lines.push(` --color-fd-primary-foreground: ${textColorFor(palette.light.accent)};`);
132
+ lines.push(` --color-fd-primary: ${lightAccent};`);
133
+ lines.push(` --color-fd-primary-foreground: ${textColorFor(lightAccent)};`);
134
134
  lines.push(` --color-fd-accent: ${palette.light.accentLow};`);
135
135
  lines.push(` --color-fd-accent-foreground: ${textColorFor(palette.light.accentLow)};`);
136
- lines.push(` --color-fd-ring: ${palette.light.accent};`);
136
+ lines.push(` --color-fd-ring: ${lightAccent};`);
137
137
  lines.push("}");
138
138
  lines.push("");
139
139
  }
@@ -141,11 +141,11 @@ function generateThemeCss(config: ThemeConfig): string {
141
141
  if (darkAccent) {
142
142
  const palette = deriveAccentPalette(darkAccent);
143
143
  lines.push(".dark {");
144
- lines.push(` --color-fd-primary: ${palette.dark.accent};`);
145
- lines.push(` --color-fd-primary-foreground: ${textColorFor(palette.dark.accent)};`);
144
+ lines.push(` --color-fd-primary: ${darkAccent};`);
145
+ lines.push(` --color-fd-primary-foreground: ${textColorFor(darkAccent)};`);
146
146
  lines.push(` --color-fd-accent: ${palette.dark.accentLow};`);
147
147
  lines.push(` --color-fd-accent-foreground: ${textColorFor(palette.dark.accentLow)};`);
148
- lines.push(` --color-fd-ring: ${palette.dark.accent};`);
148
+ lines.push(` --color-fd-ring: ${darkAccent};`);
149
149
  lines.push("}");
150
150
  lines.push("");
151
151
  }
package/src/validate.ts CHANGED
@@ -2,57 +2,240 @@ import Ajv, { type AnySchema } from "ajv";
2
2
  import addFormats from "ajv-formats";
3
3
  import { readFileSync, existsSync } from "node:fs";
4
4
  import { resolve, join } from "node:path";
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
+ }
16
+
17
+ interface VeluSeparator {
18
+ separator: string;
19
+ }
20
+
21
+ interface VeluLink {
22
+ href: string;
23
+ label: string;
24
+ icon?: string;
25
+ iconType?: string;
26
+ }
27
+
28
+ interface VeluAnchor {
29
+ anchor: string;
30
+ href?: string;
31
+ icon?: string;
32
+ iconType?: string;
33
+ version?: string;
34
+ openapi?: VeluOpenApiSource;
35
+ asyncapi?: VeluOpenApiSource;
36
+ color?: {
37
+ light: string;
38
+ dark: string;
39
+ };
40
+ tabs?: VeluTab[];
41
+ hidden?: boolean;
42
+ }
43
+
44
+ interface VeluGlobalTab {
45
+ tab: string;
46
+ href: string;
47
+ icon?: string;
48
+ iconType?: string;
49
+ }
5
50
 
6
51
  interface VeluGroup {
7
52
  group: string;
8
- slug: string;
53
+ slug?: string;
9
54
  icon?: string;
55
+ iconType?: string;
10
56
  tag?: string;
57
+ version?: string;
58
+ openapi?: VeluOpenApiSource;
59
+ asyncapi?: VeluOpenApiSource;
11
60
  expanded?: boolean;
12
- pages: (string | VeluGroup)[];
61
+ description?: string;
62
+ hidden?: boolean;
63
+ pages: (string | VeluGroup | VeluSeparator | VeluLink)[];
64
+ }
65
+
66
+ interface VeluMenuItem {
67
+ item: string;
68
+ icon?: string;
69
+ iconType?: string;
70
+ openapi?: VeluOpenApiSource;
71
+ asyncapi?: VeluOpenApiSource;
72
+ groups?: VeluGroup[];
73
+ pages?: (string | VeluSeparator | VeluLink)[];
13
74
  }
14
75
 
15
76
  interface VeluTab {
16
77
  tab: string;
17
- slug: string;
78
+ slug?: string;
18
79
  icon?: string;
80
+ iconType?: string;
81
+ version?: string;
19
82
  href?: string;
20
- pages?: string[];
83
+ openapi?: VeluOpenApiSource;
84
+ asyncapi?: VeluOpenApiSource;
85
+ pages?: (string | VeluSeparator | VeluLink)[];
21
86
  groups?: VeluGroup[];
87
+ menu?: VeluMenuItem[];
88
+ }
89
+
90
+ interface VeluLanguageNav {
91
+ language: string;
92
+ openapi?: VeluOpenApiSource;
93
+ asyncapi?: VeluOpenApiSource;
94
+ tabs: VeluTab[];
95
+ }
96
+
97
+ interface VeluProductNav {
98
+ product: string;
99
+ icon?: string;
100
+ iconType?: string;
101
+ openapi?: VeluOpenApiSource;
102
+ asyncapi?: VeluOpenApiSource;
103
+ tabs?: VeluTab[];
104
+ pages?: (string | VeluSeparator | VeluLink)[];
22
105
  }
23
106
 
107
+ interface VeluVersionNav {
108
+ version: string;
109
+ openapi?: VeluOpenApiSource;
110
+ asyncapi?: VeluOpenApiSource;
111
+ tabs: VeluTab[];
112
+ }
113
+
114
+ type VeluOpenApiSource = string | string[] | Record<string, unknown>;
115
+
24
116
  interface VeluConfig {
25
117
  $schema?: string;
118
+ icons?: {
119
+ library?: "fontawesome" | "lucide" | "tabler";
120
+ };
26
121
  theme?: string;
27
122
  colors?: { primary?: string; light?: string; dark?: string };
28
123
  appearance?: "system" | "light" | "dark";
29
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
+ };
30
148
  navigation: {
31
- tabs: VeluTab[];
149
+ openapi?: VeluOpenApiSource;
150
+ asyncapi?: VeluOpenApiSource;
151
+ tabs?: VeluTab[];
152
+ languages?: VeluLanguageNav[];
153
+ products?: VeluProductNav[];
154
+ versions?: VeluVersionNav[];
155
+ anchors?: VeluAnchor[];
156
+ global?: {
157
+ anchors?: VeluAnchor[];
158
+ tabs?: VeluGlobalTab[];
159
+ };
32
160
  };
33
161
  }
34
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
+
35
206
  function loadJson(filePath: string): unknown {
36
207
  const raw = readFileSync(filePath, "utf-8");
37
208
  return JSON.parse(raw);
38
209
  }
39
210
 
40
- function collectPages(config: VeluConfig): string[] {
211
+ function isGroup(item: unknown): item is VeluGroup {
212
+ return typeof item === "object" && item !== null && "group" in item;
213
+ }
214
+
215
+ function isPageString(item: unknown): item is string {
216
+ return typeof item === "string";
217
+ }
218
+
219
+ function collectPagesFromTabs(tabs: VeluTab[]): string[] {
41
220
  const pages: string[] = [];
42
221
 
43
222
  function collectFromGroup(group: VeluGroup) {
44
223
  for (const item of group.pages) {
45
- if (typeof item === "string") {
224
+ if (isPageString(item)) {
46
225
  pages.push(item);
47
- } else {
226
+ } else if (isGroup(item)) {
48
227
  collectFromGroup(item);
49
228
  }
50
229
  }
51
230
  }
52
231
 
53
- for (const tab of config.navigation.tabs) {
232
+ for (const tab of tabs) {
54
233
  if (tab.pages) {
55
- pages.push(...tab.pages);
234
+ for (const item of tab.pages) {
235
+ if (isPageString(item)) {
236
+ pages.push(item);
237
+ }
238
+ }
56
239
  }
57
240
  if (tab.groups) {
58
241
  for (const group of tab.groups) {
@@ -64,12 +247,22 @@ function collectPages(config: VeluConfig): string[] {
64
247
  return pages;
65
248
  }
66
249
 
250
+ function collectPages(config: VeluConfig): string[] {
251
+ const tabs = config.navigation.languages && config.navigation.languages.length > 0
252
+ ? config.navigation.languages.flatMap((lang) => lang.tabs)
253
+ : (config.navigation.tabs ?? []);
254
+ return collectPagesFromTabs(tabs);
255
+ }
256
+
67
257
  function validateVeluConfig(docsDir: string, schemaPath: string): { valid: boolean; errors: string[] } {
68
258
  const errors: string[] = [];
69
259
 
70
- const configPath = join(docsDir, "velu.json");
71
- if (!existsSync(configPath)) {
72
- 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
+ };
73
266
  }
74
267
 
75
268
  if (!existsSync(schemaPath)) {
@@ -77,13 +270,13 @@ function validateVeluConfig(docsDir: string, schemaPath: string): { valid: boole
77
270
  }
78
271
 
79
272
  const schema = loadJson(schemaPath) as AnySchema;
80
- const config = loadJson(configPath) as VeluConfig;
273
+ const rawConfig = loadJson(configPath) as VeluConfig;
81
274
 
82
275
  // Validate against JSON schema
83
276
  const ajv = new Ajv({ allErrors: true, strict: false });
84
277
  addFormats(ajv);
85
278
  const validate = ajv.compile(schema);
86
- const schemaValid = validate(config);
279
+ const schemaValid = validate(rawConfig);
87
280
 
88
281
  if (!schemaValid && validate.errors) {
89
282
  for (const err of validate.errors) {
@@ -91,25 +284,43 @@ function validateVeluConfig(docsDir: string, schemaPath: string): { valid: boole
91
284
  }
92
285
  }
93
286
 
94
- // Validate that all referenced .md files exist
287
+ const config = normalizeConfigNavigation(rawConfig);
288
+
289
+ // Validate that all referenced page files exist (.mdx or .md)
95
290
  const pages = collectPages(config);
96
291
  for (const page of pages) {
292
+ if (isOpenApiOperationReference(page)) continue;
293
+ if (isAsyncApiChannelReference(page)) continue;
294
+ const mdxPath = join(docsDir, `${page}.mdx`);
97
295
  const mdPath = join(docsDir, `${page}.md`);
98
- if (!existsSync(mdPath)) {
99
- 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})`);
100
298
  }
101
299
  }
102
300
 
103
301
  // Check for duplicate page references
104
- const seen = new Set<string>();
105
- for (const page of pages) {
106
- if (seen.has(page)) {
107
- errors.push(`Duplicate page reference: ${page}`);
302
+ if (config.navigation.languages && config.navigation.languages.length > 0) {
303
+ for (const lang of config.navigation.languages) {
304
+ const seen = new Set<string>();
305
+ const langPages = collectPagesFromTabs(lang.tabs);
306
+ for (const page of langPages) {
307
+ if (seen.has(page)) {
308
+ errors.push(`Duplicate page reference in language '${lang.language}': ${page}`);
309
+ }
310
+ seen.add(page);
311
+ }
312
+ }
313
+ } else {
314
+ const seen = new Set<string>();
315
+ for (const page of pages) {
316
+ if (seen.has(page)) {
317
+ errors.push(`Duplicate page reference: ${page}`);
318
+ }
319
+ seen.add(page);
108
320
  }
109
- seen.add(page);
110
321
  }
111
322
 
112
323
  return { valid: errors.length === 0, errors };
113
324
  }
114
325
 
115
- export { validateVeluConfig, collectPages, VeluConfig, VeluGroup, VeluTab };
326
+ export { validateVeluConfig, collectPages, VeluConfig, VeluGroup, VeluTab, VeluSeparator, VeluLink, VeluAnchor };
@@ -1,69 +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 { CopyPageButton } from '@/components/copy-page';
13
-
14
- interface RouteParams {
15
- slug?: string[];
16
- }
17
-
18
- interface PageProps {
19
- params: Promise<RouteParams>;
20
- }
21
-
22
- export default async function Page({ params }: PageProps) {
23
- const resolvedParams = await params;
24
- const page = source.getPage(resolvedParams.slug);
25
-
26
- if (!page) notFound();
27
-
28
- const MDX = page.data.body;
29
-
30
- return (
31
- <DocsPage toc={page.data.toc} full={page.data.full}>
32
- <div data-pagefind-body data-pagefind-meta={`title:${page.data.title}`}>
33
- <div className="velu-title-row">
34
- <DocsTitle>{page.data.title}</DocsTitle>
35
- <CopyPageButton />
36
- </div>
37
- {page.data.description ? <DocsDescription>{page.data.description}</DocsDescription> : null}
38
- <DocsBody>
39
- <MDX
40
- components={getMDXComponents({
41
- a: createRelativeLink(source, page),
42
- })}
43
- />
44
- </DocsBody>
45
- </div>
46
- <footer className="velu-footer">
47
- Powered by <a href="https://getvelu.com" target="_blank" rel="noopener noreferrer">Velu</a>
48
- </footer>
49
- </DocsPage>
50
- );
51
- }
52
-
53
- export async function generateStaticParams() {
54
- const params = source.generateParams();
55
- // Include root path for the optional catch-all [[...slug]]
56
- return [{ slug: [] }, ...params];
57
- }
58
-
59
- export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
60
- const resolvedParams = await params;
61
- const page = source.getPage(resolvedParams.slug);
62
-
63
- if (!page) notFound();
64
-
65
- return {
66
- title: page.data.title,
67
- description: page.data.description,
68
- };
69
- }