@aravindc26/velu 0.11.16 → 0.11.17
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 +46 -38
- package/src/build.ts +269 -258
- package/src/cli.ts +28 -4
- package/src/engine/app/global.css +3 -3
- package/src/engine/app/layout.tsx +15 -1
- package/src/engine/lib/velu.ts +26 -0
- package/src/engine/mdx-components.tsx +1 -3
- package/src/engine/src/lib/velu.ts +0 -1
- package/src/themes.ts +52 -6
- package/src/validate.ts +9 -1
package/package.json
CHANGED
package/schema/velu.schema.json
CHANGED
|
@@ -92,6 +92,26 @@
|
|
|
92
92
|
],
|
|
93
93
|
"default": "system"
|
|
94
94
|
},
|
|
95
|
+
"fonts": {
|
|
96
|
+
"description": "Custom font configuration. Can be a single font definition (applied to both headings and body) or an object with separate heading and body font definitions.",
|
|
97
|
+
"oneOf": [
|
|
98
|
+
{ "$ref": "#/definitions/fontDefinition" },
|
|
99
|
+
{
|
|
100
|
+
"type": "object",
|
|
101
|
+
"properties": {
|
|
102
|
+
"heading": {
|
|
103
|
+
"$ref": "#/definitions/fontDefinition",
|
|
104
|
+
"description": "Font used for headings (h1–h6)."
|
|
105
|
+
},
|
|
106
|
+
"body": {
|
|
107
|
+
"$ref": "#/definitions/fontDefinition",
|
|
108
|
+
"description": "Font used for body text."
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"additionalProperties": false
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
},
|
|
95
115
|
"icons": {
|
|
96
116
|
"type": "object",
|
|
97
117
|
"description": "Icon settings compatible with Mint-style configuration.",
|
|
@@ -112,44 +132,6 @@
|
|
|
112
132
|
],
|
|
113
133
|
"additionalProperties": false
|
|
114
134
|
},
|
|
115
|
-
"styling": {
|
|
116
|
-
"type": "object",
|
|
117
|
-
"description": "Fine-grained styling options.",
|
|
118
|
-
"properties": {
|
|
119
|
-
"codeblocks": {
|
|
120
|
-
"type": "object",
|
|
121
|
-
"description": "Code block styling options.",
|
|
122
|
-
"properties": {
|
|
123
|
-
"theme": {
|
|
124
|
-
"description": "Shiki theme for syntax highlighting. Use a string for a single theme, or an object with light/dark keys.",
|
|
125
|
-
"oneOf": [
|
|
126
|
-
{
|
|
127
|
-
"type": "string"
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
"type": "object",
|
|
131
|
-
"properties": {
|
|
132
|
-
"light": {
|
|
133
|
-
"type": "string"
|
|
134
|
-
},
|
|
135
|
-
"dark": {
|
|
136
|
-
"type": "string"
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
"required": [
|
|
140
|
-
"light",
|
|
141
|
-
"dark"
|
|
142
|
-
],
|
|
143
|
-
"additionalProperties": false
|
|
144
|
-
}
|
|
145
|
-
]
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
"additionalProperties": false
|
|
149
|
-
}
|
|
150
|
-
},
|
|
151
|
-
"additionalProperties": false
|
|
152
|
-
},
|
|
153
135
|
"languages": {
|
|
154
136
|
"type": "array",
|
|
155
137
|
"description": "Supported language codes (e.g. [\"en\", \"es\", \"ja\"]). The first is the default. Display labels are auto-derived.",
|
|
@@ -658,6 +640,32 @@
|
|
|
658
640
|
},
|
|
659
641
|
"additionalProperties": false,
|
|
660
642
|
"definitions": {
|
|
643
|
+
"fontDefinition": {
|
|
644
|
+
"type": "object",
|
|
645
|
+
"description": "A font definition with family name and optional weight, source URL, and format.",
|
|
646
|
+
"properties": {
|
|
647
|
+
"family": {
|
|
648
|
+
"type": "string",
|
|
649
|
+
"description": "The font family name, e.g. \"Inter\", \"Playfair Display\"."
|
|
650
|
+
},
|
|
651
|
+
"weight": {
|
|
652
|
+
"type": "number",
|
|
653
|
+
"description": "The font weight, e.g. 400, 700. Variable font weights like 550 are supported."
|
|
654
|
+
},
|
|
655
|
+
"source": {
|
|
656
|
+
"type": "string",
|
|
657
|
+
"minLength": 1,
|
|
658
|
+
"description": "URL or path to a custom font file, e.g. /fonts/custom.woff2 or https://example.com/font.woff2."
|
|
659
|
+
},
|
|
660
|
+
"format": {
|
|
661
|
+
"type": "string",
|
|
662
|
+
"enum": ["woff", "woff2"],
|
|
663
|
+
"description": "The font file format when using a custom source."
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
"required": ["family"],
|
|
667
|
+
"additionalProperties": false
|
|
668
|
+
},
|
|
661
669
|
"footerSocials": {
|
|
662
670
|
"type": "object",
|
|
663
671
|
"description": "Map of social network keys to URLs (for example {\"x\":\"https://x.com/org\",\"github\":\"https://github.com/org\"}).",
|
package/src/build.ts
CHANGED
|
@@ -2,19 +2,19 @@ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, cpSync, existsSyn
|
|
|
2
2
|
import { join, dirname, relative, extname, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
|
-
import { generateThemeCss, resolveThemeName, type VeluColors
|
|
5
|
+
import { generateThemeCss, resolveThemeName, type VeluColors } from "./themes.js";
|
|
6
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);
|
|
10
10
|
const __dirname = dirname(__filename);
|
|
11
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 CLI_PACKAGE_JSON_PATH = join(__dirname, "..", "package.json");
|
|
15
|
-
const PRIMARY_CONFIG_NAME = "docs.json";
|
|
16
|
-
const LEGACY_CONFIG_NAME = "velu.json";
|
|
17
|
-
const SOURCE_MIRROR_DIR = "velu-imports";
|
|
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 CLI_PACKAGE_JSON_PATH = join(__dirname, "..", "package.json");
|
|
15
|
+
const PRIMARY_CONFIG_NAME = "docs.json";
|
|
16
|
+
const LEGACY_CONFIG_NAME = "velu.json";
|
|
17
|
+
const SOURCE_MIRROR_DIR = "velu-imports";
|
|
18
18
|
|
|
19
19
|
const SOURCE_MIRROR_EXTENSIONS = new Set([
|
|
20
20
|
".md", ".mdx", ".jsx", ".js", ".tsx", ".ts",
|
|
@@ -25,11 +25,11 @@ const SOURCE_MIRROR_EXTENSIONS = new Set([
|
|
|
25
25
|
".pdf", ".txt", ".xml", ".csv", ".zip",
|
|
26
26
|
]);
|
|
27
27
|
|
|
28
|
-
const IMPORT_REWRITE_EXTENSIONS = new Set([".md", ".mdx", ".jsx", ".js", ".tsx", ".ts"]);
|
|
29
|
-
const VARIABLE_SUBSTITUTION_EXTENSIONS = new Set([
|
|
30
|
-
".md", ".mdx", ".jsx", ".js", ".tsx", ".ts",
|
|
31
|
-
".json", ".yaml", ".yml", ".css", ".txt", ".xml", ".csv",
|
|
32
|
-
]);
|
|
28
|
+
const IMPORT_REWRITE_EXTENSIONS = new Set([".md", ".mdx", ".jsx", ".js", ".tsx", ".ts"]);
|
|
29
|
+
const VARIABLE_SUBSTITUTION_EXTENSIONS = new Set([
|
|
30
|
+
".md", ".mdx", ".jsx", ".js", ".tsx", ".ts",
|
|
31
|
+
".json", ".yaml", ".yml", ".css", ".txt", ".xml", ".csv",
|
|
32
|
+
]);
|
|
33
33
|
|
|
34
34
|
function resolveConfigPath(docsDir: string): string {
|
|
35
35
|
const primary = join(docsDir, PRIMARY_CONFIG_NAME);
|
|
@@ -136,22 +136,22 @@ interface VeluRedirect {
|
|
|
136
136
|
permanent?: boolean;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
interface VeluConfig {
|
|
140
|
-
$schema?: string;
|
|
141
|
-
name?: string;
|
|
142
|
-
title?: string;
|
|
143
|
-
description?: string;
|
|
144
|
-
theme?: string;
|
|
145
|
-
variables?: Record<string, string>;
|
|
146
|
-
colors?: VeluColors;
|
|
147
|
-
appearance?: "system" | "light" | "dark";
|
|
148
|
-
|
|
149
|
-
metadata?: {
|
|
150
|
-
timestamp?: boolean;
|
|
151
|
-
};
|
|
152
|
-
openapi?: VeluOpenApiSource;
|
|
153
|
-
languages?: string[];
|
|
154
|
-
redirects?: VeluRedirect[];
|
|
139
|
+
interface VeluConfig {
|
|
140
|
+
$schema?: string;
|
|
141
|
+
name?: string;
|
|
142
|
+
title?: string;
|
|
143
|
+
description?: string;
|
|
144
|
+
theme?: string;
|
|
145
|
+
variables?: Record<string, string>;
|
|
146
|
+
colors?: VeluColors;
|
|
147
|
+
appearance?: "system" | "light" | "dark";
|
|
148
|
+
fonts?: { family: string; weight?: number; source?: string; format?: "woff" | "woff2" } | { heading?: { family: string; weight?: number; source?: string; format?: "woff" | "woff2" }; body?: { family: string; weight?: number; source?: string; format?: "woff" | "woff2" } };
|
|
149
|
+
metadata?: {
|
|
150
|
+
timestamp?: boolean;
|
|
151
|
+
};
|
|
152
|
+
openapi?: VeluOpenApiSource;
|
|
153
|
+
languages?: string[];
|
|
154
|
+
redirects?: VeluRedirect[];
|
|
155
155
|
navigation: {
|
|
156
156
|
openapi?: VeluOpenApiSource;
|
|
157
157
|
tabs?: VeluTab[];
|
|
@@ -405,115 +405,115 @@ function normalizeOpenApiSpecForFrontmatter(spec: string | undefined): string |
|
|
|
405
405
|
return `/${trimmed.replace(/^\.?\/*/, "")}`;
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
409
|
-
|
|
410
|
-
const VARIABLE_TOKEN_PATTERN = /\{\{\s*([A-Za-z0-9.-]+)\s*\}\}/g;
|
|
411
|
-
const VARIABLE_NAME_PATTERN = /^[A-Za-z0-9.-]+$/;
|
|
412
|
-
|
|
413
|
-
function sanitizeVariableValue(value: string): string {
|
|
414
|
-
return value.replace(/</g, "<").replace(/>/g, ">");
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function extractVariables(input: unknown): Record<string, string> {
|
|
418
|
-
if (!input || typeof input !== "object" || Array.isArray(input)) return {};
|
|
419
|
-
|
|
420
|
-
const output: Record<string, string> = {};
|
|
421
|
-
for (const [rawKey, rawValue] of Object.entries(input as Record<string, unknown>)) {
|
|
422
|
-
const key = rawKey.trim();
|
|
423
|
-
if (!key) continue;
|
|
424
|
-
if (!VARIABLE_NAME_PATTERN.test(key)) {
|
|
425
|
-
throw new Error(`Invalid variable name '${rawKey}'. Variable names can only contain letters, numbers, periods, and hyphens.`);
|
|
426
|
-
}
|
|
427
|
-
if (typeof rawValue !== "string") {
|
|
428
|
-
throw new Error(`Invalid value for variable '${rawKey}'. Variables must be strings.`);
|
|
429
|
-
}
|
|
430
|
-
output[key] = rawValue;
|
|
431
|
-
}
|
|
432
|
-
return output;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function resolveVariableMap(rawVariables: Record<string, string>): Record<string, string> {
|
|
436
|
-
const cache = new Map<string, string>();
|
|
437
|
-
const activeStack = new Set<string>();
|
|
438
|
-
|
|
439
|
-
function resolveOne(name: string): string {
|
|
440
|
-
const cached = cache.get(name);
|
|
441
|
-
if (cached !== undefined) return cached;
|
|
442
|
-
|
|
443
|
-
if (activeStack.has(name)) {
|
|
444
|
-
throw new Error(`Circular variable reference detected for '{{${name}}}'.`);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const raw = rawVariables[name];
|
|
448
|
-
if (raw === undefined) {
|
|
449
|
-
throw new Error(`Undefined variable '{{${name}}}' referenced in variable definitions.`);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
activeStack.add(name);
|
|
453
|
-
const resolved = raw.replace(VARIABLE_TOKEN_PATTERN, (_match, token: string) => resolveOne(token));
|
|
454
|
-
activeStack.delete(name);
|
|
455
|
-
cache.set(name, resolved);
|
|
456
|
-
return resolved;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const output: Record<string, string> = {};
|
|
460
|
-
for (const name of Object.keys(rawVariables)) {
|
|
461
|
-
output[name] = resolveOne(name);
|
|
462
|
-
}
|
|
463
|
-
return output;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function replaceVariablesInString(
|
|
467
|
-
value: string,
|
|
468
|
-
variables: Record<string, string>,
|
|
469
|
-
context: string,
|
|
470
|
-
sanitizeValues: boolean,
|
|
471
|
-
): string {
|
|
472
|
-
const undefinedVariables = new Set<string>();
|
|
473
|
-
const replaced = value.replace(VARIABLE_TOKEN_PATTERN, (match, rawName: string) => {
|
|
474
|
-
const name = rawName.trim();
|
|
475
|
-
const resolved = variables[name];
|
|
476
|
-
if (resolved === undefined) {
|
|
477
|
-
undefinedVariables.add(name);
|
|
478
|
-
return match;
|
|
479
|
-
}
|
|
480
|
-
return sanitizeValues ? sanitizeVariableValue(resolved) : resolved;
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
if (undefinedVariables.size > 0) {
|
|
484
|
-
throw new Error(
|
|
485
|
-
`Undefined variable(s) ${Array.from(undefinedVariables).map((name) => `'{{${name}}}'`).join(", ")} in ${context}.`
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
return replaced;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function applyVariablesToConfig(value: unknown, variables: Record<string, string>, path = "docs.json"): unknown {
|
|
493
|
-
if (typeof value === "string") return replaceVariablesInString(value, variables, path, false);
|
|
494
|
-
if (Array.isArray(value)) return value.map((entry, index) => applyVariablesToConfig(entry, variables, `${path}[${index}]`));
|
|
495
|
-
if (!value || typeof value !== "object") return value;
|
|
496
|
-
|
|
497
|
-
const output: Record<string, unknown> = {};
|
|
498
|
-
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
499
|
-
output[key] = applyVariablesToConfig(entry, variables, `${path}.${key}`);
|
|
500
|
-
}
|
|
501
|
-
return output;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function loadConfig(docsDir: string): { config: VeluConfig; rawConfig: VeluConfig; variables: Record<string, string> } {
|
|
505
|
-
const raw = readFileSync(resolveConfigPath(docsDir), "utf-8");
|
|
506
|
-
const parsed = JSON.parse(raw) as VeluConfig;
|
|
507
|
-
const rawVariables = extractVariables(parsed.variables);
|
|
508
|
-
const resolvedVariables = resolveVariableMap(rawVariables);
|
|
509
|
-
const withVariables = applyVariablesToConfig(parsed, resolvedVariables) as VeluConfig;
|
|
510
|
-
withVariables.variables = resolvedVariables;
|
|
511
|
-
return {
|
|
512
|
-
config: normalizeConfigNavigation(withVariables),
|
|
513
|
-
rawConfig: withVariables,
|
|
514
|
-
variables: resolvedVariables,
|
|
515
|
-
};
|
|
516
|
-
}
|
|
408
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
const VARIABLE_TOKEN_PATTERN = /\{\{\s*([A-Za-z0-9.-]+)\s*\}\}/g;
|
|
411
|
+
const VARIABLE_NAME_PATTERN = /^[A-Za-z0-9.-]+$/;
|
|
412
|
+
|
|
413
|
+
function sanitizeVariableValue(value: string): string {
|
|
414
|
+
return value.replace(/</g, "<").replace(/>/g, ">");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function extractVariables(input: unknown): Record<string, string> {
|
|
418
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return {};
|
|
419
|
+
|
|
420
|
+
const output: Record<string, string> = {};
|
|
421
|
+
for (const [rawKey, rawValue] of Object.entries(input as Record<string, unknown>)) {
|
|
422
|
+
const key = rawKey.trim();
|
|
423
|
+
if (!key) continue;
|
|
424
|
+
if (!VARIABLE_NAME_PATTERN.test(key)) {
|
|
425
|
+
throw new Error(`Invalid variable name '${rawKey}'. Variable names can only contain letters, numbers, periods, and hyphens.`);
|
|
426
|
+
}
|
|
427
|
+
if (typeof rawValue !== "string") {
|
|
428
|
+
throw new Error(`Invalid value for variable '${rawKey}'. Variables must be strings.`);
|
|
429
|
+
}
|
|
430
|
+
output[key] = rawValue;
|
|
431
|
+
}
|
|
432
|
+
return output;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function resolveVariableMap(rawVariables: Record<string, string>): Record<string, string> {
|
|
436
|
+
const cache = new Map<string, string>();
|
|
437
|
+
const activeStack = new Set<string>();
|
|
438
|
+
|
|
439
|
+
function resolveOne(name: string): string {
|
|
440
|
+
const cached = cache.get(name);
|
|
441
|
+
if (cached !== undefined) return cached;
|
|
442
|
+
|
|
443
|
+
if (activeStack.has(name)) {
|
|
444
|
+
throw new Error(`Circular variable reference detected for '{{${name}}}'.`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const raw = rawVariables[name];
|
|
448
|
+
if (raw === undefined) {
|
|
449
|
+
throw new Error(`Undefined variable '{{${name}}}' referenced in variable definitions.`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
activeStack.add(name);
|
|
453
|
+
const resolved = raw.replace(VARIABLE_TOKEN_PATTERN, (_match, token: string) => resolveOne(token));
|
|
454
|
+
activeStack.delete(name);
|
|
455
|
+
cache.set(name, resolved);
|
|
456
|
+
return resolved;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const output: Record<string, string> = {};
|
|
460
|
+
for (const name of Object.keys(rawVariables)) {
|
|
461
|
+
output[name] = resolveOne(name);
|
|
462
|
+
}
|
|
463
|
+
return output;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function replaceVariablesInString(
|
|
467
|
+
value: string,
|
|
468
|
+
variables: Record<string, string>,
|
|
469
|
+
context: string,
|
|
470
|
+
sanitizeValues: boolean,
|
|
471
|
+
): string {
|
|
472
|
+
const undefinedVariables = new Set<string>();
|
|
473
|
+
const replaced = value.replace(VARIABLE_TOKEN_PATTERN, (match, rawName: string) => {
|
|
474
|
+
const name = rawName.trim();
|
|
475
|
+
const resolved = variables[name];
|
|
476
|
+
if (resolved === undefined) {
|
|
477
|
+
undefinedVariables.add(name);
|
|
478
|
+
return match;
|
|
479
|
+
}
|
|
480
|
+
return sanitizeValues ? sanitizeVariableValue(resolved) : resolved;
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
if (undefinedVariables.size > 0) {
|
|
484
|
+
throw new Error(
|
|
485
|
+
`Undefined variable(s) ${Array.from(undefinedVariables).map((name) => `'{{${name}}}'`).join(", ")} in ${context}.`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return replaced;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function applyVariablesToConfig(value: unknown, variables: Record<string, string>, path = "docs.json"): unknown {
|
|
493
|
+
if (typeof value === "string") return replaceVariablesInString(value, variables, path, false);
|
|
494
|
+
if (Array.isArray(value)) return value.map((entry, index) => applyVariablesToConfig(entry, variables, `${path}[${index}]`));
|
|
495
|
+
if (!value || typeof value !== "object") return value;
|
|
496
|
+
|
|
497
|
+
const output: Record<string, unknown> = {};
|
|
498
|
+
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
499
|
+
output[key] = applyVariablesToConfig(entry, variables, `${path}.${key}`);
|
|
500
|
+
}
|
|
501
|
+
return output;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function loadConfig(docsDir: string): { config: VeluConfig; rawConfig: VeluConfig; variables: Record<string, string> } {
|
|
505
|
+
const raw = readFileSync(resolveConfigPath(docsDir), "utf-8");
|
|
506
|
+
const parsed = JSON.parse(raw) as VeluConfig;
|
|
507
|
+
const rawVariables = extractVariables(parsed.variables);
|
|
508
|
+
const resolvedVariables = resolveVariableMap(rawVariables);
|
|
509
|
+
const withVariables = applyVariablesToConfig(parsed, resolvedVariables) as VeluConfig;
|
|
510
|
+
withVariables.variables = resolvedVariables;
|
|
511
|
+
return {
|
|
512
|
+
config: normalizeConfigNavigation(withVariables),
|
|
513
|
+
rawConfig: withVariables,
|
|
514
|
+
variables: resolvedVariables,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
517
|
|
|
518
518
|
function isExternalDestination(value: string): boolean {
|
|
519
519
|
return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(value);
|
|
@@ -591,7 +591,7 @@ const STATIC_EXTENSIONS = new Set([
|
|
|
591
591
|
".zip",
|
|
592
592
|
]);
|
|
593
593
|
|
|
594
|
-
function copyStaticAssets(docsDir: string, publicDir: string) {
|
|
594
|
+
function copyStaticAssets(docsDir: string, publicDir: string) {
|
|
595
595
|
function walk(dir: string) {
|
|
596
596
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
597
597
|
for (const entry of entries) {
|
|
@@ -615,49 +615,49 @@ function copyStaticAssets(docsDir: string, publicDir: string) {
|
|
|
615
615
|
}
|
|
616
616
|
}
|
|
617
617
|
|
|
618
|
-
walk(docsDir);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
function resolveProjectName(config: VeluConfig): string {
|
|
622
|
-
const fromName = typeof config.name === "string" ? config.name.trim() : "";
|
|
623
|
-
if (fromName) return fromName;
|
|
624
|
-
const fromTitle = typeof config.title === "string" ? config.title.trim() : "";
|
|
625
|
-
if (fromTitle) return fromTitle;
|
|
626
|
-
return "Documentation";
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
function resolveProjectDescription(config: VeluConfig): string {
|
|
630
|
-
if (typeof config.description === "string") return config.description.trim();
|
|
631
|
-
return "";
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function resolveCliVersion(): string {
|
|
635
|
-
try {
|
|
636
|
-
const raw = readFileSync(CLI_PACKAGE_JSON_PATH, "utf-8");
|
|
637
|
-
const parsed = JSON.parse(raw) as { version?: unknown };
|
|
638
|
-
if (typeof parsed.version === "string" && parsed.version.trim().length > 0) {
|
|
639
|
-
return parsed.version.trim();
|
|
640
|
-
}
|
|
641
|
-
} catch {
|
|
642
|
-
// ignore and fallback
|
|
643
|
-
}
|
|
644
|
-
return "unknown";
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
function writeProjectConstFile(config: VeluConfig, outDir: string) {
|
|
648
|
-
const constPayload = {
|
|
649
|
-
name: resolveProjectName(config),
|
|
650
|
-
description: resolveProjectDescription(config),
|
|
651
|
-
version: resolveCliVersion(),
|
|
652
|
-
};
|
|
653
|
-
|
|
654
|
-
const constPath = join(outDir, "public", "const.json");
|
|
655
|
-
writeFileSync(constPath, `${JSON.stringify(constPayload, null, 2)}\n`, "utf-8");
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function toPosixPath(value: string): string {
|
|
659
|
-
return value.replace(/\\/g, "/");
|
|
660
|
-
}
|
|
618
|
+
walk(docsDir);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function resolveProjectName(config: VeluConfig): string {
|
|
622
|
+
const fromName = typeof config.name === "string" ? config.name.trim() : "";
|
|
623
|
+
if (fromName) return fromName;
|
|
624
|
+
const fromTitle = typeof config.title === "string" ? config.title.trim() : "";
|
|
625
|
+
if (fromTitle) return fromTitle;
|
|
626
|
+
return "Documentation";
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function resolveProjectDescription(config: VeluConfig): string {
|
|
630
|
+
if (typeof config.description === "string") return config.description.trim();
|
|
631
|
+
return "";
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function resolveCliVersion(): string {
|
|
635
|
+
try {
|
|
636
|
+
const raw = readFileSync(CLI_PACKAGE_JSON_PATH, "utf-8");
|
|
637
|
+
const parsed = JSON.parse(raw) as { version?: unknown };
|
|
638
|
+
if (typeof parsed.version === "string" && parsed.version.trim().length > 0) {
|
|
639
|
+
return parsed.version.trim();
|
|
640
|
+
}
|
|
641
|
+
} catch {
|
|
642
|
+
// ignore and fallback
|
|
643
|
+
}
|
|
644
|
+
return "unknown";
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function writeProjectConstFile(config: VeluConfig, outDir: string) {
|
|
648
|
+
const constPayload = {
|
|
649
|
+
name: resolveProjectName(config),
|
|
650
|
+
description: resolveProjectDescription(config),
|
|
651
|
+
version: resolveCliVersion(),
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
const constPath = join(outDir, "public", "const.json");
|
|
655
|
+
writeFileSync(constPath, `${JSON.stringify(constPayload, null, 2)}\n`, "utf-8");
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function toPosixPath(value: string): string {
|
|
659
|
+
return value.replace(/\\/g, "/");
|
|
660
|
+
}
|
|
661
661
|
|
|
662
662
|
function isInsideDocsRoot(docsDir: string, targetPath: string): boolean {
|
|
663
663
|
const relPath = relative(docsDir, targetPath);
|
|
@@ -824,41 +824,41 @@ function rewriteImportsInContent(
|
|
|
824
824
|
return out.join("\n");
|
|
825
825
|
}
|
|
826
826
|
|
|
827
|
-
function copyMirroredSourceFile(
|
|
828
|
-
srcPath: string,
|
|
829
|
-
docsDir: string,
|
|
830
|
-
mirrorDir: string,
|
|
831
|
-
variables: Record<string, string>,
|
|
832
|
-
) {
|
|
833
|
-
if (!shouldMirrorSourceFile(srcPath)) return;
|
|
834
|
-
if (!isInsideDocsRoot(docsDir, srcPath)) return;
|
|
835
|
-
|
|
836
|
-
const relPath = relative(docsDir, srcPath);
|
|
837
|
-
const destPath = join(mirrorDir, relPath);
|
|
838
|
-
mkdirSync(dirname(destPath), { recursive: true });
|
|
839
|
-
|
|
840
|
-
if (shouldRewriteImports(srcPath)) {
|
|
841
|
-
let raw = readFileSync(srcPath, "utf-8");
|
|
842
|
-
raw = replaceVariablesInString(raw, variables, relPath, true);
|
|
843
|
-
const rewritten = rewriteImportsInContent(raw, srcPath, destPath, docsDir, mirrorDir);
|
|
844
|
-
writeFileSync(destPath, rewritten, "utf-8");
|
|
845
|
-
return;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
const extension = extname(srcPath).toLowerCase();
|
|
849
|
-
if (VARIABLE_SUBSTITUTION_EXTENSIONS.has(extension)) {
|
|
850
|
-
const raw = readFileSync(srcPath, "utf-8");
|
|
851
|
-
const substituted = replaceVariablesInString(raw, variables, relPath, true);
|
|
852
|
-
writeFileSync(destPath, substituted, "utf-8");
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
copyFileSync(srcPath, destPath);
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
function rebuildSourceMirror(docsDir: string, mirrorDir: string, variables: Record<string, string>) {
|
|
860
|
-
rmSync(mirrorDir, { recursive: true, force: true });
|
|
861
|
-
mkdirSync(mirrorDir, { recursive: true });
|
|
827
|
+
function copyMirroredSourceFile(
|
|
828
|
+
srcPath: string,
|
|
829
|
+
docsDir: string,
|
|
830
|
+
mirrorDir: string,
|
|
831
|
+
variables: Record<string, string>,
|
|
832
|
+
) {
|
|
833
|
+
if (!shouldMirrorSourceFile(srcPath)) return;
|
|
834
|
+
if (!isInsideDocsRoot(docsDir, srcPath)) return;
|
|
835
|
+
|
|
836
|
+
const relPath = relative(docsDir, srcPath);
|
|
837
|
+
const destPath = join(mirrorDir, relPath);
|
|
838
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
839
|
+
|
|
840
|
+
if (shouldRewriteImports(srcPath)) {
|
|
841
|
+
let raw = readFileSync(srcPath, "utf-8");
|
|
842
|
+
raw = replaceVariablesInString(raw, variables, relPath, true);
|
|
843
|
+
const rewritten = rewriteImportsInContent(raw, srcPath, destPath, docsDir, mirrorDir);
|
|
844
|
+
writeFileSync(destPath, rewritten, "utf-8");
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const extension = extname(srcPath).toLowerCase();
|
|
849
|
+
if (VARIABLE_SUBSTITUTION_EXTENSIONS.has(extension)) {
|
|
850
|
+
const raw = readFileSync(srcPath, "utf-8");
|
|
851
|
+
const substituted = replaceVariablesInString(raw, variables, relPath, true);
|
|
852
|
+
writeFileSync(destPath, substituted, "utf-8");
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
copyFileSync(srcPath, destPath);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function rebuildSourceMirror(docsDir: string, mirrorDir: string, variables: Record<string, string>) {
|
|
860
|
+
rmSync(mirrorDir, { recursive: true, force: true });
|
|
861
|
+
mkdirSync(mirrorDir, { recursive: true });
|
|
862
862
|
|
|
863
863
|
function walk(dir: string) {
|
|
864
864
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -866,14 +866,14 @@ function rebuildSourceMirror(docsDir: string, mirrorDir: string, variables: Reco
|
|
|
866
866
|
if (entry.name.startsWith(".")) continue;
|
|
867
867
|
if (entry.name === "node_modules") continue;
|
|
868
868
|
const srcPath = join(dir, entry.name);
|
|
869
|
-
if (entry.isDirectory()) {
|
|
870
|
-
walk(srcPath);
|
|
871
|
-
continue;
|
|
872
|
-
}
|
|
873
|
-
if (!shouldMirrorSourceFile(srcPath)) continue;
|
|
874
|
-
copyMirroredSourceFile(srcPath, docsDir, mirrorDir, variables);
|
|
875
|
-
}
|
|
876
|
-
}
|
|
869
|
+
if (entry.isDirectory()) {
|
|
870
|
+
walk(srcPath);
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
if (!shouldMirrorSourceFile(srcPath)) continue;
|
|
874
|
+
copyMirroredSourceFile(srcPath, docsDir, mirrorDir, variables);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
877
|
|
|
878
878
|
walk(docsDir);
|
|
879
879
|
}
|
|
@@ -1179,11 +1179,11 @@ function buildArtifacts(config: VeluConfig, docsDirForOpenApi?: string): BuildAr
|
|
|
1179
1179
|
|
|
1180
1180
|
// ── Build ──────────────────────────────────────────────────────────────────────
|
|
1181
1181
|
|
|
1182
|
-
function build(docsDir: string, outDir: string) {
|
|
1183
|
-
const configPath = resolveConfigPath(docsDir);
|
|
1184
|
-
const configName = configPath.endsWith(PRIMARY_CONFIG_NAME) ? PRIMARY_CONFIG_NAME : LEGACY_CONFIG_NAME;
|
|
1185
|
-
console.log(`📖 Loading ${configName} from: ${docsDir}`);
|
|
1186
|
-
const { config, rawConfig, variables } = loadConfig(docsDir);
|
|
1182
|
+
function build(docsDir: string, outDir: string) {
|
|
1183
|
+
const configPath = resolveConfigPath(docsDir);
|
|
1184
|
+
const configName = configPath.endsWith(PRIMARY_CONFIG_NAME) ? PRIMARY_CONFIG_NAME : LEGACY_CONFIG_NAME;
|
|
1185
|
+
console.log(`📖 Loading ${configName} from: ${docsDir}`);
|
|
1186
|
+
const { config, rawConfig, variables } = loadConfig(docsDir);
|
|
1187
1187
|
|
|
1188
1188
|
if (existsSync(outDir)) {
|
|
1189
1189
|
rmSync(outDir, { recursive: true, force: true });
|
|
@@ -1196,39 +1196,39 @@ function build(docsDir: string, outDir: string) {
|
|
|
1196
1196
|
console.log("📦 Copied engine files");
|
|
1197
1197
|
|
|
1198
1198
|
// ── 2. Create additional directories ─────────────────────────────────────
|
|
1199
|
-
mkdirSync(join(outDir, "content", "docs"), { recursive: true });
|
|
1200
|
-
mkdirSync(join(outDir, "public"), { recursive: true });
|
|
1201
|
-
const sourceMirrorDir = join(outDir, SOURCE_MIRROR_DIR);
|
|
1202
|
-
rebuildSourceMirror(docsDir, sourceMirrorDir, variables);
|
|
1199
|
+
mkdirSync(join(outDir, "content", "docs"), { recursive: true });
|
|
1200
|
+
mkdirSync(join(outDir, "public"), { recursive: true });
|
|
1201
|
+
const sourceMirrorDir = join(outDir, SOURCE_MIRROR_DIR);
|
|
1202
|
+
rebuildSourceMirror(docsDir, sourceMirrorDir, variables);
|
|
1203
1203
|
|
|
1204
1204
|
// ── 3. Copy config into the generated project ────────────────────────────
|
|
1205
|
-
const serializedConfig = `${JSON.stringify(rawConfig, null, 2)}\n`;
|
|
1206
|
-
writeFileSync(join(outDir, PRIMARY_CONFIG_NAME), serializedConfig, "utf-8");
|
|
1207
|
-
writeFileSync(join(outDir, LEGACY_CONFIG_NAME), serializedConfig, "utf-8");
|
|
1208
|
-
console.log(`📋 Copied ${configName} as ${PRIMARY_CONFIG_NAME} (and legacy ${LEGACY_CONFIG_NAME})`);
|
|
1205
|
+
const serializedConfig = `${JSON.stringify(rawConfig, null, 2)}\n`;
|
|
1206
|
+
writeFileSync(join(outDir, PRIMARY_CONFIG_NAME), serializedConfig, "utf-8");
|
|
1207
|
+
writeFileSync(join(outDir, LEGACY_CONFIG_NAME), serializedConfig, "utf-8");
|
|
1208
|
+
console.log(`📋 Copied ${configName} as ${PRIMARY_CONFIG_NAME} (and legacy ${LEGACY_CONFIG_NAME})`);
|
|
1209
1209
|
|
|
1210
1210
|
// ── 3b. Copy static assets from docs project into public/ ─────────────────
|
|
1211
|
-
copyStaticAssets(docsDir, join(outDir, "public"));
|
|
1212
|
-
writeRedirectArtifacts(config, outDir);
|
|
1213
|
-
writeProjectConstFile(rawConfig, outDir);
|
|
1214
|
-
if ((config.redirects ?? []).length > 0) {
|
|
1215
|
-
console.log("↪️ Generated redirect artifacts");
|
|
1216
|
-
}
|
|
1217
|
-
console.log("🧾 Generated const.json");
|
|
1218
|
-
console.log("🖼️ Copied static assets");
|
|
1211
|
+
copyStaticAssets(docsDir, join(outDir, "public"));
|
|
1212
|
+
writeRedirectArtifacts(config, outDir);
|
|
1213
|
+
writeProjectConstFile(rawConfig, outDir);
|
|
1214
|
+
if ((config.redirects ?? []).length > 0) {
|
|
1215
|
+
console.log("↪️ Generated redirect artifacts");
|
|
1216
|
+
}
|
|
1217
|
+
console.log("🧾 Generated const.json");
|
|
1218
|
+
console.log("🖼️ Copied static assets");
|
|
1219
1219
|
|
|
1220
1220
|
// ── 4. Build content + metadata artifacts ────────────────────────────────
|
|
1221
1221
|
const contentDir = join(outDir, "content", "docs");
|
|
1222
1222
|
const navLanguages = config.navigation.languages;
|
|
1223
1223
|
const simpleLanguages = config.languages || [];
|
|
1224
1224
|
|
|
1225
|
-
function processPage(srcPath: string, destPath: string, slug: string) {
|
|
1226
|
-
mkdirSync(dirname(destPath), { recursive: true });
|
|
1227
|
-
let content = readFileSync(srcPath, "utf-8");
|
|
1228
|
-
content = replaceVariablesInString(content, variables, relative(docsDir, srcPath), true);
|
|
1229
|
-
if (!content.startsWith("---")) {
|
|
1230
|
-
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
1231
|
-
const title = titleMatch ? titleMatch[1] : pageLabelFromSlug(slug);
|
|
1225
|
+
function processPage(srcPath: string, destPath: string, slug: string) {
|
|
1226
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
1227
|
+
let content = readFileSync(srcPath, "utf-8");
|
|
1228
|
+
content = replaceVariablesInString(content, variables, relative(docsDir, srcPath), true);
|
|
1229
|
+
if (!content.startsWith("---")) {
|
|
1230
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
1231
|
+
const title = titleMatch ? titleMatch[1] : pageLabelFromSlug(slug);
|
|
1232
1232
|
if (titleMatch) {
|
|
1233
1233
|
content = content.replace(/^#\s+.+$/m, "").trimStart();
|
|
1234
1234
|
}
|
|
@@ -1289,11 +1289,11 @@ function build(docsDir: string, outDir: string) {
|
|
|
1289
1289
|
const descriptionLine = description ? `\ndescription: "${description}"` : "";
|
|
1290
1290
|
const deprecatedLine = mapping.deprecated === true ? `\ndeprecated: true` : "";
|
|
1291
1291
|
const statusLine = mapping.deprecated === true ? `\nstatus: "deprecated"` : "";
|
|
1292
|
-
const versionLine = version ? `\nversion: "${version}"` : "";
|
|
1293
|
-
const content = typeof mapping.content === "string"
|
|
1294
|
-
? `${replaceVariablesInString(mapping.content.trim(), variables, `openapi:${mapping.dest}`, true)}\n`
|
|
1295
|
-
: "";
|
|
1296
|
-
writeFileSync(
|
|
1292
|
+
const versionLine = version ? `\nversion: "${version}"` : "";
|
|
1293
|
+
const content = typeof mapping.content === "string"
|
|
1294
|
+
? `${replaceVariablesInString(mapping.content.trim(), variables, `openapi:${mapping.dest}`, true)}\n`
|
|
1295
|
+
: "";
|
|
1296
|
+
writeFileSync(
|
|
1297
1297
|
destPath,
|
|
1298
1298
|
`---\ntitle: "${title}"${descriptionLine}${deprecatedLine}${statusLine}${versionLine}\nopenapi: "${openapi}"\n---\n${warning}${content}`,
|
|
1299
1299
|
"utf-8",
|
|
@@ -1370,11 +1370,22 @@ function build(docsDir: string, outDir: string) {
|
|
|
1370
1370
|
console.log(`📄 Generated ${totalPages} pages + ${totalMeta} navigation meta files`);
|
|
1371
1371
|
|
|
1372
1372
|
// ── 5. Generate theme CSS (dynamic — depends on user config) ─────────────
|
|
1373
|
+
// Resolve fonts config into { heading?, body? } shape
|
|
1374
|
+
const resolvedFonts = (() => {
|
|
1375
|
+
const raw = config.fonts;
|
|
1376
|
+
if (!raw) return undefined;
|
|
1377
|
+
if ('family' in raw && typeof (raw as Record<string, unknown>).family === 'string') {
|
|
1378
|
+
return { heading: raw as { family: string; weight?: number; source?: string; format?: "woff" | "woff2" }, body: raw as { family: string; weight?: number; source?: string; format?: "woff" | "woff2" } };
|
|
1379
|
+
}
|
|
1380
|
+
const obj = raw as { heading?: { family: string; weight?: number; source?: string; format?: "woff" | "woff2" }; body?: { family: string; weight?: number; source?: string; format?: "woff" | "woff2" } };
|
|
1381
|
+
return (obj.heading || obj.body) ? obj : undefined;
|
|
1382
|
+
})();
|
|
1383
|
+
|
|
1373
1384
|
const themeCss = generateThemeCss({
|
|
1374
1385
|
theme: config.theme,
|
|
1375
1386
|
colors: config.colors,
|
|
1376
1387
|
appearance: config.appearance,
|
|
1377
|
-
|
|
1388
|
+
fonts: resolvedFonts,
|
|
1378
1389
|
});
|
|
1379
1390
|
writeFileSync(join(outDir, "app", "velu-theme.css"), themeCss, "utf-8");
|
|
1380
1391
|
console.log(`🎨 Generated theme: ${resolveThemeName(config.theme)}`);
|
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolve, join, dirname, delimiter } from "node:path";
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync, readdirSync, copyFileSync, cpSync, rmSync, renameSync, readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync, copyFileSync, cpSync, rmSync, renameSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
@@ -237,6 +237,19 @@ function samePath(a: string, b: string): boolean {
|
|
|
237
237
|
return resolve(a).replace(/\\/g, "/").toLowerCase() === resolve(b).replace(/\\/g, "/").toLowerCase();
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
function copyDirMerge(src: string, dest: string): void {
|
|
241
|
+
mkdirSync(dest, { recursive: true });
|
|
242
|
+
for (const entry of readdirSync(src)) {
|
|
243
|
+
const srcPath = join(src, entry);
|
|
244
|
+
const destPath = join(dest, entry);
|
|
245
|
+
if (statSync(srcPath).isDirectory()) {
|
|
246
|
+
copyDirMerge(srcPath, destPath);
|
|
247
|
+
} else {
|
|
248
|
+
copyFileSync(srcPath, destPath);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
240
253
|
function prepareRuntimeOutDir(docsOutDir: string): string {
|
|
241
254
|
const runtimeOutDir = join(PACKAGE_ROOT, ".velu-out");
|
|
242
255
|
if (samePath(docsOutDir, runtimeOutDir)) return runtimeOutDir;
|
|
@@ -249,13 +262,24 @@ function prepareRuntimeOutDir(docsOutDir: string): string {
|
|
|
249
262
|
const stale = `${runtimeOutDir}-stale-${Date.now()}`;
|
|
250
263
|
try {
|
|
251
264
|
renameSync(runtimeOutDir, stale);
|
|
252
|
-
// Best-effort async cleanup — ignore errors if still locked.
|
|
253
265
|
try { rmSync(stale, { recursive: true, force: true }); } catch {}
|
|
254
266
|
} catch {
|
|
255
|
-
// If even rename fails,
|
|
267
|
+
// If even rename fails, clear contents so cpSync can work.
|
|
268
|
+
try {
|
|
269
|
+
for (const entry of readdirSync(runtimeOutDir)) {
|
|
270
|
+
rmSync(join(runtimeOutDir, entry), { recursive: true, force: true });
|
|
271
|
+
}
|
|
272
|
+
} catch {}
|
|
256
273
|
}
|
|
257
274
|
}
|
|
258
|
-
|
|
275
|
+
|
|
276
|
+
if (existsSync(runtimeOutDir)) {
|
|
277
|
+
// cpSync fails with EEXIST when the destination directory exists (Node 20).
|
|
278
|
+
// Fall back to a manual recursive copy that handles existing directories.
|
|
279
|
+
copyDirMerge(docsOutDir, runtimeOutDir);
|
|
280
|
+
} else {
|
|
281
|
+
cpSync(docsOutDir, runtimeOutDir, { recursive: true, force: true });
|
|
282
|
+
}
|
|
259
283
|
return runtimeOutDir;
|
|
260
284
|
}
|
|
261
285
|
|
|
@@ -987,14 +987,14 @@ nextjs-portal {
|
|
|
987
987
|
display: grid !important;
|
|
988
988
|
grid-template-columns: 2.25rem minmax(0, 1fr);
|
|
989
989
|
column-gap: 0.75rem;
|
|
990
|
-
align-items:
|
|
990
|
+
align-items: center !important;
|
|
991
991
|
}
|
|
992
992
|
|
|
993
993
|
[data-card].velu-card-horizontal > div.not-prose {
|
|
994
994
|
grid-row: 1 / span 3;
|
|
995
995
|
margin-bottom: 0;
|
|
996
|
-
margin-top: 0
|
|
997
|
-
align-self:
|
|
996
|
+
margin-top: 0;
|
|
997
|
+
align-self: center;
|
|
998
998
|
justify-self: start;
|
|
999
999
|
}
|
|
1000
1000
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Metadata } from 'next';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
|
-
import { getAppearance, getBannerConfig, getSeoConfig, getSiteDescription, getSiteFavicon, getSiteName, getSiteOrigin, getSitePrimaryColor } from '@/lib/velu';
|
|
3
|
+
import { getAppearance, getBannerConfig, getFontsConfig, getSeoConfig, getSiteDescription, getSiteFavicon, getSiteName, getSiteOrigin, getSitePrimaryColor } from '@/lib/velu';
|
|
4
4
|
import { Providers } from '@/components/providers';
|
|
5
5
|
import { VeluAssistant } from '@/components/assistant';
|
|
6
6
|
import { VeluBanner } from '@/components/banner';
|
|
@@ -24,6 +24,7 @@ const seo = getSeoConfig();
|
|
|
24
24
|
const favicon = getSiteFavicon();
|
|
25
25
|
const primaryColor = getSitePrimaryColor();
|
|
26
26
|
const bannerConfig = getBannerConfig();
|
|
27
|
+
const fontsConfig = getFontsConfig();
|
|
27
28
|
const generatedDefaultSocialImage = '/og/index.svg';
|
|
28
29
|
const defaultSocialImage = seo.metatags['og:image'] ?? seo.metatags['twitter:image'] ?? generatedDefaultSocialImage;
|
|
29
30
|
const absoluteDefaultSocialImage = defaultSocialImage ? toAbsoluteUrl(siteOrigin, defaultSocialImage) : undefined;
|
|
@@ -79,6 +80,19 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
|
|
79
80
|
<html lang="en" suppressHydrationWarning>
|
|
80
81
|
<head>
|
|
81
82
|
<link rel="sitemap" type="application/xml" href={`${siteOrigin}/sitemap.xml`} />
|
|
83
|
+
{fontsConfig && (() => {
|
|
84
|
+
// Collect Google Fonts families that don't use a custom source
|
|
85
|
+
const families = new Set<string>();
|
|
86
|
+
for (const def of [fontsConfig.heading, fontsConfig.body]) {
|
|
87
|
+
if (def && !def.source) {
|
|
88
|
+
const weight = def.weight ? `:wght@${def.weight}` : ':wght@400;500;600;700';
|
|
89
|
+
families.add(`${def.family.replace(/ /g, '+')}${weight}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (families.size === 0) return null;
|
|
93
|
+
const url = `https://fonts.googleapis.com/css2?${[...families].map(f => `family=${f}`).join('&')}&display=swap`;
|
|
94
|
+
return <link rel="stylesheet" href={url} />;
|
|
95
|
+
})()}
|
|
82
96
|
</head>
|
|
83
97
|
<body className="min-h-screen" suppressHydrationWarning>
|
|
84
98
|
<Providers theme={theme}>
|
package/src/engine/lib/velu.ts
CHANGED
|
@@ -173,6 +173,18 @@ interface VeluContextualCustomOption {
|
|
|
173
173
|
href: string;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
interface VeluFontDef {
|
|
177
|
+
family: string;
|
|
178
|
+
weight?: number;
|
|
179
|
+
source?: string;
|
|
180
|
+
format?: 'woff' | 'woff2';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface VeluResolvedFonts {
|
|
184
|
+
heading?: VeluFontDef;
|
|
185
|
+
body?: VeluFontDef;
|
|
186
|
+
}
|
|
187
|
+
|
|
176
188
|
interface VeluConfig {
|
|
177
189
|
name?: string;
|
|
178
190
|
description?: string;
|
|
@@ -188,6 +200,7 @@ interface VeluConfig {
|
|
|
188
200
|
library?: string;
|
|
189
201
|
};
|
|
190
202
|
appearance?: 'system' | 'light' | 'dark';
|
|
203
|
+
fonts?: VeluFontDef | { heading?: VeluFontDef; body?: VeluFontDef };
|
|
191
204
|
languages?: string[];
|
|
192
205
|
openapi?: string | string[] | Record<string, unknown>;
|
|
193
206
|
asyncapi?: string | string[] | Record<string, unknown>;
|
|
@@ -691,6 +704,19 @@ export function getAppearance(): 'system' | 'light' | 'dark' {
|
|
|
691
704
|
|
|
692
705
|
export type VeluIconLibrary = 'fontawesome' | 'lucide' | 'tabler';
|
|
693
706
|
|
|
707
|
+
export function getFontsConfig(): VeluResolvedFonts | null {
|
|
708
|
+
const raw = loadVeluConfig().fonts;
|
|
709
|
+
if (!raw) return null;
|
|
710
|
+
// Single font definition (has 'family') → apply to both heading and body
|
|
711
|
+
if ('family' in raw && typeof raw.family === 'string') {
|
|
712
|
+
return { heading: raw as VeluFontDef, body: raw as VeluFontDef };
|
|
713
|
+
}
|
|
714
|
+
// Separate heading/body
|
|
715
|
+
const obj = raw as { heading?: VeluFontDef; body?: VeluFontDef };
|
|
716
|
+
if (!obj.heading && !obj.body) return null;
|
|
717
|
+
return { heading: obj.heading, body: obj.body };
|
|
718
|
+
}
|
|
719
|
+
|
|
694
720
|
export function getIconLibrary(): VeluIconLibrary {
|
|
695
721
|
const raw = loadVeluConfig().icons?.library;
|
|
696
722
|
if (raw === 'lucide' || raw === 'tabler' || raw === 'fontawesome') return raw;
|
|
@@ -152,9 +152,7 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents {
|
|
|
152
152
|
...props
|
|
153
153
|
}: any) => {
|
|
154
154
|
const hasImage = Boolean(img || image);
|
|
155
|
-
const
|
|
156
|
-
// Mintlify-like default: icon cards are horizontal unless explicitly disabled.
|
|
157
|
-
const useHorizontal = typeof horizontal === 'boolean' ? horizontal : (hasIcon && !hasImage);
|
|
155
|
+
const useHorizontal = horizontal === true;
|
|
158
156
|
|
|
159
157
|
return (
|
|
160
158
|
<Card
|
|
@@ -65,7 +65,6 @@ export interface VeluConfig {
|
|
|
65
65
|
theme?: string;
|
|
66
66
|
colors?: { primary?: string; light?: string; dark?: string };
|
|
67
67
|
appearance?: 'system' | 'light' | 'dark';
|
|
68
|
-
styling?: { codeblocks?: { theme?: string | { light: string; dark: string } } };
|
|
69
68
|
navigation: {
|
|
70
69
|
tabs?: VeluTab[];
|
|
71
70
|
anchors?: VeluAnchor[];
|
package/src/themes.ts
CHANGED
|
@@ -6,17 +6,23 @@ interface VeluColors {
|
|
|
6
6
|
dark?: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
interface
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
interface VeluFontDef {
|
|
10
|
+
family: string;
|
|
11
|
+
weight?: number;
|
|
12
|
+
source?: string;
|
|
13
|
+
format?: "woff" | "woff2";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface VeluFontsConfig {
|
|
17
|
+
heading?: VeluFontDef;
|
|
18
|
+
body?: VeluFontDef;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
interface ThemeConfig {
|
|
16
22
|
theme?: string;
|
|
17
23
|
colors?: VeluColors;
|
|
18
24
|
appearance?: "system" | "light" | "dark";
|
|
19
|
-
|
|
25
|
+
fonts?: VeluFontsConfig;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
const FUMADOCS_THEMES = [
|
|
@@ -160,6 +166,46 @@ function generateThemeCss(config: ThemeConfig): string {
|
|
|
160
166
|
}
|
|
161
167
|
lines.push("");
|
|
162
168
|
|
|
169
|
+
// Font configuration
|
|
170
|
+
if (config.fonts) {
|
|
171
|
+
const { heading, body } = config.fonts;
|
|
172
|
+
// @font-face declarations for custom sources
|
|
173
|
+
for (const def of [heading, body].filter(Boolean) as VeluFontDef[]) {
|
|
174
|
+
if (def.source) {
|
|
175
|
+
const fmt = def.format || (def.source.endsWith(".woff2") ? "woff2" : "woff");
|
|
176
|
+
lines.push(`@font-face {`);
|
|
177
|
+
lines.push(` font-family: '${def.family}';`);
|
|
178
|
+
lines.push(` src: url('${def.source}') format('${fmt}');`);
|
|
179
|
+
if (def.weight) lines.push(` font-weight: ${def.weight};`);
|
|
180
|
+
lines.push(` font-display: swap;`);
|
|
181
|
+
lines.push(`}`);
|
|
182
|
+
lines.push("");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// CSS variable overrides
|
|
186
|
+
const vars: string[] = [];
|
|
187
|
+
if (body) {
|
|
188
|
+
vars.push(` --font-fd-sans: '${body.family}', ui-sans-serif, system-ui, sans-serif;`);
|
|
189
|
+
}
|
|
190
|
+
if (heading) {
|
|
191
|
+
vars.push(` --velu-font-heading: '${heading.family}', ui-sans-serif, system-ui, sans-serif;`);
|
|
192
|
+
}
|
|
193
|
+
if (vars.length) {
|
|
194
|
+
lines.push(":root {");
|
|
195
|
+
lines.push(...vars);
|
|
196
|
+
lines.push("}");
|
|
197
|
+
lines.push("");
|
|
198
|
+
}
|
|
199
|
+
// Heading font-family rule
|
|
200
|
+
if (heading) {
|
|
201
|
+
lines.push("h1, h2, h3, h4, h5, h6 {");
|
|
202
|
+
lines.push(` font-family: var(--velu-font-heading);`);
|
|
203
|
+
if (heading.weight) lines.push(` font-weight: ${heading.weight};`);
|
|
204
|
+
lines.push("}");
|
|
205
|
+
lines.push("");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
163
209
|
return lines.join("\n");
|
|
164
210
|
}
|
|
165
211
|
|
|
@@ -169,4 +215,4 @@ function getThemeNames(): string[] {
|
|
|
169
215
|
|
|
170
216
|
const THEMES = [...FUMADOCS_THEMES];
|
|
171
217
|
|
|
172
|
-
export { generateThemeCss, getThemeNames, resolveThemeName, THEMES, ThemeConfig, VeluColors
|
|
218
|
+
export { generateThemeCss, getThemeNames, resolveThemeName, THEMES, ThemeConfig, VeluColors };
|
package/src/validate.ts
CHANGED
|
@@ -113,6 +113,13 @@ interface VeluVersionNav {
|
|
|
113
113
|
|
|
114
114
|
type VeluOpenApiSource = string | string[] | Record<string, unknown>;
|
|
115
115
|
|
|
116
|
+
interface VeluFontDefinition {
|
|
117
|
+
family: string;
|
|
118
|
+
weight?: number;
|
|
119
|
+
source?: string;
|
|
120
|
+
format?: "woff" | "woff2";
|
|
121
|
+
}
|
|
122
|
+
|
|
116
123
|
interface VeluConfig {
|
|
117
124
|
$schema?: string;
|
|
118
125
|
variables?: Record<string, string>;
|
|
@@ -123,7 +130,8 @@ interface VeluConfig {
|
|
|
123
130
|
theme?: string;
|
|
124
131
|
colors?: { primary?: string; light?: string; dark?: string };
|
|
125
132
|
appearance?: "system" | "light" | "dark";
|
|
126
|
-
|
|
133
|
+
|
|
134
|
+
fonts?: VeluFontDefinition | { heading?: VeluFontDefinition; body?: VeluFontDefinition };
|
|
127
135
|
openapi?: VeluOpenApiSource;
|
|
128
136
|
asyncapi?: VeluOpenApiSource;
|
|
129
137
|
api?: {
|