@aravindc26/velu 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -6
- package/schema/velu.schema.json +1864 -30
- package/src/build.ts +1161 -180
- package/src/cli.ts +121 -16
- package/src/engine/_server.mjs +1708 -192
- package/src/engine/app/(docs)/[...slug]/layout.tsx +377 -0
- package/src/engine/app/(docs)/[...slug]/page.tsx +917 -0
- package/src/engine/app/(docs)/layout.tsx +1 -13
- package/src/engine/app/api/proxy/route.ts +23 -0
- package/src/engine/app/copy-page.css +59 -1
- package/src/engine/app/global.css +3487 -6
- package/src/engine/app/layout.tsx +59 -8
- package/src/engine/app/llms-file/route.ts +87 -0
- package/src/engine/app/llms-full-file/route.ts +62 -0
- package/src/engine/app/md-file/[...slug]/route.ts +409 -0
- package/src/engine/app/page.tsx +45 -0
- package/src/engine/app/robots.txt/route.ts +61 -0
- package/src/engine/app/rss-file/[...slug]/route.ts +176 -0
- package/src/engine/app/search.css +20 -0
- package/src/engine/app/sitemap.xml/route.ts +80 -0
- package/src/engine/components/assistant.tsx +16 -5
- package/src/engine/components/changelog-filters.tsx +114 -0
- package/src/engine/components/code-group.tsx +383 -0
- package/src/engine/components/color.tsx +118 -0
- package/src/engine/components/expandable.tsx +77 -0
- package/src/engine/components/icon.tsx +136 -0
- package/src/engine/components/image-zoom-fallback.tsx +147 -0
- package/src/engine/components/image.tsx +111 -0
- package/src/engine/components/lang-switcher.tsx +95 -0
- package/src/engine/components/manual-api-playground.tsx +154 -0
- package/src/engine/components/mermaid.tsx +142 -0
- package/src/engine/components/openapi-toc-sync.tsx +59 -0
- package/src/engine/components/openapi.tsx +1679 -0
- package/src/engine/components/page-feedback.tsx +153 -0
- package/src/engine/components/product-switcher.tsx +102 -0
- package/src/engine/components/prompt.tsx +90 -0
- package/src/engine/components/providers.tsx +21 -0
- package/src/engine/components/search.tsx +70 -3
- package/src/engine/components/sidebar-links.tsx +49 -0
- package/src/engine/components/synced-tabs.tsx +57 -0
- package/src/engine/components/theme-toggle.tsx +39 -0
- package/src/engine/components/toc-examples.tsx +110 -0
- package/src/engine/components/version-switcher.tsx +89 -0
- package/src/engine/components/view.tsx +344 -0
- package/src/engine/generated/redirects.ts +3 -0
- package/src/engine/lib/changelog.ts +246 -0
- package/src/engine/lib/layout.shared.ts +57 -7
- package/src/engine/lib/llms.ts +444 -0
- package/src/engine/lib/navigation-normalize.mjs +525 -0
- package/src/engine/lib/navigation-normalize.ts +695 -0
- package/src/engine/lib/redirects.ts +194 -0
- package/src/engine/lib/source.ts +121 -4
- package/src/engine/lib/velu.ts +635 -5
- package/src/engine/mdx-components.tsx +648 -0
- package/src/engine/middleware.ts +66 -0
- package/src/engine/next.config.mjs +2 -2
- package/src/engine/public/icons/cursor-dark.svg +12 -0
- package/src/engine/public/icons/cursor-light.svg +12 -0
- package/src/engine/source.config.ts +98 -1
- package/src/engine/src/components/PageTitle.astro +16 -5
- package/src/engine/src/lib/velu.ts +97 -16
- package/src/navigation-normalize.ts +686 -0
- package/src/themes.ts +6 -6
- package/src/validate.ts +235 -24
- package/src/engine/app/(docs)/[[...slug]]/page.tsx +0 -69
package/src/build.ts
CHANGED
|
@@ -1,244 +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
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
import { normalizeConfigNavigation } from "./navigation-normalize.js";
|
|
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
|
+
|
|
50
|
+
interface VeluAnchor {
|
|
51
|
+
anchor: string;
|
|
52
|
+
href?: string;
|
|
53
|
+
icon?: string;
|
|
54
|
+
iconType?: string;
|
|
55
|
+
openapi?: VeluOpenApiSource;
|
|
56
|
+
version?: string;
|
|
57
|
+
color?: {
|
|
58
|
+
light: string;
|
|
59
|
+
dark: string;
|
|
60
|
+
};
|
|
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
|
+
|
|
13
72
|
interface VeluGroup {
|
|
14
73
|
group: string;
|
|
15
74
|
slug: string;
|
|
16
75
|
icon?: string;
|
|
76
|
+
iconType?: string;
|
|
77
|
+
version?: string;
|
|
78
|
+
openapi?: VeluOpenApiSource;
|
|
17
79
|
expanded?: boolean;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
80
|
+
description?: string;
|
|
81
|
+
hidden?: boolean;
|
|
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
|
+
|
|
21
94
|
interface VeluTab {
|
|
22
95
|
tab: string;
|
|
23
96
|
slug: string;
|
|
24
97
|
icon?: string;
|
|
98
|
+
iconType?: string;
|
|
25
99
|
href?: string;
|
|
26
|
-
|
|
100
|
+
openapi?: VeluOpenApiSource;
|
|
101
|
+
version?: string;
|
|
102
|
+
pages?: (string | VeluSeparator | VeluLink)[];
|
|
27
103
|
groups?: VeluGroup[];
|
|
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;
|
|
28
160
|
}
|
|
29
161
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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;
|
|
39
191
|
}
|
|
40
192
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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;
|
|
46
204
|
}
|
|
47
205
|
|
|
48
|
-
function
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
206
|
+
function parseOpenApiOperationRef(value: string, inheritedSpec?: string): ParsedOpenApiOperationRef | null {
|
|
207
|
+
const trimmed = value.trim();
|
|
208
|
+
if (!trimmed) return null;
|
|
209
|
+
|
|
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
|
+
}
|
|
52
222
|
|
|
53
|
-
|
|
54
|
-
|
|
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}`;
|
|
55
247
|
}
|
|
56
248
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 [];
|
|
60
262
|
}
|
|
61
263
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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;
|
|
65
271
|
}
|
|
66
272
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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;
|
|
71
279
|
}
|
|
72
280
|
|
|
73
|
-
function
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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;
|
|
86
295
|
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
87
298
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
};
|
|
312
|
+
}
|
|
91
313
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
} else {
|
|
100
|
-
addGroup(item, groupDir);
|
|
101
|
-
pages.push(item.slug);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
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;
|
|
320
|
+
}
|
|
104
321
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
pages,
|
|
108
|
-
defaultOpen: group.expanded !== false,
|
|
109
|
-
};
|
|
322
|
+
function loadOpenApiOperations(specSource: string, docsDir: string): ParsedOpenApiOperationRef[] {
|
|
323
|
+
if (/^https?:\/\//i.test(specSource) || specSource.startsWith("file://")) return [];
|
|
110
324
|
|
|
111
|
-
|
|
325
|
+
const resolvedPath = specSource.startsWith("/")
|
|
326
|
+
? join(docsDir, specSource.replace(/^\/+/, ""))
|
|
327
|
+
: resolve(docsDir, specSource);
|
|
328
|
+
if (!existsSync(resolvedPath)) return [];
|
|
112
329
|
|
|
113
|
-
|
|
114
|
-
|
|
330
|
+
const parsed = parseOpenApiDocument(readFileSync(resolvedPath, "utf-8"));
|
|
331
|
+
if (!parsed) return [];
|
|
115
332
|
|
|
116
|
-
|
|
117
|
-
|
|
333
|
+
const paths = parsed.paths;
|
|
334
|
+
const webhooks = parsed.webhooks;
|
|
118
335
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
+
});
|
|
123
358
|
}
|
|
124
359
|
}
|
|
360
|
+
}
|
|
125
361
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
});
|
|
134
382
|
}
|
|
135
|
-
|
|
136
|
-
const tabMeta: Record<string, unknown> = {
|
|
137
|
-
title: tab.tab,
|
|
138
|
-
root: true,
|
|
139
|
-
pages: tabPages,
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
if (tab.icon) tabMeta.icon = tab.icon;
|
|
143
|
-
|
|
144
|
-
metaFiles.push({ dir: tab.slug, data: tabMeta });
|
|
145
383
|
}
|
|
384
|
+
return output;
|
|
385
|
+
}
|
|
146
386
|
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
|
|
725
|
+
interface PageMapping {
|
|
726
|
+
src: string; // original page reference
|
|
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;
|
|
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;
|
|
149
789
|
}
|
|
150
790
|
|
|
151
|
-
|
|
152
|
-
|
|
791
|
+
function metaEntryForDestination(baseDir: string, destination: string): string {
|
|
792
|
+
const fromParts = baseDir.split("/").filter(Boolean);
|
|
793
|
+
const toParts = destination.split("/").filter(Boolean);
|
|
153
794
|
|
|
154
|
-
|
|
795
|
+
let index = 0;
|
|
796
|
+
while (index < fromParts.length && index < toParts.length && fromParts[index] === toParts[index]) {
|
|
797
|
+
index += 1;
|
|
798
|
+
}
|
|
155
799
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
}
|
|
159
805
|
|
|
160
|
-
|
|
161
|
-
|
|
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" };
|
|
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
|
+
};
|
|
162
838
|
}
|
|
163
839
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
console.log("📦 Copied engine files");
|
|
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
|
+
}
|
|
169
844
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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;
|
|
854
|
+
}
|
|
855
|
+
return mapping;
|
|
856
|
+
}
|
|
173
857
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
}
|
|
177
880
|
|
|
178
|
-
|
|
179
|
-
|
|
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 [];
|
|
180
891
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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;
|
|
186
903
|
}
|
|
187
904
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
905
|
+
function addGroup(
|
|
906
|
+
group: VeluGroup,
|
|
907
|
+
parentDir: string,
|
|
908
|
+
inheritedOpenApiSpec?: string,
|
|
909
|
+
inheritedVersion?: string,
|
|
910
|
+
) {
|
|
911
|
+
const groupDir = `${parentDir}/${group.slug}`;
|
|
912
|
+
const pages: string[] = [];
|
|
913
|
+
const openApiSpec = resolveDefaultOpenApiSpec(group.openapi) ?? inheritedOpenApiSpec;
|
|
914
|
+
const groupVersion = resolveInheritedVersion(group.version, inheritedVersion);
|
|
192
915
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
916
|
+
const groupPageItems = Array.isArray(group.pages) ? group.pages : [];
|
|
917
|
+
for (const item of groupPageItems) {
|
|
918
|
+
if (typeof item === "string") {
|
|
919
|
+
const mapping = toPageMappingWithVersion(item, groupDir, openApiSpec, groupVersion);
|
|
920
|
+
pageMap.push(mapping);
|
|
921
|
+
pages.push(metaEntryForDestination(groupDir, mapping.dest));
|
|
922
|
+
trackFirstPage(mapping.dest);
|
|
923
|
+
} else if (isGroup(item)) {
|
|
924
|
+
addGroup(item, groupDir, openApiSpec, groupVersion);
|
|
925
|
+
pages.push(item.hidden ? `!${item.slug}` : item.slug);
|
|
926
|
+
} else if (isSeparator(item)) {
|
|
927
|
+
pages.push(`---${item.separator}---`);
|
|
928
|
+
} else if (isLink(item)) {
|
|
929
|
+
pages.push(
|
|
930
|
+
item.icon
|
|
931
|
+
? `[${item.icon}][${item.label}](${item.href})`
|
|
932
|
+
: `[${item.label}](${item.href})`
|
|
933
|
+
);
|
|
934
|
+
}
|
|
196
935
|
}
|
|
197
936
|
|
|
198
|
-
|
|
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
|
+
|
|
960
|
+
for (const tab of rootTabs) {
|
|
961
|
+
const tabPages: string[] = [];
|
|
962
|
+
const tabOpenApiSpec = resolveDefaultOpenApiSpec(tab.openapi) ?? defaultOpenApiSpec;
|
|
963
|
+
const tabVersion = resolveInheritedVersion(tab.version);
|
|
199
964
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
965
|
+
if (tab.groups) {
|
|
966
|
+
for (const group of tab.groups) {
|
|
967
|
+
addGroup(group, tab.slug, tabOpenApiSpec, tabVersion);
|
|
968
|
+
tabPages.push(group.hidden ? `!${group.slug}` : group.slug);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const tabPageItems = Array.isArray(tab.pages) ? tab.pages : [];
|
|
973
|
+
if (tabPageItems.length > 0) {
|
|
974
|
+
for (const item of tabPageItems) {
|
|
975
|
+
if (typeof item === "string") {
|
|
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);
|
|
980
|
+
} else {
|
|
981
|
+
tabPages.push(metaEntry(item));
|
|
982
|
+
}
|
|
206
983
|
}
|
|
207
|
-
content = `---\ntitle: "${title}"\n---\n\n${content}`;
|
|
208
984
|
}
|
|
209
985
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
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
|
+
|
|
1072
|
+
function writeLangContent(
|
|
1073
|
+
langCode: string,
|
|
1074
|
+
artifacts: BuildArtifacts,
|
|
1075
|
+
isDefault: boolean,
|
|
1076
|
+
useLangFolders = false
|
|
1077
|
+
) {
|
|
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;
|
|
1085
|
+
for (const meta of metas) {
|
|
1086
|
+
const metaPath = join(contentDir, meta.dir, "meta.json");
|
|
1087
|
+
mkdirSync(dirname(metaPath), { recursive: true });
|
|
1088
|
+
writeFileSync(metaPath, JSON.stringify(meta.data, null, 2) + "\n", "utf-8");
|
|
1089
|
+
}
|
|
239
1090
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
1091
|
+
function sanitizeFrontmatterValue(value: string): string {
|
|
1092
|
+
return value.replace(/\r?\n+/g, " ").replace(/"/g, '\\"').trim();
|
|
1093
|
+
}
|
|
243
1094
|
|
|
244
|
-
|
|
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;
|
|
1131
|
+
}
|
|
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 };
|