@aravindc26/velu 0.10.0 → 0.11.0
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 +714 -16
- package/src/build.ts +207 -43
- package/src/cli.ts +65 -2
- package/src/engine/_server.mjs +127 -18
- package/src/engine/app/(docs)/[[...slug]]/layout.tsx +87 -0
- package/src/engine/app/(docs)/[[...slug]]/page.tsx +83 -6
- package/src/engine/app/(docs)/layout.tsx +1 -13
- package/src/engine/app/global.css +327 -0
- package/src/engine/app/layout.tsx +3 -7
- package/src/engine/app/search.css +20 -0
- package/src/engine/components/lang-switcher.tsx +95 -0
- package/src/engine/components/product-switcher.tsx +78 -0
- package/src/engine/components/providers.tsx +26 -0
- package/src/engine/components/search.tsx +66 -3
- package/src/engine/components/sidebar-links.tsx +51 -0
- package/src/engine/components/theme-toggle.tsx +39 -0
- package/src/engine/components/version-switcher.tsx +89 -0
- package/src/engine/lib/layout.shared.ts +28 -6
- package/src/engine/lib/navigation-normalize.mjs +456 -0
- package/src/engine/lib/navigation-normalize.ts +488 -0
- package/src/engine/lib/source.ts +14 -0
- package/src/engine/lib/velu.ts +267 -3
- package/src/engine/next.config.mjs +2 -2
- package/src/engine/src/lib/velu.ts +86 -13
- package/src/navigation-normalize.ts +488 -0
- package/src/validate.ts +116 -18
package/src/build.ts
CHANGED
|
@@ -2,20 +2,60 @@ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, cpSync, existsSyn
|
|
|
2
2
|
import { join, dirname } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { generateThemeCss, resolveThemeName, type VeluColors, type VeluStyling } from "./themes.js";
|
|
5
|
+
import { normalizeConfigNavigation } from "./navigation-normalize.js";
|
|
5
6
|
|
|
6
7
|
// ── Engine directory (shipped with the CLI package) ──────────────────────────
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
9
|
const __dirname = dirname(__filename);
|
|
9
|
-
const
|
|
10
|
+
const PACKAGED_ENGINE_DIR = join(__dirname, "engine");
|
|
11
|
+
const DEV_ENGINE_DIR = join(__dirname, "..", "src", "engine");
|
|
12
|
+
const ENGINE_DIR = existsSync(DEV_ENGINE_DIR) ? DEV_ENGINE_DIR : PACKAGED_ENGINE_DIR;
|
|
10
13
|
|
|
11
14
|
// ── Types (used only by build.ts for page copying) ─────────────────────────────
|
|
12
15
|
|
|
16
|
+
interface VeluSeparator {
|
|
17
|
+
separator: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface VeluLink {
|
|
21
|
+
href: string;
|
|
22
|
+
label: string;
|
|
23
|
+
icon?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface VeluAnchor {
|
|
27
|
+
anchor: string;
|
|
28
|
+
href?: string;
|
|
29
|
+
icon?: string;
|
|
30
|
+
color?: {
|
|
31
|
+
light: string;
|
|
32
|
+
dark: string;
|
|
33
|
+
};
|
|
34
|
+
tabs?: VeluTab[];
|
|
35
|
+
hidden?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface VeluGlobalTab {
|
|
39
|
+
tab: string;
|
|
40
|
+
href: string;
|
|
41
|
+
icon?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
13
44
|
interface VeluGroup {
|
|
14
45
|
group: string;
|
|
15
46
|
slug: string;
|
|
16
47
|
icon?: string;
|
|
17
48
|
expanded?: boolean;
|
|
18
|
-
|
|
49
|
+
description?: string;
|
|
50
|
+
hidden?: boolean;
|
|
51
|
+
pages: (string | VeluGroup | VeluSeparator | VeluLink)[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface VeluMenuItem {
|
|
55
|
+
item: string;
|
|
56
|
+
icon?: string;
|
|
57
|
+
groups?: VeluGroup[];
|
|
58
|
+
pages?: (string | VeluSeparator | VeluLink)[];
|
|
19
59
|
}
|
|
20
60
|
|
|
21
61
|
interface VeluTab {
|
|
@@ -23,8 +63,26 @@ interface VeluTab {
|
|
|
23
63
|
slug: string;
|
|
24
64
|
icon?: string;
|
|
25
65
|
href?: string;
|
|
26
|
-
pages?: string[];
|
|
66
|
+
pages?: (string | VeluSeparator | VeluLink)[];
|
|
27
67
|
groups?: VeluGroup[];
|
|
68
|
+
menu?: VeluMenuItem[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface VeluLanguageNav {
|
|
72
|
+
language: string;
|
|
73
|
+
tabs: VeluTab[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface VeluProductNav {
|
|
77
|
+
product: string;
|
|
78
|
+
icon?: string;
|
|
79
|
+
tabs?: VeluTab[];
|
|
80
|
+
pages?: (string | VeluSeparator | VeluLink)[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface VeluVersionNav {
|
|
84
|
+
version: string;
|
|
85
|
+
tabs: VeluTab[];
|
|
28
86
|
}
|
|
29
87
|
|
|
30
88
|
interface VeluConfig {
|
|
@@ -33,16 +91,38 @@ interface VeluConfig {
|
|
|
33
91
|
colors?: VeluColors;
|
|
34
92
|
appearance?: "system" | "light" | "dark";
|
|
35
93
|
styling?: VeluStyling;
|
|
94
|
+
languages?: string[];
|
|
36
95
|
navigation: {
|
|
37
|
-
tabs
|
|
96
|
+
tabs?: VeluTab[];
|
|
97
|
+
languages?: VeluLanguageNav[];
|
|
98
|
+
products?: VeluProductNav[];
|
|
99
|
+
versions?: VeluVersionNav[];
|
|
100
|
+
anchors?: VeluAnchor[];
|
|
101
|
+
global?: {
|
|
102
|
+
anchors?: VeluAnchor[];
|
|
103
|
+
tabs?: VeluGlobalTab[];
|
|
104
|
+
};
|
|
38
105
|
};
|
|
39
106
|
}
|
|
40
107
|
|
|
108
|
+
function isSeparator(item: unknown): item is VeluSeparator {
|
|
109
|
+
return typeof item === "object" && item !== null && "separator" in item;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isLink(item: unknown): item is VeluLink {
|
|
113
|
+
return typeof item === "object" && item !== null && "href" in item && "label" in item;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isGroup(item: unknown): item is VeluGroup {
|
|
117
|
+
return typeof item === "object" && item !== null && "group" in item;
|
|
118
|
+
}
|
|
119
|
+
|
|
41
120
|
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
42
121
|
|
|
43
122
|
function loadConfig(docsDir: string): VeluConfig {
|
|
44
123
|
const raw = readFileSync(join(docsDir, "velu.json"), "utf-8");
|
|
45
|
-
|
|
124
|
+
const parsed = JSON.parse(raw) as VeluConfig;
|
|
125
|
+
return normalizeConfigNavigation(parsed);
|
|
46
126
|
}
|
|
47
127
|
|
|
48
128
|
function pageLabelFromSlug(slug: string): string {
|
|
@@ -73,7 +153,7 @@ interface BuildArtifacts {
|
|
|
73
153
|
function buildArtifacts(config: VeluConfig): BuildArtifacts {
|
|
74
154
|
const pageMap: PageMapping[] = [];
|
|
75
155
|
const metaFiles: MetaFile[] = [];
|
|
76
|
-
const rootTabs = config.navigation.tabs.filter((tab) => !tab.href);
|
|
156
|
+
const rootTabs = (config.navigation.tabs || []).filter((tab) => !tab.href);
|
|
77
157
|
const rootPages = rootTabs.map((tab) => tab.slug);
|
|
78
158
|
let firstPage = "quickstart";
|
|
79
159
|
let hasFirstPage = false;
|
|
@@ -85,6 +165,17 @@ function buildArtifacts(config: VeluConfig): BuildArtifacts {
|
|
|
85
165
|
}
|
|
86
166
|
}
|
|
87
167
|
|
|
168
|
+
function metaEntry(item: string | VeluSeparator | VeluLink): string {
|
|
169
|
+
if (typeof item === "string") return item;
|
|
170
|
+
if (isSeparator(item)) return `---${item.separator}---`;
|
|
171
|
+
if (isLink(item)) {
|
|
172
|
+
return item.icon
|
|
173
|
+
? `[${item.icon}][${item.label}](${item.href})`
|
|
174
|
+
: `[${item.label}](${item.href})`;
|
|
175
|
+
}
|
|
176
|
+
return String(item);
|
|
177
|
+
}
|
|
178
|
+
|
|
88
179
|
function addGroup(group: VeluGroup, parentDir: string) {
|
|
89
180
|
const groupDir = `${parentDir}/${group.slug}`;
|
|
90
181
|
const pages: string[] = [];
|
|
@@ -96,9 +187,17 @@ function buildArtifacts(config: VeluConfig): BuildArtifacts {
|
|
|
96
187
|
pageMap.push({ src: item, dest });
|
|
97
188
|
pages.push(basename);
|
|
98
189
|
trackFirstPage(dest);
|
|
99
|
-
} else {
|
|
190
|
+
} else if (isGroup(item)) {
|
|
100
191
|
addGroup(item, groupDir);
|
|
101
|
-
pages.push(item.slug);
|
|
192
|
+
pages.push(item.hidden ? `!${item.slug}` : item.slug);
|
|
193
|
+
} else if (isSeparator(item)) {
|
|
194
|
+
pages.push(`---${item.separator}---`);
|
|
195
|
+
} else if (isLink(item)) {
|
|
196
|
+
pages.push(
|
|
197
|
+
item.icon
|
|
198
|
+
? `[${item.icon}][${item.label}](${item.href})`
|
|
199
|
+
: `[${item.label}](${item.href})`
|
|
200
|
+
);
|
|
102
201
|
}
|
|
103
202
|
}
|
|
104
203
|
|
|
@@ -109,6 +208,7 @@ function buildArtifacts(config: VeluConfig): BuildArtifacts {
|
|
|
109
208
|
};
|
|
110
209
|
|
|
111
210
|
if (group.icon) groupMeta.icon = group.icon;
|
|
211
|
+
if (group.description) groupMeta.description = group.description;
|
|
112
212
|
|
|
113
213
|
metaFiles.push({ dir: groupDir, data: groupMeta });
|
|
114
214
|
}
|
|
@@ -119,17 +219,21 @@ function buildArtifacts(config: VeluConfig): BuildArtifacts {
|
|
|
119
219
|
if (tab.groups) {
|
|
120
220
|
for (const group of tab.groups) {
|
|
121
221
|
addGroup(group, tab.slug);
|
|
122
|
-
tabPages.push(group.slug);
|
|
222
|
+
tabPages.push(group.hidden ? `!${group.slug}` : group.slug);
|
|
123
223
|
}
|
|
124
224
|
}
|
|
125
225
|
|
|
126
226
|
if (tab.pages) {
|
|
127
|
-
for (const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
227
|
+
for (const item of tab.pages) {
|
|
228
|
+
if (typeof item === "string") {
|
|
229
|
+
const basename = pageBasename(item);
|
|
230
|
+
const dest = `${tab.slug}/${basename}`;
|
|
231
|
+
pageMap.push({ src: item, dest });
|
|
232
|
+
tabPages.push(basename);
|
|
233
|
+
trackFirstPage(dest);
|
|
234
|
+
} else {
|
|
235
|
+
tabPages.push(metaEntry(item));
|
|
236
|
+
}
|
|
133
237
|
}
|
|
134
238
|
}
|
|
135
239
|
|
|
@@ -176,40 +280,106 @@ function build(docsDir: string, outDir: string) {
|
|
|
176
280
|
console.log("📋 Copied velu.json");
|
|
177
281
|
|
|
178
282
|
// ── 4. Build content + metadata artifacts ────────────────────────────────
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
for (const meta of metaFiles) {
|
|
183
|
-
const metaPath = join(outDir, "content", "docs", meta.dir, "meta.json");
|
|
184
|
-
mkdirSync(dirname(metaPath), { recursive: true });
|
|
185
|
-
writeFileSync(metaPath, JSON.stringify(meta.data, null, 2) + "\n", "utf-8");
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// 4b) Copy all referenced .md files (slug-based destinations)
|
|
189
|
-
for (const { src, dest } of pageMap) {
|
|
190
|
-
const srcPath = join(docsDir, `${src}.md`);
|
|
191
|
-
const destPath = join(outDir, "content", "docs", `${dest}.mdx`);
|
|
192
|
-
|
|
193
|
-
if (!existsSync(srcPath)) {
|
|
194
|
-
console.warn(`⚠️ Missing: ${srcPath}`);
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
283
|
+
const contentDir = join(outDir, "content", "docs");
|
|
284
|
+
const navLanguages = config.navigation.languages;
|
|
285
|
+
const simpleLanguages = config.languages || [];
|
|
197
286
|
|
|
287
|
+
function processPage(srcPath: string, destPath: string, slug: string) {
|
|
198
288
|
mkdirSync(dirname(destPath), { recursive: true });
|
|
199
|
-
|
|
200
289
|
let content = readFileSync(srcPath, "utf-8");
|
|
201
290
|
if (!content.startsWith("---")) {
|
|
202
291
|
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
203
|
-
const title = titleMatch ? titleMatch[1] : pageLabelFromSlug(
|
|
292
|
+
const title = titleMatch ? titleMatch[1] : pageLabelFromSlug(slug);
|
|
204
293
|
if (titleMatch) {
|
|
205
294
|
content = content.replace(/^#\s+.+$/m, "").trimStart();
|
|
206
295
|
}
|
|
207
296
|
content = `---\ntitle: "${title}"\n---\n\n${content}`;
|
|
208
297
|
}
|
|
209
|
-
|
|
210
298
|
writeFileSync(destPath, content, "utf-8");
|
|
211
299
|
}
|
|
212
|
-
|
|
300
|
+
|
|
301
|
+
function writeLangContent(
|
|
302
|
+
langCode: string,
|
|
303
|
+
artifacts: BuildArtifacts,
|
|
304
|
+
isDefault: boolean,
|
|
305
|
+
useLangFolders = false
|
|
306
|
+
) {
|
|
307
|
+
const storagePrefix = useLangFolders ? langCode : (isDefault ? "" : langCode);
|
|
308
|
+
const urlPrefix = isDefault ? "" : langCode;
|
|
309
|
+
|
|
310
|
+
// Write meta files
|
|
311
|
+
const metas = storagePrefix
|
|
312
|
+
? artifacts.metaFiles.map((m) => ({ dir: m.dir ? `${storagePrefix}/${m.dir}` : storagePrefix, data: { ...m.data } }))
|
|
313
|
+
: artifacts.metaFiles;
|
|
314
|
+
for (const meta of metas) {
|
|
315
|
+
const metaPath = join(contentDir, meta.dir, "meta.json");
|
|
316
|
+
mkdirSync(dirname(metaPath), { recursive: true });
|
|
317
|
+
writeFileSync(metaPath, JSON.stringify(meta.data, null, 2) + "\n", "utf-8");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Copy pages using explicit source paths from velu.json
|
|
321
|
+
for (const { src, dest } of artifacts.pageMap) {
|
|
322
|
+
const srcPath = join(docsDir, `${src}.md`);
|
|
323
|
+
if (!existsSync(srcPath)) {
|
|
324
|
+
console.warn(`⚠️ Missing page source: ${src}.md (language: ${langCode})`);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const destPath = join(contentDir, storagePrefix ? `${storagePrefix}/${dest}.mdx` : `${dest}.mdx`);
|
|
328
|
+
processPage(srcPath, destPath, src);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Index page
|
|
332
|
+
const href = urlPrefix ? `/${urlPrefix}/${artifacts.firstPage}/` : `/${artifacts.firstPage}/`;
|
|
333
|
+
const indexPath = storagePrefix ? join(contentDir, storagePrefix, "index.mdx") : join(contentDir, "index.mdx");
|
|
334
|
+
writeFileSync(
|
|
335
|
+
indexPath,
|
|
336
|
+
`---\ntitle: "Overview"\ndescription: Documentation powered by Velu\n---\n\nimport { Card, Cards } from "fumadocs-ui/components/card"\nimport { Callout } from "fumadocs-ui/components/callout"\n\n<Callout type="info">\n Welcome to your documentation site.\n</Callout>\n\n## Start here\n\n<Cards>\n <Card\n title="Read the docs"\n href="${href}"\n description="Begin with the first page in your configured navigation."\n />\n</Cards>\n`,
|
|
337
|
+
"utf-8"
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let totalPages = 0;
|
|
342
|
+
let totalMeta = 0;
|
|
343
|
+
|
|
344
|
+
if (navLanguages && navLanguages.length > 0) {
|
|
345
|
+
// ── Mode 1: Per-language navigation (Mintlify-style) ──────────────
|
|
346
|
+
const rootPages: string[] = [];
|
|
347
|
+
|
|
348
|
+
for (let i = 0; i < navLanguages.length; i++) {
|
|
349
|
+
const langEntry = navLanguages[i];
|
|
350
|
+
const isDefault = i === 0;
|
|
351
|
+
const langConfig = { ...config, navigation: { ...config.navigation, tabs: langEntry.tabs } } as VeluConfig;
|
|
352
|
+
const artifacts = buildArtifacts(langConfig);
|
|
353
|
+
writeLangContent(langEntry.language, artifacts, isDefault, true);
|
|
354
|
+
totalPages += artifacts.pageMap.length;
|
|
355
|
+
totalMeta += artifacts.metaFiles.length;
|
|
356
|
+
rootPages.push(`!${langEntry.language}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const rootMetaPath = join(contentDir, "meta.json");
|
|
360
|
+
writeFileSync(rootMetaPath, JSON.stringify({ pages: rootPages }, null, 2) + "\n", "utf-8");
|
|
361
|
+
} else {
|
|
362
|
+
// ── Mode 2: Simple (single-lang or same-nav multi-lang) ───────────
|
|
363
|
+
const artifacts = buildArtifacts(config);
|
|
364
|
+
const useLangFolders = simpleLanguages.length > 1;
|
|
365
|
+
writeLangContent(simpleLanguages[0] || "en", artifacts, true, useLangFolders);
|
|
366
|
+
totalPages += artifacts.pageMap.length;
|
|
367
|
+
totalMeta += artifacts.metaFiles.length;
|
|
368
|
+
|
|
369
|
+
if (simpleLanguages.length > 1) {
|
|
370
|
+
const rootMetaPath = join(contentDir, "meta.json");
|
|
371
|
+
const rootPages = [`!${simpleLanguages[0] || "en"}`];
|
|
372
|
+
for (const lang of simpleLanguages.slice(1)) {
|
|
373
|
+
writeLangContent(lang, artifacts, false, true);
|
|
374
|
+
rootPages.push(`!${lang}`);
|
|
375
|
+
totalPages += artifacts.pageMap.length;
|
|
376
|
+
totalMeta += artifacts.metaFiles.length;
|
|
377
|
+
}
|
|
378
|
+
writeFileSync(rootMetaPath, JSON.stringify({ pages: rootPages }, null, 2) + "\n", "utf-8");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
console.log(`📄 Generated ${totalPages} pages + ${totalMeta} navigation meta files`);
|
|
213
383
|
|
|
214
384
|
// ── 5. Generate theme CSS (dynamic — depends on user config) ─────────────
|
|
215
385
|
const themeCss = generateThemeCss({
|
|
@@ -221,12 +391,6 @@ function build(docsDir: string, outDir: string) {
|
|
|
221
391
|
writeFileSync(join(outDir, "app", "velu-theme.css"), themeCss, "utf-8");
|
|
222
392
|
console.log(`🎨 Generated theme: ${resolveThemeName(config.theme)}`);
|
|
223
393
|
|
|
224
|
-
// ── 6. Generate index.mdx (dynamic — references first page) ──────────────
|
|
225
|
-
writeFileSync(
|
|
226
|
-
join(outDir, "content", "docs", "index.mdx"),
|
|
227
|
-
`---\ntitle: "Overview"\ndescription: Documentation powered by Velu\n---\n\nimport { Card, Cards } from "fumadocs-ui/components/card"\nimport { Callout } from "fumadocs-ui/components/callout"\n\n<Callout type="info">\n Welcome to your documentation site.\n</Callout>\n\n## Start here\n\n<Cards>\n <Card\n title="Read the docs"\n href="/${firstPage}/"\n description="Begin with the first page in your configured navigation."\n />\n</Cards>\n`,
|
|
228
|
-
"utf-8"
|
|
229
|
-
);
|
|
230
394
|
|
|
231
395
|
// ── 7. Generate minimal package.json (type: module, no local deps) ───────
|
|
232
396
|
const sitePkg = {
|
package/src/cli.ts
CHANGED
|
@@ -29,12 +29,13 @@ function printHelp() {
|
|
|
29
29
|
velu lint Validate velu.json and check referenced pages
|
|
30
30
|
velu run [--port N] Build site and start dev server (default: 4321)
|
|
31
31
|
velu build Build a deployable static site (SSG)
|
|
32
|
+
velu paths Output all navigation paths and their source files as JSON
|
|
32
33
|
|
|
33
34
|
Options:
|
|
34
35
|
--port <number> Port for the dev server (default: 4321)
|
|
35
36
|
--help Show this help message
|
|
36
37
|
|
|
37
|
-
Run lint/run/build from a directory containing velu.json.
|
|
38
|
+
Run lint/run/build/paths from a directory containing velu.json.
|
|
38
39
|
`);
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -55,11 +56,17 @@ function init(targetDir: string) {
|
|
|
55
56
|
{
|
|
56
57
|
tab: "Getting Started",
|
|
57
58
|
slug: "getting-started",
|
|
58
|
-
pages: [
|
|
59
|
+
pages: [
|
|
60
|
+
"quickstart",
|
|
61
|
+
"installation",
|
|
62
|
+
{ separator: "Resources" },
|
|
63
|
+
{ label: "Velu Website", href: "https://getvelu.com" },
|
|
64
|
+
],
|
|
59
65
|
groups: [
|
|
60
66
|
{
|
|
61
67
|
group: "Guides",
|
|
62
68
|
slug: "guides",
|
|
69
|
+
description: "Step-by-step guides to configure and deploy your docs.",
|
|
63
70
|
pages: ["guides/configuration", "guides/deployment"],
|
|
64
71
|
},
|
|
65
72
|
],
|
|
@@ -70,6 +77,13 @@ function init(targetDir: string) {
|
|
|
70
77
|
pages: ["api-reference/overview", "api-reference/authentication"],
|
|
71
78
|
},
|
|
72
79
|
],
|
|
80
|
+
anchors: [
|
|
81
|
+
{
|
|
82
|
+
anchor: "GitHub",
|
|
83
|
+
href: "https://github.com/aravindc26/velu",
|
|
84
|
+
icon: "Github",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
73
87
|
},
|
|
74
88
|
};
|
|
75
89
|
writeFileSync(join(targetDir, "velu.json"), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
@@ -122,6 +136,51 @@ async function lint(docsDir: string) {
|
|
|
122
136
|
}
|
|
123
137
|
}
|
|
124
138
|
|
|
139
|
+
// ── paths ───────────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
interface PathEntry {
|
|
142
|
+
path: string;
|
|
143
|
+
file: string | null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function paths(docsDir: string) {
|
|
147
|
+
const { collectPages } = await import("./validate.js");
|
|
148
|
+
const { normalizeConfigNavigation } = await import("./navigation-normalize.js");
|
|
149
|
+
const { readFileSync, existsSync } = await import("node:fs");
|
|
150
|
+
const { join } = await import("node:path");
|
|
151
|
+
|
|
152
|
+
const configPath = join(docsDir, "velu.json");
|
|
153
|
+
if (!existsSync(configPath)) {
|
|
154
|
+
console.error("❌ velu.json not found.");
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
159
|
+
const config = normalizeConfigNavigation(raw);
|
|
160
|
+
const pages = collectPages(config);
|
|
161
|
+
|
|
162
|
+
const entries: PathEntry[] = pages.map((pagePath) => {
|
|
163
|
+
// Check for .mdx first, then .md
|
|
164
|
+
const mdxPath = join(docsDir, `${pagePath}.mdx`);
|
|
165
|
+
const mdPath = join(docsDir, `${pagePath}.md`);
|
|
166
|
+
|
|
167
|
+
if (existsSync(mdxPath)) {
|
|
168
|
+
return { path: pagePath, file: `${pagePath}.mdx` };
|
|
169
|
+
}
|
|
170
|
+
if (existsSync(mdPath)) {
|
|
171
|
+
return { path: pagePath, file: `${pagePath}.md` };
|
|
172
|
+
}
|
|
173
|
+
return { path: pagePath, file: null };
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const output = {
|
|
177
|
+
paths: entries,
|
|
178
|
+
count: entries.length,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
console.log(JSON.stringify(output, null, 2));
|
|
182
|
+
}
|
|
183
|
+
|
|
125
184
|
// ── build ────────────────────────────────────────────────────────────────────────
|
|
126
185
|
|
|
127
186
|
async function generateProject(docsDir: string): Promise<string> {
|
|
@@ -201,6 +260,10 @@ switch (command) {
|
|
|
201
260
|
await lint(docsDir);
|
|
202
261
|
break;
|
|
203
262
|
|
|
263
|
+
case "paths":
|
|
264
|
+
await paths(docsDir);
|
|
265
|
+
break;
|
|
266
|
+
|
|
204
267
|
case "build":
|
|
205
268
|
await buildSite(docsDir);
|
|
206
269
|
break;
|
package/src/engine/_server.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { createRequire } from 'node:module';
|
|
|
3
3
|
import { watch } from 'node:fs';
|
|
4
4
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { dirname, extname, join, resolve } from 'node:path';
|
|
6
|
+
import { normalizeConfigNavigation } from './lib/navigation-normalize.mjs';
|
|
6
7
|
|
|
7
8
|
const require = createRequire(import.meta.url);
|
|
8
9
|
const nextBinPath = require.resolve('next/dist/bin/next');
|
|
@@ -13,7 +14,7 @@ const contentDir = resolve('content', 'docs');
|
|
|
13
14
|
|
|
14
15
|
function loadConfig() {
|
|
15
16
|
const raw = readFileSync(join(docsDir, 'velu.json'), 'utf-8');
|
|
16
|
-
return JSON.parse(raw);
|
|
17
|
+
return normalizeConfigNavigation(JSON.parse(raw));
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function pageBasename(page) {
|
|
@@ -25,6 +26,29 @@ function pageLabelFromSlug(slug) {
|
|
|
25
26
|
return last.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
function isSeparator(item) {
|
|
30
|
+
return typeof item === 'object' && item !== null && 'separator' in item;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isLink(item) {
|
|
34
|
+
return typeof item === 'object' && item !== null && 'href' in item && 'label' in item;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isGroup(item) {
|
|
38
|
+
return typeof item === 'object' && item !== null && 'group' in item;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function metaEntry(item) {
|
|
42
|
+
if (typeof item === 'string') return item;
|
|
43
|
+
if (isSeparator(item)) return `---${item.separator}---`;
|
|
44
|
+
if (isLink(item)) {
|
|
45
|
+
return item.icon
|
|
46
|
+
? `[${item.icon}][${item.label}](${item.href})`
|
|
47
|
+
: `[${item.label}](${item.href})`;
|
|
48
|
+
}
|
|
49
|
+
return String(item);
|
|
50
|
+
}
|
|
51
|
+
|
|
28
52
|
function buildArtifacts(config) {
|
|
29
53
|
const pageMap = [];
|
|
30
54
|
const metaFiles = [];
|
|
@@ -51,9 +75,17 @@ function buildArtifacts(config) {
|
|
|
51
75
|
pageMap.push({ src: item, dest });
|
|
52
76
|
pages.push(basename);
|
|
53
77
|
trackFirstPage(dest);
|
|
54
|
-
} else {
|
|
78
|
+
} else if (isGroup(item)) {
|
|
55
79
|
addGroup(item, groupDir);
|
|
56
|
-
pages.push(item.slug);
|
|
80
|
+
pages.push(item.hidden ? `!${item.slug}` : item.slug);
|
|
81
|
+
} else if (isSeparator(item)) {
|
|
82
|
+
pages.push(`---${item.separator}---`);
|
|
83
|
+
} else if (isLink(item)) {
|
|
84
|
+
pages.push(
|
|
85
|
+
item.icon
|
|
86
|
+
? `[${item.icon}][${item.label}](${item.href})`
|
|
87
|
+
: `[${item.label}](${item.href})`
|
|
88
|
+
);
|
|
57
89
|
}
|
|
58
90
|
}
|
|
59
91
|
|
|
@@ -64,6 +96,7 @@ function buildArtifacts(config) {
|
|
|
64
96
|
};
|
|
65
97
|
|
|
66
98
|
if (group.icon) groupMeta.icon = group.icon;
|
|
99
|
+
if (group.description) groupMeta.description = group.description;
|
|
67
100
|
|
|
68
101
|
metaFiles.push({ dir: groupDir, data: groupMeta });
|
|
69
102
|
}
|
|
@@ -73,15 +106,19 @@ function buildArtifacts(config) {
|
|
|
73
106
|
|
|
74
107
|
for (const group of tab.groups || []) {
|
|
75
108
|
addGroup(group, tab.slug);
|
|
76
|
-
tabPages.push(group.slug);
|
|
109
|
+
tabPages.push(group.hidden ? `!${group.slug}` : group.slug);
|
|
77
110
|
}
|
|
78
111
|
|
|
79
|
-
for (const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
112
|
+
for (const item of tab.pages || []) {
|
|
113
|
+
if (typeof item === 'string') {
|
|
114
|
+
const basename = pageBasename(item);
|
|
115
|
+
const dest = `${tab.slug}/${basename}`;
|
|
116
|
+
pageMap.push({ src: item, dest });
|
|
117
|
+
tabPages.push(basename);
|
|
118
|
+
trackFirstPage(dest);
|
|
119
|
+
} else {
|
|
120
|
+
tabPages.push(metaEntry(item));
|
|
121
|
+
}
|
|
85
122
|
}
|
|
86
123
|
|
|
87
124
|
const tabMeta = {
|
|
@@ -133,23 +170,95 @@ function writeIndexPage(firstPage) {
|
|
|
133
170
|
);
|
|
134
171
|
}
|
|
135
172
|
|
|
173
|
+
function writeLangContent(langCode, artifacts, isDefault, useLangFolders = false) {
|
|
174
|
+
const storagePrefix = useLangFolders ? langCode : (isDefault ? '' : langCode);
|
|
175
|
+
const urlPrefix = isDefault ? '' : langCode;
|
|
176
|
+
|
|
177
|
+
// Write meta files (prefixed for non-default)
|
|
178
|
+
const metaFiles = storagePrefix
|
|
179
|
+
? artifacts.metaFiles.map((meta) => ({
|
|
180
|
+
dir: meta.dir ? `${storagePrefix}/${meta.dir}` : storagePrefix,
|
|
181
|
+
data: { ...meta.data },
|
|
182
|
+
}))
|
|
183
|
+
: artifacts.metaFiles;
|
|
184
|
+
writeMetaFiles(metaFiles);
|
|
185
|
+
|
|
186
|
+
// Copy pages using explicit source paths from velu.json
|
|
187
|
+
for (const { src, dest } of artifacts.pageMap) {
|
|
188
|
+
const srcPath = join(docsDir, `${src}.md`);
|
|
189
|
+
if (!existsSync(srcPath)) {
|
|
190
|
+
console.warn(` \x1b[33m⚠\x1b[0m Missing page source: ${src}.md (language: ${langCode})`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const destPath = join(contentDir, storagePrefix ? `${storagePrefix}/${dest}.mdx` : `${dest}.mdx`);
|
|
194
|
+
processPage(srcPath, destPath, src);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Index page
|
|
198
|
+
const href = urlPrefix ? `/${urlPrefix}/${artifacts.firstPage}/` : `/${artifacts.firstPage}/`;
|
|
199
|
+
const indexPath = storagePrefix ? join(contentDir, storagePrefix, 'index.mdx') : join(contentDir, 'index.mdx');
|
|
200
|
+
writeFileSync(
|
|
201
|
+
indexPath,
|
|
202
|
+
`---\ntitle: "Overview"\ndescription: Documentation powered by Velu\n---\n\nimport { Card, Cards } from "fumadocs-ui/components/card"\nimport { Callout } from "fumadocs-ui/components/callout"\n\n<Callout type="info">\n Welcome to your documentation site.\n</Callout>\n\n## Start here\n\n<Cards>\n <Card\n title="Read the docs"\n href="${href}"\n description="Begin with the first page in your configured navigation."\n />\n</Cards>\n`,
|
|
203
|
+
'utf-8'
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
136
207
|
function rebuildFromConfig() {
|
|
137
208
|
const config = loadConfig();
|
|
138
|
-
const
|
|
209
|
+
const navLanguages = config.navigation?.languages;
|
|
210
|
+
const simpleLanguages = config.languages || [];
|
|
139
211
|
|
|
140
212
|
rmSync(contentDir, { recursive: true, force: true });
|
|
141
213
|
mkdirSync(contentDir, { recursive: true });
|
|
142
214
|
|
|
143
|
-
|
|
215
|
+
// ── Mode 1: Per-language navigation (Mintlify-style) ──────────────
|
|
216
|
+
if (navLanguages && navLanguages.length > 0) {
|
|
217
|
+
const rootPages = [];
|
|
144
218
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
219
|
+
for (let i = 0; i < navLanguages.length; i++) {
|
|
220
|
+
const langEntry = navLanguages[i];
|
|
221
|
+
const langCode = langEntry.language;
|
|
222
|
+
const isDefault = i === 0;
|
|
223
|
+
|
|
224
|
+
// Build artifacts using this language's own tabs
|
|
225
|
+
const langConfig = { ...config, navigation: { ...config.navigation, tabs: langEntry.tabs } };
|
|
226
|
+
const artifacts = buildArtifacts(langConfig);
|
|
227
|
+
|
|
228
|
+
writeLangContent(langCode, artifacts, isDefault, true);
|
|
229
|
+
rootPages.push(`!${langCode}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Write root meta with default tabs + hidden language folders
|
|
233
|
+
writeFileSync(
|
|
234
|
+
join(contentDir, 'meta.json'),
|
|
235
|
+
JSON.stringify({ pages: rootPages }, null, 2) + '\n',
|
|
236
|
+
'utf-8'
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// Return the default language's page map for file watching
|
|
240
|
+
const defaultConfig = { ...config, navigation: { ...config.navigation, tabs: navLanguages[0].tabs } };
|
|
241
|
+
return buildArtifacts(defaultConfig).pageMap;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── Mode 2: Simple multi-lang (same nav, content in docs/<lang>/) ─
|
|
245
|
+
const artifacts = buildArtifacts(config);
|
|
246
|
+
|
|
247
|
+
const useLangFolders = simpleLanguages.length > 1;
|
|
248
|
+
writeLangContent(simpleLanguages[0] || 'en', artifacts, true, useLangFolders);
|
|
249
|
+
|
|
250
|
+
if (simpleLanguages.length > 1) {
|
|
251
|
+
const rootMetaPath = join(contentDir, 'meta.json');
|
|
252
|
+
const rootPages = [`!${simpleLanguages[0] || 'en'}`];
|
|
253
|
+
|
|
254
|
+
for (const lang of simpleLanguages.slice(1)) {
|
|
255
|
+
writeLangContent(lang, artifacts, false, true);
|
|
256
|
+
rootPages.push(`!${lang}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
writeFileSync(rootMetaPath, JSON.stringify({ pages: rootPages }, null, 2) + '\n', 'utf-8');
|
|
150
260
|
}
|
|
151
261
|
|
|
152
|
-
writeIndexPage(artifacts.firstPage);
|
|
153
262
|
return artifacts.pageMap;
|
|
154
263
|
}
|
|
155
264
|
|