@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/build.ts
CHANGED
|
@@ -1,408 +1,1225 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, cpSync, existsSync, rmSync } from "node:fs";
|
|
2
|
-
import { join, dirname } from "node:path";
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, cpSync, existsSync, rmSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join, dirname, relative, extname, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
4
5
|
import { generateThemeCss, resolveThemeName, type VeluColors, type VeluStyling } from "./themes.js";
|
|
5
6
|
import { normalizeConfigNavigation } from "./navigation-normalize.js";
|
|
6
|
-
|
|
7
|
-
// ── Engine directory (shipped with the CLI package) ──────────────────────────
|
|
8
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
-
const __dirname = dirname(__filename);
|
|
10
|
-
const PACKAGED_ENGINE_DIR = join(__dirname, "engine");
|
|
11
|
-
const DEV_ENGINE_DIR = join(__dirname, "..", "src", "engine");
|
|
12
|
-
const ENGINE_DIR = existsSync(DEV_ENGINE_DIR) ? DEV_ENGINE_DIR : PACKAGED_ENGINE_DIR;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
7
|
+
|
|
8
|
+
// ── Engine directory (shipped with the CLI package) ──────────────────────────
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
const PACKAGED_ENGINE_DIR = join(__dirname, "engine");
|
|
12
|
+
const DEV_ENGINE_DIR = join(__dirname, "..", "src", "engine");
|
|
13
|
+
const ENGINE_DIR = existsSync(DEV_ENGINE_DIR) ? DEV_ENGINE_DIR : PACKAGED_ENGINE_DIR;
|
|
14
|
+
const PRIMARY_CONFIG_NAME = "docs.json";
|
|
15
|
+
const LEGACY_CONFIG_NAME = "velu.json";
|
|
16
|
+
const SOURCE_MIRROR_DIR = "velu-imports";
|
|
17
|
+
|
|
18
|
+
const SOURCE_MIRROR_EXTENSIONS = new Set([
|
|
19
|
+
".md", ".mdx", ".jsx", ".js", ".tsx", ".ts",
|
|
20
|
+
".json", ".yaml", ".yml", ".css",
|
|
21
|
+
".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico",
|
|
22
|
+
".woff", ".woff2", ".ttf", ".eot",
|
|
23
|
+
".mp4", ".webm", ".mp3", ".wav",
|
|
24
|
+
".pdf", ".txt", ".xml", ".csv", ".zip",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const IMPORT_REWRITE_EXTENSIONS = new Set([".md", ".mdx", ".jsx", ".js", ".tsx", ".ts"]);
|
|
28
|
+
|
|
29
|
+
function resolveConfigPath(docsDir: string): string {
|
|
30
|
+
const primary = join(docsDir, PRIMARY_CONFIG_NAME);
|
|
31
|
+
if (existsSync(primary)) return primary;
|
|
32
|
+
const legacy = join(docsDir, LEGACY_CONFIG_NAME);
|
|
33
|
+
if (existsSync(legacy)) return legacy;
|
|
34
|
+
throw new Error(`No ${PRIMARY_CONFIG_NAME} or ${LEGACY_CONFIG_NAME} found in ${docsDir}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Types (used only by build.ts for page copying) ─────────────────────────────
|
|
38
|
+
|
|
39
|
+
interface VeluSeparator {
|
|
40
|
+
separator: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface VeluLink {
|
|
44
|
+
href: string;
|
|
45
|
+
label: string;
|
|
46
|
+
icon?: string;
|
|
47
|
+
iconType?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
26
50
|
interface VeluAnchor {
|
|
27
51
|
anchor: string;
|
|
28
52
|
href?: string;
|
|
29
53
|
icon?: string;
|
|
54
|
+
iconType?: string;
|
|
55
|
+
openapi?: VeluOpenApiSource;
|
|
56
|
+
version?: string;
|
|
30
57
|
color?: {
|
|
31
58
|
light: string;
|
|
32
59
|
dark: string;
|
|
33
60
|
};
|
|
34
|
-
tabs?: VeluTab[];
|
|
35
|
-
hidden?: boolean;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface VeluGlobalTab {
|
|
39
|
-
tab: string;
|
|
40
|
-
href: string;
|
|
41
|
-
icon?: string;
|
|
42
|
-
|
|
43
|
-
|
|
61
|
+
tabs?: VeluTab[];
|
|
62
|
+
hidden?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface VeluGlobalTab {
|
|
66
|
+
tab: string;
|
|
67
|
+
href: string;
|
|
68
|
+
icon?: string;
|
|
69
|
+
iconType?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
44
72
|
interface VeluGroup {
|
|
45
73
|
group: string;
|
|
46
74
|
slug: string;
|
|
47
75
|
icon?: string;
|
|
76
|
+
iconType?: string;
|
|
77
|
+
version?: string;
|
|
78
|
+
openapi?: VeluOpenApiSource;
|
|
48
79
|
expanded?: boolean;
|
|
49
80
|
description?: string;
|
|
50
81
|
hidden?: boolean;
|
|
51
|
-
pages: (string | VeluGroup | VeluSeparator | VeluLink)[];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
interface VeluMenuItem {
|
|
55
|
-
item: string;
|
|
56
|
-
icon?: string;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
82
|
+
pages: (string | VeluGroup | VeluSeparator | VeluLink)[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface VeluMenuItem {
|
|
86
|
+
item: string;
|
|
87
|
+
icon?: string;
|
|
88
|
+
iconType?: string;
|
|
89
|
+
openapi?: VeluOpenApiSource;
|
|
90
|
+
groups?: VeluGroup[];
|
|
91
|
+
pages?: (string | VeluSeparator | VeluLink)[];
|
|
92
|
+
}
|
|
93
|
+
|
|
61
94
|
interface VeluTab {
|
|
62
95
|
tab: string;
|
|
63
96
|
slug: string;
|
|
64
97
|
icon?: string;
|
|
98
|
+
iconType?: string;
|
|
65
99
|
href?: string;
|
|
100
|
+
openapi?: VeluOpenApiSource;
|
|
101
|
+
version?: string;
|
|
66
102
|
pages?: (string | VeluSeparator | VeluLink)[];
|
|
67
103
|
groups?: VeluGroup[];
|
|
68
104
|
menu?: VeluMenuItem[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface VeluLanguageNav {
|
|
108
|
+
language: string;
|
|
109
|
+
openapi?: VeluOpenApiSource;
|
|
110
|
+
tabs: VeluTab[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface VeluProductNav {
|
|
114
|
+
product: string;
|
|
115
|
+
icon?: string;
|
|
116
|
+
iconType?: string;
|
|
117
|
+
openapi?: VeluOpenApiSource;
|
|
118
|
+
tabs?: VeluTab[];
|
|
119
|
+
pages?: (string | VeluSeparator | VeluLink)[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface VeluVersionNav {
|
|
123
|
+
version: string;
|
|
124
|
+
openapi?: VeluOpenApiSource;
|
|
125
|
+
tabs: VeluTab[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface VeluRedirect {
|
|
129
|
+
source: string;
|
|
130
|
+
destination: string;
|
|
131
|
+
permanent?: boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface VeluConfig {
|
|
135
|
+
$schema?: string;
|
|
136
|
+
theme?: string;
|
|
137
|
+
colors?: VeluColors;
|
|
138
|
+
appearance?: "system" | "light" | "dark";
|
|
139
|
+
styling?: VeluStyling;
|
|
140
|
+
openapi?: VeluOpenApiSource;
|
|
141
|
+
languages?: string[];
|
|
142
|
+
redirects?: VeluRedirect[];
|
|
143
|
+
navigation: {
|
|
144
|
+
openapi?: VeluOpenApiSource;
|
|
145
|
+
tabs?: VeluTab[];
|
|
146
|
+
languages?: VeluLanguageNav[];
|
|
147
|
+
products?: VeluProductNav[];
|
|
148
|
+
versions?: VeluVersionNav[];
|
|
149
|
+
anchors?: VeluAnchor[];
|
|
150
|
+
global?: {
|
|
151
|
+
anchors?: VeluAnchor[];
|
|
152
|
+
tabs?: VeluGlobalTab[];
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
interface VeluOpenApiConfigObject {
|
|
158
|
+
source?: string | string[];
|
|
159
|
+
directory?: string;
|
|
69
160
|
}
|
|
70
161
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
162
|
+
type VeluOpenApiSource = string | string[] | VeluOpenApiConfigObject;
|
|
163
|
+
|
|
164
|
+
function isSeparator(item: unknown): item is VeluSeparator {
|
|
165
|
+
return typeof item === "object" && item !== null && "separator" in item;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isLink(item: unknown): item is VeluLink {
|
|
169
|
+
return typeof item === "object" && item !== null && "href" in item && "label" in item;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isGroup(item: unknown): item is VeluGroup {
|
|
173
|
+
return typeof item === "object" && item !== null && "group" in item;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const HTTP_METHODS = new Set([
|
|
177
|
+
"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", "TRACE", "CONNECT", "WEBHOOK",
|
|
178
|
+
]);
|
|
179
|
+
const OPENAPI_PATH_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
|
|
180
|
+
|
|
181
|
+
interface ParsedOpenApiOperationRef {
|
|
182
|
+
spec?: string;
|
|
183
|
+
method: string;
|
|
184
|
+
endpoint: string;
|
|
185
|
+
kind?: "path" | "webhook";
|
|
186
|
+
title?: string;
|
|
187
|
+
description?: string;
|
|
188
|
+
deprecated?: boolean;
|
|
189
|
+
version?: string;
|
|
190
|
+
content?: string;
|
|
74
191
|
}
|
|
75
192
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
193
|
+
function resolveDefaultOpenApiSpec(openapi: VeluOpenApiSource | undefined): string | undefined {
|
|
194
|
+
const source = extractOpenApiSource(openapi);
|
|
195
|
+
if (typeof source === "string") {
|
|
196
|
+
const trimmed = source.trim();
|
|
197
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
198
|
+
}
|
|
199
|
+
if (Array.isArray(source)) {
|
|
200
|
+
const first = source.find((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
201
|
+
return typeof first === "string" ? first.trim() : undefined;
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
81
204
|
}
|
|
82
205
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
206
|
+
function parseOpenApiOperationRef(value: string, inheritedSpec?: string): ParsedOpenApiOperationRef | null {
|
|
207
|
+
const trimmed = value.trim();
|
|
208
|
+
if (!trimmed) return null;
|
|
87
209
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
210
|
+
const withSpec = trimmed.match(/^(\S+)\s+([A-Za-z]+)\s+(.+)$/);
|
|
211
|
+
if (withSpec) {
|
|
212
|
+
const method = withSpec[2].toUpperCase();
|
|
213
|
+
const endpoint = withSpec[3].trim();
|
|
214
|
+
if (!HTTP_METHODS.has(method)) return null;
|
|
215
|
+
if (method === "WEBHOOK") {
|
|
216
|
+
if (!endpoint) return null;
|
|
217
|
+
return { spec: withSpec[1].trim(), method, endpoint, kind: "webhook" };
|
|
218
|
+
}
|
|
219
|
+
if (!endpoint.startsWith("/")) return null;
|
|
220
|
+
return { spec: withSpec[1].trim(), method, endpoint, kind: "path" };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const noSpec = trimmed.match(/^([A-Za-z]+)\s+(.+)$/);
|
|
224
|
+
if (!noSpec) return null;
|
|
225
|
+
const method = noSpec[1].toUpperCase();
|
|
226
|
+
const endpoint = noSpec[2].trim();
|
|
227
|
+
if (!HTTP_METHODS.has(method)) return null;
|
|
228
|
+
if (method === "WEBHOOK") {
|
|
229
|
+
if (!endpoint) return null;
|
|
230
|
+
return { spec: inheritedSpec, method, endpoint, kind: "webhook" };
|
|
231
|
+
}
|
|
232
|
+
if (!endpoint.startsWith("/")) return null;
|
|
233
|
+
return { spec: inheritedSpec, method, endpoint, kind: "path" };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function slugFromOpenApiOperation(method: string, endpoint: string): string {
|
|
237
|
+
const cleaned = endpoint
|
|
238
|
+
.toLowerCase()
|
|
239
|
+
.replace(/^\/+/, "")
|
|
240
|
+
.replace(/[{}]/g, "")
|
|
241
|
+
.replace(/[^a-z0-9/._-]+/g, "-")
|
|
242
|
+
.replace(/\/+/g, "-")
|
|
243
|
+
.replace(/[-_.]{2,}/g, "-")
|
|
244
|
+
.replace(/^[-_.]+|[-_.]+$/g, "");
|
|
245
|
+
const body = cleaned || "endpoint";
|
|
246
|
+
return `${method.toLowerCase()}-${body}`;
|
|
106
247
|
}
|
|
107
248
|
|
|
108
|
-
function
|
|
109
|
-
|
|
249
|
+
function resolveOpenApiSpecList(openapi: VeluOpenApiSource | undefined): string[] {
|
|
250
|
+
const source = extractOpenApiSource(openapi);
|
|
251
|
+
if (typeof source === "string") {
|
|
252
|
+
const trimmed = source.trim();
|
|
253
|
+
return trimmed ? [trimmed] : [];
|
|
254
|
+
}
|
|
255
|
+
if (Array.isArray(source)) {
|
|
256
|
+
return source
|
|
257
|
+
.filter((entry): entry is string => typeof entry === "string")
|
|
258
|
+
.map((entry) => entry.trim())
|
|
259
|
+
.filter((entry) => entry.length > 0);
|
|
260
|
+
}
|
|
261
|
+
return [];
|
|
110
262
|
}
|
|
111
263
|
|
|
112
|
-
function
|
|
113
|
-
|
|
264
|
+
function extractOpenApiSource(openapi: VeluOpenApiSource | undefined): string | string[] | undefined {
|
|
265
|
+
if (typeof openapi === "string" || Array.isArray(openapi)) return openapi;
|
|
266
|
+
if (openapi && typeof openapi === "object") {
|
|
267
|
+
const source = (openapi as VeluOpenApiConfigObject).source;
|
|
268
|
+
if (typeof source === "string" || Array.isArray(source)) return source;
|
|
269
|
+
}
|
|
270
|
+
return undefined;
|
|
114
271
|
}
|
|
115
272
|
|
|
116
|
-
function
|
|
117
|
-
|
|
273
|
+
function resolveOpenApiDirectory(openapi: VeluOpenApiSource | undefined): string | undefined {
|
|
274
|
+
if (!openapi || typeof openapi !== "object" || Array.isArray(openapi)) return undefined;
|
|
275
|
+
const raw = (openapi as VeluOpenApiConfigObject).directory;
|
|
276
|
+
if (typeof raw !== "string") return undefined;
|
|
277
|
+
const trimmed = raw.trim().replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
278
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
118
279
|
}
|
|
119
280
|
|
|
120
|
-
|
|
281
|
+
function parseOpenApiDocument(rawSource: string): Record<string, unknown> | null {
|
|
282
|
+
const source = rawSource.trim();
|
|
283
|
+
if (!source) return null;
|
|
284
|
+
try {
|
|
285
|
+
const parsed = JSON.parse(source);
|
|
286
|
+
if (parsed && typeof parsed === "object") return parsed as Record<string, unknown>;
|
|
287
|
+
} catch {
|
|
288
|
+
// fall through and attempt YAML parse.
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const parsed = parseYaml(source);
|
|
292
|
+
if (parsed && typeof parsed === "object") return parsed as Record<string, unknown>;
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
121
298
|
|
|
122
|
-
function
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
299
|
+
function readMintMetadata(operation: Record<string, unknown>) {
|
|
300
|
+
const xMint = operation["x-mint"];
|
|
301
|
+
if (!xMint || typeof xMint !== "object") return {};
|
|
302
|
+
const metadata = (xMint as Record<string, unknown>).metadata;
|
|
303
|
+
const content = (xMint as Record<string, unknown>).content;
|
|
304
|
+
const meta = metadata && typeof metadata === "object" ? (metadata as Record<string, unknown>) : {};
|
|
305
|
+
return {
|
|
306
|
+
title: typeof meta.title === "string" ? meta.title : undefined,
|
|
307
|
+
description: typeof meta.description === "string" ? meta.description : undefined,
|
|
308
|
+
deprecated: typeof meta.deprecated === "boolean" ? meta.deprecated : undefined,
|
|
309
|
+
version: typeof meta.version === "string" ? meta.version : undefined,
|
|
310
|
+
content: typeof content === "string" ? content : undefined,
|
|
311
|
+
};
|
|
126
312
|
}
|
|
127
313
|
|
|
128
|
-
function
|
|
129
|
-
const
|
|
130
|
-
|
|
314
|
+
function pickOperationMethod(pathItem: Record<string, unknown>): string | undefined {
|
|
315
|
+
for (const method of OPENAPI_PATH_METHODS) {
|
|
316
|
+
const operation = pathItem[method];
|
|
317
|
+
if (operation && typeof operation === "object") return method.toUpperCase();
|
|
318
|
+
}
|
|
319
|
+
return undefined;
|
|
131
320
|
}
|
|
132
321
|
|
|
133
|
-
function
|
|
134
|
-
|
|
322
|
+
function loadOpenApiOperations(specSource: string, docsDir: string): ParsedOpenApiOperationRef[] {
|
|
323
|
+
if (/^https?:\/\//i.test(specSource) || specSource.startsWith("file://")) return [];
|
|
324
|
+
|
|
325
|
+
const resolvedPath = specSource.startsWith("/")
|
|
326
|
+
? join(docsDir, specSource.replace(/^\/+/, ""))
|
|
327
|
+
: resolve(docsDir, specSource);
|
|
328
|
+
if (!existsSync(resolvedPath)) return [];
|
|
329
|
+
|
|
330
|
+
const parsed = parseOpenApiDocument(readFileSync(resolvedPath, "utf-8"));
|
|
331
|
+
if (!parsed) return [];
|
|
332
|
+
|
|
333
|
+
const paths = parsed.paths;
|
|
334
|
+
const webhooks = parsed.webhooks;
|
|
335
|
+
|
|
336
|
+
const output: ParsedOpenApiOperationRef[] = [];
|
|
337
|
+
if (paths && typeof paths === "object") {
|
|
338
|
+
for (const [endpoint, methods] of Object.entries(paths as Record<string, unknown>)) {
|
|
339
|
+
if (!endpoint.startsWith("/") || !methods || typeof methods !== "object") continue;
|
|
340
|
+
for (const method of Object.keys(methods as Record<string, unknown>)) {
|
|
341
|
+
const normalized = method.toLowerCase();
|
|
342
|
+
if (!OPENAPI_PATH_METHODS.has(normalized)) continue;
|
|
343
|
+
const operation = (methods as Record<string, unknown>)[method];
|
|
344
|
+
if (!operation || typeof operation !== "object") continue;
|
|
345
|
+
if ((operation as Record<string, unknown>)["x-hidden"] === true) continue;
|
|
346
|
+
const mintMeta = readMintMetadata(operation as Record<string, unknown>);
|
|
347
|
+
output.push({
|
|
348
|
+
kind: "path",
|
|
349
|
+
spec: specSource,
|
|
350
|
+
method: normalized.toUpperCase(),
|
|
351
|
+
endpoint,
|
|
352
|
+
title: mintMeta.title ?? (typeof (operation as Record<string, unknown>).summary === "string" ? String((operation as Record<string, unknown>).summary) : undefined),
|
|
353
|
+
description: mintMeta.description ?? (typeof (operation as Record<string, unknown>).description === "string" ? String((operation as Record<string, unknown>).description) : undefined),
|
|
354
|
+
deprecated: mintMeta.deprecated ?? ((operation as Record<string, unknown>).deprecated === true),
|
|
355
|
+
version: mintMeta.version,
|
|
356
|
+
content: mintMeta.content,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (webhooks && typeof webhooks === "object") {
|
|
363
|
+
for (const [webhookName, pathItem] of Object.entries(webhooks as Record<string, unknown>)) {
|
|
364
|
+
if (!pathItem || typeof pathItem !== "object") continue;
|
|
365
|
+
const resolvedMethod = pickOperationMethod(pathItem as Record<string, unknown>);
|
|
366
|
+
if (!resolvedMethod) continue;
|
|
367
|
+
const operation = (pathItem as Record<string, unknown>)[resolvedMethod.toLowerCase()];
|
|
368
|
+
if (!operation || typeof operation !== "object") continue;
|
|
369
|
+
if ((operation as Record<string, unknown>)["x-hidden"] === true) continue;
|
|
370
|
+
const mintMeta = readMintMetadata(operation as Record<string, unknown>);
|
|
371
|
+
output.push({
|
|
372
|
+
kind: "webhook",
|
|
373
|
+
spec: specSource,
|
|
374
|
+
method: "WEBHOOK",
|
|
375
|
+
endpoint: webhookName,
|
|
376
|
+
title: mintMeta.title ?? (typeof (operation as Record<string, unknown>).summary === "string" ? String((operation as Record<string, unknown>).summary) : undefined),
|
|
377
|
+
description: mintMeta.description ?? (typeof (operation as Record<string, unknown>).description === "string" ? String((operation as Record<string, unknown>).description) : undefined),
|
|
378
|
+
deprecated: mintMeta.deprecated ?? ((operation as Record<string, unknown>).deprecated === true),
|
|
379
|
+
version: mintMeta.version,
|
|
380
|
+
content: mintMeta.content,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return output;
|
|
135
385
|
}
|
|
136
386
|
|
|
387
|
+
function normalizeOpenApiSpecForFrontmatter(spec: string | undefined): string | undefined {
|
|
388
|
+
if (!spec) return undefined;
|
|
389
|
+
const trimmed = spec.trim();
|
|
390
|
+
if (!trimmed) return undefined;
|
|
391
|
+
if (/^https?:\/\//i.test(trimmed) || trimmed.startsWith("file://")) return trimmed;
|
|
392
|
+
if (trimmed.startsWith("/")) return trimmed;
|
|
393
|
+
return `/${trimmed.replace(/^\.?\/*/, "")}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
function loadConfig(docsDir: string): VeluConfig {
|
|
399
|
+
const raw = readFileSync(resolveConfigPath(docsDir), "utf-8");
|
|
400
|
+
const parsed = JSON.parse(raw) as VeluConfig;
|
|
401
|
+
return normalizeConfigNavigation(parsed);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function isExternalDestination(value: string): boolean {
|
|
405
|
+
return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(value);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function normalizePath(value: string): string {
|
|
409
|
+
const trimmed = value.trim();
|
|
410
|
+
if (!trimmed) return "/";
|
|
411
|
+
const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
412
|
+
const collapsed = withLeadingSlash.replace(/\/{2,}/g, "/");
|
|
413
|
+
if (collapsed !== "/" && collapsed.endsWith("/")) return collapsed.slice(0, -1);
|
|
414
|
+
return collapsed;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function collectRedirectRules(config: VeluConfig): Array<{ source: string; destination: string; permanent: boolean }> {
|
|
418
|
+
const redirects = Array.isArray(config.redirects) ? config.redirects : [];
|
|
419
|
+
const output: Array<{ source: string; destination: string; permanent: boolean }> = [];
|
|
420
|
+
|
|
421
|
+
for (const redirect of redirects) {
|
|
422
|
+
if (!redirect || typeof redirect.source !== "string" || typeof redirect.destination !== "string") continue;
|
|
423
|
+
const source = redirect.source.trim();
|
|
424
|
+
const destination = redirect.destination.trim();
|
|
425
|
+
if (!source || !destination) continue;
|
|
426
|
+
if (/[?#]/.test(source) || /[?#]/.test(destination)) continue;
|
|
427
|
+
|
|
428
|
+
const normalizedSource = normalizePath(source);
|
|
429
|
+
const normalizedDestination = isExternalDestination(destination)
|
|
430
|
+
? destination
|
|
431
|
+
: normalizePath(destination);
|
|
432
|
+
if (!isExternalDestination(normalizedDestination) && normalizedSource === normalizedDestination) continue;
|
|
433
|
+
|
|
434
|
+
output.push({
|
|
435
|
+
source: normalizedSource,
|
|
436
|
+
destination: normalizedDestination,
|
|
437
|
+
permanent: redirect.permanent !== false,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return output;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function writeRedirectArtifacts(config: VeluConfig, outDir: string) {
|
|
445
|
+
const redirects = collectRedirectRules(config);
|
|
446
|
+
const generatedDir = join(outDir, "generated");
|
|
447
|
+
mkdirSync(generatedDir, { recursive: true });
|
|
448
|
+
|
|
449
|
+
writeFileSync(
|
|
450
|
+
join(generatedDir, "redirects.ts"),
|
|
451
|
+
`const redirects: Array<{ source: string; destination: string; permanent: boolean }> = ${JSON.stringify(redirects, null, 2)};\n\nexport default redirects;\n`,
|
|
452
|
+
"utf-8"
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const redirectsFilePath = join(outDir, "public", "_redirects");
|
|
456
|
+
if (redirects.length === 0) {
|
|
457
|
+
rmSync(redirectsFilePath, { force: true });
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const netlifyBody = redirects
|
|
462
|
+
.map((redirect) => `${redirect.source} ${redirect.destination} ${redirect.permanent ? 301 : 307}`)
|
|
463
|
+
.join("\n");
|
|
464
|
+
writeFileSync(redirectsFilePath, `${netlifyBody}\n`, "utf-8");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const STATIC_EXTENSIONS = new Set([
|
|
468
|
+
".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico",
|
|
469
|
+
".mp4", ".webm",
|
|
470
|
+
".mp3", ".wav",
|
|
471
|
+
".json", ".yaml", ".yml",
|
|
472
|
+
".css",
|
|
473
|
+
".js",
|
|
474
|
+
".woff", ".woff2", ".ttf", ".eot",
|
|
475
|
+
".pdf", ".txt",
|
|
476
|
+
".xml", ".csv",
|
|
477
|
+
".zip",
|
|
478
|
+
]);
|
|
479
|
+
|
|
480
|
+
function copyStaticAssets(docsDir: string, publicDir: string) {
|
|
481
|
+
function walk(dir: string) {
|
|
482
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
483
|
+
for (const entry of entries) {
|
|
484
|
+
if (entry.name.startsWith(".")) continue;
|
|
485
|
+
if (entry.name === "node_modules") continue;
|
|
486
|
+
const srcPath = join(dir, entry.name);
|
|
487
|
+
if (entry.isDirectory()) {
|
|
488
|
+
walk(srcPath);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const ext = entry.name.includes(".")
|
|
493
|
+
? `.${entry.name.split(".").pop()!.toLowerCase()}`
|
|
494
|
+
: "";
|
|
495
|
+
if (!STATIC_EXTENSIONS.has(ext)) continue;
|
|
496
|
+
|
|
497
|
+
const rel = relative(docsDir, srcPath);
|
|
498
|
+
const destPath = join(publicDir, rel);
|
|
499
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
500
|
+
copyFileSync(srcPath, destPath);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
walk(docsDir);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function toPosixPath(value: string): string {
|
|
508
|
+
return value.replace(/\\/g, "/");
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function isInsideDocsRoot(docsDir: string, targetPath: string): boolean {
|
|
512
|
+
const relPath = relative(docsDir, targetPath);
|
|
513
|
+
if (!relPath) return true;
|
|
514
|
+
if (relPath.startsWith("..")) return false;
|
|
515
|
+
if (/^[a-zA-Z]:/.test(relPath)) return false;
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function shouldMirrorSourceFile(filePath: string): boolean {
|
|
520
|
+
return SOURCE_MIRROR_EXTENSIONS.has(extname(filePath).toLowerCase());
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function shouldRewriteImports(filePath: string): boolean {
|
|
524
|
+
return IMPORT_REWRITE_EXTENSIONS.has(extname(filePath).toLowerCase());
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function rewriteImportSpecifier(
|
|
528
|
+
specifier: string,
|
|
529
|
+
sourceFilePath: string,
|
|
530
|
+
outputFilePath: string,
|
|
531
|
+
docsDir: string,
|
|
532
|
+
mirrorDir: string
|
|
533
|
+
): string {
|
|
534
|
+
const match = specifier.match(/^([^?#]+)([?#].*)?$/);
|
|
535
|
+
if (!match) return specifier;
|
|
536
|
+
const rawPath = match[1];
|
|
537
|
+
const suffix = match[2] ?? "";
|
|
538
|
+
|
|
539
|
+
let resolvedSourcePath: string | null = null;
|
|
540
|
+
if (rawPath.startsWith("/")) {
|
|
541
|
+
resolvedSourcePath = join(docsDir, rawPath.slice(1));
|
|
542
|
+
} else if (rawPath.startsWith("./") || rawPath.startsWith("../")) {
|
|
543
|
+
resolvedSourcePath = resolve(dirname(sourceFilePath), rawPath);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (!resolvedSourcePath || !isInsideDocsRoot(docsDir, resolvedSourcePath)) {
|
|
547
|
+
return specifier;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const relToDocs = relative(docsDir, resolvedSourcePath);
|
|
551
|
+
const mirrorTargetPath = join(mirrorDir, relToDocs);
|
|
552
|
+
const relFromOutput = relative(dirname(outputFilePath), mirrorTargetPath);
|
|
553
|
+
const normalizedRel = toPosixPath(relFromOutput || ".");
|
|
554
|
+
const withDotPrefix = normalizedRel.startsWith(".") ? normalizedRel : `./${normalizedRel}`;
|
|
555
|
+
return `${withDotPrefix}${suffix}`;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function rewriteImportsInContent(
|
|
559
|
+
content: string,
|
|
560
|
+
sourceFilePath: string,
|
|
561
|
+
outputFilePath: string,
|
|
562
|
+
docsDir: string,
|
|
563
|
+
mirrorDir: string
|
|
564
|
+
): string {
|
|
565
|
+
const importFromPattern = /^(\s*import\s+)(.+?)(\s+from\s*["'])([^"']+)(["']\s*;?\s*)$/;
|
|
566
|
+
const exportFromPattern = /^(\s*export\b[^\n]*?\bfrom\s*["'])([^"']+)(["'])/;
|
|
567
|
+
const sideEffectImportPattern = /^(\s*import\s*["'])([^"']+)(["'])/;
|
|
568
|
+
const fencePattern = /^\s*(```+|~~~+)/;
|
|
569
|
+
const mdxOutput = (() => {
|
|
570
|
+
const ext = extname(outputFilePath).toLowerCase();
|
|
571
|
+
return ext === ".md" || ext === ".mdx";
|
|
572
|
+
})();
|
|
573
|
+
|
|
574
|
+
const lines = content.split(/\r?\n/);
|
|
575
|
+
const out: string[] = [];
|
|
576
|
+
let inFence = false;
|
|
577
|
+
let fenceChar = "";
|
|
578
|
+
let injectedMdxHelperImport = false;
|
|
579
|
+
|
|
580
|
+
function importPathFromSpecifier(specifier: string): string {
|
|
581
|
+
const match = specifier.match(/^([^?#]+)/);
|
|
582
|
+
return match ? match[1] : specifier;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function isLocalSpecifier(specifier: string): boolean {
|
|
586
|
+
return specifier.startsWith("/") || specifier.startsWith("./") || specifier.startsWith("../");
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function isMdxSpecifier(specifier: string): boolean {
|
|
590
|
+
const base = importPathFromSpecifier(specifier).toLowerCase();
|
|
591
|
+
return base.endsWith(".mdx") || base.endsWith(".md");
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function parseDefaultImport(clause: string): { defaultName?: string; namedPart?: string } {
|
|
595
|
+
const trimmed = clause.trim();
|
|
596
|
+
if (!trimmed || trimmed.startsWith("{") || trimmed.startsWith("*")) return {};
|
|
597
|
+
|
|
598
|
+
const commaIdx = trimmed.indexOf(",");
|
|
599
|
+
if (commaIdx === -1) {
|
|
600
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(trimmed)) return { defaultName: trimmed };
|
|
601
|
+
return {};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const defaultName = trimmed.slice(0, commaIdx).trim();
|
|
605
|
+
const remainder = trimmed.slice(commaIdx + 1).trim();
|
|
606
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(defaultName)) return {};
|
|
607
|
+
if (!remainder.startsWith("{") && !remainder.startsWith("*")) return {};
|
|
608
|
+
return { defaultName, namedPart: remainder };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
for (const line of lines) {
|
|
612
|
+
const fenceMatch = line.match(fencePattern);
|
|
613
|
+
if (fenceMatch) {
|
|
614
|
+
const currentFenceChar = fenceMatch[1][0];
|
|
615
|
+
if (!inFence) {
|
|
616
|
+
inFence = true;
|
|
617
|
+
fenceChar = currentFenceChar;
|
|
618
|
+
} else if (fenceChar === currentFenceChar) {
|
|
619
|
+
inFence = false;
|
|
620
|
+
fenceChar = "";
|
|
621
|
+
}
|
|
622
|
+
out.push(line);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (inFence) {
|
|
627
|
+
out.push(line);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const importMatch = line.match(importFromPattern);
|
|
632
|
+
if (importMatch) {
|
|
633
|
+
const importPrefix = importMatch[1];
|
|
634
|
+
const importClause = importMatch[2];
|
|
635
|
+
const fromPrefix = importMatch[3];
|
|
636
|
+
const specifier = importMatch[4];
|
|
637
|
+
const importSuffix = importMatch[5];
|
|
638
|
+
const rewritten = rewriteImportSpecifier(specifier, sourceFilePath, outputFilePath, docsDir, mirrorDir);
|
|
639
|
+
const { defaultName, namedPart } = parseDefaultImport(importClause);
|
|
640
|
+
const shouldWrapDefaultImport =
|
|
641
|
+
mdxOutput && Boolean(defaultName) && isLocalSpecifier(specifier) && isMdxSpecifier(specifier);
|
|
642
|
+
|
|
643
|
+
if (shouldWrapDefaultImport && defaultName) {
|
|
644
|
+
if (!injectedMdxHelperImport) {
|
|
645
|
+
out.push('import { getMDXComponents as __veluGetMDXComponents } from "@/mdx-components";');
|
|
646
|
+
injectedMdxHelperImport = true;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const rawName = `__veluRaw_${defaultName}`;
|
|
650
|
+
const wrappedClause = namedPart ? `${rawName}, ${namedPart}` : rawName;
|
|
651
|
+
out.push(`${importPrefix}${wrappedClause}${fromPrefix}${rewritten}${importSuffix}`);
|
|
652
|
+
out.push(`export const ${defaultName} = (props) => <${rawName} {...props} components={__veluGetMDXComponents()} />;`);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
out.push(`${importPrefix}${importClause}${fromPrefix}${rewritten}${importSuffix}`);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
let nextLine = line.replace(exportFromPattern, (_, prefix: string, specifier: string, suffix: string) => {
|
|
661
|
+
const rewritten = rewriteImportSpecifier(specifier, sourceFilePath, outputFilePath, docsDir, mirrorDir);
|
|
662
|
+
return `${prefix}${rewritten}${suffix}`;
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
nextLine = nextLine.replace(sideEffectImportPattern, (_, prefix: string, specifier: string, suffix: string) => {
|
|
666
|
+
const rewritten = rewriteImportSpecifier(specifier, sourceFilePath, outputFilePath, docsDir, mirrorDir);
|
|
667
|
+
return `${prefix}${rewritten}${suffix}`;
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
out.push(nextLine);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return out.join("\n");
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function copyMirroredSourceFile(srcPath: string, docsDir: string, mirrorDir: string) {
|
|
677
|
+
if (!shouldMirrorSourceFile(srcPath)) return;
|
|
678
|
+
if (!isInsideDocsRoot(docsDir, srcPath)) return;
|
|
679
|
+
|
|
680
|
+
const relPath = relative(docsDir, srcPath);
|
|
681
|
+
const destPath = join(mirrorDir, relPath);
|
|
682
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
683
|
+
|
|
684
|
+
if (shouldRewriteImports(srcPath)) {
|
|
685
|
+
const raw = readFileSync(srcPath, "utf-8");
|
|
686
|
+
const rewritten = rewriteImportsInContent(raw, srcPath, destPath, docsDir, mirrorDir);
|
|
687
|
+
writeFileSync(destPath, rewritten, "utf-8");
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
copyFileSync(srcPath, destPath);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function rebuildSourceMirror(docsDir: string, mirrorDir: string) {
|
|
695
|
+
rmSync(mirrorDir, { recursive: true, force: true });
|
|
696
|
+
mkdirSync(mirrorDir, { recursive: true });
|
|
697
|
+
|
|
698
|
+
function walk(dir: string) {
|
|
699
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
700
|
+
for (const entry of entries) {
|
|
701
|
+
if (entry.name.startsWith(".")) continue;
|
|
702
|
+
if (entry.name === "node_modules") continue;
|
|
703
|
+
const srcPath = join(dir, entry.name);
|
|
704
|
+
if (entry.isDirectory()) {
|
|
705
|
+
walk(srcPath);
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
if (!shouldMirrorSourceFile(srcPath)) continue;
|
|
709
|
+
copyMirroredSourceFile(srcPath, docsDir, mirrorDir);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
walk(docsDir);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function pageLabelFromSlug(slug: string): string {
|
|
717
|
+
const last = slug.split("/").pop()!;
|
|
718
|
+
return last.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function pageBasename(page: string): string {
|
|
722
|
+
return page.split("/").pop()!;
|
|
723
|
+
}
|
|
724
|
+
|
|
137
725
|
interface PageMapping {
|
|
138
|
-
src: string; // original page reference
|
|
726
|
+
src: string; // original page reference
|
|
139
727
|
dest: string; // destination path under content/docs (without extension)
|
|
728
|
+
kind: "file" | "openapi-operation";
|
|
729
|
+
openapiSpec?: string;
|
|
730
|
+
openapiMethod?: string;
|
|
731
|
+
openapiEndpoint?: string;
|
|
732
|
+
openapiKind?: "path" | "webhook";
|
|
733
|
+
title?: string;
|
|
734
|
+
description?: string;
|
|
735
|
+
deprecated?: boolean;
|
|
736
|
+
version?: string;
|
|
737
|
+
content?: string;
|
|
140
738
|
}
|
|
739
|
+
|
|
740
|
+
interface MetaFile {
|
|
741
|
+
dir: string;
|
|
742
|
+
data: Record<string, unknown>;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
interface BuildArtifacts {
|
|
746
|
+
pageMap: PageMapping[];
|
|
747
|
+
metaFiles: MetaFile[];
|
|
748
|
+
firstPage: string;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function buildArtifacts(config: VeluConfig, docsDirForOpenApi?: string): BuildArtifacts {
|
|
752
|
+
const pageMap: PageMapping[] = [];
|
|
753
|
+
const metaFiles: MetaFile[] = [];
|
|
754
|
+
const rootTabs = (config.navigation.tabs || []).filter((tab) => !tab.href);
|
|
755
|
+
const rootPages = rootTabs.map((tab) => tab.slug);
|
|
756
|
+
const defaultOpenApiSpec = resolveDefaultOpenApiSpec(config.navigation.openapi ?? config.openapi);
|
|
757
|
+
let firstPage = "quickstart";
|
|
758
|
+
let hasFirstPage = false;
|
|
759
|
+
const usedDestinations = new Set<string>();
|
|
760
|
+
|
|
761
|
+
function trackFirstPage(dest: string) {
|
|
762
|
+
if (!hasFirstPage) {
|
|
763
|
+
firstPage = dest;
|
|
764
|
+
hasFirstPage = true;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function metaEntry(item: string | VeluSeparator | VeluLink): string {
|
|
769
|
+
if (typeof item === "string") return item;
|
|
770
|
+
if (isSeparator(item)) return `---${item.separator}---`;
|
|
771
|
+
if (isLink(item)) {
|
|
772
|
+
return item.icon
|
|
773
|
+
? `[${item.icon}][${item.label}](${item.href})`
|
|
774
|
+
: `[${item.label}](${item.href})`;
|
|
775
|
+
}
|
|
776
|
+
return String(item);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function uniqueDestination(dest: string): string {
|
|
780
|
+
if (!usedDestinations.has(dest)) {
|
|
781
|
+
usedDestinations.add(dest);
|
|
782
|
+
return dest;
|
|
783
|
+
}
|
|
784
|
+
let count = 2;
|
|
785
|
+
while (usedDestinations.has(`${dest}-${count}`)) count += 1;
|
|
786
|
+
const candidate = `${dest}-${count}`;
|
|
787
|
+
usedDestinations.add(candidate);
|
|
788
|
+
return candidate;
|
|
789
|
+
}
|
|
141
790
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
791
|
+
function metaEntryForDestination(baseDir: string, destination: string): string {
|
|
792
|
+
const fromParts = baseDir.split("/").filter(Boolean);
|
|
793
|
+
const toParts = destination.split("/").filter(Boolean);
|
|
146
794
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
795
|
+
let index = 0;
|
|
796
|
+
while (index < fromParts.length && index < toParts.length && fromParts[index] === toParts[index]) {
|
|
797
|
+
index += 1;
|
|
798
|
+
}
|
|
152
799
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (!
|
|
163
|
-
|
|
164
|
-
|
|
800
|
+
const up = Array(fromParts.length - index).fill("..");
|
|
801
|
+
const down = toParts.slice(index);
|
|
802
|
+
const rel = [...up, ...down].join("/");
|
|
803
|
+
return rel || pageBasename(destination);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function resolveGenerationDestination(openapi: VeluOpenApiSource | undefined, fallback: string): string {
|
|
807
|
+
const override = resolveOpenApiDirectory(openapi);
|
|
808
|
+
if (!override) return fallback;
|
|
809
|
+
if (!fallback) return override;
|
|
810
|
+
if (override === fallback || override.startsWith(`${fallback}/`)) return override;
|
|
811
|
+
return `${fallback}/${override}`;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function toPageMapping(item: string, destDir: string, inheritedSpec?: string): PageMapping {
|
|
815
|
+
const parsedOpenApi = parseOpenApiOperationRef(item, inheritedSpec);
|
|
816
|
+
if (!parsedOpenApi) {
|
|
817
|
+
const basename = pageBasename(item);
|
|
818
|
+
const dest = uniqueDestination(`${destDir}/${basename}`);
|
|
819
|
+
return { src: item, dest, kind: "file" };
|
|
165
820
|
}
|
|
821
|
+
|
|
822
|
+
const slug = slugFromOpenApiOperation(parsedOpenApi.method, parsedOpenApi.endpoint);
|
|
823
|
+
const dest = uniqueDestination(`${destDir}/${slug}`);
|
|
824
|
+
return {
|
|
825
|
+
src: item,
|
|
826
|
+
dest,
|
|
827
|
+
kind: "openapi-operation",
|
|
828
|
+
openapiSpec: parsedOpenApi.spec,
|
|
829
|
+
openapiMethod: parsedOpenApi.method,
|
|
830
|
+
openapiEndpoint: parsedOpenApi.endpoint,
|
|
831
|
+
openapiKind: parsedOpenApi.kind,
|
|
832
|
+
title: parsedOpenApi.title,
|
|
833
|
+
description: parsedOpenApi.description,
|
|
834
|
+
deprecated: parsedOpenApi.deprecated,
|
|
835
|
+
version: parsedOpenApi.version,
|
|
836
|
+
content: parsedOpenApi.content,
|
|
837
|
+
};
|
|
166
838
|
}
|
|
167
839
|
|
|
168
|
-
function
|
|
169
|
-
if (typeof
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
840
|
+
function resolveInheritedVersion(value: unknown, inherited?: string): string | undefined {
|
|
841
|
+
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
842
|
+
return inherited;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function toPageMappingWithVersion(
|
|
846
|
+
item: string,
|
|
847
|
+
destDir: string,
|
|
848
|
+
inheritedSpec?: string,
|
|
849
|
+
inheritedVersion?: string,
|
|
850
|
+
): PageMapping {
|
|
851
|
+
const mapping = toPageMapping(item, destDir, inheritedSpec);
|
|
852
|
+
if (mapping.kind === "openapi-operation" && mapping.version === undefined) {
|
|
853
|
+
mapping.version = inheritedVersion;
|
|
175
854
|
}
|
|
176
|
-
return
|
|
855
|
+
return mapping;
|
|
177
856
|
}
|
|
178
857
|
|
|
179
|
-
function
|
|
858
|
+
function toOperationMapping(
|
|
859
|
+
ref: ParsedOpenApiOperationRef,
|
|
860
|
+
destDir: string,
|
|
861
|
+
inheritedVersion?: string,
|
|
862
|
+
): PageMapping {
|
|
863
|
+
const slug = slugFromOpenApiOperation(ref.method, ref.endpoint);
|
|
864
|
+
const dest = uniqueDestination(`${destDir}/${slug}`);
|
|
865
|
+
return {
|
|
866
|
+
src: `${ref.spec ? `${ref.spec} ` : ""}${ref.method} ${ref.endpoint}`,
|
|
867
|
+
dest,
|
|
868
|
+
kind: "openapi-operation",
|
|
869
|
+
openapiSpec: ref.spec,
|
|
870
|
+
openapiMethod: ref.method,
|
|
871
|
+
openapiEndpoint: ref.endpoint,
|
|
872
|
+
openapiKind: ref.kind,
|
|
873
|
+
title: ref.title,
|
|
874
|
+
description: ref.description,
|
|
875
|
+
deprecated: ref.deprecated,
|
|
876
|
+
version: ref.version ?? inheritedVersion,
|
|
877
|
+
content: ref.content,
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function buildOpenApiMappings(
|
|
882
|
+
openapi: VeluOpenApiSource | undefined,
|
|
883
|
+
destDir: string,
|
|
884
|
+
fallbackSpec?: string,
|
|
885
|
+
inheritedVersion?: string,
|
|
886
|
+
): PageMapping[] {
|
|
887
|
+
if (!docsDirForOpenApi) return [];
|
|
888
|
+
const specs = resolveOpenApiSpecList(openapi);
|
|
889
|
+
if (specs.length === 0 && fallbackSpec) specs.push(fallbackSpec);
|
|
890
|
+
if (specs.length === 0) return [];
|
|
891
|
+
|
|
892
|
+
const output: PageMapping[] = [];
|
|
893
|
+
const seen = new Set<string>();
|
|
894
|
+
for (const spec of specs) {
|
|
895
|
+
for (const operation of loadOpenApiOperations(spec, docsDirForOpenApi)) {
|
|
896
|
+
const key = `${operation.spec ?? ""}::${operation.kind ?? "path"}::${operation.method}::${operation.endpoint}`;
|
|
897
|
+
if (seen.has(key)) continue;
|
|
898
|
+
seen.add(key);
|
|
899
|
+
output.push(toOperationMapping(operation, destDir, inheritedVersion));
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return output;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function addGroup(
|
|
906
|
+
group: VeluGroup,
|
|
907
|
+
parentDir: string,
|
|
908
|
+
inheritedOpenApiSpec?: string,
|
|
909
|
+
inheritedVersion?: string,
|
|
910
|
+
) {
|
|
180
911
|
const groupDir = `${parentDir}/${group.slug}`;
|
|
181
912
|
const pages: string[] = [];
|
|
913
|
+
const openApiSpec = resolveDefaultOpenApiSpec(group.openapi) ?? inheritedOpenApiSpec;
|
|
914
|
+
const groupVersion = resolveInheritedVersion(group.version, inheritedVersion);
|
|
182
915
|
|
|
183
|
-
|
|
916
|
+
const groupPageItems = Array.isArray(group.pages) ? group.pages : [];
|
|
917
|
+
for (const item of groupPageItems) {
|
|
184
918
|
if (typeof item === "string") {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
trackFirstPage(dest);
|
|
919
|
+
const mapping = toPageMappingWithVersion(item, groupDir, openApiSpec, groupVersion);
|
|
920
|
+
pageMap.push(mapping);
|
|
921
|
+
pages.push(metaEntryForDestination(groupDir, mapping.dest));
|
|
922
|
+
trackFirstPage(mapping.dest);
|
|
190
923
|
} else if (isGroup(item)) {
|
|
191
|
-
addGroup(item, groupDir);
|
|
924
|
+
addGroup(item, groupDir, openApiSpec, groupVersion);
|
|
192
925
|
pages.push(item.hidden ? `!${item.slug}` : item.slug);
|
|
193
926
|
} else if (isSeparator(item)) {
|
|
194
927
|
pages.push(`---${item.separator}---`);
|
|
195
928
|
} else if (isLink(item)) {
|
|
196
|
-
pages.push(
|
|
197
|
-
item.icon
|
|
198
|
-
? `[${item.icon}][${item.label}](${item.href})`
|
|
199
|
-
: `[${item.label}](${item.href})`
|
|
200
|
-
);
|
|
929
|
+
pages.push(
|
|
930
|
+
item.icon
|
|
931
|
+
? `[${item.icon}][${item.label}](${item.href})`
|
|
932
|
+
: `[${item.label}](${item.href})`
|
|
933
|
+
);
|
|
201
934
|
}
|
|
202
935
|
}
|
|
203
936
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
937
|
+
if (groupPageItems.length === 0 && group.openapi !== undefined) {
|
|
938
|
+
const generatedDestDir = resolveGenerationDestination(group.openapi, groupDir);
|
|
939
|
+
const generatedMappings = buildOpenApiMappings(group.openapi, generatedDestDir, openApiSpec, groupVersion);
|
|
940
|
+
for (const mapping of generatedMappings) {
|
|
941
|
+
pageMap.push(mapping);
|
|
942
|
+
pages.push(metaEntryForDestination(groupDir, mapping.dest));
|
|
943
|
+
trackFirstPage(mapping.dest);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const groupMeta: Record<string, unknown> = {
|
|
948
|
+
title: group.group,
|
|
949
|
+
pages,
|
|
950
|
+
defaultOpen: group.expanded !== false,
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
if (group.icon) groupMeta.icon = group.icon;
|
|
954
|
+
if (group.iconType) groupMeta.iconType = group.iconType;
|
|
955
|
+
if (group.description) groupMeta.description = group.description;
|
|
956
|
+
|
|
957
|
+
metaFiles.push({ dir: groupDir, data: groupMeta });
|
|
958
|
+
}
|
|
959
|
+
|
|
216
960
|
for (const tab of rootTabs) {
|
|
217
961
|
const tabPages: string[] = [];
|
|
962
|
+
const tabOpenApiSpec = resolveDefaultOpenApiSpec(tab.openapi) ?? defaultOpenApiSpec;
|
|
963
|
+
const tabVersion = resolveInheritedVersion(tab.version);
|
|
218
964
|
|
|
219
965
|
if (tab.groups) {
|
|
220
966
|
for (const group of tab.groups) {
|
|
221
|
-
addGroup(group, tab.slug);
|
|
967
|
+
addGroup(group, tab.slug, tabOpenApiSpec, tabVersion);
|
|
222
968
|
tabPages.push(group.hidden ? `!${group.slug}` : group.slug);
|
|
223
969
|
}
|
|
224
970
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
971
|
+
|
|
972
|
+
const tabPageItems = Array.isArray(tab.pages) ? tab.pages : [];
|
|
973
|
+
if (tabPageItems.length > 0) {
|
|
974
|
+
for (const item of tabPageItems) {
|
|
228
975
|
if (typeof item === "string") {
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
trackFirstPage(dest);
|
|
976
|
+
const mapping = toPageMappingWithVersion(item, tab.slug, tabOpenApiSpec, tabVersion);
|
|
977
|
+
pageMap.push(mapping);
|
|
978
|
+
tabPages.push(metaEntryForDestination(tab.slug, mapping.dest));
|
|
979
|
+
trackFirstPage(mapping.dest);
|
|
234
980
|
} else {
|
|
235
981
|
tabPages.push(metaEntry(item));
|
|
236
982
|
}
|
|
237
983
|
}
|
|
238
984
|
}
|
|
239
985
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
metaFiles.push({ dir: tab.slug, data: tabMeta });
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (rootPages.length > 0) {
|
|
252
|
-
metaFiles.push({ dir: "", data: { pages: rootPages } });
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return { pageMap, metaFiles, firstPage };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ── Build ──────────────────────────────────────────────────────────────────────
|
|
259
|
-
|
|
260
|
-
function build(docsDir: string, outDir: string) {
|
|
261
|
-
console.log(`📖 Loading velu.json from: ${docsDir}`);
|
|
262
|
-
const config = loadConfig(docsDir);
|
|
263
|
-
|
|
264
|
-
if (existsSync(outDir)) {
|
|
265
|
-
rmSync(outDir, { recursive: true, force: true });
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// ── 1. Copy engine static files ──────────────────────────────────────────
|
|
269
|
-
cpSync(ENGINE_DIR, outDir, { recursive: true });
|
|
270
|
-
// Remove legacy Astro template leftovers if present in the packaged engine.
|
|
271
|
-
rmSync(join(outDir, "src"), { recursive: true, force: true });
|
|
272
|
-
console.log("📦 Copied engine files");
|
|
273
|
-
|
|
274
|
-
// ── 2. Create additional directories ─────────────────────────────────────
|
|
275
|
-
mkdirSync(join(outDir, "content", "docs"), { recursive: true });
|
|
276
|
-
mkdirSync(join(outDir, "public"), { recursive: true });
|
|
277
|
-
|
|
278
|
-
// ── 3. Copy velu.json into the generated project ─────────────────────────
|
|
279
|
-
copyFileSync(join(docsDir, "velu.json"), join(outDir, "velu.json"));
|
|
280
|
-
console.log("📋 Copied velu.json");
|
|
281
|
-
|
|
282
|
-
// ── 4. Build content + metadata artifacts ────────────────────────────────
|
|
283
|
-
const contentDir = join(outDir, "content", "docs");
|
|
284
|
-
const navLanguages = config.navigation.languages;
|
|
285
|
-
const simpleLanguages = config.languages || [];
|
|
286
|
-
|
|
287
|
-
function processPage(srcPath: string, destPath: string, slug: string) {
|
|
288
|
-
mkdirSync(dirname(destPath), { recursive: true });
|
|
289
|
-
let content = readFileSync(srcPath, "utf-8");
|
|
290
|
-
if (!content.startsWith("---")) {
|
|
291
|
-
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
292
|
-
const title = titleMatch ? titleMatch[1] : pageLabelFromSlug(slug);
|
|
293
|
-
if (titleMatch) {
|
|
294
|
-
content = content.replace(/^#\s+.+$/m, "").trimStart();
|
|
986
|
+
if ((tab.groups?.length ?? 0) === 0 && tabPageItems.length === 0 && tab.openapi !== undefined) {
|
|
987
|
+
const generatedDestDir = resolveGenerationDestination(tab.openapi, tab.slug);
|
|
988
|
+
const generatedMappings = buildOpenApiMappings(tab.openapi, generatedDestDir, tabOpenApiSpec, tabVersion);
|
|
989
|
+
for (const mapping of generatedMappings) {
|
|
990
|
+
pageMap.push(mapping);
|
|
991
|
+
tabPages.push(metaEntryForDestination(tab.slug, mapping.dest));
|
|
992
|
+
trackFirstPage(mapping.dest);
|
|
295
993
|
}
|
|
296
|
-
content = `---\ntitle: "${title}"\n---\n\n${content}`;
|
|
297
994
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
995
|
+
|
|
996
|
+
const tabMeta: Record<string, unknown> = {
|
|
997
|
+
title: tab.tab,
|
|
998
|
+
root: true,
|
|
999
|
+
pages: tabPages,
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
if (tab.icon) tabMeta.icon = tab.icon;
|
|
1003
|
+
if (tab.iconType) tabMeta.iconType = tab.iconType;
|
|
1004
|
+
|
|
1005
|
+
metaFiles.push({ dir: tab.slug, data: tabMeta });
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (rootPages.length > 0) {
|
|
1009
|
+
metaFiles.push({ dir: "", data: { pages: rootPages } });
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
return { pageMap, metaFiles, firstPage };
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// ── Build ──────────────────────────────────────────────────────────────────────
|
|
1016
|
+
|
|
1017
|
+
function build(docsDir: string, outDir: string) {
|
|
1018
|
+
const configPath = resolveConfigPath(docsDir);
|
|
1019
|
+
const configName = configPath.endsWith(PRIMARY_CONFIG_NAME) ? PRIMARY_CONFIG_NAME : LEGACY_CONFIG_NAME;
|
|
1020
|
+
console.log(`📖 Loading ${configName} from: ${docsDir}`);
|
|
1021
|
+
const config = loadConfig(docsDir);
|
|
1022
|
+
|
|
1023
|
+
if (existsSync(outDir)) {
|
|
1024
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// ── 1. Copy engine static files ──────────────────────────────────────────
|
|
1028
|
+
cpSync(ENGINE_DIR, outDir, { recursive: true });
|
|
1029
|
+
// Remove legacy Astro template leftovers if present in the packaged engine.
|
|
1030
|
+
rmSync(join(outDir, "src"), { recursive: true, force: true });
|
|
1031
|
+
console.log("📦 Copied engine files");
|
|
1032
|
+
|
|
1033
|
+
// ── 2. Create additional directories ─────────────────────────────────────
|
|
1034
|
+
mkdirSync(join(outDir, "content", "docs"), { recursive: true });
|
|
1035
|
+
mkdirSync(join(outDir, "public"), { recursive: true });
|
|
1036
|
+
const sourceMirrorDir = join(outDir, SOURCE_MIRROR_DIR);
|
|
1037
|
+
rebuildSourceMirror(docsDir, sourceMirrorDir);
|
|
1038
|
+
|
|
1039
|
+
// ── 3. Copy config into the generated project ────────────────────────────
|
|
1040
|
+
copyFileSync(configPath, join(outDir, PRIMARY_CONFIG_NAME));
|
|
1041
|
+
copyFileSync(configPath, join(outDir, LEGACY_CONFIG_NAME));
|
|
1042
|
+
console.log(`📋 Copied ${configName} as ${PRIMARY_CONFIG_NAME} (and legacy ${LEGACY_CONFIG_NAME})`);
|
|
1043
|
+
|
|
1044
|
+
// ── 3b. Copy static assets from docs project into public/ ─────────────────
|
|
1045
|
+
copyStaticAssets(docsDir, join(outDir, "public"));
|
|
1046
|
+
writeRedirectArtifacts(config, outDir);
|
|
1047
|
+
if ((config.redirects ?? []).length > 0) {
|
|
1048
|
+
console.log("↪️ Generated redirect artifacts");
|
|
1049
|
+
}
|
|
1050
|
+
console.log("🖼️ Copied static assets");
|
|
1051
|
+
|
|
1052
|
+
// ── 4. Build content + metadata artifacts ────────────────────────────────
|
|
1053
|
+
const contentDir = join(outDir, "content", "docs");
|
|
1054
|
+
const navLanguages = config.navigation.languages;
|
|
1055
|
+
const simpleLanguages = config.languages || [];
|
|
1056
|
+
|
|
1057
|
+
function processPage(srcPath: string, destPath: string, slug: string) {
|
|
1058
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
1059
|
+
let content = readFileSync(srcPath, "utf-8");
|
|
1060
|
+
if (!content.startsWith("---")) {
|
|
1061
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
1062
|
+
const title = titleMatch ? titleMatch[1] : pageLabelFromSlug(slug);
|
|
1063
|
+
if (titleMatch) {
|
|
1064
|
+
content = content.replace(/^#\s+.+$/m, "").trimStart();
|
|
1065
|
+
}
|
|
1066
|
+
content = `---\ntitle: "${title}"\n---\n\n${content}`;
|
|
1067
|
+
}
|
|
1068
|
+
content = rewriteImportsInContent(content, srcPath, destPath, docsDir, sourceMirrorDir);
|
|
1069
|
+
writeFileSync(destPath, content, "utf-8");
|
|
1070
|
+
}
|
|
1071
|
+
|
|
301
1072
|
function writeLangContent(
|
|
302
1073
|
langCode: string,
|
|
303
1074
|
artifacts: BuildArtifacts,
|
|
304
1075
|
isDefault: boolean,
|
|
305
1076
|
useLangFolders = false
|
|
306
1077
|
) {
|
|
307
|
-
const storagePrefix = useLangFolders ? langCode : (isDefault ? "" : langCode);
|
|
308
|
-
const urlPrefix = isDefault ? "" : langCode;
|
|
309
|
-
|
|
310
|
-
// Write meta files
|
|
311
|
-
const metas = storagePrefix
|
|
312
|
-
? artifacts.metaFiles.map((m) => ({ dir: m.dir ? `${storagePrefix}/${m.dir}` : storagePrefix, data: { ...m.data } }))
|
|
313
|
-
: artifacts.metaFiles;
|
|
1078
|
+
const storagePrefix = useLangFolders ? langCode : (isDefault ? "" : langCode);
|
|
1079
|
+
const urlPrefix = isDefault ? "" : langCode;
|
|
1080
|
+
|
|
1081
|
+
// Write meta files
|
|
1082
|
+
const metas = storagePrefix
|
|
1083
|
+
? artifacts.metaFiles.map((m) => ({ dir: m.dir ? `${storagePrefix}/${m.dir}` : storagePrefix, data: { ...m.data } }))
|
|
1084
|
+
: artifacts.metaFiles;
|
|
314
1085
|
for (const meta of metas) {
|
|
315
1086
|
const metaPath = join(contentDir, meta.dir, "meta.json");
|
|
316
1087
|
mkdirSync(dirname(metaPath), { recursive: true });
|
|
317
1088
|
writeFileSync(metaPath, JSON.stringify(meta.data, null, 2) + "\n", "utf-8");
|
|
318
1089
|
}
|
|
319
1090
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const srcPath = join(docsDir, `${src}.md`);
|
|
323
|
-
if (!existsSync(srcPath)) {
|
|
324
|
-
console.warn(`⚠️ Missing page source: ${src}.md (language: ${langCode})`);
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
const destPath = join(contentDir, storagePrefix ? `${storagePrefix}/${dest}.mdx` : `${dest}.mdx`);
|
|
328
|
-
processPage(srcPath, destPath, src);
|
|
1091
|
+
function sanitizeFrontmatterValue(value: string): string {
|
|
1092
|
+
return value.replace(/\r?\n+/g, " ").replace(/"/g, '\\"').trim();
|
|
329
1093
|
}
|
|
330
1094
|
|
|
331
|
-
//
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
totalMeta += artifacts.metaFiles.length;
|
|
368
|
-
|
|
369
|
-
if (simpleLanguages.length > 1) {
|
|
370
|
-
const rootMetaPath = join(contentDir, "meta.json");
|
|
371
|
-
const rootPages = [`!${simpleLanguages[0] || "en"}`];
|
|
372
|
-
for (const lang of simpleLanguages.slice(1)) {
|
|
373
|
-
writeLangContent(lang, artifacts, false, true);
|
|
374
|
-
rootPages.push(`!${lang}`);
|
|
375
|
-
totalPages += artifacts.pageMap.length;
|
|
376
|
-
totalMeta += artifacts.metaFiles.length;
|
|
1095
|
+
// Copy pages using explicit source paths from docs.json/velu.json
|
|
1096
|
+
for (const mapping of artifacts.pageMap) {
|
|
1097
|
+
const destPath = join(
|
|
1098
|
+
contentDir,
|
|
1099
|
+
storagePrefix ? `${storagePrefix}/${mapping.dest}.mdx` : `${mapping.dest}.mdx`,
|
|
1100
|
+
);
|
|
1101
|
+
|
|
1102
|
+
if (mapping.kind === "openapi-operation") {
|
|
1103
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
1104
|
+
const operationLabel = `${mapping.openapiMethod ?? "GET"} ${mapping.openapiEndpoint ?? "/"}`;
|
|
1105
|
+
const normalizedSpec = normalizeOpenApiSpecForFrontmatter(mapping.openapiSpec);
|
|
1106
|
+
const openapiValue = normalizedSpec
|
|
1107
|
+
? `${normalizedSpec} ${operationLabel}`
|
|
1108
|
+
: operationLabel;
|
|
1109
|
+
const title = sanitizeFrontmatterValue(mapping.title ?? operationLabel);
|
|
1110
|
+
const description = typeof mapping.description === "string"
|
|
1111
|
+
? sanitizeFrontmatterValue(mapping.description)
|
|
1112
|
+
: "";
|
|
1113
|
+
const version = typeof mapping.version === "string"
|
|
1114
|
+
? sanitizeFrontmatterValue(mapping.version)
|
|
1115
|
+
: "";
|
|
1116
|
+
const openapi = openapiValue.replace(/"/g, '\\"');
|
|
1117
|
+
const warning = normalizedSpec
|
|
1118
|
+
? ""
|
|
1119
|
+
: "\n> Warning: No OpenAPI spec source was resolved for this operation. Set `openapi` on this tab/group/navigation or at the top level.\n";
|
|
1120
|
+
const descriptionLine = description ? `\ndescription: "${description}"` : "";
|
|
1121
|
+
const deprecatedLine = mapping.deprecated === true ? `\ndeprecated: true` : "";
|
|
1122
|
+
const statusLine = mapping.deprecated === true ? `\nstatus: "deprecated"` : "";
|
|
1123
|
+
const versionLine = version ? `\nversion: "${version}"` : "";
|
|
1124
|
+
const content = typeof mapping.content === "string" ? `${mapping.content.trim()}\n` : "";
|
|
1125
|
+
writeFileSync(
|
|
1126
|
+
destPath,
|
|
1127
|
+
`---\ntitle: "${title}"${descriptionLine}${deprecatedLine}${statusLine}${versionLine}\nopenapi: "${openapi}"\n---\n${warning}${content}`,
|
|
1128
|
+
"utf-8",
|
|
1129
|
+
);
|
|
1130
|
+
continue;
|
|
377
1131
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
1132
|
+
|
|
1133
|
+
const src = mapping.src;
|
|
1134
|
+
// Check for .mdx first, then .md
|
|
1135
|
+
let srcPath = join(docsDir, `${src}.mdx`);
|
|
1136
|
+
let ext = ".mdx";
|
|
1137
|
+
if (!existsSync(srcPath)) {
|
|
1138
|
+
srcPath = join(docsDir, `${src}.md`);
|
|
1139
|
+
ext = ".md";
|
|
1140
|
+
}
|
|
1141
|
+
if (!existsSync(srcPath)) {
|
|
1142
|
+
console.warn(`Warning: Missing page source: ${src}${ext} (language: ${langCode})`);
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
processPage(srcPath, destPath, src);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// Index page
|
|
1149
|
+
const href = urlPrefix ? `/${urlPrefix}/${artifacts.firstPage}/` : `/${artifacts.firstPage}/`;
|
|
1150
|
+
const indexPath = storagePrefix ? join(contentDir, storagePrefix, "index.mdx") : join(contentDir, "index.mdx");
|
|
1151
|
+
writeFileSync(
|
|
1152
|
+
indexPath,
|
|
1153
|
+
`---\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`,
|
|
1154
|
+
"utf-8"
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
let totalPages = 0;
|
|
1159
|
+
let totalMeta = 0;
|
|
1160
|
+
|
|
1161
|
+
if (navLanguages && navLanguages.length > 0) {
|
|
1162
|
+
// ── Mode 1: Per-language navigation (Mintlify-style) ──────────────
|
|
1163
|
+
const rootPages: string[] = [];
|
|
1164
|
+
|
|
1165
|
+
for (let i = 0; i < navLanguages.length; i++) {
|
|
1166
|
+
const langEntry = navLanguages[i];
|
|
1167
|
+
const isDefault = i === 0;
|
|
1168
|
+
const langConfig = { ...config, navigation: { ...config.navigation, tabs: langEntry.tabs } } as VeluConfig;
|
|
1169
|
+
const artifacts = buildArtifacts(langConfig, docsDir);
|
|
1170
|
+
writeLangContent(langEntry.language, artifacts, isDefault, true);
|
|
1171
|
+
totalPages += artifacts.pageMap.length;
|
|
1172
|
+
totalMeta += artifacts.metaFiles.length;
|
|
1173
|
+
rootPages.push(`!${langEntry.language}`);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
const rootMetaPath = join(contentDir, "meta.json");
|
|
1177
|
+
writeFileSync(rootMetaPath, JSON.stringify({ pages: rootPages }, null, 2) + "\n", "utf-8");
|
|
1178
|
+
} else {
|
|
1179
|
+
// ── Mode 2: Simple (single-lang or same-nav multi-lang) ───────────
|
|
1180
|
+
const artifacts = buildArtifacts(config, docsDir);
|
|
1181
|
+
const useLangFolders = simpleLanguages.length > 1;
|
|
1182
|
+
writeLangContent(simpleLanguages[0] || "en", artifacts, true, useLangFolders);
|
|
1183
|
+
totalPages += artifacts.pageMap.length;
|
|
1184
|
+
totalMeta += artifacts.metaFiles.length;
|
|
1185
|
+
|
|
1186
|
+
if (simpleLanguages.length > 1) {
|
|
1187
|
+
const rootMetaPath = join(contentDir, "meta.json");
|
|
1188
|
+
const rootPages = [`!${simpleLanguages[0] || "en"}`];
|
|
1189
|
+
for (const lang of simpleLanguages.slice(1)) {
|
|
1190
|
+
writeLangContent(lang, artifacts, false, true);
|
|
1191
|
+
rootPages.push(`!${lang}`);
|
|
1192
|
+
totalPages += artifacts.pageMap.length;
|
|
1193
|
+
totalMeta += artifacts.metaFiles.length;
|
|
1194
|
+
}
|
|
1195
|
+
writeFileSync(rootMetaPath, JSON.stringify({ pages: rootPages }, null, 2) + "\n", "utf-8");
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
console.log(`📄 Generated ${totalPages} pages + ${totalMeta} navigation meta files`);
|
|
1200
|
+
|
|
1201
|
+
// ── 5. Generate theme CSS (dynamic — depends on user config) ─────────────
|
|
1202
|
+
const themeCss = generateThemeCss({
|
|
1203
|
+
theme: config.theme,
|
|
1204
|
+
colors: config.colors,
|
|
1205
|
+
appearance: config.appearance,
|
|
1206
|
+
styling: config.styling,
|
|
1207
|
+
});
|
|
1208
|
+
writeFileSync(join(outDir, "app", "velu-theme.css"), themeCss, "utf-8");
|
|
1209
|
+
console.log(`🎨 Generated theme: ${resolveThemeName(config.theme)}`);
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
// ── 7. Generate minimal package.json (type: module, no local deps) ───────
|
|
1213
|
+
const sitePkg = {
|
|
1214
|
+
name: "velu-docs-site",
|
|
1215
|
+
version: "0.0.1",
|
|
1216
|
+
private: true,
|
|
1217
|
+
type: "module",
|
|
1218
|
+
};
|
|
1219
|
+
writeFileSync(join(outDir, "package.json"), JSON.stringify(sitePkg, null, 2) + "\n", "utf-8");
|
|
1220
|
+
|
|
1221
|
+
console.log("📦 Generated boilerplate");
|
|
1222
|
+
console.log(`\n✅ Site generated at: ${outDir}`);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
export { build };
|