@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,194 @@
1
+ export interface RedirectRule {
2
+ source: string;
3
+ destination: string;
4
+ permanent?: boolean;
5
+ }
6
+
7
+ interface RedirectCaptureDescriptor {
8
+ kind: "named" | "star";
9
+ key: string;
10
+ }
11
+
12
+ interface CompiledRedirectRule {
13
+ source: string;
14
+ destination: string;
15
+ permanent: boolean;
16
+ matcher: RegExp;
17
+ captures: RedirectCaptureDescriptor[];
18
+ }
19
+
20
+ const INVALID_PATH_PARTS = /[?#]/;
21
+ const EXTERNAL_PROTOCOL = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
22
+ const NAMED_WILDCARD = /^:([A-Za-z_][A-Za-z0-9_-]*)\*$/;
23
+ const NAMED_SEGMENT = /^:([A-Za-z_][A-Za-z0-9_-]*)$/;
24
+
25
+ function isObject(value: unknown): value is Record<string, unknown> {
26
+ return typeof value === "object" && value !== null;
27
+ }
28
+
29
+ function escapeRegExp(input: string): string {
30
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31
+ }
32
+
33
+ export function isExternalDestination(value: string): boolean {
34
+ return EXTERNAL_PROTOCOL.test(value);
35
+ }
36
+
37
+ function normalizePath(input: string): string {
38
+ const trimmed = input.trim();
39
+ if (trimmed.length === 0) return "/";
40
+
41
+ const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
42
+ const collapsed = withLeadingSlash.replace(/\/{2,}/g, "/");
43
+ if (collapsed !== "/" && collapsed.endsWith("/")) {
44
+ return collapsed.slice(0, -1);
45
+ }
46
+ return collapsed;
47
+ }
48
+
49
+ function compileSourcePattern(source: string): { matcher: RegExp; captures: RedirectCaptureDescriptor[] } {
50
+ const normalized = normalizePath(source);
51
+ if (normalized === "/") {
52
+ return { matcher: /^\/$/, captures: [] };
53
+ }
54
+
55
+ const captures: RedirectCaptureDescriptor[] = [];
56
+ let starIndex = 0;
57
+ const segments = normalized.slice(1).split("/");
58
+ let pattern = "^";
59
+
60
+ for (let i = 0; i < segments.length; i += 1) {
61
+ const segment = segments[i];
62
+ pattern += "/";
63
+
64
+ const namedWildcard = segment.match(NAMED_WILDCARD);
65
+ if (namedWildcard) {
66
+ captures.push({ kind: "named", key: namedWildcard[1] });
67
+ pattern += "(.*)";
68
+ continue;
69
+ }
70
+
71
+ const namedSegment = segment.match(NAMED_SEGMENT);
72
+ if (namedSegment) {
73
+ captures.push({ kind: "named", key: namedSegment[1] });
74
+ pattern += "([^/]+)";
75
+ continue;
76
+ }
77
+
78
+ if (!segment.includes("*")) {
79
+ pattern += escapeRegExp(segment);
80
+ continue;
81
+ }
82
+
83
+ let segmentPattern = "";
84
+ for (const char of segment) {
85
+ if (char === "*") {
86
+ const key = String(starIndex);
87
+ starIndex += 1;
88
+ captures.push({ kind: "star", key });
89
+ segmentPattern += "([^/]*)";
90
+ } else {
91
+ segmentPattern += escapeRegExp(char);
92
+ }
93
+ }
94
+ pattern += segmentPattern;
95
+ }
96
+
97
+ pattern += "/?$";
98
+ return { matcher: new RegExp(pattern), captures };
99
+ }
100
+
101
+ function substituteDestination(
102
+ destination: string,
103
+ captures: RedirectCaptureDescriptor[],
104
+ values: string[]
105
+ ): string {
106
+ const named = new Map<string, string>();
107
+ const stars: string[] = [];
108
+
109
+ for (let i = 0; i < captures.length; i += 1) {
110
+ const capture = captures[i];
111
+ const value = values[i] ?? "";
112
+ if (capture.kind === "named") {
113
+ named.set(capture.key, value);
114
+ } else {
115
+ stars.push(value);
116
+ }
117
+ }
118
+
119
+ let resolved = destination;
120
+ resolved = resolved.replace(/:([A-Za-z_][A-Za-z0-9_-]*)\*/g, (_, key: string) => named.get(key) ?? "");
121
+ resolved = resolved.replace(/:([A-Za-z_][A-Za-z0-9_-]*)/g, (_, key: string) => named.get(key) ?? "");
122
+
123
+ let starIndex = 0;
124
+ resolved = resolved.replace(/\*/g, () => stars[starIndex++] ?? "");
125
+
126
+ if (isExternalDestination(resolved)) return resolved;
127
+ return normalizePath(resolved);
128
+ }
129
+
130
+ export function normalizeRedirectRules(value: unknown): RedirectRule[] {
131
+ if (!Array.isArray(value)) return [];
132
+
133
+ const redirects: RedirectRule[] = [];
134
+ for (const entry of value) {
135
+ if (!isObject(entry)) continue;
136
+ if (typeof entry.source !== "string" || typeof entry.destination !== "string") continue;
137
+
138
+ const source = entry.source.trim();
139
+ const destination = entry.destination.trim();
140
+ if (source.length === 0 || destination.length === 0) continue;
141
+ if (INVALID_PATH_PARTS.test(source) || INVALID_PATH_PARTS.test(destination)) continue;
142
+
143
+ const normalizedSource = normalizePath(source);
144
+ const normalizedDestination = isExternalDestination(destination)
145
+ ? destination
146
+ : normalizePath(destination);
147
+ const permanent = entry.permanent === false ? false : true;
148
+
149
+ if (!isExternalDestination(normalizedDestination) && normalizedSource === normalizedDestination) continue;
150
+ redirects.push({
151
+ source: normalizedSource,
152
+ destination: normalizedDestination,
153
+ permanent,
154
+ });
155
+ }
156
+
157
+ return redirects;
158
+ }
159
+
160
+ export function compileRedirectRules(rules: RedirectRule[]): CompiledRedirectRule[] {
161
+ return rules.map((rule) => {
162
+ const compiled = compileSourcePattern(rule.source);
163
+ return {
164
+ source: rule.source,
165
+ destination: rule.destination,
166
+ permanent: rule.permanent !== false,
167
+ matcher: compiled.matcher,
168
+ captures: compiled.captures,
169
+ };
170
+ });
171
+ }
172
+
173
+ export function resolveRedirect(
174
+ pathname: string,
175
+ compiledRules: CompiledRedirectRule[]
176
+ ): { destination: string; statusCode: 307 | 308 } | null {
177
+ const normalizedPath = normalizePath(pathname);
178
+
179
+ for (const rule of compiledRules) {
180
+ const match = normalizedPath.match(rule.matcher);
181
+ if (!match) continue;
182
+
183
+ const values = match.slice(1);
184
+ const destination = substituteDestination(rule.destination, rule.captures, values);
185
+
186
+ if (!isExternalDestination(destination) && destination === normalizedPath) continue;
187
+ return {
188
+ destination,
189
+ statusCode: rule.permanent ? 308 : 307,
190
+ };
191
+ }
192
+
193
+ return null;
194
+ }
@@ -1,15 +1,118 @@
1
1
  import { loader } from 'fumadocs-core/source';
2
- import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
3
- import { docs } from 'fumadocs-mdx:collections/server';
2
+ import { statusBadgesPlugin } from 'fumadocs-core/source/status-badges';
3
+ import * as mdxCollections from 'fumadocs-mdx:collections/server';
4
+ import { createElement } from 'react';
4
5
  import { getLanguages } from '@/lib/velu';
5
6
 
6
7
  const languages = getLanguages();
7
8
  const defaultLanguage = languages[0] ?? 'en';
9
+ const OPENAPI_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'TRACE', 'WEBHOOK']);
10
+ const docsCollection = (mdxCollections as { docs?: { toFumadocsSource?: () => unknown } }).docs;
11
+
12
+ if (!docsCollection?.toFumadocsSource) {
13
+ throw new Error('MDX collections are not ready yet. Please retry in a moment.');
14
+ }
15
+
16
+ function methodBadgeClass(method: string): string {
17
+ const upper = method.toUpperCase();
18
+ if (upper === 'POST') return 'velu-openapi-method-badge velu-openapi-method-post';
19
+ if (upper === 'PUT') return 'velu-openapi-method-badge velu-openapi-method-put';
20
+ if (upper === 'PATCH') return 'velu-openapi-method-badge velu-openapi-method-patch';
21
+ if (upper === 'DELETE') return 'velu-openapi-method-badge velu-openapi-method-delete';
22
+ if (upper === 'WEBHOOK') return 'velu-openapi-method-badge velu-openapi-method-webhook';
23
+ return 'velu-openapi-method-badge velu-openapi-method-get';
24
+ }
25
+
26
+ function parseOperationReference(value: string, requireUppercaseMethod = false): { method: string; target: string } | null {
27
+ const trimmed = value.trim();
28
+ if (!trimmed) return null;
29
+ const withSpec = trimmed.match(/^(\S+)\s+([A-Za-z]+)\s+(.+)$/);
30
+ if (withSpec) {
31
+ const rawMethod = withSpec[2];
32
+ const method = withSpec[2].toUpperCase();
33
+ if (requireUppercaseMethod && rawMethod !== method) return null;
34
+ if (!OPENAPI_METHODS.has(method)) return null;
35
+ return { method, target: withSpec[3].trim() };
36
+ }
37
+ const noSpec = trimmed.match(/^([A-Za-z]+)\s+(.+)$/);
38
+ if (noSpec) {
39
+ const rawMethod = noSpec[1];
40
+ const method = noSpec[1].toUpperCase();
41
+ if (requireUppercaseMethod && rawMethod !== method) return null;
42
+ if (!OPENAPI_METHODS.has(method)) return null;
43
+ return { method, target: noSpec[2].trim() };
44
+ }
45
+ return null;
46
+ }
47
+
48
+ function stripMethodPrefix(name: string, method: string): string {
49
+ const regex = new RegExp(`^${method}\\s+`, 'i');
50
+ return name.replace(regex, '').trim();
51
+ }
52
+
53
+ function openApiSidebarMethodBadgePlugin() {
54
+ return {
55
+ name: 'velu:openapi-sidebar-method-badge',
56
+ transformPageTree: {
57
+ file(node: Record<string, unknown>, filePath?: string) {
58
+ let data: Record<string, unknown> = {};
59
+ if (filePath) {
60
+ const file = (this as { storage?: { read?: (path: string) => unknown } }).storage?.read?.(filePath) as
61
+ | { format?: string; data?: Record<string, unknown> }
62
+ | undefined;
63
+ if (file?.format === 'page') data = file.data ?? {};
64
+ }
65
+
66
+ const nameCandidate = typeof node.name === 'string' ? node.name.trim() : '';
67
+ const titleCandidate = typeof data.title === 'string' ? data.title.trim() : '';
68
+ const openApiCandidate = typeof data.openapi === 'string' ? data.openapi.trim() : '';
69
+ const parsed = openApiCandidate
70
+ ? parseOperationReference(openApiCandidate)
71
+ : parseOperationReference(nameCandidate, true) ?? parseOperationReference(titleCandidate, true);
72
+ if (!parsed) return node;
73
+
74
+ const method = parsed.method;
75
+ const rawName = nameCandidate || titleCandidate || parsed.target;
76
+ const text = stripMethodPrefix(rawName, method) || parsed.target || rawName || method;
77
+ const stableIdRaw = filePath || openApiCandidate || rawName || `${method}-${parsed.target}`;
78
+ const stableId = stableIdRaw.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
79
+
80
+ node.name = createElement(
81
+ 'span',
82
+ { className: 'velu-openapi-sidebar-item', key: `openapi-item-${stableId || 'unknown'}` },
83
+ createElement(
84
+ 'span',
85
+ { className: methodBadgeClass(method), key: `openapi-item-${stableId || 'unknown'}-method` },
86
+ method,
87
+ ),
88
+ createElement(
89
+ 'span',
90
+ { className: 'velu-openapi-sidebar-label', key: `openapi-item-${stableId || 'unknown'}-label` },
91
+ text,
92
+ ),
93
+ );
94
+ return node;
95
+ },
96
+ },
97
+ };
98
+ }
8
99
 
9
100
  export const source = loader({
10
101
  baseUrl: '/',
11
- source: docs.toFumadocsSource(),
12
- plugins: [lucideIconsPlugin()],
102
+ source: docsCollection.toFumadocsSource() as any,
103
+ plugins: [
104
+ openApiSidebarMethodBadgePlugin() as any,
105
+ statusBadgesPlugin({
106
+ renderBadge: (status: string) => {
107
+ const normalized = status.trim().toLowerCase();
108
+ const label = normalized === 'deprecated' ? 'Deprecated' : status;
109
+ const className = normalized === 'deprecated'
110
+ ? 'velu-status-badge velu-status-badge-deprecated'
111
+ : 'velu-status-badge';
112
+ return createElement('span', { className, 'data-status': normalized }, label);
113
+ },
114
+ }),
115
+ ],
13
116
  i18n:
14
117
  languages.length > 1
15
118
  ? {