@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.
- package/package.json +15 -6
- package/schema/velu.schema.json +1251 -115
- package/src/build.ts +1121 -304
- package/src/cli.ts +90 -26
- package/src/engine/_server.mjs +1684 -277
- package/src/engine/app/(docs)/[...slug]/layout.tsx +371 -0
- package/src/engine/app/(docs)/[...slug]/page.tsx +926 -0
- 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 +3157 -3
- package/src/engine/app/layout.tsx +56 -1
- 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 +63 -0
- package/src/engine/app/rss-file/[...slug]/route.ts +169 -0
- package/src/engine/app/sitemap.xml/route.ts +82 -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/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 +1682 -0
- package/src/engine/components/page-feedback.tsx +153 -0
- package/src/engine/components/product-switcher.tsx +27 -3
- package/src/engine/components/prompt.tsx +90 -0
- package/src/engine/components/providers.tsx +1 -6
- package/src/engine/components/search.tsx +4 -0
- package/src/engine/components/sidebar-links.tsx +13 -15
- package/src/engine/components/synced-tabs.tsx +57 -0
- package/src/engine/components/toc-examples.tsx +110 -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 +30 -2
- package/src/engine/lib/llms.ts +444 -0
- package/src/engine/lib/navigation-normalize.mjs +481 -412
- package/src/engine/lib/navigation-normalize.ts +261 -54
- package/src/engine/lib/redirects.ts +194 -0
- package/src/engine/lib/source.ts +107 -4
- package/src/engine/lib/velu.ts +368 -2
- package/src/engine/mdx-components.tsx +648 -0
- package/src/engine/middleware.ts +66 -0
- 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 +11 -3
- package/src/navigation-normalize.ts +252 -54
- package/src/themes.ts +6 -6
- package/src/validate.ts +119 -6
- package/src/engine/app/(docs)/[[...slug]]/layout.tsx +0 -87
- package/src/engine/app/(docs)/[[...slug]]/page.tsx +0 -146
package/src/engine/_server.mjs
CHANGED
|
@@ -1,380 +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
|
-
import { normalizeConfigNavigation } from './lib/navigation-normalize.mjs';
|
|
7
|
-
|
|
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
|
+
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
9
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
|
+
}
|
|
10
75
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
76
|
+
function escapeXml(value) {
|
|
77
|
+
return String(value ?? '')
|
|
78
|
+
.replace(/&/g, '&')
|
|
79
|
+
.replace(/</g, '<')
|
|
80
|
+
.replace(/>/g, '>')
|
|
81
|
+
.replace(/"/g, '"')
|
|
82
|
+
.replace(/'/g, ''');
|
|
83
|
+
}
|
|
14
84
|
|
|
15
|
-
function
|
|
16
|
-
const
|
|
17
|
-
|
|
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;
|
|
18
89
|
}
|
|
19
90
|
|
|
20
|
-
function
|
|
21
|
-
|
|
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
|
+
};
|
|
22
102
|
}
|
|
23
103
|
|
|
24
|
-
function
|
|
25
|
-
const
|
|
26
|
-
return
|
|
104
|
+
function rgbaFromHex(hex, alpha) {
|
|
105
|
+
const { r, g, b } = hexToRgb(hex);
|
|
106
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
27
107
|
}
|
|
28
108
|
|
|
29
|
-
function
|
|
30
|
-
|
|
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());
|
|
31
116
|
}
|
|
32
117
|
|
|
33
|
-
function
|
|
34
|
-
|
|
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 {};
|
|
127
|
+
}
|
|
35
128
|
}
|
|
36
129
|
|
|
37
|
-
function
|
|
38
|
-
|
|
130
|
+
function ensureLeadingSlash(value) {
|
|
131
|
+
const text = String(value ?? '').trim();
|
|
132
|
+
if (!text) return '';
|
|
133
|
+
return text.startsWith('/') ? text : `/${text}`;
|
|
39
134
|
}
|
|
40
135
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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();
|
|
48
145
|
}
|
|
49
|
-
|
|
146
|
+
|
|
147
|
+
if (!rawLogo) return null;
|
|
148
|
+
if (/^https?:\/\//i.test(rawLogo)) return rawLogo;
|
|
149
|
+
|
|
150
|
+
const localPath = join(docsDir, rawLogo.replace(/^\/+/, ''));
|
|
151
|
+
if (!existsSync(localPath)) return ensureLeadingSlash(rawLogo);
|
|
152
|
+
|
|
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}`;
|
|
50
167
|
}
|
|
51
168
|
|
|
52
|
-
function
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
let firstPage = 'quickstart';
|
|
58
|
-
let hasFirstPage = false;
|
|
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
|
+
}
|
|
59
174
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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;
|
|
65
192
|
}
|
|
193
|
+
}
|
|
66
194
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const pages = [];
|
|
195
|
+
function resolveSectionLabel(routeSegments, siteName) {
|
|
196
|
+
if (!Array.isArray(routeSegments) || routeSegments.length === 0) return siteName;
|
|
70
197
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
trackFirstPage(dest);
|
|
78
|
-
} else if (isGroup(item)) {
|
|
79
|
-
addGroup(item, groupDir);
|
|
80
|
-
pages.push(item.hidden ? `!${item.slug}` : item.slug);
|
|
81
|
-
} else if (isSeparator(item)) {
|
|
82
|
-
pages.push(`---${item.separator}---`);
|
|
83
|
-
} else if (isLink(item)) {
|
|
84
|
-
pages.push(
|
|
85
|
-
item.icon
|
|
86
|
-
? `[${item.icon}][${item.label}](${item.href})`
|
|
87
|
-
: `[${item.label}](${item.href})`
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
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
|
+
}
|
|
91
204
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
defaultOpen: group.expanded !== false,
|
|
96
|
-
};
|
|
205
|
+
if (firstMeta?.title) return firstMeta.title;
|
|
206
|
+
return humanizeSegment(routeSegments[0]);
|
|
207
|
+
}
|
|
97
208
|
|
|
98
|
-
|
|
99
|
-
|
|
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
|
+
}
|
|
100
216
|
|
|
101
|
-
|
|
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;
|
|
223
|
+
|
|
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;
|
|
231
|
+
}
|
|
232
|
+
lines.push(current);
|
|
233
|
+
current = '';
|
|
234
|
+
if (lines.length === maxLines - 1) break;
|
|
102
235
|
}
|
|
103
236
|
|
|
104
|
-
|
|
105
|
-
|
|
237
|
+
const tail = [...(current ? [current] : []), ...words.slice(index)].join(' ').trim();
|
|
238
|
+
if (tail) lines.push(tail);
|
|
106
239
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
+
}
|
|
111
246
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
247
|
+
return lines;
|
|
248
|
+
}
|
|
249
|
+
|
|
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;
|
|
121
260
|
}
|
|
261
|
+
if (!entry.isFile()) continue;
|
|
262
|
+
if (!/\.(md|mdx)$/i.test(entry.name)) continue;
|
|
263
|
+
files.push(fullPath);
|
|
122
264
|
}
|
|
265
|
+
}
|
|
266
|
+
walk(contentDir);
|
|
267
|
+
return files;
|
|
268
|
+
}
|
|
123
269
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
}
|
|
129
312
|
|
|
130
|
-
|
|
313
|
+
function generateOgImages(config) {
|
|
314
|
+
const ogRootDir = join(publicDir, 'og');
|
|
315
|
+
rmSync(ogRootDir, { recursive: true, force: true });
|
|
316
|
+
mkdirSync(ogRootDir, { recursive: true });
|
|
317
|
+
|
|
318
|
+
const files = collectContentMarkdownFiles();
|
|
319
|
+
const siteName = resolveSiteName(config);
|
|
320
|
+
const logoHref = resolveLogoReference(config);
|
|
321
|
+
const primaryColor = resolvePrimaryColor(config);
|
|
322
|
+
|
|
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
|
+
});
|
|
131
343
|
|
|
132
|
-
|
|
344
|
+
const outPath = toOgOutputPath(routePath);
|
|
345
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
346
|
+
writeFileSync(outPath, svg, 'utf-8');
|
|
133
347
|
}
|
|
348
|
+
}
|
|
134
349
|
|
|
135
|
-
|
|
136
|
-
|
|
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;
|
|
137
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;
|
|
663
|
+
}
|
|
664
|
+
return undefined;
|
|
665
|
+
}
|
|
138
666
|
|
|
139
|
-
|
|
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;
|
|
140
718
|
}
|
|
141
719
|
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
if (!
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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' };
|
|
149
732
|
}
|
|
150
|
-
|
|
733
|
+
if (!endpoint.startsWith('/')) return null;
|
|
734
|
+
return { spec: withSpec[1].trim(), method, endpoint, kind: 'path' };
|
|
151
735
|
}
|
|
152
736
|
|
|
153
|
-
|
|
154
|
-
|
|
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' };
|
|
155
748
|
}
|
|
156
749
|
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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 };
|
|
162
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}`;
|
|
163
779
|
}
|
|
164
780
|
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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 [];
|
|
171
794
|
}
|
|
172
795
|
|
|
173
|
-
function
|
|
174
|
-
const
|
|
175
|
-
|
|
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
|
+
}
|
|
176
809
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
dir: meta.dir ? `${storagePrefix}/${meta.dir}` : storagePrefix,
|
|
181
|
-
data: { ...meta.data },
|
|
182
|
-
}))
|
|
183
|
-
: artifacts.metaFiles;
|
|
184
|
-
writeMetaFiles(metaFiles);
|
|
810
|
+
function parseAsyncApiDocument(rawSource) {
|
|
811
|
+
return parseOpenApiDocument(rawSource);
|
|
812
|
+
}
|
|
185
813
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
processPage(srcPath, destPath, src);
|
|
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];
|
|
195
822
|
}
|
|
823
|
+
return current;
|
|
824
|
+
}
|
|
196
825
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
826
|
+
function loadAsyncApiChannels(specSource) {
|
|
827
|
+
if (/^https?:\/\//i.test(specSource) || specSource.startsWith('file://')) return [];
|
|
828
|
+
|
|
829
|
+
const resolvedPath = specSource.startsWith('/')
|
|
830
|
+
? join(docsDir, specSource.replace(/^\/+/, ''))
|
|
831
|
+
: resolve(docsDir, specSource);
|
|
832
|
+
if (!existsSync(resolvedPath)) return [];
|
|
833
|
+
|
|
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 [];
|
|
838
|
+
|
|
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);
|
|
205
852
|
}
|
|
206
853
|
|
|
207
|
-
function
|
|
208
|
-
|
|
209
|
-
const navLanguages = config.navigation?.languages;
|
|
210
|
-
const simpleLanguages = config.languages || [];
|
|
854
|
+
function getAsyncApiChannelInfo(specSource, channelName) {
|
|
855
|
+
if (/^https?:\/\//i.test(specSource) || specSource.startsWith('file://')) return null;
|
|
211
856
|
|
|
212
|
-
|
|
213
|
-
|
|
857
|
+
const resolvedPath = specSource.startsWith('/')
|
|
858
|
+
? join(docsDir, specSource.replace(/^\/+/, ''))
|
|
859
|
+
: resolve(docsDir, specSource);
|
|
860
|
+
if (!existsSync(resolvedPath)) return null;
|
|
214
861
|
|
|
215
|
-
|
|
216
|
-
if (
|
|
217
|
-
const rootPages = [];
|
|
862
|
+
const parsed = parseAsyncApiDocument(readFileSync(resolvedPath, 'utf-8'));
|
|
863
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
218
864
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const langCode = langEntry.language;
|
|
222
|
-
const isDefault = i === 0;
|
|
865
|
+
const channels = parsed.channels;
|
|
866
|
+
if (!channels || typeof channels !== 'object') return null;
|
|
223
867
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
return {
|
|
885
|
+
channel: channelName,
|
|
886
|
+
title: typeof channelObject.title === 'string' ? channelObject.title : channelName,
|
|
887
|
+
description: typeof channelObject.description === 'string' ? channelObject.description : undefined,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
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
|
+
}
|
|
905
|
+
|
|
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
|
+
}
|
|
932
|
+
|
|
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
|
+
}
|
|
227
946
|
|
|
228
|
-
|
|
229
|
-
|
|
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]);
|
|
963
|
+
}
|
|
964
|
+
|
|
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
|
+
}
|
|
971
|
+
|
|
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
|
+
}
|
|
230
1010
|
}
|
|
1011
|
+
}
|
|
231
1012
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
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(/^\.?\/*/, '')}`;
|
|
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();
|
|
238
1070
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
+
}
|
|
242
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
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function metaEntryForDestination(baseDir, destination) {
|
|
1095
|
+
const fromParts = baseDir.split('/').filter(Boolean);
|
|
1096
|
+
const toParts = destination.split('/').filter(Boolean);
|
|
243
1097
|
|
|
244
|
-
|
|
245
|
-
|
|
1098
|
+
let index = 0;
|
|
1099
|
+
while (index < fromParts.length && index < toParts.length && fromParts[index] === toParts[index]) {
|
|
1100
|
+
index += 1;
|
|
1101
|
+
}
|
|
246
1102
|
|
|
247
|
-
|
|
248
|
-
|
|
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
|
+
}
|
|
249
1108
|
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
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
|
+
}
|
|
253
1122
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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);
|
|
257
1128
|
}
|
|
258
1129
|
|
|
259
|
-
|
|
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
|
+
};
|
|
260
1152
|
}
|
|
261
1153
|
|
|
262
|
-
|
|
263
|
-
|
|
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
|
+
}
|
|
264
1174
|
|
|
265
|
-
|
|
1175
|
+
function resolveInheritedVersion(value, inherited) {
|
|
1176
|
+
if (typeof value === 'string' && value.trim().length > 0) return value.trim();
|
|
1177
|
+
return inherited;
|
|
1178
|
+
}
|
|
266
1179
|
|
|
267
|
-
function
|
|
268
|
-
|
|
269
|
-
|
|
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
|
+
}
|
|
270
1188
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
return;
|
|
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;
|
|
274
1194
|
}
|
|
275
1195
|
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
}
|
|
278
1215
|
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
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;
|
|
282
1232
|
}
|
|
283
1233
|
|
|
284
|
-
|
|
285
|
-
|
|
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
|
+
}
|
|
286
1262
|
|
|
287
|
-
function
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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 : [];
|
|
293
1270
|
|
|
294
|
-
|
|
295
|
-
|
|
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
|
+
}
|
|
296
1283
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
+
}
|
|
300
1292
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
+
}
|
|
304
1311
|
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
+
}
|
|
310
1322
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
+
}
|
|
316
1359
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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;
|
|
322
1371
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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;
|
|
1380
|
+
}
|
|
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 };
|
|
326
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
|
+
}
|
|
327
1486
|
|
|
328
|
-
|
|
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;
|
|
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
|
+
}
|
|
1663
|
+
|
|
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 = {}) {
|
|
329
1732
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
330
1733
|
const args = [nextBinPath, command];
|
|
331
1734
|
if (command === 'dev' || command === 'start') {
|
|
332
1735
|
args.push('--port', String(port));
|
|
333
1736
|
}
|
|
334
|
-
|
|
1737
|
+
|
|
335
1738
|
const child = spawn(process.execPath, args, {
|
|
336
1739
|
cwd: '.',
|
|
337
1740
|
stdio: 'inherit',
|
|
338
|
-
env: process.env,
|
|
1741
|
+
env: { ...process.env, ...envOverrides },
|
|
339
1742
|
});
|
|
340
|
-
|
|
341
|
-
child.on('exit', (code) => {
|
|
342
|
-
if (code === 0) resolvePromise();
|
|
343
|
-
else rejectPromise(new Error(`${command} exited with ${code}`));
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
//
|
|
349
|
-
const args = process.argv.slice(2);
|
|
350
|
-
const command = args[0] || 'dev';
|
|
351
|
-
const portIdx = args.indexOf('--port');
|
|
352
|
-
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : 4321;
|
|
353
|
-
|
|
354
|
-
if (command === 'dev') {
|
|
355
|
-
console.log('');
|
|
356
|
-
console.log(' \x1b[36mvelu\x1b[0m dev server');
|
|
357
|
-
console.log('');
|
|
358
|
-
console.log(' watching for file changes...');
|
|
359
|
-
startWatcher();
|
|
360
|
-
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);
|
|
361
1764
|
} else if (command === 'build') {
|
|
362
1765
|
console.log('\n Building site...\n');
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
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
|
+
|