@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.
@@ -1,15 +1,98 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
+ import { normalizeConfigNavigation } from './navigation-normalize';
3
4
 
4
5
  interface VeluTab {
5
6
  tab: string;
7
+ slug?: string;
6
8
  href?: string;
9
+ pages?: Array<string | VeluSeparator | VeluLink>;
10
+ groups?: VeluGroup[];
11
+ }
12
+
13
+ interface VeluSeparator {
14
+ separator: string;
15
+ }
16
+
17
+ interface VeluLink {
18
+ href: string;
19
+ label: string;
20
+ }
21
+
22
+ interface VeluGroup {
23
+ group: string;
24
+ slug?: string;
25
+ pages: Array<string | VeluGroup | VeluSeparator | VeluLink>;
26
+ }
27
+
28
+ interface VeluAnchor {
29
+ anchor: string;
30
+ href?: string;
31
+ icon?: string;
32
+ color?: {
33
+ light: string;
34
+ dark: string;
35
+ };
36
+ tabs?: VeluTab[];
37
+ hidden?: boolean;
38
+ }
39
+
40
+ interface VeluGlobalTab {
41
+ tab: string;
42
+ href: string;
43
+ icon?: string;
44
+ }
45
+
46
+ interface VeluLanguageNav {
47
+ language: string;
48
+ tabs: VeluTab[];
49
+ }
50
+
51
+ interface VeluProductNav {
52
+ product: string;
53
+ description?: string;
54
+ icon?: string;
55
+ hidden?: boolean;
56
+ href?: string;
57
+ }
58
+
59
+ interface VeluVersionNav {
60
+ version: string;
61
+ default?: boolean;
62
+ hidden?: boolean;
63
+ href?: string;
64
+ }
65
+
66
+ export interface VeluProductOption {
67
+ product: string;
68
+ slug: string;
69
+ description?: string;
70
+ icon?: string;
71
+ tabSlugs: string[];
72
+ defaultPath: string;
73
+ }
74
+
75
+ export interface VeluVersionOption {
76
+ version: string;
77
+ slug: string;
78
+ isDefault: boolean;
79
+ tabSlugs: string[];
80
+ defaultPath: string;
7
81
  }
8
82
 
9
83
  interface VeluConfig {
10
84
  appearance?: 'system' | 'light' | 'dark';
85
+ languages?: string[];
11
86
  navigation: {
12
- tabs: VeluTab[];
87
+ tabs?: VeluTab[];
88
+ languages?: VeluLanguageNav[];
89
+ products?: VeluProductNav[];
90
+ versions?: VeluVersionNav[];
91
+ anchors?: VeluAnchor[];
92
+ global?: {
93
+ anchors?: VeluAnchor[];
94
+ tabs?: VeluGlobalTab[];
95
+ };
13
96
  };
14
97
  }
15
98
 
@@ -19,20 +102,201 @@ function loadVeluConfig(): VeluConfig {
19
102
  if (cachedConfig) return cachedConfig;
20
103
  const configPath = resolve(process.cwd(), 'velu.json');
21
104
  const raw = readFileSync(configPath, 'utf-8');
22
- cachedConfig = JSON.parse(raw) as VeluConfig;
105
+ cachedConfig = normalizeConfigNavigation(JSON.parse(raw)) as VeluConfig;
23
106
  return cachedConfig;
24
107
  }
25
108
 
109
+ function isGroup(item: unknown): item is VeluGroup {
110
+ return typeof item === 'object' && item !== null && 'group' in item;
111
+ }
112
+
113
+ function slugify(input: string, fallback: string): string {
114
+ const slug = input
115
+ .toLowerCase()
116
+ .trim()
117
+ .replace(/[^a-z0-9]+/g, '-')
118
+ .replace(/^-+|-+$/g, '');
119
+ return slug || fallback;
120
+ }
121
+
122
+ function pageBasename(page: string): string {
123
+ const parts = page.split('/').filter(Boolean);
124
+ return parts[parts.length - 1] ?? page;
125
+ }
126
+
127
+ function findFirstPageInGroup(group: VeluGroup): string | undefined {
128
+ for (const item of group.pages) {
129
+ if (typeof item === 'string') return item;
130
+ if (isGroup(item)) {
131
+ const nested = findFirstPageInGroup(item);
132
+ if (nested) return nested;
133
+ }
134
+ }
135
+ return undefined;
136
+ }
137
+
138
+ function findFirstPageInTab(tab: VeluTab): string | undefined {
139
+ if (tab.pages) {
140
+ for (const item of tab.pages) {
141
+ if (typeof item === 'string') return item;
142
+ }
143
+ }
144
+ if (tab.groups) {
145
+ for (const group of tab.groups) {
146
+ const nested = findFirstPageInGroup(group);
147
+ if (nested) return nested;
148
+ }
149
+ }
150
+ return undefined;
151
+ }
152
+
153
+ function parseVersionParts(version: string): number[] {
154
+ const parts = version.match(/\d+/g);
155
+ return parts ? parts.map((n) => Number(n)) : [];
156
+ }
157
+
158
+ function compareVersionParts(a: number[], b: number[]): number {
159
+ const len = Math.max(a.length, b.length);
160
+ for (let i = 0; i < len; i += 1) {
161
+ const av = a[i] ?? 0;
162
+ const bv = b[i] ?? 0;
163
+ if (av !== bv) return av - bv;
164
+ }
165
+ return 0;
166
+ }
167
+
26
168
  export function getExternalTabs(): Array<{ label: string; href: string }> {
27
169
  const config = loadVeluConfig();
28
170
  const tabs = config.navigation?.tabs ?? [];
171
+ const globalTabs = config.navigation?.global?.tabs ?? [];
29
172
 
30
- return tabs
173
+ const tabLinks = tabs
31
174
  .filter((tab): tab is VeluTab & { href: string } => typeof tab.href === 'string' && tab.href.length > 0)
32
175
  .map((tab) => ({
33
176
  label: tab.tab,
34
177
  href: tab.href,
35
178
  }));
179
+
180
+ const globalLinks = globalTabs
181
+ .filter((tab): tab is VeluGlobalTab => typeof tab.href === 'string' && tab.href.length > 0)
182
+ .map((tab) => ({
183
+ label: tab.tab,
184
+ href: tab.href,
185
+ }));
186
+
187
+ return [...tabLinks, ...globalLinks];
188
+ }
189
+
190
+ export function getNavbarAnchors(): VeluAnchor[] {
191
+ const config = loadVeluConfig();
192
+ return (config.navigation.anchors ?? []).filter(
193
+ (a): a is VeluAnchor & { href: string } => typeof a.href === 'string' && a.href.length > 0 && !a.hidden
194
+ );
195
+ }
196
+
197
+ export function getGlobalAnchors(): VeluAnchor[] {
198
+ const config = loadVeluConfig();
199
+ return (config.navigation.global?.anchors ?? []).filter(
200
+ (a): a is VeluAnchor & { href: string } => typeof a.href === 'string' && a.href.length > 0 && !a.hidden
201
+ );
202
+ }
203
+
204
+ export function getLanguages(): string[] {
205
+ const config = loadVeluConfig();
206
+ // Prefer navigation.languages codes, fall back to top-level languages
207
+ if (config.navigation.languages && config.navigation.languages.length > 0) {
208
+ return config.navigation.languages.map((l) => l.language);
209
+ }
210
+ return config.languages ?? [];
211
+ }
212
+
213
+ export function getProductOptions(): VeluProductOption[] {
214
+ const config = loadVeluConfig();
215
+ const products = (config.navigation.products ?? []).filter((p) => !p.hidden);
216
+ if (products.length === 0) return [];
217
+
218
+ const allTabs = config.navigation.tabs ?? [];
219
+
220
+ return products.map((product, index) => {
221
+ const prefix = slugify(product.product, `product-${index + 1}`);
222
+ const productTabs = allTabs.filter((tab) => {
223
+ const slug = tab.slug ?? '';
224
+ return slug === prefix || slug.startsWith(`${prefix}/`);
225
+ });
226
+
227
+ const tabSlugs = productTabs
228
+ .map((tab) => tab.slug)
229
+ .filter((slug): slug is string => typeof slug === 'string' && slug.length > 0);
230
+
231
+ const firstTab = productTabs[0];
232
+ const firstPage = firstTab ? findFirstPageInTab(firstTab) : undefined;
233
+ const defaultPath = firstTab
234
+ ? (firstPage ? `/${firstTab.slug}/${pageBasename(firstPage)}` : `/${firstTab.slug}`)
235
+ : (product.href ?? '/');
236
+
237
+ return {
238
+ product: product.product,
239
+ slug: prefix,
240
+ description: product.description,
241
+ icon: product.icon,
242
+ tabSlugs,
243
+ defaultPath,
244
+ };
245
+ });
246
+ }
247
+
248
+ export function getVersionOptions(): VeluVersionOption[] {
249
+ const config = loadVeluConfig();
250
+ const versions = (config.navigation.versions ?? []).filter((v) => !v.hidden);
251
+ if (versions.length === 0) return [];
252
+
253
+ const allTabs = config.navigation.tabs ?? [];
254
+
255
+ const baseEntries = versions.map((version, index) => {
256
+ const prefix = slugify(version.version, `version-${index + 1}`);
257
+ const versionTabs = allTabs.filter((tab) => {
258
+ const slug = tab.slug ?? '';
259
+ return slug === prefix || slug.startsWith(`${prefix}/`);
260
+ });
261
+
262
+ const tabSlugs = versionTabs
263
+ .map((tab) => tab.slug)
264
+ .filter((slug): slug is string => typeof slug === 'string' && slug.length > 0);
265
+
266
+ const firstTab = versionTabs[0];
267
+ const firstPage = firstTab ? findFirstPageInTab(firstTab) : undefined;
268
+ const defaultPath = firstTab
269
+ ? (firstPage ? `/${firstTab.slug}/${pageBasename(firstPage)}` : `/${firstTab.slug}`)
270
+ : (version.href ?? '/');
271
+
272
+ return {
273
+ version: version.version,
274
+ slug: prefix,
275
+ explicitDefault: version.default === true,
276
+ versionParts: parseVersionParts(version.version),
277
+ tabSlugs,
278
+ defaultPath,
279
+ order: index,
280
+ };
281
+ });
282
+
283
+ const explicitDefault = baseEntries.find((entry) => entry.explicitDefault);
284
+ const latest = explicitDefault
285
+ ?? baseEntries
286
+ .slice()
287
+ .sort((a, b) => {
288
+ const cmp = compareVersionParts(b.versionParts, a.versionParts);
289
+ if (cmp !== 0) return cmp;
290
+ return a.order - b.order;
291
+ })[0];
292
+
293
+ return baseEntries.map((entry) => ({
294
+ version: entry.version,
295
+ slug: entry.slug,
296
+ isDefault: entry.slug === latest?.slug,
297
+ tabSlugs: entry.tabSlugs,
298
+ defaultPath: entry.defaultPath,
299
+ }));
36
300
  }
37
301
 
38
302
  export function getAppearance(): 'system' | 'light' | 'dark' {
@@ -7,8 +7,8 @@ const withMDX = createMDX({
7
7
 
8
8
  /** @type {import('next').NextConfig} */
9
9
  const config = {
10
- reactStrictMode: true,
11
- output: 'export',
10
+ reactStrictMode: false,
11
+ output: process.env.NODE_ENV === 'production' ? 'export' : undefined,
12
12
  distDir: 'dist',
13
13
  devIndicators: false,
14
14
  turbopack: {
@@ -1,15 +1,46 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
+ import { normalizeConfigNavigation } from '../../lib/navigation-normalize';
3
4
 
4
5
  // ── Types ───────────────────────────────────────────────────────────────────
5
6
 
7
+ export interface VeluSeparator {
8
+ separator: string;
9
+ }
10
+
11
+ export interface VeluLink {
12
+ href: string;
13
+ label: string;
14
+ icon?: string;
15
+ }
16
+
17
+ export interface VeluAnchor {
18
+ anchor: string;
19
+ href?: string;
20
+ icon?: string;
21
+ color?: {
22
+ light: string;
23
+ dark: string;
24
+ };
25
+ tabs?: VeluTab[];
26
+ hidden?: boolean;
27
+ }
28
+
29
+ export interface VeluGlobalTab {
30
+ tab: string;
31
+ href: string;
32
+ icon?: string;
33
+ }
34
+
6
35
  export interface VeluGroup {
7
36
  group: string;
8
37
  slug: string;
9
38
  icon?: string;
10
39
  tag?: string;
11
40
  expanded?: boolean;
12
- pages: (string | VeluGroup)[];
41
+ description?: string;
42
+ hidden?: boolean;
43
+ pages: (string | VeluGroup | VeluSeparator | VeluLink)[];
13
44
  }
14
45
 
15
46
  export interface VeluTab {
@@ -17,7 +48,7 @@ export interface VeluTab {
17
48
  slug: string;
18
49
  icon?: string;
19
50
  href?: string;
20
- pages?: string[];
51
+ pages?: (string | VeluSeparator | VeluLink)[];
21
52
  groups?: VeluGroup[];
22
53
  }
23
54
 
@@ -28,7 +59,12 @@ export interface VeluConfig {
28
59
  appearance?: 'system' | 'light' | 'dark';
29
60
  styling?: { codeblocks?: { theme?: string | { light: string; dark: string } } };
30
61
  navigation: {
31
- tabs: VeluTab[];
62
+ tabs?: VeluTab[];
63
+ anchors?: VeluAnchor[];
64
+ global?: {
65
+ anchors?: VeluAnchor[];
66
+ tabs?: VeluGlobalTab[];
67
+ };
32
68
  };
33
69
  }
34
70
 
@@ -48,7 +84,7 @@ export function loadVeluConfig(): VeluConfig {
48
84
  if (_cachedConfig) return _cachedConfig;
49
85
  const configPath = resolve(process.cwd(), 'velu.json');
50
86
  const raw = readFileSync(configPath, 'utf-8');
51
- _cachedConfig = JSON.parse(raw);
87
+ _cachedConfig = normalizeConfigNavigation(JSON.parse(raw));
52
88
  return _cachedConfig!;
53
89
  }
54
90
 
@@ -64,7 +100,7 @@ function veluGroupToSidebar(group: VeluGroup, tabSlug: string): any {
64
100
  for (const item of group.pages) {
65
101
  if (typeof item === 'string') {
66
102
  items.push(tabSlug + '/' + group.slug + '/' + pageBasename(item));
67
- } else {
103
+ } else if (isGroup(item)) {
68
104
  items.push(veluGroupToSidebar(item, tabSlug));
69
105
  }
70
106
  }
@@ -74,10 +110,18 @@ function veluGroupToSidebar(group: VeluGroup, tabSlug: string): any {
74
110
  return result;
75
111
  }
76
112
 
113
+ function isGroup(item: unknown): item is VeluGroup {
114
+ return typeof item === 'object' && item !== null && 'group' in item;
115
+ }
116
+
77
117
  /** Get the first page dest path for a tab */
78
118
  function firstTabPage(tab: VeluTab): string | undefined {
79
- if (tab.pages && tab.pages.length > 0) {
80
- return tab.slug + '/' + pageBasename(tab.pages[0]);
119
+ if (tab.pages) {
120
+ for (const item of tab.pages) {
121
+ if (typeof item === 'string') {
122
+ return tab.slug + '/' + pageBasename(item);
123
+ }
124
+ }
81
125
  }
82
126
  if (tab.groups) {
83
127
  for (const g of tab.groups) {
@@ -91,8 +135,10 @@ function firstTabPage(tab: VeluTab): string | undefined {
91
135
  function firstGroupPage(group: VeluGroup, tabSlug: string): string | undefined {
92
136
  for (const item of group.pages) {
93
137
  if (typeof item === 'string') return tabSlug + '/' + group.slug + '/' + pageBasename(item);
94
- const nested = firstGroupPage(item, tabSlug);
95
- if (nested) return nested;
138
+ if (isGroup(item)) {
139
+ const nested = firstGroupPage(item, tabSlug);
140
+ if (nested) return nested;
141
+ }
96
142
  }
97
143
  return undefined;
98
144
  }
@@ -104,12 +150,14 @@ export function getSidebar(): any[] {
104
150
  const config = loadVeluConfig();
105
151
  const sidebar: any[] = [];
106
152
 
107
- for (const tab of config.navigation.tabs) {
153
+ for (const tab of config.navigation.tabs ?? []) {
108
154
  if (tab.href) continue;
109
155
  const items: any[] = [];
110
156
  if (tab.groups) for (const g of tab.groups) items.push(veluGroupToSidebar(g, tab.slug));
111
157
  if (tab.pages) {
112
- for (const p of tab.pages) items.push(tab.slug + '/' + pageBasename(p));
158
+ for (const p of tab.pages) {
159
+ if (typeof p === 'string') items.push(tab.slug + '/' + pageBasename(p));
160
+ }
113
161
  }
114
162
  sidebar.push({ label: tab.tab, items });
115
163
  }
@@ -122,7 +170,7 @@ export function getTabs(): TabMeta[] {
122
170
  const config = loadVeluConfig();
123
171
  const tabs: TabMeta[] = [];
124
172
 
125
- for (const tab of config.navigation.tabs) {
173
+ for (const tab of config.navigation.tabs ?? []) {
126
174
  if (tab.href) {
127
175
  tabs.push({ label: tab.tab, icon: tab.icon, href: tab.href, slugs: [] });
128
176
  } else {
@@ -144,10 +192,35 @@ export function getTabSidebarMap(): Record<string, string[]> {
144
192
  const config = loadVeluConfig();
145
193
  const map: Record<string, string[]> = {};
146
194
 
147
- for (const tab of config.navigation.tabs) {
195
+ for (const tab of config.navigation.tabs ?? []) {
148
196
  if (tab.href) continue;
149
197
  map[tab.slug] = [tab.tab];
150
198
  }
151
199
 
152
200
  return map;
153
201
  }
202
+
203
+ /** Get all anchors (navigation.anchors + navigation.global.anchors), excluding hidden ones */
204
+ export function getAnchors(): VeluAnchor[] {
205
+ const config = loadVeluConfig();
206
+ const anchors: VeluAnchor[] = [];
207
+ if (config.navigation.anchors) {
208
+ anchors.push(...config.navigation.anchors.filter((a) => typeof a.href === 'string' && a.href.length > 0 && !a.hidden));
209
+ }
210
+ if (config.navigation.global?.anchors) {
211
+ anchors.push(...config.navigation.global.anchors.filter((a) => typeof a.href === 'string' && a.href.length > 0 && !a.hidden));
212
+ }
213
+ return anchors;
214
+ }
215
+
216
+ /** Get external tab links for the navbar */
217
+ export function getExternalTabs(): { label: string; href: string; icon?: string }[] {
218
+ const config = loadVeluConfig();
219
+ const tabLinks = (config.navigation.tabs ?? [])
220
+ .filter((tab) => !!tab.href)
221
+ .map((tab) => ({ label: tab.tab, href: tab.href!, icon: tab.icon }));
222
+ const globalLinks = (config.navigation.global?.tabs ?? [])
223
+ .filter((tab) => !!tab.href)
224
+ .map((tab) => ({ label: tab.tab, href: tab.href, icon: tab.icon }));
225
+ return [...tabLinks, ...globalLinks];
226
+ }