@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.
- package/package.json +15 -6
- package/schema/velu.schema.json +1864 -30
- package/src/build.ts +1161 -180
- package/src/cli.ts +121 -16
- package/src/engine/_server.mjs +1708 -192
- package/src/engine/app/(docs)/[...slug]/layout.tsx +377 -0
- package/src/engine/app/(docs)/[...slug]/page.tsx +917 -0
- package/src/engine/app/(docs)/layout.tsx +1 -13
- package/src/engine/app/api/proxy/route.ts +23 -0
- package/src/engine/app/copy-page.css +59 -1
- package/src/engine/app/global.css +3487 -6
- package/src/engine/app/layout.tsx +59 -8
- package/src/engine/app/llms-file/route.ts +87 -0
- package/src/engine/app/llms-full-file/route.ts +62 -0
- package/src/engine/app/md-file/[...slug]/route.ts +409 -0
- package/src/engine/app/page.tsx +45 -0
- package/src/engine/app/robots.txt/route.ts +61 -0
- package/src/engine/app/rss-file/[...slug]/route.ts +176 -0
- package/src/engine/app/search.css +20 -0
- package/src/engine/app/sitemap.xml/route.ts +80 -0
- package/src/engine/components/assistant.tsx +16 -5
- package/src/engine/components/changelog-filters.tsx +114 -0
- package/src/engine/components/code-group.tsx +383 -0
- package/src/engine/components/color.tsx +118 -0
- package/src/engine/components/expandable.tsx +77 -0
- package/src/engine/components/icon.tsx +136 -0
- package/src/engine/components/image-zoom-fallback.tsx +147 -0
- package/src/engine/components/image.tsx +111 -0
- package/src/engine/components/lang-switcher.tsx +95 -0
- package/src/engine/components/manual-api-playground.tsx +154 -0
- package/src/engine/components/mermaid.tsx +142 -0
- package/src/engine/components/openapi-toc-sync.tsx +59 -0
- package/src/engine/components/openapi.tsx +1679 -0
- package/src/engine/components/page-feedback.tsx +153 -0
- package/src/engine/components/product-switcher.tsx +102 -0
- package/src/engine/components/prompt.tsx +90 -0
- package/src/engine/components/providers.tsx +21 -0
- package/src/engine/components/search.tsx +70 -3
- package/src/engine/components/sidebar-links.tsx +49 -0
- package/src/engine/components/synced-tabs.tsx +57 -0
- package/src/engine/components/theme-toggle.tsx +39 -0
- package/src/engine/components/toc-examples.tsx +110 -0
- package/src/engine/components/version-switcher.tsx +89 -0
- package/src/engine/components/view.tsx +344 -0
- package/src/engine/generated/redirects.ts +3 -0
- package/src/engine/lib/changelog.ts +246 -0
- package/src/engine/lib/layout.shared.ts +57 -7
- package/src/engine/lib/llms.ts +444 -0
- package/src/engine/lib/navigation-normalize.mjs +525 -0
- package/src/engine/lib/navigation-normalize.ts +695 -0
- package/src/engine/lib/redirects.ts +194 -0
- package/src/engine/lib/source.ts +121 -4
- package/src/engine/lib/velu.ts +635 -5
- package/src/engine/mdx-components.tsx +648 -0
- package/src/engine/middleware.ts +66 -0
- package/src/engine/next.config.mjs +2 -2
- package/src/engine/public/icons/cursor-dark.svg +12 -0
- package/src/engine/public/icons/cursor-light.svg +12 -0
- package/src/engine/source.config.ts +98 -1
- package/src/engine/src/components/PageTitle.astro +16 -5
- package/src/engine/src/lib/velu.ts +97 -16
- package/src/navigation-normalize.ts +686 -0
- package/src/themes.ts +6 -6
- package/src/validate.ts +235 -24
- package/src/engine/app/(docs)/[[...slug]]/page.tsx +0 -69
package/src/engine/_server.mjs
CHANGED
|
@@ -1,271 +1,1787 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
|
-
import { watch } from 'node:fs';
|
|
4
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
-
import { dirname, extname, join, resolve } from 'node:path';
|
|
6
|
-
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { watch } from 'node:fs';
|
|
4
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, extname, join, relative, resolve } from 'node:path';
|
|
6
|
+
import { normalizeConfigNavigation } from './lib/navigation-normalize.mjs';
|
|
7
|
+
|
|
7
8
|
const require = createRequire(import.meta.url);
|
|
8
9
|
const nextBinPath = require.resolve('next/dist/bin/next');
|
|
10
|
+
const { parse: parseYaml } = require('yaml');
|
|
11
|
+
|
|
12
|
+
// ── Docs directory (passed via env var from CLI) ────────────────────────────
|
|
13
|
+
const docsDir = process.env.VELU_DOCS_DIR || resolve('..');
|
|
14
|
+
const contentDir = resolve('content', 'docs');
|
|
15
|
+
const publicDir = resolve('public');
|
|
16
|
+
const PRIMARY_CONFIG_NAME = 'docs.json';
|
|
17
|
+
const LEGACY_CONFIG_NAME = 'velu.json';
|
|
18
|
+
const SOURCE_MIRROR_DIR = 'velu-imports';
|
|
19
|
+
const sourceMirrorDir = resolve(SOURCE_MIRROR_DIR);
|
|
20
|
+
const STATIC_EXTENSIONS = new Set([
|
|
21
|
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico',
|
|
22
|
+
'.mp4', '.webm',
|
|
23
|
+
'.mp3', '.wav',
|
|
24
|
+
'.json', '.yaml', '.yml',
|
|
25
|
+
'.css',
|
|
26
|
+
'.js',
|
|
27
|
+
'.woff', '.woff2', '.ttf', '.eot',
|
|
28
|
+
'.pdf', '.txt',
|
|
29
|
+
'.xml', '.csv',
|
|
30
|
+
'.zip',
|
|
31
|
+
]);
|
|
32
|
+
const SOURCE_MIRROR_EXTENSIONS = new Set([
|
|
33
|
+
'.md', '.mdx', '.jsx', '.js', '.tsx', '.ts',
|
|
34
|
+
'.json', '.yaml', '.yml', '.css',
|
|
35
|
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico',
|
|
36
|
+
'.woff', '.woff2', '.ttf', '.eot',
|
|
37
|
+
'.mp4', '.webm', '.mp3', '.wav',
|
|
38
|
+
'.pdf', '.txt', '.xml', '.csv', '.zip',
|
|
39
|
+
]);
|
|
40
|
+
const IMPORT_REWRITE_EXTENSIONS = new Set(['.md', '.mdx', '.jsx', '.js', '.tsx', '.ts']);
|
|
41
|
+
|
|
42
|
+
function resolveConfigPath() {
|
|
43
|
+
const primary = join(docsDir, PRIMARY_CONFIG_NAME);
|
|
44
|
+
if (existsSync(primary)) return primary;
|
|
45
|
+
return join(docsDir, LEGACY_CONFIG_NAME);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isStaticAsset(filename) {
|
|
49
|
+
const ext = extname(filename).toLowerCase();
|
|
50
|
+
return STATIC_EXTENSIONS.has(ext);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function copyStaticAssets() {
|
|
54
|
+
function walk(dir) {
|
|
55
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (entry.name.startsWith('.')) continue;
|
|
58
|
+
if (entry.name === 'node_modules') continue;
|
|
59
|
+
const srcPath = join(dir, entry.name);
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
walk(srcPath);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!isStaticAsset(srcPath)) continue;
|
|
65
|
+
const rel = relative(docsDir, srcPath);
|
|
66
|
+
const destPath = join(publicDir, rel);
|
|
67
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
68
|
+
copyFileSync(srcPath, destPath);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
mkdirSync(publicDir, { recursive: true });
|
|
73
|
+
walk(docsDir);
|
|
74
|
+
}
|
|
9
75
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
76
|
+
function escapeXml(value) {
|
|
77
|
+
return String(value ?? '')
|
|
78
|
+
.replace(/&/g, '&')
|
|
79
|
+
.replace(/</g, '<')
|
|
80
|
+
.replace(/>/g, '>')
|
|
81
|
+
.replace(/"/g, '"')
|
|
82
|
+
.replace(/'/g, ''');
|
|
83
|
+
}
|
|
13
84
|
|
|
14
|
-
function
|
|
15
|
-
const
|
|
16
|
-
|
|
85
|
+
function normalizeHexColor(value, fallback = '#10b981') {
|
|
86
|
+
const text = String(value ?? '').trim();
|
|
87
|
+
if (/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(text)) return text;
|
|
88
|
+
return fallback;
|
|
17
89
|
}
|
|
18
90
|
|
|
19
|
-
function
|
|
20
|
-
|
|
91
|
+
function hexToRgb(hex) {
|
|
92
|
+
const normalized = normalizeHexColor(hex).replace('#', '');
|
|
93
|
+
const raw = normalized.length === 3
|
|
94
|
+
? normalized.split('').map((ch) => `${ch}${ch}`).join('')
|
|
95
|
+
: normalized;
|
|
96
|
+
const int = Number.parseInt(raw, 16);
|
|
97
|
+
return {
|
|
98
|
+
r: (int >> 16) & 255,
|
|
99
|
+
g: (int >> 8) & 255,
|
|
100
|
+
b: int & 255,
|
|
101
|
+
};
|
|
21
102
|
}
|
|
22
103
|
|
|
23
|
-
function
|
|
24
|
-
const
|
|
25
|
-
return
|
|
104
|
+
function rgbaFromHex(hex, alpha) {
|
|
105
|
+
const { r, g, b } = hexToRgb(hex);
|
|
106
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
26
107
|
}
|
|
27
108
|
|
|
28
|
-
function
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
109
|
+
function humanizeSegment(value) {
|
|
110
|
+
const cleaned = String(value ?? '')
|
|
111
|
+
.trim()
|
|
112
|
+
.replace(/[-_]+/g, ' ')
|
|
113
|
+
.replace(/\s+/g, ' ');
|
|
114
|
+
if (!cleaned) return 'Documentation';
|
|
115
|
+
return cleaned.replace(/\b\w/g, (ch) => ch.toUpperCase());
|
|
116
|
+
}
|
|
35
117
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
118
|
+
function parseFrontmatterData(markdown) {
|
|
119
|
+
if (!markdown || typeof markdown !== 'string') return {};
|
|
120
|
+
const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
121
|
+
if (!match) return {};
|
|
122
|
+
try {
|
|
123
|
+
const parsed = parseYaml(match[1]);
|
|
124
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
125
|
+
} catch {
|
|
126
|
+
return {};
|
|
41
127
|
}
|
|
128
|
+
}
|
|
42
129
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
130
|
+
function ensureLeadingSlash(value) {
|
|
131
|
+
const text = String(value ?? '').trim();
|
|
132
|
+
if (!text) return '';
|
|
133
|
+
return text.startsWith('/') ? text : `/${text}`;
|
|
134
|
+
}
|
|
46
135
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
}
|
|
136
|
+
function resolveLogoReference(config) {
|
|
137
|
+
const logo = config?.logo;
|
|
138
|
+
let rawLogo = null;
|
|
139
|
+
if (typeof logo === 'string') {
|
|
140
|
+
rawLogo = logo.trim();
|
|
141
|
+
} else if (logo && typeof logo === 'object') {
|
|
142
|
+
if (typeof logo.dark === 'string' && logo.dark.trim()) rawLogo = logo.dark.trim();
|
|
143
|
+
else if (typeof logo.light === 'string' && logo.light.trim()) rawLogo = logo.light.trim();
|
|
144
|
+
else if (typeof logo.default === 'string' && logo.default.trim()) rawLogo = logo.default.trim();
|
|
145
|
+
}
|
|
59
146
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
pages,
|
|
63
|
-
defaultOpen: group.expanded !== false,
|
|
64
|
-
};
|
|
147
|
+
if (!rawLogo) return null;
|
|
148
|
+
if (/^https?:\/\//i.test(rawLogo)) return rawLogo;
|
|
65
149
|
|
|
66
|
-
|
|
150
|
+
const localPath = join(docsDir, rawLogo.replace(/^\/+/, ''));
|
|
151
|
+
if (!existsSync(localPath)) return ensureLeadingSlash(rawLogo);
|
|
67
152
|
|
|
68
|
-
|
|
153
|
+
const ext = extname(localPath).toLowerCase();
|
|
154
|
+
const mime = ext === '.svg'
|
|
155
|
+
? 'image/svg+xml'
|
|
156
|
+
: ext === '.png'
|
|
157
|
+
? 'image/png'
|
|
158
|
+
: ext === '.jpg' || ext === '.jpeg'
|
|
159
|
+
? 'image/jpeg'
|
|
160
|
+
: ext === '.webp'
|
|
161
|
+
? 'image/webp'
|
|
162
|
+
: null;
|
|
163
|
+
if (!mime) return ensureLeadingSlash(rawLogo);
|
|
164
|
+
|
|
165
|
+
const encoded = readFileSync(localPath).toString('base64');
|
|
166
|
+
return `data:${mime};base64,${encoded}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function resolvePrimaryColor(config) {
|
|
170
|
+
const colors = config?.colors;
|
|
171
|
+
if (!colors || typeof colors !== 'object') return '#10b981';
|
|
172
|
+
return normalizeHexColor(colors.primary || colors.light || colors.dark || '#10b981');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function resolveSiteName(config) {
|
|
176
|
+
if (typeof config?.name === 'string' && config.name.trim()) return config.name.trim();
|
|
177
|
+
if (typeof config?.title === 'string' && config.title.trim()) return config.title.trim();
|
|
178
|
+
return 'Documentation';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function readMetaInfo(pathSegments) {
|
|
182
|
+
const metaPath = join(contentDir, ...pathSegments, 'meta.json');
|
|
183
|
+
if (!existsSync(metaPath)) return null;
|
|
184
|
+
try {
|
|
185
|
+
const parsed = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
186
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
187
|
+
const title = typeof parsed.title === 'string' && parsed.title.trim() ? parsed.title.trim() : null;
|
|
188
|
+
const root = parsed.root === true;
|
|
189
|
+
return { title, root };
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
69
192
|
}
|
|
193
|
+
}
|
|
70
194
|
|
|
71
|
-
|
|
72
|
-
|
|
195
|
+
function resolveSectionLabel(routeSegments, siteName) {
|
|
196
|
+
if (!Array.isArray(routeSegments) || routeSegments.length === 0) return siteName;
|
|
197
|
+
|
|
198
|
+
const firstMeta = readMetaInfo([routeSegments[0]]);
|
|
199
|
+
if (routeSegments.length > 1 && firstMeta?.root === true) {
|
|
200
|
+
const secondMeta = readMetaInfo([routeSegments[0], routeSegments[1]]);
|
|
201
|
+
if (secondMeta?.title) return secondMeta.title;
|
|
202
|
+
return humanizeSegment(routeSegments[1]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (firstMeta?.title) return firstMeta.title;
|
|
206
|
+
return humanizeSegment(routeSegments[0]);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function normalizeRoutePathFromContentFile(relativePath) {
|
|
210
|
+
const normalized = toPosixPath(relativePath).replace(/\.(md|mdx)$/i, '');
|
|
211
|
+
const trimmed = normalized.replace(/^\/+/, '');
|
|
212
|
+
if (!trimmed || trimmed === 'index') return '/';
|
|
213
|
+
if (trimmed.endsWith('/index')) return `/${trimmed.slice(0, -('/index'.length))}`;
|
|
214
|
+
return `/${trimmed}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function splitTitleLines(value, maxChars = 34, maxLines = 2) {
|
|
218
|
+
const words = String(value ?? '').trim().split(/\s+/).filter(Boolean);
|
|
219
|
+
if (words.length === 0) return ['Documentation'];
|
|
220
|
+
const lines = [];
|
|
221
|
+
let current = '';
|
|
222
|
+
let index = 0;
|
|
73
223
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
224
|
+
while (index < words.length) {
|
|
225
|
+
const word = words[index];
|
|
226
|
+
const next = current ? `${current} ${word}` : word;
|
|
227
|
+
if (next.length <= maxChars || !current) {
|
|
228
|
+
current = next;
|
|
229
|
+
index += 1;
|
|
230
|
+
continue;
|
|
77
231
|
}
|
|
232
|
+
lines.push(current);
|
|
233
|
+
current = '';
|
|
234
|
+
if (lines.length === maxLines - 1) break;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const tail = [...(current ? [current] : []), ...words.slice(index)].join(' ').trim();
|
|
238
|
+
if (tail) lines.push(tail);
|
|
239
|
+
|
|
240
|
+
if (lines.length > maxLines) {
|
|
241
|
+
lines.length = maxLines;
|
|
242
|
+
}
|
|
243
|
+
if (lines.length === maxLines && lines[maxLines - 1].length > maxChars) {
|
|
244
|
+
lines[maxLines - 1] = `${lines[maxLines - 1].slice(0, Math.max(1, maxChars - 1)).trim()}…`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return lines;
|
|
248
|
+
}
|
|
78
249
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
250
|
+
function collectContentMarkdownFiles() {
|
|
251
|
+
const files = [];
|
|
252
|
+
function walk(dir) {
|
|
253
|
+
if (!existsSync(dir)) return;
|
|
254
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
255
|
+
if (entry.name.startsWith('.')) continue;
|
|
256
|
+
const fullPath = join(dir, entry.name);
|
|
257
|
+
if (entry.isDirectory()) {
|
|
258
|
+
walk(fullPath);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (!entry.isFile()) continue;
|
|
262
|
+
if (!/\.(md|mdx)$/i.test(entry.name)) continue;
|
|
263
|
+
files.push(fullPath);
|
|
85
264
|
}
|
|
265
|
+
}
|
|
266
|
+
walk(contentDir);
|
|
267
|
+
return files;
|
|
268
|
+
}
|
|
86
269
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
270
|
+
function toOgOutputPath(routePath) {
|
|
271
|
+
const normalized = routePath === '/' ? 'index' : routePath.replace(/^\/+/, '');
|
|
272
|
+
return join(publicDir, 'og', `${normalized}.svg`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function buildOgSvg({ title, section, description, logoHref, primaryColor }) {
|
|
276
|
+
const gradientAccent = rgbaFromHex(primaryColor, 0.48);
|
|
277
|
+
const gradientGlow = rgbaFromHex(primaryColor, 0.14);
|
|
278
|
+
const titleLines = splitTitleLines(title, 34, 2);
|
|
279
|
+
const titleBaseY = 470 - ((titleLines.length - 1) * 74);
|
|
280
|
+
const titleTs = titleLines
|
|
281
|
+
.map((line, idx) => `<tspan x="70" y="${titleBaseY + (idx * 78)}">${escapeXml(line)}</tspan>`)
|
|
282
|
+
.join('');
|
|
283
|
+
const descriptionText = description ? `<text x="70" y="570" fill="#A9B3C2" font-size="34" font-family="Inter, Segoe UI, Arial, sans-serif">${escapeXml(description)}</text>` : '';
|
|
284
|
+
const logoNode = logoHref
|
|
285
|
+
? `<image href="${escapeXml(logoHref)}" x="70" y="52" width="220" height="56" preserveAspectRatio="xMinYMid meet" />`
|
|
286
|
+
: '';
|
|
287
|
+
|
|
288
|
+
return [
|
|
289
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
290
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">',
|
|
291
|
+
' <defs>',
|
|
292
|
+
' <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">',
|
|
293
|
+
' <stop offset="0%" stop-color="#070C11" />',
|
|
294
|
+
' <stop offset="48%" stop-color="#0A1218" />',
|
|
295
|
+
` <stop offset="100%" stop-color="${escapeXml(gradientGlow)}" />`,
|
|
296
|
+
' </linearGradient>',
|
|
297
|
+
' <radialGradient id="accent" cx="78%" cy="18%" r="70%">',
|
|
298
|
+
` <stop offset="0%" stop-color="${escapeXml(gradientAccent)}" />`,
|
|
299
|
+
' <stop offset="60%" stop-color="rgba(0,0,0,0)" />',
|
|
300
|
+
' </radialGradient>',
|
|
301
|
+
' </defs>',
|
|
302
|
+
' <rect width="1200" height="630" fill="url(#bg)" />',
|
|
303
|
+
' <rect width="1200" height="630" fill="url(#accent)" />',
|
|
304
|
+
` ${logoNode}`,
|
|
305
|
+
` <text x="70" y="365" fill="#D4DBE6" font-size="44" font-family="Inter, Segoe UI, Arial, sans-serif">${escapeXml(section)}</text>`,
|
|
306
|
+
` <text x="70" y="${titleBaseY}" fill="#FFFFFF" font-size="76" font-weight="700" font-family="Inter, Segoe UI, Arial, sans-serif">${titleTs}</text>`,
|
|
307
|
+
` ${descriptionText}`,
|
|
308
|
+
'</svg>',
|
|
309
|
+
'',
|
|
310
|
+
].join('\n');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function generateOgImages(config) {
|
|
314
|
+
const ogRootDir = join(publicDir, 'og');
|
|
315
|
+
rmSync(ogRootDir, { recursive: true, force: true });
|
|
316
|
+
mkdirSync(ogRootDir, { recursive: true });
|
|
92
317
|
|
|
93
|
-
|
|
318
|
+
const files = collectContentMarkdownFiles();
|
|
319
|
+
const siteName = resolveSiteName(config);
|
|
320
|
+
const logoHref = resolveLogoReference(config);
|
|
321
|
+
const primaryColor = resolvePrimaryColor(config);
|
|
94
322
|
|
|
95
|
-
|
|
323
|
+
for (const filePath of files) {
|
|
324
|
+
const relPath = toPosixPath(relative(contentDir, filePath));
|
|
325
|
+
const routePath = normalizeRoutePathFromContentFile(relPath);
|
|
326
|
+
const markdown = readFileSync(filePath, 'utf-8');
|
|
327
|
+
const frontmatter = parseFrontmatterData(markdown);
|
|
328
|
+
const routeSegments = routePath === '/' ? [] : routePath.replace(/^\/+/, '').split('/').filter(Boolean);
|
|
329
|
+
const fallbackTitle = humanizeSegment(routeSegments[routeSegments.length - 1] || 'overview');
|
|
330
|
+
const title = typeof frontmatter.title === 'string' && frontmatter.title.trim()
|
|
331
|
+
? frontmatter.title.trim()
|
|
332
|
+
: fallbackTitle;
|
|
333
|
+
const rawDescription = typeof frontmatter.description === 'string' ? frontmatter.description.trim() : '';
|
|
334
|
+
const description = rawDescription.length > 120 ? `${rawDescription.slice(0, 119).trim()}…` : rawDescription;
|
|
335
|
+
const section = resolveSectionLabel(routeSegments, siteName);
|
|
336
|
+
const svg = buildOgSvg({
|
|
337
|
+
title,
|
|
338
|
+
section,
|
|
339
|
+
description,
|
|
340
|
+
logoHref,
|
|
341
|
+
primaryColor,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const outPath = toOgOutputPath(routePath);
|
|
345
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
346
|
+
writeFileSync(outPath, svg, 'utf-8');
|
|
96
347
|
}
|
|
348
|
+
}
|
|
97
349
|
|
|
98
|
-
|
|
99
|
-
|
|
350
|
+
function toPosixPath(value) {
|
|
351
|
+
return value.replace(/\\/g, '/');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function isInsideDocsRoot(targetPath) {
|
|
355
|
+
const relPath = relative(docsDir, targetPath);
|
|
356
|
+
if (!relPath) return true;
|
|
357
|
+
if (relPath.startsWith('..')) return false;
|
|
358
|
+
if (/^[a-zA-Z]:/.test(relPath)) return false;
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function shouldMirrorSourceFile(filePath) {
|
|
363
|
+
return SOURCE_MIRROR_EXTENSIONS.has(extname(filePath).toLowerCase());
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function shouldRewriteImports(filePath) {
|
|
367
|
+
return IMPORT_REWRITE_EXTENSIONS.has(extname(filePath).toLowerCase());
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function rewriteImportSpecifier(specifier, sourceFilePath, outputFilePath) {
|
|
371
|
+
const match = specifier.match(/^([^?#]+)([?#].*)?$/);
|
|
372
|
+
if (!match) return specifier;
|
|
373
|
+
const rawPath = match[1];
|
|
374
|
+
const suffix = match[2] || '';
|
|
375
|
+
|
|
376
|
+
let resolvedSourcePath = null;
|
|
377
|
+
if (rawPath.startsWith('/')) {
|
|
378
|
+
resolvedSourcePath = join(docsDir, rawPath.slice(1));
|
|
379
|
+
} else if (rawPath.startsWith('./') || rawPath.startsWith('../')) {
|
|
380
|
+
resolvedSourcePath = resolve(dirname(sourceFilePath), rawPath);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!resolvedSourcePath || !isInsideDocsRoot(resolvedSourcePath)) {
|
|
384
|
+
return specifier;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const relToDocs = relative(docsDir, resolvedSourcePath);
|
|
388
|
+
const mirrorTargetPath = join(sourceMirrorDir, relToDocs);
|
|
389
|
+
const relFromOutput = relative(dirname(outputFilePath), mirrorTargetPath);
|
|
390
|
+
const normalizedRel = toPosixPath(relFromOutput || '.');
|
|
391
|
+
const withDotPrefix = normalizedRel.startsWith('.') ? normalizedRel : `./${normalizedRel}`;
|
|
392
|
+
return `${withDotPrefix}${suffix}`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function rewriteImportsInContent(content, sourceFilePath, outputFilePath) {
|
|
396
|
+
const importFromPattern = /^(\s*import\s+)(.+?)(\s+from\s*["'])([^"']+)(["']\s*;?\s*)$/;
|
|
397
|
+
const exportFromPattern = /^(\s*export\b[^\n]*?\bfrom\s*["'])([^"']+)(["'])/;
|
|
398
|
+
const sideEffectImportPattern = /^(\s*import\s*["'])([^"']+)(["'])/;
|
|
399
|
+
const fencePattern = /^\s*(```+|~~~+)/;
|
|
400
|
+
const mdxOutput = (() => {
|
|
401
|
+
const ext = extname(outputFilePath).toLowerCase();
|
|
402
|
+
return ext === '.md' || ext === '.mdx';
|
|
403
|
+
})();
|
|
404
|
+
|
|
405
|
+
const lines = content.split(/\r?\n/);
|
|
406
|
+
const out = [];
|
|
407
|
+
let inFence = false;
|
|
408
|
+
let fenceChar = '';
|
|
409
|
+
let injectedMdxHelperImport = false;
|
|
410
|
+
|
|
411
|
+
function importPathFromSpecifier(specifier) {
|
|
412
|
+
const match = specifier.match(/^([^?#]+)/);
|
|
413
|
+
return match ? match[1] : specifier;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function isLocalSpecifier(specifier) {
|
|
417
|
+
return specifier.startsWith('/') || specifier.startsWith('./') || specifier.startsWith('../');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function isMdxSpecifier(specifier) {
|
|
421
|
+
const base = importPathFromSpecifier(specifier).toLowerCase();
|
|
422
|
+
return base.endsWith('.mdx') || base.endsWith('.md');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function parseDefaultImport(clause) {
|
|
426
|
+
const trimmed = clause.trim();
|
|
427
|
+
if (!trimmed || trimmed.startsWith('{') || trimmed.startsWith('*')) return {};
|
|
428
|
+
|
|
429
|
+
const commaIdx = trimmed.indexOf(',');
|
|
430
|
+
if (commaIdx === -1) {
|
|
431
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(trimmed)) return { defaultName: trimmed };
|
|
432
|
+
return {};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const defaultName = trimmed.slice(0, commaIdx).trim();
|
|
436
|
+
const remainder = trimmed.slice(commaIdx + 1).trim();
|
|
437
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(defaultName)) return {};
|
|
438
|
+
if (!remainder.startsWith('{') && !remainder.startsWith('*')) return {};
|
|
439
|
+
return { defaultName, namedPart: remainder };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
for (const line of lines) {
|
|
443
|
+
const fenceMatch = line.match(fencePattern);
|
|
444
|
+
if (fenceMatch) {
|
|
445
|
+
const currentFenceChar = fenceMatch[1][0];
|
|
446
|
+
if (!inFence) {
|
|
447
|
+
inFence = true;
|
|
448
|
+
fenceChar = currentFenceChar;
|
|
449
|
+
} else if (fenceChar === currentFenceChar) {
|
|
450
|
+
inFence = false;
|
|
451
|
+
fenceChar = '';
|
|
452
|
+
}
|
|
453
|
+
out.push(line);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (inFence) {
|
|
458
|
+
out.push(line);
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const importMatch = line.match(importFromPattern);
|
|
463
|
+
if (importMatch) {
|
|
464
|
+
const importPrefix = importMatch[1];
|
|
465
|
+
const importClause = importMatch[2];
|
|
466
|
+
const fromPrefix = importMatch[3];
|
|
467
|
+
const specifier = importMatch[4];
|
|
468
|
+
const importSuffix = importMatch[5];
|
|
469
|
+
const rewritten = rewriteImportSpecifier(specifier, sourceFilePath, outputFilePath);
|
|
470
|
+
const { defaultName, namedPart } = parseDefaultImport(importClause);
|
|
471
|
+
const shouldWrapDefaultImport =
|
|
472
|
+
mdxOutput && Boolean(defaultName) && isLocalSpecifier(specifier) && isMdxSpecifier(specifier);
|
|
473
|
+
|
|
474
|
+
if (shouldWrapDefaultImport && defaultName) {
|
|
475
|
+
if (!injectedMdxHelperImport) {
|
|
476
|
+
out.push('import { getMDXComponents as __veluGetMDXComponents } from "@/mdx-components";');
|
|
477
|
+
injectedMdxHelperImport = true;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const rawName = `__veluRaw_${defaultName}`;
|
|
481
|
+
const wrappedClause = namedPart ? `${rawName}, ${namedPart}` : rawName;
|
|
482
|
+
out.push(`${importPrefix}${wrappedClause}${fromPrefix}${rewritten}${importSuffix}`);
|
|
483
|
+
out.push(`export const ${defaultName} = (props) => <${rawName} {...props} components={__veluGetMDXComponents()} />;`);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
out.push(`${importPrefix}${importClause}${fromPrefix}${rewritten}${importSuffix}`);
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
let nextLine = line.replace(exportFromPattern, (_, prefix, specifier, suffix) => {
|
|
492
|
+
const rewritten = rewriteImportSpecifier(specifier, sourceFilePath, outputFilePath);
|
|
493
|
+
return `${prefix}${rewritten}${suffix}`;
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
nextLine = nextLine.replace(sideEffectImportPattern, (_, prefix, specifier, suffix) => {
|
|
497
|
+
const rewritten = rewriteImportSpecifier(specifier, sourceFilePath, outputFilePath);
|
|
498
|
+
return `${prefix}${rewritten}${suffix}`;
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
out.push(nextLine);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return out.join('\n');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function copyMirroredSourceFile(srcPath) {
|
|
508
|
+
if (!shouldMirrorSourceFile(srcPath)) return;
|
|
509
|
+
if (!isInsideDocsRoot(srcPath)) return;
|
|
510
|
+
|
|
511
|
+
const relPath = relative(docsDir, srcPath);
|
|
512
|
+
const destPath = join(sourceMirrorDir, relPath);
|
|
513
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
514
|
+
|
|
515
|
+
if (shouldRewriteImports(srcPath)) {
|
|
516
|
+
const raw = readFileSync(srcPath, 'utf-8');
|
|
517
|
+
const rewritten = rewriteImportsInContent(raw, srcPath, destPath);
|
|
518
|
+
writeFileSync(destPath, rewritten, 'utf-8');
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
copyFileSync(srcPath, destPath);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function rebuildSourceMirror() {
|
|
526
|
+
rmSync(sourceMirrorDir, { recursive: true, force: true });
|
|
527
|
+
mkdirSync(sourceMirrorDir, { recursive: true });
|
|
528
|
+
|
|
529
|
+
function walk(dir) {
|
|
530
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
531
|
+
for (const entry of entries) {
|
|
532
|
+
if (entry.name.startsWith('.')) continue;
|
|
533
|
+
if (entry.name === 'node_modules') continue;
|
|
534
|
+
const srcPath = join(dir, entry.name);
|
|
535
|
+
if (entry.isDirectory()) {
|
|
536
|
+
walk(srcPath);
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (!shouldMirrorSourceFile(srcPath)) continue;
|
|
540
|
+
copyMirroredSourceFile(srcPath);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
walk(docsDir);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function syncSourceMirrorFile(filename) {
|
|
548
|
+
const srcPath = join(docsDir, filename);
|
|
549
|
+
const destPath = join(sourceMirrorDir, filename);
|
|
550
|
+
if (!shouldMirrorSourceFile(srcPath)) return;
|
|
551
|
+
|
|
552
|
+
if (existsSync(srcPath)) {
|
|
553
|
+
copyMirroredSourceFile(srcPath);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
rmSync(destPath, { force: true });
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function loadConfig() {
|
|
561
|
+
const raw = readFileSync(resolveConfigPath(), 'utf-8');
|
|
562
|
+
return normalizeConfigNavigation(JSON.parse(raw));
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function isExternalDestination(value) {
|
|
566
|
+
return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(value);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function normalizePath(value) {
|
|
570
|
+
const trimmed = String(value || '').trim();
|
|
571
|
+
if (!trimmed) return '/';
|
|
572
|
+
const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
573
|
+
const collapsed = withLeadingSlash.replace(/\/{2,}/g, '/');
|
|
574
|
+
if (collapsed !== '/' && collapsed.endsWith('/')) return collapsed.slice(0, -1);
|
|
575
|
+
return collapsed;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function collectRedirectRules(config) {
|
|
579
|
+
const redirects = Array.isArray(config?.redirects) ? config.redirects : [];
|
|
580
|
+
const output = [];
|
|
581
|
+
|
|
582
|
+
for (const redirect of redirects) {
|
|
583
|
+
if (!redirect || typeof redirect.source !== 'string' || typeof redirect.destination !== 'string') continue;
|
|
584
|
+
const source = redirect.source.trim();
|
|
585
|
+
const destination = redirect.destination.trim();
|
|
586
|
+
if (!source || !destination) continue;
|
|
587
|
+
if (/[?#]/.test(source) || /[?#]/.test(destination)) continue;
|
|
588
|
+
|
|
589
|
+
const normalizedSource = normalizePath(source);
|
|
590
|
+
const normalizedDestination = isExternalDestination(destination)
|
|
591
|
+
? destination
|
|
592
|
+
: normalizePath(destination);
|
|
593
|
+
if (!isExternalDestination(normalizedDestination) && normalizedSource === normalizedDestination) continue;
|
|
594
|
+
|
|
595
|
+
output.push({
|
|
596
|
+
source: normalizedSource,
|
|
597
|
+
destination: normalizedDestination,
|
|
598
|
+
permanent: redirect.permanent !== false,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return output;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function writeRedirectArtifacts(config) {
|
|
606
|
+
const redirects = collectRedirectRules(config);
|
|
607
|
+
const generatedDir = resolve('generated');
|
|
608
|
+
mkdirSync(generatedDir, { recursive: true });
|
|
609
|
+
writeFileSync(
|
|
610
|
+
join(generatedDir, 'redirects.ts'),
|
|
611
|
+
`const redirects: Array<{ source: string; destination: string; permanent: boolean }> = ${JSON.stringify(redirects, null, 2)};\n\nexport default redirects;\n`,
|
|
612
|
+
'utf-8'
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
const redirectsFilePath = join(publicDir, '_redirects');
|
|
616
|
+
if (redirects.length === 0) {
|
|
617
|
+
rmSync(redirectsFilePath, { force: true });
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const netlifyBody = redirects
|
|
622
|
+
.map((redirect) => `${redirect.source} ${redirect.destination} ${redirect.permanent ? 301 : 307}`)
|
|
623
|
+
.join('\n');
|
|
624
|
+
writeFileSync(redirectsFilePath, `${netlifyBody}\n`, 'utf-8');
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function pageBasename(page) {
|
|
628
|
+
return page.split('/').pop();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function pageLabelFromSlug(slug) {
|
|
632
|
+
const last = slug.split('/').pop() || slug;
|
|
633
|
+
return last.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function isSeparator(item) {
|
|
637
|
+
return typeof item === 'object' && item !== null && 'separator' in item;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function isLink(item) {
|
|
641
|
+
return typeof item === 'object' && item !== null && 'href' in item && 'label' in item;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function isGroup(item) {
|
|
645
|
+
return typeof item === 'object' && item !== null && 'group' in item;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const HTTP_METHODS = new Set([
|
|
649
|
+
'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'TRACE', 'CONNECT', 'WEBHOOK',
|
|
650
|
+
]);
|
|
651
|
+
const OPENAPI_PATH_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace']);
|
|
652
|
+
const ASYNCAPI_OPERATION_ACTIONS = new Set(['publish', 'subscribe', 'send', 'receive']);
|
|
653
|
+
|
|
654
|
+
function resolveDefaultOpenApiSpec(openapi) {
|
|
655
|
+
const source = extractOpenApiSource(openapi);
|
|
656
|
+
if (typeof source === 'string') {
|
|
657
|
+
const trimmed = source.trim();
|
|
658
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
659
|
+
}
|
|
660
|
+
if (Array.isArray(source)) {
|
|
661
|
+
const first = source.find((entry) => typeof entry === 'string' && entry.trim().length > 0);
|
|
662
|
+
return typeof first === 'string' ? first.trim() : undefined;
|
|
100
663
|
}
|
|
664
|
+
return undefined;
|
|
665
|
+
}
|
|
101
666
|
|
|
102
|
-
|
|
667
|
+
function extractOpenApiSource(openapi) {
|
|
668
|
+
if (typeof openapi === 'string' || Array.isArray(openapi)) return openapi;
|
|
669
|
+
if (openapi && typeof openapi === 'object') {
|
|
670
|
+
const source = openapi.source;
|
|
671
|
+
if (typeof source === 'string' || Array.isArray(source)) return source;
|
|
672
|
+
}
|
|
673
|
+
return undefined;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function resolveOpenApiDirectory(openapi) {
|
|
677
|
+
if (!openapi || typeof openapi !== 'object' || Array.isArray(openapi)) return undefined;
|
|
678
|
+
const raw = openapi.directory;
|
|
679
|
+
if (typeof raw !== 'string') return undefined;
|
|
680
|
+
const trimmed = raw.trim().replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
681
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function resolveDefaultAsyncApiSpec(asyncapi) {
|
|
685
|
+
const source = extractOpenApiSource(asyncapi);
|
|
686
|
+
if (typeof source === 'string') {
|
|
687
|
+
const trimmed = source.trim();
|
|
688
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
689
|
+
}
|
|
690
|
+
if (Array.isArray(source)) {
|
|
691
|
+
const first = source.find((entry) => typeof entry === 'string' && entry.trim().length > 0);
|
|
692
|
+
return typeof first === 'string' ? first.trim() : undefined;
|
|
693
|
+
}
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function resolveAsyncApiSpecList(asyncapi) {
|
|
698
|
+
const source = extractOpenApiSource(asyncapi);
|
|
699
|
+
if (typeof source === 'string') {
|
|
700
|
+
const trimmed = source.trim();
|
|
701
|
+
return trimmed ? [trimmed] : [];
|
|
702
|
+
}
|
|
703
|
+
if (Array.isArray(source)) {
|
|
704
|
+
return source
|
|
705
|
+
.filter((entry) => typeof entry === 'string')
|
|
706
|
+
.map((entry) => entry.trim())
|
|
707
|
+
.filter((entry) => entry.length > 0);
|
|
708
|
+
}
|
|
709
|
+
return [];
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function resolveAsyncApiDirectory(asyncapi) {
|
|
713
|
+
if (!asyncapi || typeof asyncapi !== 'object' || Array.isArray(asyncapi)) return undefined;
|
|
714
|
+
const raw = asyncapi.directory;
|
|
715
|
+
if (typeof raw !== 'string') return undefined;
|
|
716
|
+
const trimmed = raw.trim().replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
717
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
103
718
|
}
|
|
104
719
|
|
|
105
|
-
function
|
|
106
|
-
|
|
107
|
-
if (!
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
720
|
+
function parseOpenApiOperationRef(value, inheritedSpec) {
|
|
721
|
+
const trimmed = String(value ?? '').trim();
|
|
722
|
+
if (!trimmed) return null;
|
|
723
|
+
|
|
724
|
+
const withSpec = trimmed.match(/^(\S+)\s+([A-Za-z]+)\s+(.+)$/);
|
|
725
|
+
if (withSpec) {
|
|
726
|
+
const method = withSpec[2].toUpperCase();
|
|
727
|
+
const endpoint = withSpec[3].trim();
|
|
728
|
+
if (!HTTP_METHODS.has(method)) return null;
|
|
729
|
+
if (method === 'WEBHOOK') {
|
|
730
|
+
if (!endpoint) return null;
|
|
731
|
+
return { spec: withSpec[1].trim(), method, endpoint, kind: 'webhook' };
|
|
112
732
|
}
|
|
113
|
-
|
|
733
|
+
if (!endpoint.startsWith('/')) return null;
|
|
734
|
+
return { spec: withSpec[1].trim(), method, endpoint, kind: 'path' };
|
|
114
735
|
}
|
|
115
736
|
|
|
116
|
-
|
|
117
|
-
|
|
737
|
+
const noSpec = trimmed.match(/^([A-Za-z]+)\s+(.+)$/);
|
|
738
|
+
if (!noSpec) return null;
|
|
739
|
+
const method = noSpec[1].toUpperCase();
|
|
740
|
+
const endpoint = noSpec[2].trim();
|
|
741
|
+
if (!HTTP_METHODS.has(method)) return null;
|
|
742
|
+
if (method === 'WEBHOOK') {
|
|
743
|
+
if (!endpoint) return null;
|
|
744
|
+
return { spec: inheritedSpec, method, endpoint, kind: 'webhook' };
|
|
745
|
+
}
|
|
746
|
+
if (!endpoint.startsWith('/')) return null;
|
|
747
|
+
return { spec: inheritedSpec, method, endpoint, kind: 'path' };
|
|
118
748
|
}
|
|
119
749
|
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
750
|
+
function parseAsyncApiChannelRef(value, inheritedSpec) {
|
|
751
|
+
const trimmed = String(value ?? '').trim();
|
|
752
|
+
if (!trimmed) return null;
|
|
753
|
+
|
|
754
|
+
const withSpec = trimmed.match(/^(\S+)\s+(.+)$/);
|
|
755
|
+
if (withSpec) {
|
|
756
|
+
const maybeMethod = withSpec[1].toUpperCase();
|
|
757
|
+
if (HTTP_METHODS.has(maybeMethod)) return null;
|
|
758
|
+
const spec = withSpec[1].trim();
|
|
759
|
+
const channel = withSpec[2].trim();
|
|
760
|
+
if (!channel) return null;
|
|
761
|
+
return { spec, channel };
|
|
125
762
|
}
|
|
763
|
+
|
|
764
|
+
if (!inheritedSpec) return null;
|
|
765
|
+
return { spec: inheritedSpec, channel: trimmed };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function slugFromOpenApiOperation(method, endpoint) {
|
|
769
|
+
const cleaned = endpoint
|
|
770
|
+
.toLowerCase()
|
|
771
|
+
.replace(/^\/+/, '')
|
|
772
|
+
.replace(/[{}]/g, '')
|
|
773
|
+
.replace(/[^a-z0-9/._-]+/g, '-')
|
|
774
|
+
.replace(/\/+/g, '-')
|
|
775
|
+
.replace(/[-_.]{2,}/g, '-')
|
|
776
|
+
.replace(/^[-_.]+|[-_.]+$/g, '');
|
|
777
|
+
const body = cleaned || 'endpoint';
|
|
778
|
+
return `${method.toLowerCase()}-${body}`;
|
|
126
779
|
}
|
|
127
780
|
|
|
128
|
-
function
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
781
|
+
function resolveOpenApiSpecList(openapi) {
|
|
782
|
+
const source = extractOpenApiSource(openapi);
|
|
783
|
+
if (typeof source === 'string') {
|
|
784
|
+
const trimmed = source.trim();
|
|
785
|
+
return trimmed ? [trimmed] : [];
|
|
786
|
+
}
|
|
787
|
+
if (Array.isArray(source)) {
|
|
788
|
+
return source
|
|
789
|
+
.filter((entry) => typeof entry === 'string')
|
|
790
|
+
.map((entry) => entry.trim())
|
|
791
|
+
.filter((entry) => entry.length > 0);
|
|
792
|
+
}
|
|
793
|
+
return [];
|
|
134
794
|
}
|
|
135
795
|
|
|
136
|
-
function
|
|
137
|
-
const
|
|
138
|
-
|
|
796
|
+
function parseOpenApiDocument(rawSource) {
|
|
797
|
+
const source = String(rawSource ?? '').trim();
|
|
798
|
+
if (!source) return null;
|
|
799
|
+
try {
|
|
800
|
+
const parsed = JSON.parse(source);
|
|
801
|
+
if (parsed && typeof parsed === 'object') return parsed;
|
|
802
|
+
} catch {}
|
|
803
|
+
try {
|
|
804
|
+
const parsed = parseYaml(source);
|
|
805
|
+
if (parsed && typeof parsed === 'object') return parsed;
|
|
806
|
+
} catch {}
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function parseAsyncApiDocument(rawSource) {
|
|
811
|
+
return parseOpenApiDocument(rawSource);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function resolveRef(root, ref) {
|
|
815
|
+
const refText = String(ref ?? '');
|
|
816
|
+
if (!refText.startsWith('#/')) return undefined;
|
|
817
|
+
const parts = refText.slice(2).split('/').map((part) => part.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
818
|
+
let current = root;
|
|
819
|
+
for (const part of parts) {
|
|
820
|
+
if (!current || typeof current !== 'object') return undefined;
|
|
821
|
+
current = current[part];
|
|
822
|
+
}
|
|
823
|
+
return current;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function loadAsyncApiChannels(specSource) {
|
|
827
|
+
if (/^https?:\/\//i.test(specSource) || specSource.startsWith('file://')) return [];
|
|
139
828
|
|
|
140
|
-
|
|
141
|
-
|
|
829
|
+
const resolvedPath = specSource.startsWith('/')
|
|
830
|
+
? join(docsDir, specSource.replace(/^\/+/, ''))
|
|
831
|
+
: resolve(docsDir, specSource);
|
|
832
|
+
if (!existsSync(resolvedPath)) return [];
|
|
142
833
|
|
|
143
|
-
|
|
834
|
+
const parsed = parseAsyncApiDocument(readFileSync(resolvedPath, 'utf-8'));
|
|
835
|
+
if (!parsed || typeof parsed !== 'object') return [];
|
|
836
|
+
const channels = parsed.channels;
|
|
837
|
+
if (!channels || typeof channels !== 'object') return [];
|
|
144
838
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
839
|
+
return Object.entries(channels)
|
|
840
|
+
.map(([channelName, rawChannel]) => {
|
|
841
|
+
const channel = rawChannel && typeof rawChannel === 'object' ? rawChannel : {};
|
|
842
|
+
const title = typeof channel.title === 'string' ? channel.title : undefined;
|
|
843
|
+
const description = typeof channel.description === 'string' ? channel.description : undefined;
|
|
844
|
+
return {
|
|
845
|
+
spec: specSource,
|
|
846
|
+
channel: channelName,
|
|
847
|
+
title: title ?? channelName,
|
|
848
|
+
description,
|
|
849
|
+
};
|
|
850
|
+
})
|
|
851
|
+
.filter((entry) => typeof entry.channel === 'string' && entry.channel.trim().length > 0);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function getAsyncApiChannelInfo(specSource, channelName) {
|
|
855
|
+
if (/^https?:\/\//i.test(specSource) || specSource.startsWith('file://')) return null;
|
|
856
|
+
|
|
857
|
+
const resolvedPath = specSource.startsWith('/')
|
|
858
|
+
? join(docsDir, specSource.replace(/^\/+/, ''))
|
|
859
|
+
: resolve(docsDir, specSource);
|
|
860
|
+
if (!existsSync(resolvedPath)) return null;
|
|
861
|
+
|
|
862
|
+
const parsed = parseAsyncApiDocument(readFileSync(resolvedPath, 'utf-8'));
|
|
863
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
864
|
+
|
|
865
|
+
const channels = parsed.channels;
|
|
866
|
+
if (!channels || typeof channels !== 'object') return null;
|
|
867
|
+
|
|
868
|
+
const channelObject = channels[channelName];
|
|
869
|
+
if (!channelObject || typeof channelObject !== 'object') {
|
|
870
|
+
const byAddress = Object.entries(channels).find(([, rawChannel]) => {
|
|
871
|
+
if (!rawChannel || typeof rawChannel !== 'object') return false;
|
|
872
|
+
const address = rawChannel.address;
|
|
873
|
+
return typeof address === 'string' && address === channelName;
|
|
874
|
+
});
|
|
875
|
+
if (!byAddress) return null;
|
|
876
|
+
const [, matched] = byAddress;
|
|
877
|
+
return {
|
|
878
|
+
channel: byAddress[0],
|
|
879
|
+
title: typeof matched.title === 'string' ? matched.title : byAddress[0],
|
|
880
|
+
description: typeof matched.description === 'string' ? matched.description : undefined,
|
|
881
|
+
};
|
|
150
882
|
}
|
|
151
883
|
|
|
152
|
-
|
|
153
|
-
|
|
884
|
+
return {
|
|
885
|
+
channel: channelName,
|
|
886
|
+
title: typeof channelObject.title === 'string' ? channelObject.title : channelName,
|
|
887
|
+
description: typeof channelObject.description === 'string' ? channelObject.description : undefined,
|
|
888
|
+
};
|
|
154
889
|
}
|
|
155
890
|
|
|
156
|
-
|
|
891
|
+
function readMintMetadata(operation) {
|
|
892
|
+
const xMint = operation['x-mint'];
|
|
893
|
+
if (!xMint || typeof xMint !== 'object') return {};
|
|
894
|
+
const metadata = xMint.metadata;
|
|
895
|
+
const content = xMint.content;
|
|
896
|
+
const meta = metadata && typeof metadata === 'object' ? metadata : {};
|
|
897
|
+
return {
|
|
898
|
+
title: typeof meta.title === 'string' ? meta.title : undefined,
|
|
899
|
+
description: typeof meta.description === 'string' ? meta.description : undefined,
|
|
900
|
+
deprecated: typeof meta.deprecated === 'boolean' ? meta.deprecated : undefined,
|
|
901
|
+
version: typeof meta.version === 'string' ? meta.version : undefined,
|
|
902
|
+
content: typeof content === 'string' ? content : undefined,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
157
905
|
|
|
158
|
-
function
|
|
159
|
-
|
|
160
|
-
|
|
906
|
+
function readVisibilityMetadata(operation) {
|
|
907
|
+
return {
|
|
908
|
+
hidden: operation['x-hidden'] === true,
|
|
909
|
+
excluded: operation['x-excluded'] === true,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function normalizeWebhookKey(name) {
|
|
914
|
+
const value = String(name ?? '').trim();
|
|
915
|
+
if (!value) return value;
|
|
916
|
+
return value.startsWith('/') ? value : `/${value}`;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function readOpenApiOperationInfo(operation) {
|
|
920
|
+
if (!operation || typeof operation !== 'object') return {};
|
|
921
|
+
const mintMeta = readMintMetadata(operation);
|
|
922
|
+
const visibility = readVisibilityMetadata(operation);
|
|
923
|
+
return {
|
|
924
|
+
...visibility,
|
|
925
|
+
title: mintMeta.title ?? (typeof operation.summary === 'string' ? operation.summary : undefined),
|
|
926
|
+
description: mintMeta.description ?? (typeof operation.description === 'string' ? operation.description : undefined),
|
|
927
|
+
deprecated: mintMeta.deprecated ?? (operation.deprecated === true),
|
|
928
|
+
version: mintMeta.version,
|
|
929
|
+
content: mintMeta.content,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
161
932
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
933
|
+
function resolveSpecPath(specSource) {
|
|
934
|
+
if (/^https?:\/\//i.test(specSource) || specSource.startsWith('file://')) return undefined;
|
|
935
|
+
const resolvedPath = specSource.startsWith('/')
|
|
936
|
+
? join(docsDir, specSource.replace(/^\/+/, ''))
|
|
937
|
+
: resolve(docsDir, specSource);
|
|
938
|
+
return existsSync(resolvedPath) ? resolvedPath : undefined;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function loadOpenApiDocument(specSource) {
|
|
942
|
+
const resolvedPath = resolveSpecPath(specSource);
|
|
943
|
+
if (!resolvedPath) return null;
|
|
944
|
+
return parseOpenApiDocument(readFileSync(resolvedPath, 'utf-8'));
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function getOpenApiOperationInfo(specSource, kind, method, endpoint) {
|
|
948
|
+
const parsed = loadOpenApiDocument(specSource);
|
|
949
|
+
if (!parsed) return null;
|
|
950
|
+
|
|
951
|
+
if (kind === 'webhook') {
|
|
952
|
+
const webhooks = parsed.webhooks;
|
|
953
|
+
if (!webhooks || typeof webhooks !== 'object') return null;
|
|
954
|
+
const target = normalizeWebhookKey(endpoint);
|
|
955
|
+
const entries = Object.entries(webhooks);
|
|
956
|
+
const resolvedEntry = entries.find(([name]) => name === endpoint || normalizeWebhookKey(name) === target);
|
|
957
|
+
if (!resolvedEntry) return null;
|
|
958
|
+
const pathItem = resolvedEntry[1];
|
|
959
|
+
if (!pathItem || typeof pathItem !== 'object') return null;
|
|
960
|
+
const methodKey = method === 'WEBHOOK' ? pickOperationMethod(pathItem)?.toLowerCase() : method.toLowerCase();
|
|
961
|
+
if (!methodKey) return null;
|
|
962
|
+
return readOpenApiOperationInfo(pathItem[methodKey]);
|
|
165
963
|
}
|
|
166
964
|
|
|
167
|
-
const
|
|
168
|
-
if (
|
|
965
|
+
const paths = parsed.paths;
|
|
966
|
+
if (!paths || typeof paths !== 'object') return null;
|
|
967
|
+
const pathItem = paths[endpoint];
|
|
968
|
+
if (!pathItem || typeof pathItem !== 'object') return null;
|
|
969
|
+
return readOpenApiOperationInfo(pathItem[method.toLowerCase()]);
|
|
970
|
+
}
|
|
169
971
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
972
|
+
function pickOperationMethod(pathItem) {
|
|
973
|
+
for (const method of OPENAPI_PATH_METHODS) {
|
|
974
|
+
const operation = pathItem[method];
|
|
975
|
+
if (operation && typeof operation === 'object') return method.toUpperCase();
|
|
976
|
+
}
|
|
977
|
+
return undefined;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function loadOpenApiOperations(specSource) {
|
|
981
|
+
const parsed = loadOpenApiDocument(specSource);
|
|
982
|
+
if (!parsed) return [];
|
|
983
|
+
const paths = parsed.paths;
|
|
984
|
+
const webhooks = parsed.webhooks;
|
|
985
|
+
|
|
986
|
+
const output = [];
|
|
987
|
+
if (paths && typeof paths === 'object') {
|
|
988
|
+
for (const [endpoint, methods] of Object.entries(paths)) {
|
|
989
|
+
if (!endpoint.startsWith('/') || !methods || typeof methods !== 'object') continue;
|
|
990
|
+
for (const method of Object.keys(methods)) {
|
|
991
|
+
const normalized = method.toLowerCase();
|
|
992
|
+
if (!OPENAPI_PATH_METHODS.has(normalized)) continue;
|
|
993
|
+
const operation = methods[method];
|
|
994
|
+
if (!operation || typeof operation !== 'object') continue;
|
|
995
|
+
const operationInfo = readOpenApiOperationInfo(operation);
|
|
996
|
+
if (operationInfo.excluded) continue;
|
|
997
|
+
output.push({
|
|
998
|
+
kind: 'path',
|
|
999
|
+
spec: specSource,
|
|
1000
|
+
method: normalized.toUpperCase(),
|
|
1001
|
+
endpoint,
|
|
1002
|
+
title: operationInfo.title,
|
|
1003
|
+
description: operationInfo.description,
|
|
1004
|
+
deprecated: operationInfo.deprecated,
|
|
1005
|
+
version: operationInfo.version,
|
|
1006
|
+
content: operationInfo.content,
|
|
1007
|
+
hidden: operationInfo.hidden,
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
173
1011
|
}
|
|
174
1012
|
|
|
175
|
-
|
|
1013
|
+
if (webhooks && typeof webhooks === 'object') {
|
|
1014
|
+
for (const [webhookName, pathItem] of Object.entries(webhooks)) {
|
|
1015
|
+
if (!pathItem || typeof pathItem !== 'object') continue;
|
|
1016
|
+
const resolvedMethod = pickOperationMethod(pathItem);
|
|
1017
|
+
if (!resolvedMethod) continue;
|
|
1018
|
+
const operation = pathItem[resolvedMethod.toLowerCase()];
|
|
1019
|
+
if (!operation || typeof operation !== 'object') continue;
|
|
1020
|
+
const operationInfo = readOpenApiOperationInfo(operation);
|
|
1021
|
+
if (operationInfo.excluded) continue;
|
|
1022
|
+
output.push({
|
|
1023
|
+
kind: 'webhook',
|
|
1024
|
+
spec: specSource,
|
|
1025
|
+
method: 'WEBHOOK',
|
|
1026
|
+
endpoint: webhookName,
|
|
1027
|
+
title: operationInfo.title,
|
|
1028
|
+
description: operationInfo.description,
|
|
1029
|
+
deprecated: operationInfo.deprecated,
|
|
1030
|
+
version: operationInfo.version,
|
|
1031
|
+
content: operationInfo.content,
|
|
1032
|
+
hidden: operationInfo.hidden,
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return output;
|
|
176
1037
|
}
|
|
177
1038
|
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
1039
|
+
function normalizeOpenApiSpecForFrontmatter(spec) {
|
|
1040
|
+
if (!spec) return undefined;
|
|
1041
|
+
const trimmed = String(spec).trim();
|
|
1042
|
+
if (!trimmed) return undefined;
|
|
1043
|
+
if (/^https?:\/\//i.test(trimmed) || trimmed.startsWith('file://')) return trimmed;
|
|
1044
|
+
if (trimmed.startsWith('/')) return trimmed;
|
|
1045
|
+
return `/${trimmed.replace(/^\.?\/*/, '')}`;
|
|
183
1046
|
}
|
|
1047
|
+
|
|
1048
|
+
function metaEntry(item) {
|
|
1049
|
+
if (typeof item === 'string') return item;
|
|
1050
|
+
if (isSeparator(item)) return `---${item.separator}---`;
|
|
1051
|
+
if (isLink(item)) {
|
|
1052
|
+
return item.icon
|
|
1053
|
+
? `[${item.icon}][${item.label}](${item.href})`
|
|
1054
|
+
: `[${item.label}](${item.href})`;
|
|
1055
|
+
}
|
|
1056
|
+
return String(item);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function buildArtifacts(config) {
|
|
1060
|
+
const pageMap = [];
|
|
1061
|
+
const metaFiles = [];
|
|
1062
|
+
const rootTabs = (config.navigation?.tabs || []).filter((tab) => !tab.href);
|
|
1063
|
+
const rootPages = rootTabs.map((tab) => tab.slug);
|
|
1064
|
+
const defaultOpenApiSpec = resolveDefaultOpenApiSpec(config.navigation?.openapi ?? config.openapi);
|
|
1065
|
+
const defaultAsyncApiSpec = resolveDefaultAsyncApiSpec(config.navigation?.asyncapi ?? config.asyncapi);
|
|
1066
|
+
let firstPage = 'quickstart';
|
|
1067
|
+
let hasFirstPage = false;
|
|
1068
|
+
let firstHiddenPageCandidate;
|
|
1069
|
+
const usedDestinations = new Set();
|
|
184
1070
|
|
|
185
|
-
function
|
|
186
|
-
|
|
1071
|
+
function trackFirstPage(dest, hidden = false) {
|
|
1072
|
+
if (!hidden && !hasFirstPage) {
|
|
1073
|
+
firstPage = dest;
|
|
1074
|
+
hasFirstPage = true;
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (hidden && !hasFirstPage && !firstHiddenPageCandidate) {
|
|
1078
|
+
firstHiddenPageCandidate = dest;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function uniqueDestination(dest) {
|
|
1083
|
+
if (!usedDestinations.has(dest)) {
|
|
1084
|
+
usedDestinations.add(dest);
|
|
1085
|
+
return dest;
|
|
1086
|
+
}
|
|
1087
|
+
let count = 2;
|
|
1088
|
+
while (usedDestinations.has(`${dest}-${count}`)) count += 1;
|
|
1089
|
+
const candidate = `${dest}-${count}`;
|
|
1090
|
+
usedDestinations.add(candidate);
|
|
1091
|
+
return candidate;
|
|
1092
|
+
}
|
|
187
1093
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
1094
|
+
function metaEntryForDestination(baseDir, destination) {
|
|
1095
|
+
const fromParts = baseDir.split('/').filter(Boolean);
|
|
1096
|
+
const toParts = destination.split('/').filter(Boolean);
|
|
191
1097
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
1098
|
+
let index = 0;
|
|
1099
|
+
while (index < fromParts.length && index < toParts.length && fromParts[index] === toParts[index]) {
|
|
1100
|
+
index += 1;
|
|
1101
|
+
}
|
|
195
1102
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
1103
|
+
const up = Array(fromParts.length - index).fill('..');
|
|
1104
|
+
const down = toParts.slice(index);
|
|
1105
|
+
const rel = [...up, ...down].join('/');
|
|
1106
|
+
return rel || pageBasename(destination);
|
|
1107
|
+
}
|
|
201
1108
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
1109
|
+
function resolveGenerationDestination(openapi, fallback) {
|
|
1110
|
+
const override = resolveOpenApiDirectory(openapi) ?? resolveAsyncApiDirectory(openapi);
|
|
1111
|
+
if (!override) return fallback;
|
|
1112
|
+
if (!fallback) return override;
|
|
1113
|
+
if (override === fallback || override.startsWith(`${fallback}/`)) return override;
|
|
1114
|
+
return `${fallback}/${override}`;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function toFilePageMapping(item, destDir) {
|
|
1118
|
+
const basename = pageBasename(item);
|
|
1119
|
+
const dest = uniqueDestination(`${destDir}/${basename}`);
|
|
1120
|
+
return { src: item, dest, kind: 'file' };
|
|
1121
|
+
}
|
|
207
1122
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
1123
|
+
function toPageMapping(item, destDir, inheritedSpec, mode = 'file-fallback') {
|
|
1124
|
+
const parsedOpenApi = parseOpenApiOperationRef(item, inheritedSpec);
|
|
1125
|
+
if (!parsedOpenApi) {
|
|
1126
|
+
if (mode === 'operation-only') return undefined;
|
|
1127
|
+
return toFilePageMapping(item, destDir);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const operationInfo = parsedOpenApi.spec
|
|
1131
|
+
? getOpenApiOperationInfo(parsedOpenApi.spec, parsedOpenApi.kind, parsedOpenApi.method, parsedOpenApi.endpoint)
|
|
1132
|
+
: null;
|
|
1133
|
+
if (operationInfo?.excluded) return null;
|
|
1134
|
+
|
|
1135
|
+
const slug = slugFromOpenApiOperation(parsedOpenApi.method, parsedOpenApi.endpoint);
|
|
1136
|
+
const dest = uniqueDestination(`${destDir}/${slug}`);
|
|
1137
|
+
return {
|
|
1138
|
+
src: item,
|
|
1139
|
+
dest,
|
|
1140
|
+
kind: 'openapi-operation',
|
|
1141
|
+
openapiSpec: parsedOpenApi.spec,
|
|
1142
|
+
openapiMethod: parsedOpenApi.method,
|
|
1143
|
+
openapiEndpoint: parsedOpenApi.endpoint,
|
|
1144
|
+
openapiKind: parsedOpenApi.kind,
|
|
1145
|
+
title: operationInfo?.title,
|
|
1146
|
+
description: operationInfo?.description,
|
|
1147
|
+
deprecated: operationInfo?.deprecated,
|
|
1148
|
+
version: operationInfo?.version,
|
|
1149
|
+
content: operationInfo?.content,
|
|
1150
|
+
hidden: operationInfo?.hidden === true,
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function toAsyncApiPageMapping(item, destDir, inheritedSpec, mode = 'channel-only') {
|
|
1155
|
+
const parsedAsyncApi = parseAsyncApiChannelRef(item, inheritedSpec);
|
|
1156
|
+
if (!parsedAsyncApi) return mode === 'channel-only' ? undefined : null;
|
|
1157
|
+
|
|
1158
|
+
const channelInfo = parsedAsyncApi.spec
|
|
1159
|
+
? getAsyncApiChannelInfo(parsedAsyncApi.spec, parsedAsyncApi.channel)
|
|
1160
|
+
: null;
|
|
1161
|
+
const resolvedChannel = channelInfo?.channel ?? parsedAsyncApi.channel;
|
|
1162
|
+
const slug = slugFromOpenApiOperation('channel', resolvedChannel);
|
|
1163
|
+
const dest = uniqueDestination(`${destDir}/${slug}`);
|
|
1164
|
+
return {
|
|
1165
|
+
src: item,
|
|
1166
|
+
dest,
|
|
1167
|
+
kind: 'asyncapi-channel',
|
|
1168
|
+
asyncapiSpec: parsedAsyncApi.spec,
|
|
1169
|
+
asyncapiChannel: resolvedChannel,
|
|
1170
|
+
title: channelInfo?.title ?? parsedAsyncApi.channel,
|
|
1171
|
+
description: channelInfo?.description,
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function resolveInheritedVersion(value, inherited) {
|
|
1176
|
+
if (typeof value === 'string' && value.trim().length > 0) return value.trim();
|
|
1177
|
+
return inherited;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function toPageMappingWithVersion(item, destDir, inheritedSpec, inheritedVersion, mode = 'file-fallback') {
|
|
1181
|
+
const mapping = toPageMapping(item, destDir, inheritedSpec, mode);
|
|
1182
|
+
if (!mapping) return null;
|
|
1183
|
+
if (mapping.kind === 'openapi-operation' && mapping.version === undefined) {
|
|
1184
|
+
mapping.version = inheritedVersion;
|
|
1185
|
+
}
|
|
1186
|
+
return mapping;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function toAsyncApiPageMappingWithVersion(item, destDir, inheritedSpec, inheritedVersion, mode = 'channel-only') {
|
|
1190
|
+
const mapping = toAsyncApiPageMapping(item, destDir, inheritedSpec, mode);
|
|
1191
|
+
if (!mapping) return null;
|
|
1192
|
+
if (mapping.version === undefined) mapping.version = inheritedVersion;
|
|
1193
|
+
return mapping;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function toOperationMapping(ref, destDir, inheritedVersion) {
|
|
1197
|
+
const slug = slugFromOpenApiOperation(ref.method, ref.endpoint);
|
|
1198
|
+
const dest = uniqueDestination(`${destDir}/${slug}`);
|
|
1199
|
+
return {
|
|
1200
|
+
src: `${ref.spec ? `${ref.spec} ` : ''}${ref.method} ${ref.endpoint}`,
|
|
1201
|
+
dest,
|
|
1202
|
+
kind: 'openapi-operation',
|
|
1203
|
+
openapiSpec: ref.spec,
|
|
1204
|
+
openapiMethod: ref.method,
|
|
1205
|
+
openapiEndpoint: ref.endpoint,
|
|
1206
|
+
openapiKind: ref.kind,
|
|
1207
|
+
title: ref.title,
|
|
1208
|
+
description: ref.description,
|
|
1209
|
+
deprecated: ref.deprecated,
|
|
1210
|
+
version: ref.version ?? inheritedVersion,
|
|
1211
|
+
content: ref.content,
|
|
1212
|
+
hidden: ref.hidden === true,
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function buildOpenApiMappings(openapi, destDir, fallbackSpec, inheritedVersion) {
|
|
1217
|
+
const specs = resolveOpenApiSpecList(openapi);
|
|
1218
|
+
if (specs.length === 0 && fallbackSpec) specs.push(fallbackSpec);
|
|
1219
|
+
if (specs.length === 0) return [];
|
|
1220
|
+
|
|
1221
|
+
const output = [];
|
|
1222
|
+
const seen = new Set();
|
|
1223
|
+
for (const spec of specs) {
|
|
1224
|
+
for (const operation of loadOpenApiOperations(spec)) {
|
|
1225
|
+
const key = `${operation.spec ?? ''}::${operation.kind ?? 'path'}::${operation.method}::${operation.endpoint}`;
|
|
1226
|
+
if (seen.has(key)) continue;
|
|
1227
|
+
seen.add(key);
|
|
1228
|
+
output.push(toOperationMapping(operation, destDir, inheritedVersion));
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return output;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function buildAsyncApiMappings(asyncapi, destDir, fallbackSpec, inheritedVersion) {
|
|
1235
|
+
const specs = resolveAsyncApiSpecList(asyncapi);
|
|
1236
|
+
if (specs.length === 0 && fallbackSpec) specs.push(fallbackSpec);
|
|
1237
|
+
if (specs.length === 0) return [];
|
|
1238
|
+
|
|
1239
|
+
const output = [];
|
|
1240
|
+
const seen = new Set();
|
|
1241
|
+
for (const spec of specs) {
|
|
1242
|
+
for (const channel of loadAsyncApiChannels(spec)) {
|
|
1243
|
+
const key = `${channel.spec ?? ''}::${channel.channel}`;
|
|
1244
|
+
if (seen.has(key)) continue;
|
|
1245
|
+
seen.add(key);
|
|
1246
|
+
const slug = slugFromOpenApiOperation('channel', channel.channel);
|
|
1247
|
+
const dest = uniqueDestination(`${destDir}/${slug}`);
|
|
1248
|
+
output.push({
|
|
1249
|
+
src: `${channel.spec ? `${channel.spec} ` : ''}${channel.channel}`,
|
|
1250
|
+
dest,
|
|
1251
|
+
kind: 'asyncapi-channel',
|
|
1252
|
+
asyncapiSpec: channel.spec,
|
|
1253
|
+
asyncapiChannel: channel.channel,
|
|
1254
|
+
title: channel.title,
|
|
1255
|
+
description: channel.description,
|
|
1256
|
+
version: inheritedVersion,
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return output;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function addGroup(group, parentDir, inheritedOpenApiSpec, inheritedVersion, inheritedAsyncApiSpec) {
|
|
1264
|
+
const groupDir = `${parentDir}/${group.slug}`;
|
|
1265
|
+
const pages = [];
|
|
1266
|
+
const groupOpenApiSpec = resolveDefaultOpenApiSpec(group.openapi) ?? inheritedOpenApiSpec;
|
|
1267
|
+
const groupAsyncApiSpec = resolveDefaultAsyncApiSpec(group.asyncapi) ?? inheritedAsyncApiSpec;
|
|
1268
|
+
const groupVersion = resolveInheritedVersion(group.version, inheritedVersion);
|
|
1269
|
+
const groupPageItems = Array.isArray(group.pages) ? group.pages : [];
|
|
1270
|
+
|
|
1271
|
+
for (const item of groupPageItems) {
|
|
1272
|
+
if (typeof item === 'string') {
|
|
1273
|
+
const parsedOpenApiRef = parseOpenApiOperationRef(item, groupOpenApiSpec);
|
|
1274
|
+
if (parsedOpenApiRef) {
|
|
1275
|
+
const openApiMapping = toPageMappingWithVersion(item, groupDir, groupOpenApiSpec, groupVersion, 'operation-only');
|
|
1276
|
+
if (!openApiMapping) continue;
|
|
1277
|
+
pageMap.push(openApiMapping);
|
|
1278
|
+
const pageEntry = metaEntryForDestination(groupDir, openApiMapping.dest);
|
|
1279
|
+
pages.push(openApiMapping.hidden ? `!${pageEntry}` : pageEntry);
|
|
1280
|
+
trackFirstPage(openApiMapping.dest, openApiMapping.hidden);
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
const asyncMapping = toAsyncApiPageMappingWithVersion(item, groupDir, groupAsyncApiSpec, groupVersion, 'channel-only');
|
|
1285
|
+
if (asyncMapping) {
|
|
1286
|
+
pageMap.push(asyncMapping);
|
|
1287
|
+
const pageEntry = metaEntryForDestination(groupDir, asyncMapping.dest);
|
|
1288
|
+
pages.push(asyncMapping.hidden ? `!${pageEntry}` : pageEntry);
|
|
1289
|
+
trackFirstPage(asyncMapping.dest, asyncMapping.hidden);
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
const fileMapping = toFilePageMapping(item, groupDir);
|
|
1294
|
+
pageMap.push(fileMapping);
|
|
1295
|
+
const pageEntry = metaEntryForDestination(groupDir, fileMapping.dest);
|
|
1296
|
+
pages.push(pageEntry);
|
|
1297
|
+
trackFirstPage(fileMapping.dest, false);
|
|
1298
|
+
} else if (isGroup(item)) {
|
|
1299
|
+
addGroup(item, groupDir, groupOpenApiSpec, groupVersion, groupAsyncApiSpec);
|
|
1300
|
+
pages.push(item.hidden ? `!${item.slug}` : item.slug);
|
|
1301
|
+
} else if (isSeparator(item)) {
|
|
1302
|
+
pages.push(`---${item.separator}---`);
|
|
1303
|
+
} else if (isLink(item)) {
|
|
1304
|
+
pages.push(
|
|
1305
|
+
item.icon
|
|
1306
|
+
? `[${item.icon}][${item.label}](${item.href})`
|
|
1307
|
+
: `[${item.label}](${item.href})`
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (groupPageItems.length === 0 && group.openapi !== undefined) {
|
|
1313
|
+
const generatedDestDir = resolveGenerationDestination(group.openapi, groupDir);
|
|
1314
|
+
const generatedMappings = buildOpenApiMappings(group.openapi, generatedDestDir, groupOpenApiSpec, groupVersion);
|
|
1315
|
+
for (const mapping of generatedMappings) {
|
|
1316
|
+
pageMap.push(mapping);
|
|
1317
|
+
const pageEntry = metaEntryForDestination(groupDir, mapping.dest);
|
|
1318
|
+
pages.push(mapping.hidden ? `!${pageEntry}` : pageEntry);
|
|
1319
|
+
trackFirstPage(mapping.dest, mapping.hidden);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (groupPageItems.length === 0 && group.asyncapi !== undefined) {
|
|
1324
|
+
const generatedDestDir = resolveGenerationDestination(group.asyncapi, groupDir);
|
|
1325
|
+
const generatedMappings = buildAsyncApiMappings(group.asyncapi, generatedDestDir, groupAsyncApiSpec, groupVersion);
|
|
1326
|
+
for (const mapping of generatedMappings) {
|
|
1327
|
+
pageMap.push(mapping);
|
|
1328
|
+
const pageEntry = metaEntryForDestination(groupDir, mapping.dest);
|
|
1329
|
+
pages.push(mapping.hidden ? `!${pageEntry}` : pageEntry);
|
|
1330
|
+
trackFirstPage(mapping.dest, mapping.hidden);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const groupMeta = {
|
|
1335
|
+
title: group.group,
|
|
1336
|
+
pages,
|
|
1337
|
+
defaultOpen: group.expanded !== false,
|
|
1338
|
+
};
|
|
1339
|
+
|
|
1340
|
+
if (group.icon) groupMeta.icon = group.icon;
|
|
1341
|
+
if (group.iconType) groupMeta.iconType = group.iconType;
|
|
1342
|
+
if (group.description) groupMeta.description = group.description;
|
|
1343
|
+
|
|
1344
|
+
metaFiles.push({ dir: groupDir, data: groupMeta });
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
for (const tab of rootTabs) {
|
|
1348
|
+
const tabPages = [];
|
|
1349
|
+
const tabOpenApiSpec = resolveDefaultOpenApiSpec(tab.openapi) ?? defaultOpenApiSpec;
|
|
1350
|
+
const tabAsyncApiSpec = resolveDefaultAsyncApiSpec(tab.asyncapi) ?? defaultAsyncApiSpec;
|
|
1351
|
+
const tabVersion = resolveInheritedVersion(tab.version);
|
|
1352
|
+
const tabGroups = Array.isArray(tab.groups) ? tab.groups : [];
|
|
1353
|
+
const tabPageItems = Array.isArray(tab.pages) ? tab.pages : [];
|
|
1354
|
+
|
|
1355
|
+
for (const group of tabGroups) {
|
|
1356
|
+
addGroup(group, tab.slug, tabOpenApiSpec, tabVersion, tabAsyncApiSpec);
|
|
1357
|
+
tabPages.push(group.hidden ? `!${group.slug}` : group.slug);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
for (const item of tabPageItems) {
|
|
1361
|
+
if (typeof item === 'string') {
|
|
1362
|
+
const parsedOpenApiRef = parseOpenApiOperationRef(item, tabOpenApiSpec);
|
|
1363
|
+
if (parsedOpenApiRef) {
|
|
1364
|
+
const openApiMapping = toPageMappingWithVersion(item, tab.slug, tabOpenApiSpec, tabVersion, 'operation-only');
|
|
1365
|
+
if (!openApiMapping) continue;
|
|
1366
|
+
pageMap.push(openApiMapping);
|
|
1367
|
+
const pageEntry = metaEntryForDestination(tab.slug, openApiMapping.dest);
|
|
1368
|
+
tabPages.push(openApiMapping.hidden ? `!${pageEntry}` : pageEntry);
|
|
1369
|
+
trackFirstPage(openApiMapping.dest, openApiMapping.hidden);
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
const asyncMapping = toAsyncApiPageMappingWithVersion(item, tab.slug, tabAsyncApiSpec, tabVersion, 'channel-only');
|
|
1374
|
+
if (asyncMapping) {
|
|
1375
|
+
pageMap.push(asyncMapping);
|
|
1376
|
+
const pageEntry = metaEntryForDestination(tab.slug, asyncMapping.dest);
|
|
1377
|
+
tabPages.push(asyncMapping.hidden ? `!${pageEntry}` : pageEntry);
|
|
1378
|
+
trackFirstPage(asyncMapping.dest, asyncMapping.hidden);
|
|
1379
|
+
continue;
|
|
213
1380
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
1381
|
+
|
|
1382
|
+
const fileMapping = toFilePageMapping(item, tab.slug);
|
|
1383
|
+
pageMap.push(fileMapping);
|
|
1384
|
+
const pageEntry = metaEntryForDestination(tab.slug, fileMapping.dest);
|
|
1385
|
+
tabPages.push(pageEntry);
|
|
1386
|
+
trackFirstPage(fileMapping.dest, false);
|
|
1387
|
+
} else {
|
|
1388
|
+
tabPages.push(metaEntry(item));
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
if (tabGroups.length === 0 && tabPageItems.length === 0 && tab.openapi !== undefined) {
|
|
1393
|
+
const generatedDestDir = resolveGenerationDestination(tab.openapi, tab.slug);
|
|
1394
|
+
const generatedMappings = buildOpenApiMappings(tab.openapi, generatedDestDir, tabOpenApiSpec, tabVersion);
|
|
1395
|
+
for (const mapping of generatedMappings) {
|
|
1396
|
+
pageMap.push(mapping);
|
|
1397
|
+
const pageEntry = metaEntryForDestination(tab.slug, mapping.dest);
|
|
1398
|
+
tabPages.push(mapping.hidden ? `!${pageEntry}` : pageEntry);
|
|
1399
|
+
trackFirstPage(mapping.dest, mapping.hidden);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if (tabGroups.length === 0 && tabPageItems.length === 0 && tab.asyncapi !== undefined) {
|
|
1404
|
+
const generatedDestDir = resolveGenerationDestination(tab.asyncapi, tab.slug);
|
|
1405
|
+
const generatedMappings = buildAsyncApiMappings(tab.asyncapi, generatedDestDir, tabAsyncApiSpec, tabVersion);
|
|
1406
|
+
for (const mapping of generatedMappings) {
|
|
1407
|
+
pageMap.push(mapping);
|
|
1408
|
+
const pageEntry = metaEntryForDestination(tab.slug, mapping.dest);
|
|
1409
|
+
tabPages.push(mapping.hidden ? `!${pageEntry}` : pageEntry);
|
|
1410
|
+
trackFirstPage(mapping.dest, mapping.hidden);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const tabMeta = {
|
|
1415
|
+
title: tab.tab,
|
|
1416
|
+
root: true,
|
|
1417
|
+
pages: tabPages,
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
if (tab.icon) tabMeta.icon = tab.icon;
|
|
1421
|
+
if (tab.iconType) tabMeta.iconType = tab.iconType;
|
|
1422
|
+
|
|
1423
|
+
metaFiles.push({ dir: tab.slug, data: tabMeta });
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
if (rootPages.length > 0) {
|
|
1427
|
+
metaFiles.push({ dir: '', data: { pages: rootPages } });
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
if (!hasFirstPage && firstHiddenPageCandidate) {
|
|
1431
|
+
firstPage = firstHiddenPageCandidate;
|
|
1432
|
+
hasFirstPage = true;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
return { pageMap, metaFiles, firstPage };
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function processPage(srcPath, destPath, slug) {
|
|
1439
|
+
let content = readFileSync(srcPath, 'utf-8');
|
|
1440
|
+
if (!content.startsWith('---')) {
|
|
1441
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
1442
|
+
const title = titleMatch ? titleMatch[1] : pageLabelFromSlug(slug);
|
|
1443
|
+
if (titleMatch) {
|
|
1444
|
+
content = content.replace(/^#\s+.+$/m, '').trimStart();
|
|
1445
|
+
}
|
|
1446
|
+
content = `---\ntitle: "${title}"\n---\n\n${content}`;
|
|
1447
|
+
}
|
|
1448
|
+
content = rewriteImportsInContent(content, srcPath, destPath);
|
|
1449
|
+
|
|
1450
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
1451
|
+
writeFileSync(destPath, content, 'utf-8');
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
function writeMetaFiles(metaFiles) {
|
|
1455
|
+
for (const meta of metaFiles) {
|
|
1456
|
+
const metaPath = join(contentDir, meta.dir, 'meta.json');
|
|
1457
|
+
mkdirSync(dirname(metaPath), { recursive: true });
|
|
1458
|
+
writeFileSync(metaPath, JSON.stringify(meta.data, null, 2) + '\n', 'utf-8');
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function writeIndexPage(firstPage) {
|
|
1463
|
+
writeFileSync(
|
|
1464
|
+
join(contentDir, 'index.mdx'),
|
|
1465
|
+
`---\ntitle: "Overview"\ndescription: Documentation powered by Velu\n---\n\nimport { Card, Cards } from "fumadocs-ui/components/card"\nimport { Callout } from "fumadocs-ui/components/callout"\n\n<Callout type="info">\n Welcome to your documentation site.\n</Callout>\n\n## Start here\n\n<Cards>\n <Card\n title="Read the docs"\n href="/${firstPage}/"\n description="Begin with the first page in your configured navigation."\n />\n</Cards>\n`,
|
|
1466
|
+
'utf-8'
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function writeLangContent(langCode, artifacts, isDefault, useLangFolders = false) {
|
|
1471
|
+
const storagePrefix = useLangFolders ? langCode : (isDefault ? '' : langCode);
|
|
1472
|
+
const urlPrefix = isDefault ? '' : langCode;
|
|
1473
|
+
|
|
1474
|
+
// Write meta files (prefixed for non-default)
|
|
1475
|
+
const metaFiles = storagePrefix
|
|
1476
|
+
? artifacts.metaFiles.map((meta) => ({
|
|
1477
|
+
dir: meta.dir ? `${storagePrefix}/${meta.dir}` : storagePrefix,
|
|
1478
|
+
data: { ...meta.data },
|
|
1479
|
+
}))
|
|
1480
|
+
: artifacts.metaFiles;
|
|
1481
|
+
writeMetaFiles(metaFiles);
|
|
1482
|
+
|
|
1483
|
+
function sanitizeFrontmatterValue(value) {
|
|
1484
|
+
return String(value).replace(/\r?\n+/g, ' ').replace(/"/g, '\\"').trim();
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Copy pages using explicit source paths from docs.json/velu.json
|
|
1488
|
+
for (const mapping of artifacts.pageMap) {
|
|
1489
|
+
const destPath = join(
|
|
1490
|
+
contentDir,
|
|
1491
|
+
storagePrefix ? `${storagePrefix}/${mapping.dest}.mdx` : `${mapping.dest}.mdx`,
|
|
1492
|
+
);
|
|
1493
|
+
|
|
1494
|
+
if (mapping.kind === 'openapi-operation') {
|
|
1495
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
1496
|
+
const operationLabel = `${mapping.openapiMethod || 'GET'} ${mapping.openapiEndpoint || '/'}`;
|
|
1497
|
+
const normalizedSpec = normalizeOpenApiSpecForFrontmatter(mapping.openapiSpec);
|
|
1498
|
+
const openapiValue = normalizedSpec
|
|
1499
|
+
? `${normalizedSpec} ${operationLabel}`
|
|
1500
|
+
: operationLabel;
|
|
1501
|
+
const title = sanitizeFrontmatterValue(mapping.title ?? operationLabel);
|
|
1502
|
+
const description = typeof mapping.description === 'string'
|
|
1503
|
+
? sanitizeFrontmatterValue(mapping.description)
|
|
1504
|
+
: '';
|
|
1505
|
+
const version = typeof mapping.version === 'string'
|
|
1506
|
+
? sanitizeFrontmatterValue(mapping.version)
|
|
1507
|
+
: '';
|
|
1508
|
+
const openapi = openapiValue.replace(/"/g, '\\"');
|
|
1509
|
+
const warning = normalizedSpec
|
|
1510
|
+
? ''
|
|
1511
|
+
: '\n> Warning: No OpenAPI spec source was resolved for this operation. Set `openapi` on this tab/group/navigation or at the top level.\n';
|
|
1512
|
+
const descriptionLine = description ? `\ndescription: "${description}"` : '';
|
|
1513
|
+
const deprecatedLine = mapping.deprecated === true ? `\ndeprecated: true` : '';
|
|
1514
|
+
const statusLine = mapping.deprecated === true ? `\nstatus: "deprecated"` : '';
|
|
1515
|
+
const versionLine = version ? `\nversion: "${version}"` : '';
|
|
1516
|
+
const content = typeof mapping.content === 'string' ? `${mapping.content.trim()}\n` : '';
|
|
1517
|
+
writeFileSync(
|
|
1518
|
+
destPath,
|
|
1519
|
+
`---\ntitle: "${title}"${descriptionLine}${deprecatedLine}${statusLine}${versionLine}\nopenapi: "${openapi}"\n---\n${warning}${content}`,
|
|
1520
|
+
'utf-8',
|
|
1521
|
+
);
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
if (mapping.kind === 'asyncapi-channel') {
|
|
1526
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
1527
|
+
const channelLabel = `${mapping.asyncapiChannel || 'channel'}`;
|
|
1528
|
+
const normalizedSpec = normalizeOpenApiSpecForFrontmatter(mapping.asyncapiSpec);
|
|
1529
|
+
const asyncapiValue = normalizedSpec
|
|
1530
|
+
? `${normalizedSpec} ${channelLabel}`
|
|
1531
|
+
: channelLabel;
|
|
1532
|
+
const title = sanitizeFrontmatterValue(mapping.title ?? channelLabel);
|
|
1533
|
+
const description = typeof mapping.description === 'string'
|
|
1534
|
+
? sanitizeFrontmatterValue(mapping.description)
|
|
1535
|
+
: '';
|
|
1536
|
+
const version = typeof mapping.version === 'string'
|
|
1537
|
+
? sanitizeFrontmatterValue(mapping.version)
|
|
1538
|
+
: '';
|
|
1539
|
+
const asyncapi = asyncapiValue.replace(/"/g, '\\"');
|
|
1540
|
+
const warning = normalizedSpec
|
|
1541
|
+
? ''
|
|
1542
|
+
: '\n> Warning: No AsyncAPI spec source was resolved for this channel. Set `asyncapi` on this tab/group/navigation or at the top level.\n';
|
|
1543
|
+
const descriptionLine = description ? `\ndescription: "${description}"` : '';
|
|
1544
|
+
const versionLine = version ? `\nversion: "${version}"` : '';
|
|
1545
|
+
writeFileSync(
|
|
1546
|
+
destPath,
|
|
1547
|
+
`---\ntitle: "${title}"${descriptionLine}${versionLine}\nasyncapi: "${asyncapi}"\n---\n${warning}`,
|
|
1548
|
+
'utf-8',
|
|
1549
|
+
);
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
const src = mapping.src;
|
|
1554
|
+
let srcPath = join(docsDir, `${src}.mdx`);
|
|
1555
|
+
let ext = '.mdx';
|
|
1556
|
+
if (!existsSync(srcPath)) {
|
|
1557
|
+
srcPath = join(docsDir, `${src}.md`);
|
|
1558
|
+
ext = '.md';
|
|
1559
|
+
}
|
|
1560
|
+
if (!existsSync(srcPath)) {
|
|
1561
|
+
console.warn(` \x1b[33mWarning\x1b[0m Missing page source: ${src}${ext} (language: ${langCode})`);
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
processPage(srcPath, destPath, src);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// Index page
|
|
1568
|
+
const href = urlPrefix ? `/${urlPrefix}/${artifacts.firstPage}/` : `/${artifacts.firstPage}/`;
|
|
1569
|
+
const indexPath = storagePrefix ? join(contentDir, storagePrefix, 'index.mdx') : join(contentDir, 'index.mdx');
|
|
1570
|
+
writeFileSync(
|
|
1571
|
+
indexPath,
|
|
1572
|
+
`---\ntitle: "Overview"\ndescription: Documentation powered by Velu\n---\n\nimport { Card, Cards } from "fumadocs-ui/components/card"\nimport { Callout } from "fumadocs-ui/components/callout"\n\n<Callout type="info">\n Welcome to your documentation site.\n</Callout>\n\n## Start here\n\n<Cards>\n <Card\n title="Read the docs"\n href="${href}"\n description="Begin with the first page in your configured navigation."\n />\n</Cards>\n`,
|
|
1573
|
+
'utf-8'
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function rebuildFromConfig() {
|
|
1578
|
+
const config = loadConfig();
|
|
1579
|
+
const navLanguages = config.navigation?.languages;
|
|
1580
|
+
const simpleLanguages = config.languages || [];
|
|
1581
|
+
|
|
1582
|
+
rmSync(contentDir, { recursive: true, force: true });
|
|
1583
|
+
mkdirSync(contentDir, { recursive: true });
|
|
1584
|
+
writeRedirectArtifacts(config);
|
|
1585
|
+
rebuildSourceMirror();
|
|
1586
|
+
|
|
1587
|
+
// ── Mode 1: Per-language navigation (Mintlify-style) ──────────────
|
|
1588
|
+
if (navLanguages && navLanguages.length > 0) {
|
|
1589
|
+
const rootPages = [];
|
|
1590
|
+
|
|
1591
|
+
for (let i = 0; i < navLanguages.length; i++) {
|
|
1592
|
+
const langEntry = navLanguages[i];
|
|
1593
|
+
const langCode = langEntry.language;
|
|
1594
|
+
const isDefault = i === 0;
|
|
1595
|
+
|
|
1596
|
+
// Build artifacts using this language's own tabs
|
|
1597
|
+
const langConfig = { ...config, navigation: { ...config.navigation, tabs: langEntry.tabs } };
|
|
1598
|
+
const artifacts = buildArtifacts(langConfig);
|
|
1599
|
+
|
|
1600
|
+
writeLangContent(langCode, artifacts, isDefault, true);
|
|
1601
|
+
rootPages.push(`!${langCode}`);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// Write root meta with default tabs + hidden language folders
|
|
1605
|
+
writeFileSync(
|
|
1606
|
+
join(contentDir, 'meta.json'),
|
|
1607
|
+
JSON.stringify({ pages: rootPages }, null, 2) + '\n',
|
|
1608
|
+
'utf-8'
|
|
1609
|
+
);
|
|
1610
|
+
|
|
1611
|
+
// Return the default language's page map for file watching
|
|
1612
|
+
const defaultConfig = { ...config, navigation: { ...config.navigation, tabs: navLanguages[0].tabs } };
|
|
1613
|
+
const generatedPageMap = buildArtifacts(defaultConfig).pageMap;
|
|
1614
|
+
generateOgImages(config);
|
|
1615
|
+
return generatedPageMap;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// ── Mode 2: Simple multi-lang (same nav, content in docs/<lang>/) ─
|
|
1619
|
+
const artifacts = buildArtifacts(config);
|
|
1620
|
+
|
|
1621
|
+
const useLangFolders = simpleLanguages.length > 1;
|
|
1622
|
+
writeLangContent(simpleLanguages[0] || 'en', artifacts, true, useLangFolders);
|
|
1623
|
+
|
|
1624
|
+
if (simpleLanguages.length > 1) {
|
|
1625
|
+
const rootMetaPath = join(contentDir, 'meta.json');
|
|
1626
|
+
const rootPages = [`!${simpleLanguages[0] || 'en'}`];
|
|
1627
|
+
|
|
1628
|
+
for (const lang of simpleLanguages.slice(1)) {
|
|
1629
|
+
writeLangContent(lang, artifacts, false, true);
|
|
1630
|
+
rootPages.push(`!${lang}`);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
writeFileSync(rootMetaPath, JSON.stringify({ pages: rootPages }, null, 2) + '\n', 'utf-8');
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
generateOgImages(config);
|
|
1637
|
+
return artifacts.pageMap;
|
|
217
1638
|
}
|
|
1639
|
+
|
|
1640
|
+
let pageMap = rebuildFromConfig();
|
|
1641
|
+
copyStaticAssets();
|
|
1642
|
+
|
|
1643
|
+
function syncMarkdownFile(filename) {
|
|
1644
|
+
syncSourceMirrorFile(filename);
|
|
1645
|
+
const srcSlug = filename.replace(/\\/g, '/').replace(/\.(md|mdx)$/, '');
|
|
1646
|
+
let srcPath = join(docsDir, `${srcSlug}.mdx`);
|
|
1647
|
+
if (!existsSync(srcPath)) {
|
|
1648
|
+
srcPath = join(docsDir, `${srcSlug}.md`);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
if (!existsSync(srcPath)) {
|
|
1652
|
+
pageMap = rebuildFromConfig();
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
const matches = pageMap.filter((entry) => entry.src === srcSlug);
|
|
1657
|
+
if (matches.length === 0) return;
|
|
1658
|
+
|
|
1659
|
+
for (const match of matches) {
|
|
1660
|
+
const destPath = join(contentDir, `${match.dest}.mdx`);
|
|
1661
|
+
processPage(srcPath, destPath, srcSlug);
|
|
1662
|
+
}
|
|
218
1663
|
|
|
219
|
-
|
|
1664
|
+
generateOgImages(loadConfig());
|
|
1665
|
+
|
|
1666
|
+
console.log(' \x1b[32m↻\x1b[0m ' + srcSlug);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
function syncConfig() {
|
|
1670
|
+
const srcPath = resolveConfigPath();
|
|
1671
|
+
copyFileSync(srcPath, resolve(PRIMARY_CONFIG_NAME));
|
|
1672
|
+
copyFileSync(srcPath, resolve(LEGACY_CONFIG_NAME));
|
|
1673
|
+
pageMap = rebuildFromConfig();
|
|
1674
|
+
copyStaticAssets();
|
|
1675
|
+
console.log(' \x1b[32m↻\x1b[0m docs.json/velu.json updated (navigation/content synced)');
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
function startWatcher() {
|
|
1679
|
+
const debounce = new Map();
|
|
1680
|
+
|
|
1681
|
+
watch(docsDir, { recursive: true }, (_, rawFilename) => {
|
|
1682
|
+
if (!rawFilename) return;
|
|
1683
|
+
const filename = rawFilename.replace(/\\/g, '/');
|
|
1684
|
+
|
|
1685
|
+
if (filename.startsWith('.velu-out/')) return;
|
|
1686
|
+
if (filename.includes('node_modules')) return;
|
|
1687
|
+
if (filename.startsWith('.')) return;
|
|
1688
|
+
|
|
1689
|
+
if (debounce.has(filename)) clearTimeout(debounce.get(filename));
|
|
1690
|
+
debounce.set(
|
|
1691
|
+
filename,
|
|
1692
|
+
setTimeout(() => {
|
|
1693
|
+
debounce.delete(filename);
|
|
1694
|
+
|
|
1695
|
+
try {
|
|
1696
|
+
if (filename === PRIMARY_CONFIG_NAME || filename === LEGACY_CONFIG_NAME) {
|
|
1697
|
+
syncConfig();
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
syncSourceMirrorFile(filename);
|
|
1702
|
+
|
|
1703
|
+
const ext = extname(filename);
|
|
1704
|
+
if (ext === '.md' || ext === '.mdx') {
|
|
1705
|
+
syncMarkdownFile(filename);
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
if (isStaticAsset(filename)) {
|
|
1710
|
+
const src = join(docsDir, filename);
|
|
1711
|
+
const dest = join(publicDir, filename);
|
|
1712
|
+
if (existsSync(src)) {
|
|
1713
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
1714
|
+
copyFileSync(src, dest);
|
|
1715
|
+
generateOgImages(loadConfig());
|
|
1716
|
+
console.log(' \x1b[32m↻\x1b[0m ' + filename);
|
|
1717
|
+
} else {
|
|
1718
|
+
rmSync(dest, { force: true });
|
|
1719
|
+
generateOgImages(loadConfig());
|
|
1720
|
+
console.log(' \x1b[32m↻\x1b[0m removed ' + filename);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
} catch (error) {
|
|
1724
|
+
console.error(' \x1b[31m✗\x1b[0m Failed to sync ' + filename + ': ' + error.message);
|
|
1725
|
+
}
|
|
1726
|
+
}, 120)
|
|
1727
|
+
);
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
function runNext(command, port, envOverrides = {}) {
|
|
220
1732
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
221
1733
|
const args = [nextBinPath, command];
|
|
222
1734
|
if (command === 'dev' || command === 'start') {
|
|
223
1735
|
args.push('--port', String(port));
|
|
224
1736
|
}
|
|
225
|
-
|
|
1737
|
+
|
|
226
1738
|
const child = spawn(process.execPath, args, {
|
|
227
1739
|
cwd: '.',
|
|
228
1740
|
stdio: 'inherit',
|
|
229
|
-
env: process.env,
|
|
1741
|
+
env: { ...process.env, ...envOverrides },
|
|
230
1742
|
});
|
|
231
|
-
|
|
232
|
-
child.on('exit', (code) => {
|
|
233
|
-
if (code === 0) resolvePromise();
|
|
234
|
-
else rejectPromise(new Error(`${command} exited with ${code}`));
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
//
|
|
240
|
-
const args = process.argv.slice(2);
|
|
241
|
-
const command = args[0] || 'dev';
|
|
242
|
-
const portIdx = args.indexOf('--port');
|
|
243
|
-
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : 4321;
|
|
244
|
-
|
|
245
|
-
if (command === 'dev') {
|
|
246
|
-
console.log('');
|
|
247
|
-
console.log(' \x1b[36mvelu\x1b[0m dev server');
|
|
248
|
-
console.log('');
|
|
249
|
-
console.log(' watching for file changes...');
|
|
250
|
-
startWatcher();
|
|
251
|
-
await runNext('dev', port);
|
|
1743
|
+
|
|
1744
|
+
child.on('exit', (code) => {
|
|
1745
|
+
if (code === 0) resolvePromise();
|
|
1746
|
+
else rejectPromise(new Error(`${command} exited with ${code}`));
|
|
1747
|
+
});
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// ── CLI ──────────────────────────────────────────────────────────────────────
|
|
1752
|
+
const args = process.argv.slice(2);
|
|
1753
|
+
const command = args[0] || 'dev';
|
|
1754
|
+
const portIdx = args.indexOf('--port');
|
|
1755
|
+
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : 4321;
|
|
1756
|
+
|
|
1757
|
+
if (command === 'dev') {
|
|
1758
|
+
console.log('');
|
|
1759
|
+
console.log(' \x1b[36mvelu\x1b[0m dev server');
|
|
1760
|
+
console.log('');
|
|
1761
|
+
console.log(' watching for file changes...');
|
|
1762
|
+
startWatcher();
|
|
1763
|
+
await runNext('dev', port);
|
|
252
1764
|
} else if (command === 'build') {
|
|
253
1765
|
console.log('\n Building site...\n');
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
1766
|
+
// Static export cannot include API route handlers.
|
|
1767
|
+
// Keep proxy routes for dev, but remove them for production export build.
|
|
1768
|
+
rmSync(resolve('app', 'api'), { recursive: true, force: true });
|
|
1769
|
+
await runNext('build', port, { VELU_STATIC_EXPORT: '1' });
|
|
1770
|
+
|
|
1771
|
+
// Run Pagefind to index the static output for search
|
|
1772
|
+
console.log(' Indexing for search...');
|
|
1773
|
+
const pagefindBin = join(dirname(require.resolve('next/package.json')), '..', 'pagefind', 'lib', 'runner', 'bin.cjs');
|
|
1774
|
+
await new Promise((res, rej) => {
|
|
1775
|
+
const pf = spawn(process.execPath, [pagefindBin, '--site', 'dist', '--output-path', 'dist/pagefind'], {
|
|
1776
|
+
cwd: '.',
|
|
1777
|
+
stdio: 'inherit',
|
|
1778
|
+
});
|
|
1779
|
+
pf.on('exit', (code) => (code === 0 ? res() : rej(new Error(`pagefind exited with ${code}`))));
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
console.log('\n ✅ Site built successfully.\n');
|
|
1783
|
+
} else {
|
|
1784
|
+
console.error(`Unknown server command: ${command}`);
|
|
1785
|
+
process.exit(1);
|
|
1786
|
+
}
|
|
1787
|
+
|