@aravindc26/velu 0.11.6 → 0.11.9
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 +1 -1
- package/schema/velu.schema.json +383 -122
- package/src/build.ts +679 -551
- package/src/cli.ts +65 -2
- package/src/engine/app/(docs)/[...slug]/layout.tsx +155 -13
- package/src/engine/app/(docs)/[...slug]/page.tsx +77 -9
- package/src/engine/app/copy-page.css +17 -1
- package/src/engine/app/global.css +111 -5
- package/src/engine/app/layout.tsx +8 -1
- package/src/engine/app/search.css +4 -0
- package/src/engine/components/banner.tsx +80 -0
- package/src/engine/components/copy-page.tsx +162 -35
- package/src/engine/components/dropdown-switcher.tsx +142 -0
- package/src/engine/components/header-tab-link.tsx +43 -0
- package/src/engine/components/search.tsx +136 -49
- package/src/engine/lib/layout.shared.ts +68 -68
- package/src/engine/lib/velu.ts +297 -0
- package/src/validate.ts +8 -0
package/src/build.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, cpSync, existsSync, rmSync, readdirSync } from "node:fs";
|
|
2
|
-
import { join, dirname, relative, extname, resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { parse as parseYaml } from "yaml";
|
|
5
|
-
import { generateThemeCss, resolveThemeName, type VeluColors, type VeluStyling } from "./themes.js";
|
|
6
|
-
import { normalizeConfigNavigation } from "./navigation-normalize.js";
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, cpSync, existsSync, rmSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join, dirname, relative, extname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
5
|
+
import { generateThemeCss, resolveThemeName, type VeluColors, type VeluStyling } from "./themes.js";
|
|
6
|
+
import { normalizeConfigNavigation } from "./navigation-normalize.js";
|
|
7
7
|
|
|
8
8
|
// ── Engine directory (shipped with the CLI package) ──────────────────────────
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -24,7 +24,11 @@ const SOURCE_MIRROR_EXTENSIONS = new Set([
|
|
|
24
24
|
".pdf", ".txt", ".xml", ".csv", ".zip",
|
|
25
25
|
]);
|
|
26
26
|
|
|
27
|
-
const IMPORT_REWRITE_EXTENSIONS = new Set([".md", ".mdx", ".jsx", ".js", ".tsx", ".ts"]);
|
|
27
|
+
const IMPORT_REWRITE_EXTENSIONS = new Set([".md", ".mdx", ".jsx", ".js", ".tsx", ".ts"]);
|
|
28
|
+
const VARIABLE_SUBSTITUTION_EXTENSIONS = new Set([
|
|
29
|
+
".md", ".mdx", ".jsx", ".js", ".tsx", ".ts",
|
|
30
|
+
".json", ".yaml", ".yml", ".css", ".txt", ".xml", ".csv",
|
|
31
|
+
]);
|
|
28
32
|
|
|
29
33
|
function resolveConfigPath(docsDir: string): string {
|
|
30
34
|
const primary = join(docsDir, PRIMARY_CONFIG_NAME);
|
|
@@ -47,17 +51,17 @@ interface VeluLink {
|
|
|
47
51
|
iconType?: string;
|
|
48
52
|
}
|
|
49
53
|
|
|
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
|
-
};
|
|
54
|
+
interface VeluAnchor {
|
|
55
|
+
anchor: string;
|
|
56
|
+
href?: string;
|
|
57
|
+
icon?: string;
|
|
58
|
+
iconType?: string;
|
|
59
|
+
openapi?: VeluOpenApiSource;
|
|
60
|
+
version?: string;
|
|
61
|
+
color?: {
|
|
62
|
+
light: string;
|
|
63
|
+
dark: string;
|
|
64
|
+
};
|
|
61
65
|
tabs?: VeluTab[];
|
|
62
66
|
hidden?: boolean;
|
|
63
67
|
}
|
|
@@ -69,16 +73,16 @@ interface VeluGlobalTab {
|
|
|
69
73
|
iconType?: string;
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
interface VeluGroup {
|
|
73
|
-
group: string;
|
|
74
|
-
slug: string;
|
|
75
|
-
icon?: string;
|
|
76
|
-
iconType?: string;
|
|
77
|
-
version?: string;
|
|
78
|
-
openapi?: VeluOpenApiSource;
|
|
79
|
-
expanded?: boolean;
|
|
80
|
-
description?: string;
|
|
81
|
-
hidden?: boolean;
|
|
76
|
+
interface VeluGroup {
|
|
77
|
+
group: string;
|
|
78
|
+
slug: string;
|
|
79
|
+
icon?: string;
|
|
80
|
+
iconType?: string;
|
|
81
|
+
version?: string;
|
|
82
|
+
openapi?: VeluOpenApiSource;
|
|
83
|
+
expanded?: boolean;
|
|
84
|
+
description?: string;
|
|
85
|
+
hidden?: boolean;
|
|
82
86
|
pages: (string | VeluGroup | VeluSeparator | VeluLink)[];
|
|
83
87
|
}
|
|
84
88
|
|
|
@@ -91,17 +95,17 @@ interface VeluMenuItem {
|
|
|
91
95
|
pages?: (string | VeluSeparator | VeluLink)[];
|
|
92
96
|
}
|
|
93
97
|
|
|
94
|
-
interface VeluTab {
|
|
95
|
-
tab: string;
|
|
96
|
-
slug: string;
|
|
97
|
-
icon?: string;
|
|
98
|
-
iconType?: string;
|
|
99
|
-
href?: string;
|
|
100
|
-
openapi?: VeluOpenApiSource;
|
|
101
|
-
version?: string;
|
|
102
|
-
pages?: (string | VeluSeparator | VeluLink)[];
|
|
103
|
-
groups?: VeluGroup[];
|
|
104
|
-
menu?: VeluMenuItem[];
|
|
98
|
+
interface VeluTab {
|
|
99
|
+
tab: string;
|
|
100
|
+
slug: string;
|
|
101
|
+
icon?: string;
|
|
102
|
+
iconType?: string;
|
|
103
|
+
href?: string;
|
|
104
|
+
openapi?: VeluOpenApiSource;
|
|
105
|
+
version?: string;
|
|
106
|
+
pages?: (string | VeluSeparator | VeluLink)[];
|
|
107
|
+
groups?: VeluGroup[];
|
|
108
|
+
menu?: VeluMenuItem[];
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
interface VeluLanguageNav {
|
|
@@ -131,15 +135,19 @@ interface VeluRedirect {
|
|
|
131
135
|
permanent?: boolean;
|
|
132
136
|
}
|
|
133
137
|
|
|
134
|
-
interface VeluConfig {
|
|
135
|
-
$schema?: string;
|
|
136
|
-
theme?: string;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
138
|
+
interface VeluConfig {
|
|
139
|
+
$schema?: string;
|
|
140
|
+
theme?: string;
|
|
141
|
+
variables?: Record<string, string>;
|
|
142
|
+
colors?: VeluColors;
|
|
143
|
+
appearance?: "system" | "light" | "dark";
|
|
144
|
+
styling?: VeluStyling;
|
|
145
|
+
metadata?: {
|
|
146
|
+
timestamp?: boolean;
|
|
147
|
+
};
|
|
148
|
+
openapi?: VeluOpenApiSource;
|
|
149
|
+
languages?: string[];
|
|
150
|
+
redirects?: VeluRedirect[];
|
|
143
151
|
navigation: {
|
|
144
152
|
openapi?: VeluOpenApiSource;
|
|
145
153
|
tabs?: VeluTab[];
|
|
@@ -154,12 +162,12 @@ interface VeluConfig {
|
|
|
154
162
|
};
|
|
155
163
|
}
|
|
156
164
|
|
|
157
|
-
interface VeluOpenApiConfigObject {
|
|
158
|
-
source?: string | string[];
|
|
159
|
-
directory?: string;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
type VeluOpenApiSource = string | string[] | VeluOpenApiConfigObject;
|
|
165
|
+
interface VeluOpenApiConfigObject {
|
|
166
|
+
source?: string | string[];
|
|
167
|
+
directory?: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
type VeluOpenApiSource = string | string[] | VeluOpenApiConfigObject;
|
|
163
171
|
|
|
164
172
|
function isSeparator(item: unknown): item is VeluSeparator {
|
|
165
173
|
return typeof item === "object" && item !== null && "separator" in item;
|
|
@@ -173,67 +181,67 @@ function isGroup(item: unknown): item is VeluGroup {
|
|
|
173
181
|
return typeof item === "object" && item !== null && "group" in item;
|
|
174
182
|
}
|
|
175
183
|
|
|
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;
|
|
191
|
-
}
|
|
192
|
-
|
|
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;
|
|
204
|
-
}
|
|
205
|
-
|
|
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
|
-
}
|
|
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
|
-
}
|
|
184
|
+
const HTTP_METHODS = new Set([
|
|
185
|
+
"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", "TRACE", "CONNECT", "WEBHOOK",
|
|
186
|
+
]);
|
|
187
|
+
const OPENAPI_PATH_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
|
|
188
|
+
|
|
189
|
+
interface ParsedOpenApiOperationRef {
|
|
190
|
+
spec?: string;
|
|
191
|
+
method: string;
|
|
192
|
+
endpoint: string;
|
|
193
|
+
kind?: "path" | "webhook";
|
|
194
|
+
title?: string;
|
|
195
|
+
description?: string;
|
|
196
|
+
deprecated?: boolean;
|
|
197
|
+
version?: string;
|
|
198
|
+
content?: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function resolveDefaultOpenApiSpec(openapi: VeluOpenApiSource | undefined): string | undefined {
|
|
202
|
+
const source = extractOpenApiSource(openapi);
|
|
203
|
+
if (typeof source === "string") {
|
|
204
|
+
const trimmed = source.trim();
|
|
205
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
206
|
+
}
|
|
207
|
+
if (Array.isArray(source)) {
|
|
208
|
+
const first = source.find((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
209
|
+
return typeof first === "string" ? first.trim() : undefined;
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
235
213
|
|
|
236
|
-
function
|
|
214
|
+
function parseOpenApiOperationRef(value: string, inheritedSpec?: string): ParsedOpenApiOperationRef | null {
|
|
215
|
+
const trimmed = value.trim();
|
|
216
|
+
if (!trimmed) return null;
|
|
217
|
+
|
|
218
|
+
const withSpec = trimmed.match(/^(\S+)\s+([A-Za-z]+)\s+(.+)$/);
|
|
219
|
+
if (withSpec) {
|
|
220
|
+
const method = withSpec[2].toUpperCase();
|
|
221
|
+
const endpoint = withSpec[3].trim();
|
|
222
|
+
if (!HTTP_METHODS.has(method)) return null;
|
|
223
|
+
if (method === "WEBHOOK") {
|
|
224
|
+
if (!endpoint) return null;
|
|
225
|
+
return { spec: withSpec[1].trim(), method, endpoint, kind: "webhook" };
|
|
226
|
+
}
|
|
227
|
+
if (!endpoint.startsWith("/")) return null;
|
|
228
|
+
return { spec: withSpec[1].trim(), method, endpoint, kind: "path" };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const noSpec = trimmed.match(/^([A-Za-z]+)\s+(.+)$/);
|
|
232
|
+
if (!noSpec) return null;
|
|
233
|
+
const method = noSpec[1].toUpperCase();
|
|
234
|
+
const endpoint = noSpec[2].trim();
|
|
235
|
+
if (!HTTP_METHODS.has(method)) return null;
|
|
236
|
+
if (method === "WEBHOOK") {
|
|
237
|
+
if (!endpoint) return null;
|
|
238
|
+
return { spec: inheritedSpec, method, endpoint, kind: "webhook" };
|
|
239
|
+
}
|
|
240
|
+
if (!endpoint.startsWith("/")) return null;
|
|
241
|
+
return { spec: inheritedSpec, method, endpoint, kind: "path" };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function slugFromOpenApiOperation(method: string, endpoint: string): string {
|
|
237
245
|
const cleaned = endpoint
|
|
238
246
|
.toLowerCase()
|
|
239
247
|
.replace(/^\/+/, "")
|
|
@@ -243,164 +251,266 @@ function slugFromOpenApiOperation(method: string, endpoint: string): string {
|
|
|
243
251
|
.replace(/[-_.]{2,}/g, "-")
|
|
244
252
|
.replace(/^[-_.]+|[-_.]+$/g, "");
|
|
245
253
|
const body = cleaned || "endpoint";
|
|
246
|
-
return `${method.toLowerCase()}-${body}`;
|
|
247
|
-
}
|
|
254
|
+
return `${method.toLowerCase()}-${body}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function resolveOpenApiSpecList(openapi: VeluOpenApiSource | undefined): string[] {
|
|
258
|
+
const source = extractOpenApiSource(openapi);
|
|
259
|
+
if (typeof source === "string") {
|
|
260
|
+
const trimmed = source.trim();
|
|
261
|
+
return trimmed ? [trimmed] : [];
|
|
262
|
+
}
|
|
263
|
+
if (Array.isArray(source)) {
|
|
264
|
+
return source
|
|
265
|
+
.filter((entry): entry is string => typeof entry === "string")
|
|
266
|
+
.map((entry) => entry.trim())
|
|
267
|
+
.filter((entry) => entry.length > 0);
|
|
268
|
+
}
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function extractOpenApiSource(openapi: VeluOpenApiSource | undefined): string | string[] | undefined {
|
|
273
|
+
if (typeof openapi === "string" || Array.isArray(openapi)) return openapi;
|
|
274
|
+
if (openapi && typeof openapi === "object") {
|
|
275
|
+
const source = (openapi as VeluOpenApiConfigObject).source;
|
|
276
|
+
if (typeof source === "string" || Array.isArray(source)) return source;
|
|
277
|
+
}
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function resolveOpenApiDirectory(openapi: VeluOpenApiSource | undefined): string | undefined {
|
|
282
|
+
if (!openapi || typeof openapi !== "object" || Array.isArray(openapi)) return undefined;
|
|
283
|
+
const raw = (openapi as VeluOpenApiConfigObject).directory;
|
|
284
|
+
if (typeof raw !== "string") return undefined;
|
|
285
|
+
const trimmed = raw.trim().replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
286
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function parseOpenApiDocument(rawSource: string): Record<string, unknown> | null {
|
|
290
|
+
const source = rawSource.trim();
|
|
291
|
+
if (!source) return null;
|
|
292
|
+
try {
|
|
293
|
+
const parsed = JSON.parse(source);
|
|
294
|
+
if (parsed && typeof parsed === "object") return parsed as Record<string, unknown>;
|
|
295
|
+
} catch {
|
|
296
|
+
// fall through and attempt YAML parse.
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const parsed = parseYaml(source);
|
|
300
|
+
if (parsed && typeof parsed === "object") return parsed as Record<string, unknown>;
|
|
301
|
+
} catch {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function readMintMetadata(operation: Record<string, unknown>) {
|
|
308
|
+
const xMint = operation["x-mint"];
|
|
309
|
+
if (!xMint || typeof xMint !== "object") return {};
|
|
310
|
+
const metadata = (xMint as Record<string, unknown>).metadata;
|
|
311
|
+
const content = (xMint as Record<string, unknown>).content;
|
|
312
|
+
const meta = metadata && typeof metadata === "object" ? (metadata as Record<string, unknown>) : {};
|
|
313
|
+
return {
|
|
314
|
+
title: typeof meta.title === "string" ? meta.title : undefined,
|
|
315
|
+
description: typeof meta.description === "string" ? meta.description : undefined,
|
|
316
|
+
deprecated: typeof meta.deprecated === "boolean" ? meta.deprecated : undefined,
|
|
317
|
+
version: typeof meta.version === "string" ? meta.version : undefined,
|
|
318
|
+
content: typeof content === "string" ? content : undefined,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function pickOperationMethod(pathItem: Record<string, unknown>): string | undefined {
|
|
323
|
+
for (const method of OPENAPI_PATH_METHODS) {
|
|
324
|
+
const operation = pathItem[method];
|
|
325
|
+
if (operation && typeof operation === "object") return method.toUpperCase();
|
|
326
|
+
}
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function loadOpenApiOperations(specSource: string, docsDir: string): ParsedOpenApiOperationRef[] {
|
|
331
|
+
if (/^https?:\/\//i.test(specSource) || specSource.startsWith("file://")) return [];
|
|
332
|
+
|
|
333
|
+
const resolvedPath = specSource.startsWith("/")
|
|
334
|
+
? join(docsDir, specSource.replace(/^\/+/, ""))
|
|
335
|
+
: resolve(docsDir, specSource);
|
|
336
|
+
if (!existsSync(resolvedPath)) return [];
|
|
337
|
+
|
|
338
|
+
const parsed = parseOpenApiDocument(readFileSync(resolvedPath, "utf-8"));
|
|
339
|
+
if (!parsed) return [];
|
|
340
|
+
|
|
341
|
+
const paths = parsed.paths;
|
|
342
|
+
const webhooks = parsed.webhooks;
|
|
343
|
+
|
|
344
|
+
const output: ParsedOpenApiOperationRef[] = [];
|
|
345
|
+
if (paths && typeof paths === "object") {
|
|
346
|
+
for (const [endpoint, methods] of Object.entries(paths as Record<string, unknown>)) {
|
|
347
|
+
if (!endpoint.startsWith("/") || !methods || typeof methods !== "object") continue;
|
|
348
|
+
for (const method of Object.keys(methods as Record<string, unknown>)) {
|
|
349
|
+
const normalized = method.toLowerCase();
|
|
350
|
+
if (!OPENAPI_PATH_METHODS.has(normalized)) continue;
|
|
351
|
+
const operation = (methods as Record<string, unknown>)[method];
|
|
352
|
+
if (!operation || typeof operation !== "object") continue;
|
|
353
|
+
if ((operation as Record<string, unknown>)["x-hidden"] === true) continue;
|
|
354
|
+
const mintMeta = readMintMetadata(operation as Record<string, unknown>);
|
|
355
|
+
output.push({
|
|
356
|
+
kind: "path",
|
|
357
|
+
spec: specSource,
|
|
358
|
+
method: normalized.toUpperCase(),
|
|
359
|
+
endpoint,
|
|
360
|
+
title: mintMeta.title ?? (typeof (operation as Record<string, unknown>).summary === "string" ? String((operation as Record<string, unknown>).summary) : undefined),
|
|
361
|
+
description: mintMeta.description ?? (typeof (operation as Record<string, unknown>).description === "string" ? String((operation as Record<string, unknown>).description) : undefined),
|
|
362
|
+
deprecated: mintMeta.deprecated ?? ((operation as Record<string, unknown>).deprecated === true),
|
|
363
|
+
version: mintMeta.version,
|
|
364
|
+
content: mintMeta.content,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (webhooks && typeof webhooks === "object") {
|
|
371
|
+
for (const [webhookName, pathItem] of Object.entries(webhooks as Record<string, unknown>)) {
|
|
372
|
+
if (!pathItem || typeof pathItem !== "object") continue;
|
|
373
|
+
const resolvedMethod = pickOperationMethod(pathItem as Record<string, unknown>);
|
|
374
|
+
if (!resolvedMethod) continue;
|
|
375
|
+
const operation = (pathItem as Record<string, unknown>)[resolvedMethod.toLowerCase()];
|
|
376
|
+
if (!operation || typeof operation !== "object") continue;
|
|
377
|
+
if ((operation as Record<string, unknown>)["x-hidden"] === true) continue;
|
|
378
|
+
const mintMeta = readMintMetadata(operation as Record<string, unknown>);
|
|
379
|
+
output.push({
|
|
380
|
+
kind: "webhook",
|
|
381
|
+
spec: specSource,
|
|
382
|
+
method: "WEBHOOK",
|
|
383
|
+
endpoint: webhookName,
|
|
384
|
+
title: mintMeta.title ?? (typeof (operation as Record<string, unknown>).summary === "string" ? String((operation as Record<string, unknown>).summary) : undefined),
|
|
385
|
+
description: mintMeta.description ?? (typeof (operation as Record<string, unknown>).description === "string" ? String((operation as Record<string, unknown>).description) : undefined),
|
|
386
|
+
deprecated: mintMeta.deprecated ?? ((operation as Record<string, unknown>).deprecated === true),
|
|
387
|
+
version: mintMeta.version,
|
|
388
|
+
content: mintMeta.content,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return output;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function normalizeOpenApiSpecForFrontmatter(spec: string | undefined): string | undefined {
|
|
396
|
+
if (!spec) return undefined;
|
|
397
|
+
const trimmed = spec.trim();
|
|
398
|
+
if (!trimmed) return undefined;
|
|
399
|
+
if (/^https?:\/\//i.test(trimmed) || trimmed.startsWith("file://")) return trimmed;
|
|
400
|
+
if (trimmed.startsWith("/")) return trimmed;
|
|
401
|
+
return `/${trimmed.replace(/^\.?\/*/, "")}`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
248
405
|
|
|
249
|
-
|
|
250
|
-
|
|
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 [];
|
|
262
|
-
}
|
|
406
|
+
const VARIABLE_TOKEN_PATTERN = /\{\{\s*([A-Za-z0-9.-]+)\s*\}\}/g;
|
|
407
|
+
const VARIABLE_NAME_PATTERN = /^[A-Za-z0-9.-]+$/;
|
|
263
408
|
|
|
264
|
-
function
|
|
265
|
-
|
|
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;
|
|
409
|
+
function sanitizeVariableValue(value: string): string {
|
|
410
|
+
return value.replace(/</g, "<").replace(/>/g, ">");
|
|
271
411
|
}
|
|
272
412
|
|
|
273
|
-
function
|
|
274
|
-
if (!
|
|
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;
|
|
279
|
-
}
|
|
413
|
+
function extractVariables(input: unknown): Record<string, string> {
|
|
414
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return {};
|
|
280
415
|
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (parsed && typeof parsed === "object") return parsed as Record<string, unknown>;
|
|
293
|
-
} catch {
|
|
294
|
-
return null;
|
|
416
|
+
const output: Record<string, string> = {};
|
|
417
|
+
for (const [rawKey, rawValue] of Object.entries(input as Record<string, unknown>)) {
|
|
418
|
+
const key = rawKey.trim();
|
|
419
|
+
if (!key) continue;
|
|
420
|
+
if (!VARIABLE_NAME_PATTERN.test(key)) {
|
|
421
|
+
throw new Error(`Invalid variable name '${rawKey}'. Variable names can only contain letters, numbers, periods, and hyphens.`);
|
|
422
|
+
}
|
|
423
|
+
if (typeof rawValue !== "string") {
|
|
424
|
+
throw new Error(`Invalid value for variable '${rawKey}'. Variables must be strings.`);
|
|
425
|
+
}
|
|
426
|
+
output[key] = rawValue;
|
|
295
427
|
}
|
|
296
|
-
return
|
|
428
|
+
return output;
|
|
297
429
|
}
|
|
298
430
|
|
|
299
|
-
function
|
|
300
|
-
const
|
|
301
|
-
|
|
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
|
-
}
|
|
431
|
+
function resolveVariableMap(rawVariables: Record<string, string>): Record<string, string> {
|
|
432
|
+
const cache = new Map<string, string>();
|
|
433
|
+
const activeStack = new Set<string>();
|
|
313
434
|
|
|
314
|
-
function
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (operation && typeof operation === "object") return method.toUpperCase();
|
|
318
|
-
}
|
|
319
|
-
return undefined;
|
|
320
|
-
}
|
|
435
|
+
function resolveOne(name: string): string {
|
|
436
|
+
const cached = cache.get(name);
|
|
437
|
+
if (cached !== undefined) return cached;
|
|
321
438
|
|
|
322
|
-
|
|
323
|
-
|
|
439
|
+
if (activeStack.has(name)) {
|
|
440
|
+
throw new Error(`Circular variable reference detected for '{{${name}}}'.`);
|
|
441
|
+
}
|
|
324
442
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
443
|
+
const raw = rawVariables[name];
|
|
444
|
+
if (raw === undefined) {
|
|
445
|
+
throw new Error(`Undefined variable '{{${name}}}' referenced in variable definitions.`);
|
|
446
|
+
}
|
|
329
447
|
|
|
330
|
-
|
|
331
|
-
|
|
448
|
+
activeStack.add(name);
|
|
449
|
+
const resolved = raw.replace(VARIABLE_TOKEN_PATTERN, (_match, token: string) => resolveOne(token));
|
|
450
|
+
activeStack.delete(name);
|
|
451
|
+
cache.set(name, resolved);
|
|
452
|
+
return resolved;
|
|
453
|
+
}
|
|
332
454
|
|
|
333
|
-
const
|
|
334
|
-
const
|
|
455
|
+
const output: Record<string, string> = {};
|
|
456
|
+
for (const name of Object.keys(rawVariables)) {
|
|
457
|
+
output[name] = resolveOne(name);
|
|
458
|
+
}
|
|
459
|
+
return output;
|
|
460
|
+
}
|
|
335
461
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
}
|
|
462
|
+
function replaceVariablesInString(
|
|
463
|
+
value: string,
|
|
464
|
+
variables: Record<string, string>,
|
|
465
|
+
context: string,
|
|
466
|
+
sanitizeValues: boolean,
|
|
467
|
+
): string {
|
|
468
|
+
const undefinedVariables = new Set<string>();
|
|
469
|
+
const replaced = value.replace(VARIABLE_TOKEN_PATTERN, (match, rawName: string) => {
|
|
470
|
+
const name = rawName.trim();
|
|
471
|
+
const resolved = variables[name];
|
|
472
|
+
if (resolved === undefined) {
|
|
473
|
+
undefinedVariables.add(name);
|
|
474
|
+
return match;
|
|
359
475
|
}
|
|
476
|
+
return sanitizeValues ? sanitizeVariableValue(resolved) : resolved;
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
if (undefinedVariables.size > 0) {
|
|
480
|
+
throw new Error(
|
|
481
|
+
`Undefined variable(s) ${Array.from(undefinedVariables).map((name) => `'{{${name}}}'`).join(", ")} in ${context}.`
|
|
482
|
+
);
|
|
360
483
|
}
|
|
361
484
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
}
|
|
485
|
+
return replaced;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function applyVariablesToConfig(value: unknown, variables: Record<string, string>, path = "docs.json"): unknown {
|
|
489
|
+
if (typeof value === "string") return replaceVariablesInString(value, variables, path, false);
|
|
490
|
+
if (Array.isArray(value)) return value.map((entry, index) => applyVariablesToConfig(entry, variables, `${path}[${index}]`));
|
|
491
|
+
if (!value || typeof value !== "object") return value;
|
|
492
|
+
|
|
493
|
+
const output: Record<string, unknown> = {};
|
|
494
|
+
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
495
|
+
output[key] = applyVariablesToConfig(entry, variables, `${path}.${key}`);
|
|
383
496
|
}
|
|
384
497
|
return output;
|
|
385
498
|
}
|
|
386
499
|
|
|
387
|
-
function
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
500
|
+
function loadConfig(docsDir: string): { config: VeluConfig; rawConfig: VeluConfig; variables: Record<string, string> } {
|
|
501
|
+
const raw = readFileSync(resolveConfigPath(docsDir), "utf-8");
|
|
502
|
+
const parsed = JSON.parse(raw) as VeluConfig;
|
|
503
|
+
const rawVariables = extractVariables(parsed.variables);
|
|
504
|
+
const resolvedVariables = resolveVariableMap(rawVariables);
|
|
505
|
+
const withVariables = applyVariablesToConfig(parsed, resolvedVariables) as VeluConfig;
|
|
506
|
+
withVariables.variables = resolvedVariables;
|
|
507
|
+
return {
|
|
508
|
+
config: normalizeConfigNavigation(withVariables),
|
|
509
|
+
rawConfig: withVariables,
|
|
510
|
+
variables: resolvedVariables,
|
|
511
|
+
};
|
|
394
512
|
}
|
|
395
513
|
|
|
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
514
|
function isExternalDestination(value: string): boolean {
|
|
405
515
|
return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(value);
|
|
406
516
|
}
|
|
@@ -673,27 +783,41 @@ function rewriteImportsInContent(
|
|
|
673
783
|
return out.join("\n");
|
|
674
784
|
}
|
|
675
785
|
|
|
676
|
-
function copyMirroredSourceFile(
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
786
|
+
function copyMirroredSourceFile(
|
|
787
|
+
srcPath: string,
|
|
788
|
+
docsDir: string,
|
|
789
|
+
mirrorDir: string,
|
|
790
|
+
variables: Record<string, string>,
|
|
791
|
+
) {
|
|
792
|
+
if (!shouldMirrorSourceFile(srcPath)) return;
|
|
793
|
+
if (!isInsideDocsRoot(docsDir, srcPath)) return;
|
|
794
|
+
|
|
795
|
+
const relPath = relative(docsDir, srcPath);
|
|
796
|
+
const destPath = join(mirrorDir, relPath);
|
|
797
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
798
|
+
|
|
799
|
+
if (shouldRewriteImports(srcPath)) {
|
|
800
|
+
let raw = readFileSync(srcPath, "utf-8");
|
|
801
|
+
raw = replaceVariablesInString(raw, variables, relPath, true);
|
|
802
|
+
const rewritten = rewriteImportsInContent(raw, srcPath, destPath, docsDir, mirrorDir);
|
|
803
|
+
writeFileSync(destPath, rewritten, "utf-8");
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const extension = extname(srcPath).toLowerCase();
|
|
808
|
+
if (VARIABLE_SUBSTITUTION_EXTENSIONS.has(extension)) {
|
|
809
|
+
const raw = readFileSync(srcPath, "utf-8");
|
|
810
|
+
const substituted = replaceVariablesInString(raw, variables, relPath, true);
|
|
811
|
+
writeFileSync(destPath, substituted, "utf-8");
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
copyFileSync(srcPath, destPath);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function rebuildSourceMirror(docsDir: string, mirrorDir: string, variables: Record<string, string>) {
|
|
819
|
+
rmSync(mirrorDir, { recursive: true, force: true });
|
|
820
|
+
mkdirSync(mirrorDir, { recursive: true });
|
|
697
821
|
|
|
698
822
|
function walk(dir: string) {
|
|
699
823
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -701,14 +825,14 @@ function rebuildSourceMirror(docsDir: string, mirrorDir: string) {
|
|
|
701
825
|
if (entry.name.startsWith(".")) continue;
|
|
702
826
|
if (entry.name === "node_modules") continue;
|
|
703
827
|
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
|
-
}
|
|
828
|
+
if (entry.isDirectory()) {
|
|
829
|
+
walk(srcPath);
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (!shouldMirrorSourceFile(srcPath)) continue;
|
|
833
|
+
copyMirroredSourceFile(srcPath, docsDir, mirrorDir, variables);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
712
836
|
|
|
713
837
|
walk(docsDir);
|
|
714
838
|
}
|
|
@@ -722,20 +846,20 @@ function pageBasename(page: string): string {
|
|
|
722
846
|
return page.split("/").pop()!;
|
|
723
847
|
}
|
|
724
848
|
|
|
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
|
-
}
|
|
849
|
+
interface PageMapping {
|
|
850
|
+
src: string; // original page reference
|
|
851
|
+
dest: string; // destination path under content/docs (without extension)
|
|
852
|
+
kind: "file" | "openapi-operation";
|
|
853
|
+
openapiSpec?: string;
|
|
854
|
+
openapiMethod?: string;
|
|
855
|
+
openapiEndpoint?: string;
|
|
856
|
+
openapiKind?: "path" | "webhook";
|
|
857
|
+
title?: string;
|
|
858
|
+
description?: string;
|
|
859
|
+
deprecated?: boolean;
|
|
860
|
+
version?: string;
|
|
861
|
+
content?: string;
|
|
862
|
+
}
|
|
739
863
|
|
|
740
864
|
interface MetaFile {
|
|
741
865
|
dir: string;
|
|
@@ -748,7 +872,7 @@ interface BuildArtifacts {
|
|
|
748
872
|
firstPage: string;
|
|
749
873
|
}
|
|
750
874
|
|
|
751
|
-
function buildArtifacts(config: VeluConfig, docsDirForOpenApi?: string): BuildArtifacts {
|
|
875
|
+
function buildArtifacts(config: VeluConfig, docsDirForOpenApi?: string): BuildArtifacts {
|
|
752
876
|
const pageMap: PageMapping[] = [];
|
|
753
877
|
const metaFiles: MetaFile[] = [];
|
|
754
878
|
const rootTabs = (config.navigation.tabs || []).filter((tab) => !tab.href);
|
|
@@ -776,173 +900,173 @@ function buildArtifacts(config: VeluConfig, docsDirForOpenApi?: string): BuildAr
|
|
|
776
900
|
return String(item);
|
|
777
901
|
}
|
|
778
902
|
|
|
779
|
-
function uniqueDestination(dest: string): string {
|
|
780
|
-
if (!usedDestinations.has(dest)) {
|
|
781
|
-
usedDestinations.add(dest);
|
|
782
|
-
return dest;
|
|
903
|
+
function uniqueDestination(dest: string): string {
|
|
904
|
+
if (!usedDestinations.has(dest)) {
|
|
905
|
+
usedDestinations.add(dest);
|
|
906
|
+
return dest;
|
|
783
907
|
}
|
|
784
908
|
let count = 2;
|
|
785
909
|
while (usedDestinations.has(`${dest}-${count}`)) count += 1;
|
|
786
910
|
const candidate = `${dest}-${count}`;
|
|
787
|
-
usedDestinations.add(candidate);
|
|
788
|
-
return candidate;
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
function metaEntryForDestination(baseDir: string, destination: string): string {
|
|
792
|
-
const fromParts = baseDir.split("/").filter(Boolean);
|
|
793
|
-
const toParts = destination.split("/").filter(Boolean);
|
|
794
|
-
|
|
795
|
-
let index = 0;
|
|
796
|
-
while (index < fromParts.length && index < toParts.length && fromParts[index] === toParts[index]) {
|
|
797
|
-
index += 1;
|
|
798
|
-
}
|
|
799
|
-
|
|
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
|
-
}
|
|
911
|
+
usedDestinations.add(candidate);
|
|
912
|
+
return candidate;
|
|
913
|
+
}
|
|
813
914
|
|
|
814
|
-
function
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
915
|
+
function metaEntryForDestination(baseDir: string, destination: string): string {
|
|
916
|
+
const fromParts = baseDir.split("/").filter(Boolean);
|
|
917
|
+
const toParts = destination.split("/").filter(Boolean);
|
|
918
|
+
|
|
919
|
+
let index = 0;
|
|
920
|
+
while (index < fromParts.length && index < toParts.length && fromParts[index] === toParts[index]) {
|
|
921
|
+
index += 1;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const up = Array(fromParts.length - index).fill("..");
|
|
925
|
+
const down = toParts.slice(index);
|
|
926
|
+
const rel = [...up, ...down].join("/");
|
|
927
|
+
return rel || pageBasename(destination);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function resolveGenerationDestination(openapi: VeluOpenApiSource | undefined, fallback: string): string {
|
|
931
|
+
const override = resolveOpenApiDirectory(openapi);
|
|
932
|
+
if (!override) return fallback;
|
|
933
|
+
if (!fallback) return override;
|
|
934
|
+
if (override === fallback || override.startsWith(`${fallback}/`)) return override;
|
|
935
|
+
return `${fallback}/${override}`;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function toPageMapping(item: string, destDir: string, inheritedSpec?: string): PageMapping {
|
|
939
|
+
const parsedOpenApi = parseOpenApiOperationRef(item, inheritedSpec);
|
|
940
|
+
if (!parsedOpenApi) {
|
|
941
|
+
const basename = pageBasename(item);
|
|
942
|
+
const dest = uniqueDestination(`${destDir}/${basename}`);
|
|
943
|
+
return { src: item, dest, kind: "file" };
|
|
944
|
+
}
|
|
821
945
|
|
|
822
946
|
const slug = slugFromOpenApiOperation(parsedOpenApi.method, parsedOpenApi.endpoint);
|
|
823
947
|
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
|
-
};
|
|
838
|
-
}
|
|
839
|
-
|
|
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;
|
|
854
|
-
}
|
|
855
|
-
return mapping;
|
|
856
|
-
}
|
|
857
|
-
|
|
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
|
-
) {
|
|
911
|
-
const groupDir = `${parentDir}/${group.slug}`;
|
|
912
|
-
const pages: string[] = [];
|
|
913
|
-
const openApiSpec = resolveDefaultOpenApiSpec(group.openapi) ?? inheritedOpenApiSpec;
|
|
914
|
-
const groupVersion = resolveInheritedVersion(group.version, inheritedVersion);
|
|
915
|
-
|
|
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)) {
|
|
948
|
+
return {
|
|
949
|
+
src: item,
|
|
950
|
+
dest,
|
|
951
|
+
kind: "openapi-operation",
|
|
952
|
+
openapiSpec: parsedOpenApi.spec,
|
|
953
|
+
openapiMethod: parsedOpenApi.method,
|
|
954
|
+
openapiEndpoint: parsedOpenApi.endpoint,
|
|
955
|
+
openapiKind: parsedOpenApi.kind,
|
|
956
|
+
title: parsedOpenApi.title,
|
|
957
|
+
description: parsedOpenApi.description,
|
|
958
|
+
deprecated: parsedOpenApi.deprecated,
|
|
959
|
+
version: parsedOpenApi.version,
|
|
960
|
+
content: parsedOpenApi.content,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function resolveInheritedVersion(value: unknown, inherited?: string): string | undefined {
|
|
965
|
+
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
966
|
+
return inherited;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function toPageMappingWithVersion(
|
|
970
|
+
item: string,
|
|
971
|
+
destDir: string,
|
|
972
|
+
inheritedSpec?: string,
|
|
973
|
+
inheritedVersion?: string,
|
|
974
|
+
): PageMapping {
|
|
975
|
+
const mapping = toPageMapping(item, destDir, inheritedSpec);
|
|
976
|
+
if (mapping.kind === "openapi-operation" && mapping.version === undefined) {
|
|
977
|
+
mapping.version = inheritedVersion;
|
|
978
|
+
}
|
|
979
|
+
return mapping;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function toOperationMapping(
|
|
983
|
+
ref: ParsedOpenApiOperationRef,
|
|
984
|
+
destDir: string,
|
|
985
|
+
inheritedVersion?: string,
|
|
986
|
+
): PageMapping {
|
|
987
|
+
const slug = slugFromOpenApiOperation(ref.method, ref.endpoint);
|
|
988
|
+
const dest = uniqueDestination(`${destDir}/${slug}`);
|
|
989
|
+
return {
|
|
990
|
+
src: `${ref.spec ? `${ref.spec} ` : ""}${ref.method} ${ref.endpoint}`,
|
|
991
|
+
dest,
|
|
992
|
+
kind: "openapi-operation",
|
|
993
|
+
openapiSpec: ref.spec,
|
|
994
|
+
openapiMethod: ref.method,
|
|
995
|
+
openapiEndpoint: ref.endpoint,
|
|
996
|
+
openapiKind: ref.kind,
|
|
997
|
+
title: ref.title,
|
|
998
|
+
description: ref.description,
|
|
999
|
+
deprecated: ref.deprecated,
|
|
1000
|
+
version: ref.version ?? inheritedVersion,
|
|
1001
|
+
content: ref.content,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function buildOpenApiMappings(
|
|
1006
|
+
openapi: VeluOpenApiSource | undefined,
|
|
1007
|
+
destDir: string,
|
|
1008
|
+
fallbackSpec?: string,
|
|
1009
|
+
inheritedVersion?: string,
|
|
1010
|
+
): PageMapping[] {
|
|
1011
|
+
if (!docsDirForOpenApi) return [];
|
|
1012
|
+
const specs = resolveOpenApiSpecList(openapi);
|
|
1013
|
+
if (specs.length === 0 && fallbackSpec) specs.push(fallbackSpec);
|
|
1014
|
+
if (specs.length === 0) return [];
|
|
1015
|
+
|
|
1016
|
+
const output: PageMapping[] = [];
|
|
1017
|
+
const seen = new Set<string>();
|
|
1018
|
+
for (const spec of specs) {
|
|
1019
|
+
for (const operation of loadOpenApiOperations(spec, docsDirForOpenApi)) {
|
|
1020
|
+
const key = `${operation.spec ?? ""}::${operation.kind ?? "path"}::${operation.method}::${operation.endpoint}`;
|
|
1021
|
+
if (seen.has(key)) continue;
|
|
1022
|
+
seen.add(key);
|
|
1023
|
+
output.push(toOperationMapping(operation, destDir, inheritedVersion));
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return output;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function addGroup(
|
|
1030
|
+
group: VeluGroup,
|
|
1031
|
+
parentDir: string,
|
|
1032
|
+
inheritedOpenApiSpec?: string,
|
|
1033
|
+
inheritedVersion?: string,
|
|
1034
|
+
) {
|
|
1035
|
+
const groupDir = `${parentDir}/${group.slug}`;
|
|
1036
|
+
const pages: string[] = [];
|
|
1037
|
+
const openApiSpec = resolveDefaultOpenApiSpec(group.openapi) ?? inheritedOpenApiSpec;
|
|
1038
|
+
const groupVersion = resolveInheritedVersion(group.version, inheritedVersion);
|
|
1039
|
+
|
|
1040
|
+
const groupPageItems = Array.isArray(group.pages) ? group.pages : [];
|
|
1041
|
+
for (const item of groupPageItems) {
|
|
1042
|
+
if (typeof item === "string") {
|
|
1043
|
+
const mapping = toPageMappingWithVersion(item, groupDir, openApiSpec, groupVersion);
|
|
1044
|
+
pageMap.push(mapping);
|
|
1045
|
+
pages.push(metaEntryForDestination(groupDir, mapping.dest));
|
|
1046
|
+
trackFirstPage(mapping.dest);
|
|
1047
|
+
} else if (isGroup(item)) {
|
|
1048
|
+
addGroup(item, groupDir, openApiSpec, groupVersion);
|
|
1049
|
+
pages.push(item.hidden ? `!${item.slug}` : item.slug);
|
|
1050
|
+
} else if (isSeparator(item)) {
|
|
1051
|
+
pages.push(`---${item.separator}---`);
|
|
1052
|
+
} else if (isLink(item)) {
|
|
929
1053
|
pages.push(
|
|
930
1054
|
item.icon
|
|
931
1055
|
? `[${item.icon}][${item.label}](${item.href})`
|
|
932
1056
|
: `[${item.label}](${item.href})`
|
|
933
1057
|
);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
|
|
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
|
-
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
if (groupPageItems.length === 0 && group.openapi !== undefined) {
|
|
1062
|
+
const generatedDestDir = resolveGenerationDestination(group.openapi, groupDir);
|
|
1063
|
+
const generatedMappings = buildOpenApiMappings(group.openapi, generatedDestDir, openApiSpec, groupVersion);
|
|
1064
|
+
for (const mapping of generatedMappings) {
|
|
1065
|
+
pageMap.push(mapping);
|
|
1066
|
+
pages.push(metaEntryForDestination(groupDir, mapping.dest));
|
|
1067
|
+
trackFirstPage(mapping.dest);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
946
1070
|
|
|
947
1071
|
const groupMeta: Record<string, unknown> = {
|
|
948
1072
|
title: group.group,
|
|
@@ -957,41 +1081,41 @@ function buildArtifacts(config: VeluConfig, docsDirForOpenApi?: string): BuildAr
|
|
|
957
1081
|
metaFiles.push({ dir: groupDir, data: groupMeta });
|
|
958
1082
|
}
|
|
959
1083
|
|
|
960
|
-
for (const tab of rootTabs) {
|
|
961
|
-
const tabPages: string[] = [];
|
|
962
|
-
const tabOpenApiSpec = resolveDefaultOpenApiSpec(tab.openapi) ?? defaultOpenApiSpec;
|
|
963
|
-
const tabVersion = resolveInheritedVersion(tab.version);
|
|
964
|
-
|
|
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
|
-
}
|
|
1084
|
+
for (const tab of rootTabs) {
|
|
1085
|
+
const tabPages: string[] = [];
|
|
1086
|
+
const tabOpenApiSpec = resolveDefaultOpenApiSpec(tab.openapi) ?? defaultOpenApiSpec;
|
|
1087
|
+
const tabVersion = resolveInheritedVersion(tab.version);
|
|
971
1088
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1089
|
+
if (tab.groups) {
|
|
1090
|
+
for (const group of tab.groups) {
|
|
1091
|
+
addGroup(group, tab.slug, tabOpenApiSpec, tabVersion);
|
|
1092
|
+
tabPages.push(group.hidden ? `!${group.slug}` : group.slug);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const tabPageItems = Array.isArray(tab.pages) ? tab.pages : [];
|
|
1097
|
+
if (tabPageItems.length > 0) {
|
|
1098
|
+
for (const item of tabPageItems) {
|
|
1099
|
+
if (typeof item === "string") {
|
|
1100
|
+
const mapping = toPageMappingWithVersion(item, tab.slug, tabOpenApiSpec, tabVersion);
|
|
1101
|
+
pageMap.push(mapping);
|
|
1102
|
+
tabPages.push(metaEntryForDestination(tab.slug, mapping.dest));
|
|
1103
|
+
trackFirstPage(mapping.dest);
|
|
1104
|
+
} else {
|
|
1105
|
+
tabPages.push(metaEntry(item));
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if ((tab.groups?.length ?? 0) === 0 && tabPageItems.length === 0 && tab.openapi !== undefined) {
|
|
1111
|
+
const generatedDestDir = resolveGenerationDestination(tab.openapi, tab.slug);
|
|
1112
|
+
const generatedMappings = buildOpenApiMappings(tab.openapi, generatedDestDir, tabOpenApiSpec, tabVersion);
|
|
1113
|
+
for (const mapping of generatedMappings) {
|
|
1114
|
+
pageMap.push(mapping);
|
|
1115
|
+
tabPages.push(metaEntryForDestination(tab.slug, mapping.dest));
|
|
1116
|
+
trackFirstPage(mapping.dest);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
995
1119
|
|
|
996
1120
|
const tabMeta: Record<string, unknown> = {
|
|
997
1121
|
title: tab.tab,
|
|
@@ -1014,11 +1138,11 @@ function buildArtifacts(config: VeluConfig, docsDirForOpenApi?: string): BuildAr
|
|
|
1014
1138
|
|
|
1015
1139
|
// ── Build ──────────────────────────────────────────────────────────────────────
|
|
1016
1140
|
|
|
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);
|
|
1141
|
+
function build(docsDir: string, outDir: string) {
|
|
1142
|
+
const configPath = resolveConfigPath(docsDir);
|
|
1143
|
+
const configName = configPath.endsWith(PRIMARY_CONFIG_NAME) ? PRIMARY_CONFIG_NAME : LEGACY_CONFIG_NAME;
|
|
1144
|
+
console.log(`📖 Loading ${configName} from: ${docsDir}`);
|
|
1145
|
+
const { config, rawConfig, variables } = loadConfig(docsDir);
|
|
1022
1146
|
|
|
1023
1147
|
if (existsSync(outDir)) {
|
|
1024
1148
|
rmSync(outDir, { recursive: true, force: true });
|
|
@@ -1031,15 +1155,16 @@ function build(docsDir: string, outDir: string) {
|
|
|
1031
1155
|
console.log("📦 Copied engine files");
|
|
1032
1156
|
|
|
1033
1157
|
// ── 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);
|
|
1158
|
+
mkdirSync(join(outDir, "content", "docs"), { recursive: true });
|
|
1159
|
+
mkdirSync(join(outDir, "public"), { recursive: true });
|
|
1160
|
+
const sourceMirrorDir = join(outDir, SOURCE_MIRROR_DIR);
|
|
1161
|
+
rebuildSourceMirror(docsDir, sourceMirrorDir, variables);
|
|
1038
1162
|
|
|
1039
1163
|
// ── 3. Copy config into the generated project ────────────────────────────
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1164
|
+
const serializedConfig = `${JSON.stringify(rawConfig, null, 2)}\n`;
|
|
1165
|
+
writeFileSync(join(outDir, PRIMARY_CONFIG_NAME), serializedConfig, "utf-8");
|
|
1166
|
+
writeFileSync(join(outDir, LEGACY_CONFIG_NAME), serializedConfig, "utf-8");
|
|
1167
|
+
console.log(`📋 Copied ${configName} as ${PRIMARY_CONFIG_NAME} (and legacy ${LEGACY_CONFIG_NAME})`);
|
|
1043
1168
|
|
|
1044
1169
|
// ── 3b. Copy static assets from docs project into public/ ─────────────────
|
|
1045
1170
|
copyStaticAssets(docsDir, join(outDir, "public"));
|
|
@@ -1054,12 +1179,13 @@ function build(docsDir: string, outDir: string) {
|
|
|
1054
1179
|
const navLanguages = config.navigation.languages;
|
|
1055
1180
|
const simpleLanguages = config.languages || [];
|
|
1056
1181
|
|
|
1057
|
-
function processPage(srcPath: string, destPath: string, slug: string) {
|
|
1058
|
-
mkdirSync(dirname(destPath), { recursive: true });
|
|
1059
|
-
let content = readFileSync(srcPath, "utf-8");
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
const
|
|
1182
|
+
function processPage(srcPath: string, destPath: string, slug: string) {
|
|
1183
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
1184
|
+
let content = readFileSync(srcPath, "utf-8");
|
|
1185
|
+
content = replaceVariablesInString(content, variables, relative(docsDir, srcPath), true);
|
|
1186
|
+
if (!content.startsWith("---")) {
|
|
1187
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
1188
|
+
const title = titleMatch ? titleMatch[1] : pageLabelFromSlug(slug);
|
|
1063
1189
|
if (titleMatch) {
|
|
1064
1190
|
content = content.replace(/^#\s+.+$/m, "").trimStart();
|
|
1065
1191
|
}
|
|
@@ -1069,12 +1195,12 @@ function build(docsDir: string, outDir: string) {
|
|
|
1069
1195
|
writeFileSync(destPath, content, "utf-8");
|
|
1070
1196
|
}
|
|
1071
1197
|
|
|
1072
|
-
function writeLangContent(
|
|
1073
|
-
langCode: string,
|
|
1074
|
-
artifacts: BuildArtifacts,
|
|
1075
|
-
isDefault: boolean,
|
|
1076
|
-
useLangFolders = false
|
|
1077
|
-
) {
|
|
1198
|
+
function writeLangContent(
|
|
1199
|
+
langCode: string,
|
|
1200
|
+
artifacts: BuildArtifacts,
|
|
1201
|
+
isDefault: boolean,
|
|
1202
|
+
useLangFolders = false
|
|
1203
|
+
) {
|
|
1078
1204
|
const storagePrefix = useLangFolders ? langCode : (isDefault ? "" : langCode);
|
|
1079
1205
|
const urlPrefix = isDefault ? "" : langCode;
|
|
1080
1206
|
|
|
@@ -1082,53 +1208,55 @@ function build(docsDir: string, outDir: string) {
|
|
|
1082
1208
|
const metas = storagePrefix
|
|
1083
1209
|
? artifacts.metaFiles.map((m) => ({ dir: m.dir ? `${storagePrefix}/${m.dir}` : storagePrefix, data: { ...m.data } }))
|
|
1084
1210
|
: 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
|
-
}
|
|
1090
|
-
|
|
1091
|
-
function sanitizeFrontmatterValue(value: string): string {
|
|
1092
|
-
return value.replace(/\r?\n+/g, " ").replace(/"/g, '\\"').trim();
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
// Copy pages using explicit source paths from docs.json/velu.json
|
|
1096
|
-
for (const mapping of artifacts.pageMap) {
|
|
1211
|
+
for (const meta of metas) {
|
|
1212
|
+
const metaPath = join(contentDir, meta.dir, "meta.json");
|
|
1213
|
+
mkdirSync(dirname(metaPath), { recursive: true });
|
|
1214
|
+
writeFileSync(metaPath, JSON.stringify(meta.data, null, 2) + "\n", "utf-8");
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function sanitizeFrontmatterValue(value: string): string {
|
|
1218
|
+
return value.replace(/\r?\n+/g, " ").replace(/"/g, '\\"').trim();
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Copy pages using explicit source paths from docs.json/velu.json
|
|
1222
|
+
for (const mapping of artifacts.pageMap) {
|
|
1097
1223
|
const destPath = join(
|
|
1098
1224
|
contentDir,
|
|
1099
1225
|
storagePrefix ? `${storagePrefix}/${mapping.dest}.mdx` : `${mapping.dest}.mdx`,
|
|
1100
1226
|
);
|
|
1101
1227
|
|
|
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"` : "";
|
|
1228
|
+
if (mapping.kind === "openapi-operation") {
|
|
1229
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
1230
|
+
const operationLabel = `${mapping.openapiMethod ?? "GET"} ${mapping.openapiEndpoint ?? "/"}`;
|
|
1231
|
+
const normalizedSpec = normalizeOpenApiSpecForFrontmatter(mapping.openapiSpec);
|
|
1232
|
+
const openapiValue = normalizedSpec
|
|
1233
|
+
? `${normalizedSpec} ${operationLabel}`
|
|
1234
|
+
: operationLabel;
|
|
1235
|
+
const title = sanitizeFrontmatterValue(mapping.title ?? operationLabel);
|
|
1236
|
+
const description = typeof mapping.description === "string"
|
|
1237
|
+
? sanitizeFrontmatterValue(mapping.description)
|
|
1238
|
+
: "";
|
|
1239
|
+
const version = typeof mapping.version === "string"
|
|
1240
|
+
? sanitizeFrontmatterValue(mapping.version)
|
|
1241
|
+
: "";
|
|
1242
|
+
const openapi = openapiValue.replace(/"/g, '\\"');
|
|
1243
|
+
const warning = normalizedSpec
|
|
1244
|
+
? ""
|
|
1245
|
+
: "\n> Warning: No OpenAPI spec source was resolved for this operation. Set `openapi` on this tab/group/navigation or at the top level.\n";
|
|
1246
|
+
const descriptionLine = description ? `\ndescription: "${description}"` : "";
|
|
1247
|
+
const deprecatedLine = mapping.deprecated === true ? `\ndeprecated: true` : "";
|
|
1248
|
+
const statusLine = mapping.deprecated === true ? `\nstatus: "deprecated"` : "";
|
|
1123
1249
|
const versionLine = version ? `\nversion: "${version}"` : "";
|
|
1124
|
-
const content = typeof mapping.content === "string"
|
|
1250
|
+
const content = typeof mapping.content === "string"
|
|
1251
|
+
? `${replaceVariablesInString(mapping.content.trim(), variables, `openapi:${mapping.dest}`, true)}\n`
|
|
1252
|
+
: "";
|
|
1125
1253
|
writeFileSync(
|
|
1126
|
-
destPath,
|
|
1127
|
-
`---\ntitle: "${title}"${descriptionLine}${deprecatedLine}${statusLine}${versionLine}\nopenapi: "${openapi}"\n---\n${warning}${content}`,
|
|
1128
|
-
"utf-8",
|
|
1129
|
-
);
|
|
1130
|
-
continue;
|
|
1131
|
-
}
|
|
1254
|
+
destPath,
|
|
1255
|
+
`---\ntitle: "${title}"${descriptionLine}${deprecatedLine}${statusLine}${versionLine}\nopenapi: "${openapi}"\n---\n${warning}${content}`,
|
|
1256
|
+
"utf-8",
|
|
1257
|
+
);
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1132
1260
|
|
|
1133
1261
|
const src = mapping.src;
|
|
1134
1262
|
// Check for .mdx first, then .md
|
|
@@ -1138,10 +1266,10 @@ function build(docsDir: string, outDir: string) {
|
|
|
1138
1266
|
srcPath = join(docsDir, `${src}.md`);
|
|
1139
1267
|
ext = ".md";
|
|
1140
1268
|
}
|
|
1141
|
-
if (!existsSync(srcPath)) {
|
|
1142
|
-
console.warn(`Warning: Missing page source: ${src}${ext} (language: ${langCode})`);
|
|
1143
|
-
continue;
|
|
1144
|
-
}
|
|
1269
|
+
if (!existsSync(srcPath)) {
|
|
1270
|
+
console.warn(`Warning: Missing page source: ${src}${ext} (language: ${langCode})`);
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1145
1273
|
processPage(srcPath, destPath, src);
|
|
1146
1274
|
}
|
|
1147
1275
|
|
|
@@ -1166,7 +1294,7 @@ function build(docsDir: string, outDir: string) {
|
|
|
1166
1294
|
const langEntry = navLanguages[i];
|
|
1167
1295
|
const isDefault = i === 0;
|
|
1168
1296
|
const langConfig = { ...config, navigation: { ...config.navigation, tabs: langEntry.tabs } } as VeluConfig;
|
|
1169
|
-
const artifacts = buildArtifacts(langConfig, docsDir);
|
|
1297
|
+
const artifacts = buildArtifacts(langConfig, docsDir);
|
|
1170
1298
|
writeLangContent(langEntry.language, artifacts, isDefault, true);
|
|
1171
1299
|
totalPages += artifacts.pageMap.length;
|
|
1172
1300
|
totalMeta += artifacts.metaFiles.length;
|
|
@@ -1177,7 +1305,7 @@ function build(docsDir: string, outDir: string) {
|
|
|
1177
1305
|
writeFileSync(rootMetaPath, JSON.stringify({ pages: rootPages }, null, 2) + "\n", "utf-8");
|
|
1178
1306
|
} else {
|
|
1179
1307
|
// ── Mode 2: Simple (single-lang or same-nav multi-lang) ───────────
|
|
1180
|
-
const artifacts = buildArtifacts(config, docsDir);
|
|
1308
|
+
const artifacts = buildArtifacts(config, docsDir);
|
|
1181
1309
|
const useLangFolders = simpleLanguages.length > 1;
|
|
1182
1310
|
writeLangContent(simpleLanguages[0] || "en", artifacts, true, useLangFolders);
|
|
1183
1311
|
totalPages += artifacts.pageMap.length;
|