@zpress/core 0.1.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.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.mjs +2820 -0
  3. package/package.json +43 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,2820 @@
1
+ import { loadConfig } from "c12";
2
+ import promises from "node:fs/promises";
3
+ import node_path from "node:path";
4
+ import { log } from "@clack/prompts";
5
+ import { P, match } from "ts-pattern";
6
+ import { capitalize, groupBy, isUndefined, omitBy, range, words } from "es-toolkit";
7
+ import { createHash } from "node:crypto";
8
+ import gray_matter from "gray-matter";
9
+ import node_fs, { existsSync } from "node:fs";
10
+ import fast_glob from "fast-glob";
11
+ function hasGlobChars(s) {
12
+ return /[*?{}[\]]/.test(s);
13
+ }
14
+ function syncError(type, message) {
15
+ return Object.freeze({
16
+ _tag: 'SyncError',
17
+ type,
18
+ message
19
+ });
20
+ }
21
+ function configError(type, message) {
22
+ return Object.freeze({
23
+ _tag: 'ConfigError',
24
+ type,
25
+ message
26
+ });
27
+ }
28
+ function collectResults(results) {
29
+ return results.reduce((acc, result)=>{
30
+ const [accErr] = acc;
31
+ if (null !== accErr) return acc;
32
+ const [err, val] = result;
33
+ if (null !== err) return [
34
+ err,
35
+ null
36
+ ];
37
+ const [, prevValues] = acc;
38
+ return [
39
+ null,
40
+ [
41
+ ...prevValues,
42
+ val
43
+ ]
44
+ ];
45
+ }, [
46
+ null,
47
+ []
48
+ ]);
49
+ }
50
+ function defineConfig(config) {
51
+ const [err] = validateConfig(config);
52
+ if (err) {
53
+ process.stderr.write(`[zpress] ${err.message}\n`);
54
+ process.exit(1);
55
+ }
56
+ return config;
57
+ }
58
+ function validateConfig(config) {
59
+ if (!config.sections || 0 === config.sections.length) return [
60
+ configError('empty_sections', 'config.sections must have at least one entry'),
61
+ null
62
+ ];
63
+ const [groupErr] = validateWorkspaceGroups(config.workspaces ?? []);
64
+ if (groupErr) return [
65
+ groupErr,
66
+ null
67
+ ];
68
+ const workspaceGroupItems = (config.workspaces ?? []).flatMap((g)=>g.items);
69
+ const [wsErr] = validateWorkspaceItems([
70
+ ...config.apps ?? [],
71
+ ...config.packages ?? [],
72
+ ...workspaceGroupItems
73
+ ]);
74
+ if (wsErr) return [
75
+ wsErr,
76
+ null
77
+ ];
78
+ const entryErrors = config.sections.reduce((acc, entry)=>{
79
+ if (acc) return acc;
80
+ const [entryErr] = validateEntry(entry);
81
+ if (entryErr) return entryErr;
82
+ return null;
83
+ }, null);
84
+ if (entryErrors) return [
85
+ entryErrors,
86
+ null
87
+ ];
88
+ const [featErr] = validateFeatures(config.features);
89
+ if (featErr) return [
90
+ featErr,
91
+ null
92
+ ];
93
+ const [navErr] = validateNav(config.nav);
94
+ if (navErr) return [
95
+ navErr,
96
+ null
97
+ ];
98
+ return [
99
+ null,
100
+ config
101
+ ];
102
+ }
103
+ function validateWorkspaceItems(items) {
104
+ const prefixError = items.reduce((acc, item)=>{
105
+ if (acc.error) return acc;
106
+ if (!item.text) return {
107
+ error: configError('missing_field', 'WorkspaceItem: "text" is required'),
108
+ seen: acc.seen
109
+ };
110
+ if (!item.description) return {
111
+ error: configError('missing_field', `WorkspaceItem "${item.text}": "description" is required`),
112
+ seen: acc.seen
113
+ };
114
+ if (!item.docsPrefix) return {
115
+ error: configError('missing_field', `WorkspaceItem "${item.text}": "docsPrefix" is required`),
116
+ seen: acc.seen
117
+ };
118
+ if (acc.seen.has(item.docsPrefix)) return {
119
+ error: configError('duplicate_prefix', `WorkspaceItem "${item.text}": duplicate docsPrefix "${item.docsPrefix}"`),
120
+ seen: acc.seen
121
+ };
122
+ if (item.icon && !item.icon.includes(':')) return {
123
+ error: configError('invalid_icon', `WorkspaceItem "${item.text}": icon must be an Iconify identifier (e.g. "devicon:hono")`),
124
+ seen: acc.seen
125
+ };
126
+ return {
127
+ error: null,
128
+ seen: new Set([
129
+ ...acc.seen,
130
+ item.docsPrefix
131
+ ])
132
+ };
133
+ }, {
134
+ error: null,
135
+ seen: new Set()
136
+ });
137
+ if (prefixError.error) return [
138
+ prefixError.error,
139
+ null
140
+ ];
141
+ return [
142
+ null,
143
+ true
144
+ ];
145
+ }
146
+ function validateWorkspaceGroups(groups) {
147
+ const groupError = groups.reduce((acc, group)=>{
148
+ if (acc) return acc;
149
+ if (!group.name) return configError('missing_field', 'WorkspaceGroup: "name" is required');
150
+ if (!group.description) return configError('missing_field', `WorkspaceGroup "${group.name}": "description" is required`);
151
+ if (!group.icon) return configError('missing_field', `WorkspaceGroup "${group.name}": "icon" is required`);
152
+ if (!group.items || 0 === group.items.length) return configError('missing_field', `WorkspaceGroup "${group.name}": "items" must be a non-empty array`);
153
+ return null;
154
+ }, null);
155
+ if (groupError) return [
156
+ groupError,
157
+ null
158
+ ];
159
+ return [
160
+ null,
161
+ true
162
+ ];
163
+ }
164
+ function validateEntry(entry) {
165
+ if (entry.from && entry.content) return [
166
+ configError('invalid_entry', `Entry "${entry.text}": 'from' and 'content' are mutually exclusive`),
167
+ null
168
+ ];
169
+ if (entry.link && !entry.from && !entry.content && !entry.items) return [
170
+ configError('invalid_entry', `Entry "${entry.text}": page with 'link' must have 'from', 'content', or 'items'`),
171
+ null
172
+ ];
173
+ if (entry.from && !hasGlobChars(entry.from) && !entry.items && !entry.link) return [
174
+ configError('invalid_entry', `Entry "${entry.text}": single-file 'from' requires 'link'`),
175
+ null
176
+ ];
177
+ if (entry.from && hasGlobChars(entry.from) && !entry.prefix) return [
178
+ configError('invalid_entry', `Entry "${entry.text}": glob 'from' requires 'prefix'`),
179
+ null
180
+ ];
181
+ if (entry.recursive && (!entry.from || !entry.from.includes('**'))) return [
182
+ configError('invalid_entry', `Entry "${entry.text}": 'recursive' requires a recursive glob pattern (e.g. "**/*.md")`),
183
+ null
184
+ ];
185
+ if (entry.recursive && !entry.prefix) return [
186
+ configError('invalid_entry', `Entry "${entry.text}": 'recursive' requires 'prefix'`),
187
+ null
188
+ ];
189
+ if (entry.items) {
190
+ const childErr = entry.items.reduce((acc, child)=>{
191
+ if (acc) return acc;
192
+ const [err] = validateEntry(child);
193
+ if (err) return err;
194
+ return null;
195
+ }, null);
196
+ if (childErr) return [
197
+ childErr,
198
+ null
199
+ ];
200
+ }
201
+ return [
202
+ null,
203
+ true
204
+ ];
205
+ }
206
+ function validateFeatures(features) {
207
+ if (void 0 === features) return [
208
+ null,
209
+ true
210
+ ];
211
+ const featureError = features.reduce((acc, feature)=>{
212
+ if (acc) return acc;
213
+ return validateFeature(feature);
214
+ }, null);
215
+ if (featureError) return [
216
+ featureError,
217
+ null
218
+ ];
219
+ return [
220
+ null,
221
+ true
222
+ ];
223
+ }
224
+ function validateFeature(feature) {
225
+ if (!feature.text) return configError('missing_field', 'Feature: "text" is required');
226
+ if (!feature.description) return configError('missing_field', `Feature "${feature.text}": "description" is required`);
227
+ if (feature.icon && !feature.icon.includes(':')) return configError('invalid_icon', `Feature "${feature.text}": icon must be an Iconify identifier (e.g. "pixelarticons:speed-fast")`);
228
+ return null;
229
+ }
230
+ function validateNav(nav) {
231
+ if ('auto' === nav || void 0 === nav) return [
232
+ null,
233
+ true
234
+ ];
235
+ const navError = nav.reduce((acc, item)=>{
236
+ if (acc) return acc;
237
+ return validateNavItem(item);
238
+ }, null);
239
+ if (navError) return [
240
+ navError,
241
+ null
242
+ ];
243
+ return [
244
+ null,
245
+ true
246
+ ];
247
+ }
248
+ function validateNavItem(item) {
249
+ if (!item.icon) return configError('missing_nav_icon', `NavItem "${item.text}": top-level nav items require an "icon" (Iconify identifier)`);
250
+ if (!item.icon.includes(':')) return configError('invalid_icon', `NavItem "${item.text}": icon must be an Iconify identifier (e.g. "pixelarticons:folder")`);
251
+ return null;
252
+ }
253
+ async function config_loadConfig(dir) {
254
+ const { config } = await loadConfig({
255
+ cwd: dir,
256
+ name: 'zpress',
257
+ rcFile: false,
258
+ packageJson: false,
259
+ globalRc: false,
260
+ dotenv: false
261
+ });
262
+ if (!config || !config.sections) return [
263
+ configError('empty_sections', 'Failed to load zpress.config — no sections found'),
264
+ null
265
+ ];
266
+ return [
267
+ null,
268
+ config
269
+ ];
270
+ }
271
+ const FIGLET_CHARS = Object.freeze({
272
+ A: [
273
+ ' █████╗ ',
274
+ '██╔══██╗',
275
+ '███████║',
276
+ '██╔══██║',
277
+ '██║ ██║',
278
+ '╚═╝ ╚═╝'
279
+ ],
280
+ B: [
281
+ '██████╗ ',
282
+ '██╔══██╗',
283
+ '██████╔╝',
284
+ '██╔══██╗',
285
+ '██████╔╝',
286
+ '╚═════╝ '
287
+ ],
288
+ C: [
289
+ ' ██████╗',
290
+ '██╔════╝',
291
+ '██║ ',
292
+ '██║ ',
293
+ '╚██████╗',
294
+ ' ╚═════╝'
295
+ ],
296
+ D: [
297
+ '██████╗ ',
298
+ '██╔══██╗',
299
+ '██║ ██║',
300
+ '██║ ██║',
301
+ '██████╔╝',
302
+ '╚═════╝ '
303
+ ],
304
+ E: [
305
+ '███████╗',
306
+ '██╔════╝',
307
+ '█████╗ ',
308
+ '██╔══╝ ',
309
+ '███████╗',
310
+ '╚══════╝'
311
+ ],
312
+ F: [
313
+ '███████╗',
314
+ '██╔════╝',
315
+ '█████╗ ',
316
+ '██╔══╝ ',
317
+ '██║ ',
318
+ '╚═╝ '
319
+ ],
320
+ G: [
321
+ ' ██████╗ ',
322
+ '██╔════╝ ',
323
+ '██║ ███╗',
324
+ '██║ ██║',
325
+ '╚██████╔╝',
326
+ ' ╚═════╝ '
327
+ ],
328
+ H: [
329
+ '██╗ ██╗',
330
+ '██║ ██║',
331
+ '███████║',
332
+ '██╔══██║',
333
+ '██║ ██║',
334
+ '╚═╝ ╚═╝'
335
+ ],
336
+ I: [
337
+ '██╗',
338
+ '██║',
339
+ '██║',
340
+ '██║',
341
+ '██║',
342
+ '╚═╝'
343
+ ],
344
+ J: [
345
+ ' ██╗',
346
+ ' ██║',
347
+ ' ██║',
348
+ '██ ██║',
349
+ '╚█████╔╝',
350
+ ' ╚════╝ '
351
+ ],
352
+ K: [
353
+ '██╗ ██╗',
354
+ '██║ ██╔╝',
355
+ '█████╔╝ ',
356
+ '██╔═██╗ ',
357
+ '██║ ██╗',
358
+ '╚═╝ ╚═╝'
359
+ ],
360
+ L: [
361
+ '██╗ ',
362
+ '██║ ',
363
+ '██║ ',
364
+ '██║ ',
365
+ '███████╗',
366
+ '╚══════╝'
367
+ ],
368
+ M: [
369
+ '███╗ ███╗',
370
+ '████╗ ████║',
371
+ '██╔████╔██║',
372
+ '██║╚██╔╝██║',
373
+ '██║ ╚═╝ ██║',
374
+ '╚═╝ ╚═╝'
375
+ ],
376
+ N: [
377
+ '███╗ ██╗',
378
+ '████╗ ██║',
379
+ '██╔██╗ ██║',
380
+ '██║╚██╗██║',
381
+ '██║ ╚████║',
382
+ '╚═╝ ╚═══╝'
383
+ ],
384
+ O: [
385
+ ' ██████╗ ',
386
+ '██╔═══██╗',
387
+ '██║ ██║',
388
+ '██║ ██║',
389
+ '╚██████╔╝',
390
+ ' ╚═════╝ '
391
+ ],
392
+ P: [
393
+ '██████╗ ',
394
+ '██╔══██╗',
395
+ '██████╔╝',
396
+ '██╔═══╝ ',
397
+ '██║ ',
398
+ '╚═╝ '
399
+ ],
400
+ Q: [
401
+ ' ██████╗ ',
402
+ '██╔═══██╗',
403
+ '██║ ██║',
404
+ '██║▄▄ ██║',
405
+ '╚██████╔╝',
406
+ ' ╚══▀▀═╝ '
407
+ ],
408
+ R: [
409
+ '██████╗ ',
410
+ '██╔══██╗',
411
+ '██████╔╝',
412
+ '██╔══██╗',
413
+ '██║ ██║',
414
+ '╚═╝ ╚═╝'
415
+ ],
416
+ S: [
417
+ '███████╗',
418
+ '██╔════╝',
419
+ '███████╗',
420
+ '╚════██║',
421
+ '███████║',
422
+ '╚══════╝'
423
+ ],
424
+ T: [
425
+ '████████╗',
426
+ '╚══██╔══╝',
427
+ ' ██║ ',
428
+ ' ██║ ',
429
+ ' ██║ ',
430
+ ' ╚═╝ '
431
+ ],
432
+ U: [
433
+ '██╗ ██╗',
434
+ '██║ ██║',
435
+ '██║ ██║',
436
+ '██║ ██║',
437
+ '╚██████╔╝',
438
+ ' ╚═════╝ '
439
+ ],
440
+ V: [
441
+ '██╗ ██╗',
442
+ '██║ ██║',
443
+ '██║ ██║',
444
+ '╚██╗ ██╔╝',
445
+ ' ╚████╔╝ ',
446
+ ' ╚═══╝ '
447
+ ],
448
+ W: [
449
+ '██╗ ██╗',
450
+ '██║ ██║',
451
+ '██║ █╗ ██║',
452
+ '██║███╗██║',
453
+ '╚███╔███╔╝',
454
+ ' ╚══╝╚══╝ '
455
+ ],
456
+ X: [
457
+ '██╗ ██╗',
458
+ '╚██╗██╔╝',
459
+ ' ╚███╔╝ ',
460
+ ' ██╔██╗ ',
461
+ '██╔╝ ██╗',
462
+ '╚═╝ ╚═╝'
463
+ ],
464
+ Y: [
465
+ '██╗ ██╗',
466
+ '╚██╗ ██╔╝',
467
+ ' ╚████╔╝ ',
468
+ ' ╚██╔╝ ',
469
+ ' ██║ ',
470
+ ' ╚═╝ '
471
+ ],
472
+ Z: [
473
+ '███████╗',
474
+ '╚══███╔╝',
475
+ ' ███╔╝ ',
476
+ ' ███╔╝ ',
477
+ '███████╗',
478
+ '╚══════╝'
479
+ ],
480
+ 0: [
481
+ ' ██████╗ ',
482
+ '██╔═████╗',
483
+ '██║██╔██║',
484
+ '████╔╝██║',
485
+ '╚██████╔╝',
486
+ ' ╚═════╝ '
487
+ ],
488
+ 1: [
489
+ ' ██╗',
490
+ '███║',
491
+ '╚██║',
492
+ ' ██║',
493
+ ' ██║',
494
+ ' ╚═╝'
495
+ ],
496
+ 2: [
497
+ '██████╗ ',
498
+ '╚════██╗',
499
+ ' █████╔╝',
500
+ '██╔═══╝ ',
501
+ '███████╗',
502
+ '╚══════╝'
503
+ ],
504
+ 3: [
505
+ '██████╗ ',
506
+ '╚════██╗',
507
+ ' █████╔╝',
508
+ ' ╚═══██╗',
509
+ '██████╔╝',
510
+ '╚═════╝ '
511
+ ],
512
+ 4: [
513
+ '██╗ ██╗',
514
+ '██║ ██║',
515
+ '███████║',
516
+ '╚════██║',
517
+ ' ██║',
518
+ ' ╚═╝'
519
+ ],
520
+ 5: [
521
+ '███████╗',
522
+ '██╔════╝',
523
+ '███████╗',
524
+ '╚════██║',
525
+ '███████║',
526
+ '╚══════╝'
527
+ ],
528
+ 6: [
529
+ ' ██████╗',
530
+ '██╔════╝',
531
+ '██████╗ ',
532
+ '██╔══██╗',
533
+ '╚█████╔╝',
534
+ ' ╚════╝ '
535
+ ],
536
+ 7: [
537
+ '███████╗',
538
+ '╚════██║',
539
+ ' ██╔╝',
540
+ ' ██╔╝ ',
541
+ ' ██║ ',
542
+ ' ╚═╝ '
543
+ ],
544
+ 8: [
545
+ ' █████╗ ',
546
+ '██╔══██╗',
547
+ '╚█████╔╝',
548
+ '██╔══██╗',
549
+ '╚█████╔╝',
550
+ ' ╚════╝ '
551
+ ],
552
+ 9: [
553
+ ' █████╗ ',
554
+ '██╔══██╗',
555
+ '╚██████║',
556
+ ' ╚═══██║',
557
+ ' █████╔╝',
558
+ ' ╚════╝ '
559
+ ],
560
+ ' ': [
561
+ ' ',
562
+ ' ',
563
+ ' ',
564
+ ' ',
565
+ ' ',
566
+ ' '
567
+ ],
568
+ '-': [
569
+ ' ',
570
+ ' ',
571
+ '███████╗',
572
+ '╚══════╝',
573
+ ' ',
574
+ ' '
575
+ ],
576
+ '.': [
577
+ ' ',
578
+ ' ',
579
+ ' ',
580
+ ' ',
581
+ '██╗',
582
+ '╚═╝'
583
+ ],
584
+ _: [
585
+ ' ',
586
+ ' ',
587
+ ' ',
588
+ ' ',
589
+ '███████╗',
590
+ '╚══════╝'
591
+ ]
592
+ });
593
+ const SPACE_GLYPH = FIGLET_CHARS[" "];
594
+ function lookupGlyph(c) {
595
+ const glyph = FIGLET_CHARS[c];
596
+ if (glyph) return glyph;
597
+ return SPACE_GLYPH;
598
+ }
599
+ function renderFigletText(text) {
600
+ const chars = [
601
+ ...text.toUpperCase()
602
+ ];
603
+ const glyphs = chars.map(lookupGlyph);
604
+ const lines = range(6).map((row)=>glyphs.map((glyph)=>glyph[row]).join(""));
605
+ const width = Math.max(...lines.map((line)=>line.length));
606
+ return {
607
+ lines,
608
+ width
609
+ };
610
+ }
611
+ const COLORS = Object.freeze({
612
+ base: '#1e1e2e',
613
+ mantle: '#181825',
614
+ surface0: '#313244',
615
+ overlay0: '#6c7086',
616
+ text: '#cdd6f4',
617
+ blue: '#89b4fa',
618
+ green: '#a6e3a1',
619
+ red: '#f38ba8',
620
+ yellow: '#f9e2af',
621
+ brand: '#a78bfa'
622
+ });
623
+ const FONT_STACK = "'SF Mono', 'Fira Code', 'JetBrains Mono', Consolas, monospace";
624
+ const GENERATED_MARKER = '<!-- zpress-generated -->';
625
+ function escapeXml(text) {
626
+ return text.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
627
+ }
628
+ const ART_TOP_PAD = 26;
629
+ const svg_banner_FIGLET_ROWS = 6;
630
+ const TAGLINE_GAP = 24;
631
+ const SEPARATOR_GAP = 16;
632
+ const CLI_SECTION_HEIGHT = 240;
633
+ function buildStyles() {
634
+ return [
635
+ ' <defs>',
636
+ ' <style>',
637
+ ` .text { font-family: ${FONT_STACK}; }`,
638
+ ` .code { font-family: ${FONT_STACK}; font-size: 12px; }`,
639
+ ` .brand { fill: ${COLORS.brand}; }`,
640
+ ` .dim { fill: ${COLORS.overlay0}; }`,
641
+ ` .tx { fill: ${COLORS.text}; }`,
642
+ ` .st { fill: ${COLORS.green}; }`,
643
+ ` .prompt { fill: ${COLORS.blue}; }`,
644
+ ` .tab { font-family: ${FONT_STACK}; font-size: 11px; fill: ${COLORS.text}; }`,
645
+ ' </style>',
646
+ ' </defs>'
647
+ ].join('\n');
648
+ }
649
+ function buildBackground(params) {
650
+ return [
651
+ '',
652
+ ' <!-- Background -->',
653
+ ` <rect width="${params.width}" height="${params.height}" rx="10" ry="10" fill="${COLORS.base}" />`
654
+ ].join('\n');
655
+ }
656
+ function buildTitleBar(params) {
657
+ const centerX = Math.round(params.width / 2);
658
+ const escaped = escapeXml(params.name);
659
+ return [
660
+ '',
661
+ ' <!-- Title bar -->',
662
+ ` <rect width="${params.width}" height="36" rx="10" ry="10" fill="${COLORS.mantle}" />`,
663
+ ` <rect y="26" width="${params.width}" height="10" fill="${COLORS.mantle}" />`,
664
+ '',
665
+ ' <!-- Traffic lights -->',
666
+ ` <circle cx="20" cy="18" r="6" fill="${COLORS.red}" />`,
667
+ ` <circle cx="40" cy="18" r="6" fill="${COLORS.yellow}" />`,
668
+ ` <circle cx="60" cy="18" r="6" fill="${COLORS.green}" />`,
669
+ '',
670
+ ' <!-- Title bar text -->',
671
+ ` <text class="text dim" font-size="12" x="${centerX}" y="22" text-anchor="middle">${escaped}</text>`
672
+ ].join('\n');
673
+ }
674
+ function buildFigletArt(params) {
675
+ const textLines = params.lines.map((line, i)=>{
676
+ const y = params.startY + 16 * i;
677
+ return ` <text class="text brand" font-size="13" y="${y}" xml:space="preserve">${line}</text>`;
678
+ }).join('\n');
679
+ return [
680
+ '',
681
+ ' <!-- ASCII art -->',
682
+ ` <g transform="translate(${params.translateX}, 0)">`,
683
+ textLines,
684
+ ' </g>'
685
+ ].join('\n');
686
+ }
687
+ function buildFallbackArt(params) {
688
+ const escaped = escapeXml(params.title);
689
+ return [
690
+ '',
691
+ ' <!-- Title (fallback) -->',
692
+ ` <text class="text brand" font-size="48" x="${params.centerX}" y="${params.y}" text-anchor="middle">${escaped}</text>`
693
+ ].join('\n');
694
+ }
695
+ function buildTagline(params) {
696
+ const escaped = escapeXml(params.text);
697
+ return [
698
+ '',
699
+ ' <!-- Tagline -->',
700
+ ` <text class="text dim" font-size="12" x="${params.centerX}" y="${params.y}" text-anchor="middle">${escaped}</text>`
701
+ ].join('\n');
702
+ }
703
+ function buildSeparator(params) {
704
+ return [
705
+ '',
706
+ ' <!-- Separator -->',
707
+ ` <line x1="16" y1="${params.y}" x2="${params.width - 16}" y2="${params.y}" stroke="${COLORS.surface0}" stroke-width="1" />`
708
+ ].join('\n');
709
+ }
710
+ function buildCliOutput(params) {
711
+ const baseY = params.separatorY;
712
+ const x = 18;
713
+ return [
714
+ '',
715
+ ' <!-- Terminal tab -->',
716
+ ` <rect x="4" y="${baseY + 4}" width="80" height="24" rx="4" ry="4" fill="${COLORS.mantle}" />`,
717
+ ` <text class="tab" x="${x}" y="${baseY + 20}">terminal</text>`,
718
+ '',
719
+ ' <!-- CLI output -->',
720
+ ` <text class="code" x="${x}" y="${baseY + 48}"><tspan class="prompt">~</tspan><tspan class="dim"> $ </tspan><tspan class="tx">${escapeXml(params.name)} dev</tspan></text>`,
721
+ '',
722
+ ` <text class="code" x="${x}" y="${baseY + 76}"><tspan class="dim">Starting </tspan><tspan class="brand">${escapeXml(params.name)}</tspan><tspan class="dim">...</tspan></text>`,
723
+ '',
724
+ ` <text class="code" x="${x}" y="${baseY + 100}" xml:space="preserve"><tspan class="st"> ✓</tspan><tspan class="tx"> Loaded config</tspan></text>`,
725
+ ` <text class="code" x="${x}" y="${baseY + 116}" xml:space="preserve"><tspan class="st"> ✓</tspan><tspan class="tx"> Built 24 pages</tspan></text>`,
726
+ ` <text class="code" x="${x}" y="${baseY + 132}" xml:space="preserve"><tspan class="st"> ✓</tspan><tspan class="tx"> Generated sidebar</tspan></text>`,
727
+ ` <text class="code" x="${x}" y="${baseY + 148}" xml:space="preserve"><tspan class="st"> ✓</tspan><tspan class="tx"> Ready — dev server on :5173</tspan></text>`,
728
+ '',
729
+ ' <!-- New prompt with cursor -->',
730
+ ` <text class="code" x="${x}" y="${baseY + 180}"><tspan class="prompt">~</tspan><tspan class="dim"> $ </tspan><tspan class="tx">&#x2588;</tspan></text>`
731
+ ].join('\n');
732
+ }
733
+ function computeArtLayout(params) {
734
+ const useFiglet = params.title.length <= 12;
735
+ if (useFiglet) {
736
+ const figlet = renderFigletText(params.title);
737
+ const artPixelWidth = 7.8 * figlet.width;
738
+ const contentWidth = Math.ceil(artPixelWidth + 48);
739
+ const width = Math.max(params.minWidth, contentWidth);
740
+ const artStartY = 36 + ART_TOP_PAD;
741
+ const translateX = Math.round((width - artPixelWidth) / 2);
742
+ const artEndY = artStartY + (svg_banner_FIGLET_ROWS - 1) * 16;
743
+ const artSection = buildFigletArt({
744
+ lines: figlet.lines,
745
+ translateX,
746
+ startY: artStartY
747
+ });
748
+ return {
749
+ width,
750
+ height: 0,
751
+ artSection,
752
+ artEndY
753
+ };
754
+ }
755
+ const textPixelWidth = 48 * params.title.length * 0.6;
756
+ const contentWidth = Math.ceil(textPixelWidth + 48);
757
+ const width = Math.max(params.minWidth, contentWidth);
758
+ const centerX = Math.round(width / 2);
759
+ const artCenterY = 36 + ART_TOP_PAD + 40;
760
+ const artEndY = artCenterY + 12;
761
+ const artSection = buildFallbackArt({
762
+ title: params.title,
763
+ centerX,
764
+ y: artCenterY
765
+ });
766
+ return {
767
+ width,
768
+ height: 0,
769
+ artSection,
770
+ artEndY
771
+ };
772
+ }
773
+ function composeBanner(params) {
774
+ const cmdName = params.title.toLowerCase().replaceAll(/\s+/g, '');
775
+ const art = computeArtLayout({
776
+ title: params.title,
777
+ minWidth: 600
778
+ });
779
+ const taglineSection = match(params.tagline).with(P.string, (tagline)=>{
780
+ const taglineY = art.artEndY + TAGLINE_GAP;
781
+ const separatorY = taglineY + SEPARATOR_GAP;
782
+ const centerX = Math.round(art.width / 2);
783
+ return {
784
+ markup: buildTagline({
785
+ text: tagline,
786
+ centerX,
787
+ y: taglineY
788
+ }),
789
+ separatorY
790
+ };
791
+ }).otherwise(()=>({
792
+ markup: '',
793
+ separatorY: art.artEndY + SEPARATOR_GAP
794
+ }));
795
+ const height = taglineSection.separatorY + CLI_SECTION_HEIGHT;
796
+ const sections = [
797
+ GENERATED_MARKER,
798
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${art.width} ${height}">`,
799
+ buildStyles(),
800
+ buildBackground({
801
+ width: art.width,
802
+ height
803
+ }),
804
+ buildTitleBar({
805
+ width: art.width,
806
+ name: cmdName
807
+ }),
808
+ art.artSection,
809
+ taglineSection.markup,
810
+ buildSeparator({
811
+ width: art.width,
812
+ y: taglineSection.separatorY
813
+ }),
814
+ buildCliOutput({
815
+ name: cmdName,
816
+ separatorY: taglineSection.separatorY
817
+ }),
818
+ '</svg>'
819
+ ];
820
+ return sections.filter((s)=>s.length > 0).join('\n');
821
+ }
822
+ const LOGO_TOP_PAD = 28;
823
+ const LOGO_BOTTOM_PAD = 28;
824
+ const svg_logo_FIGLET_ROWS = 6;
825
+ function svg_logo_buildFigletArt(params) {
826
+ return params.lines.map((line, i)=>{
827
+ const y = params.startY + 16 * i;
828
+ return ` <text class="text brand" font-size="13" y="${y}" xml:space="preserve">${line}</text>`;
829
+ }).join('\n');
830
+ }
831
+ function buildFallbackText(params) {
832
+ const escaped = escapeXml(params.title);
833
+ return ` <text class="text brand" font-size="48" y="${params.y}">${escaped}</text>`;
834
+ }
835
+ function composeLogo(params) {
836
+ const useFiglet = params.title.length <= 12;
837
+ if (useFiglet) {
838
+ const figlet = renderFigletText(params.title);
839
+ const artPixelWidth = 7.8 * figlet.width;
840
+ const width = Math.ceil(artPixelWidth + 48);
841
+ const height = LOGO_TOP_PAD + (svg_logo_FIGLET_ROWS - 1) * 16 + LOGO_BOTTOM_PAD;
842
+ const artLines = svg_logo_buildFigletArt({
843
+ lines: figlet.lines,
844
+ startY: 0
845
+ });
846
+ return [
847
+ GENERATED_MARKER,
848
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">`,
849
+ ' <defs>',
850
+ ' <style>',
851
+ ` .text { font-family: ${FONT_STACK}; }`,
852
+ ` .brand { fill: ${COLORS.brand}; }`,
853
+ ' </style>',
854
+ ' </defs>',
855
+ '',
856
+ ` <g transform="translate(24, ${LOGO_TOP_PAD})">`,
857
+ artLines,
858
+ ' </g>',
859
+ '</svg>'
860
+ ].join('\n');
861
+ }
862
+ const textPixelWidth = 48 * params.title.length * 0.6;
863
+ const width = Math.ceil(textPixelWidth + 48);
864
+ const height = 48 + LOGO_TOP_PAD + LOGO_BOTTOM_PAD;
865
+ const textY = LOGO_TOP_PAD + 36;
866
+ const fallback = buildFallbackText({
867
+ title: params.title,
868
+ y: textY
869
+ });
870
+ return [
871
+ GENERATED_MARKER,
872
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">`,
873
+ ' <defs>',
874
+ ' <style>',
875
+ ` .text { font-family: ${FONT_STACK}; }`,
876
+ ` .brand { fill: ${COLORS.brand}; }`,
877
+ ' </style>',
878
+ ' </defs>',
879
+ '',
880
+ ' <g transform="translate(24, 0)">',
881
+ fallback,
882
+ ' </g>',
883
+ '</svg>'
884
+ ].join('\n');
885
+ }
886
+ function assetError(type, message) {
887
+ return Object.freeze({
888
+ _tag: 'AssetError',
889
+ type,
890
+ message
891
+ });
892
+ }
893
+ function generateBannerSvg(config) {
894
+ if (0 === config.title.trim().length) return [
895
+ assetError('empty_title', 'Cannot generate banner: title is empty'),
896
+ null
897
+ ];
898
+ return [
899
+ null,
900
+ {
901
+ filename: 'banner.svg',
902
+ content: composeBanner({
903
+ title: config.title,
904
+ tagline: config.tagline
905
+ })
906
+ }
907
+ ];
908
+ }
909
+ function generateLogoSvg(config) {
910
+ if (0 === config.title.trim().length) return [
911
+ assetError('empty_title', 'Cannot generate logo: title is empty'),
912
+ null
913
+ ];
914
+ return [
915
+ null,
916
+ {
917
+ filename: 'logo.svg',
918
+ content: composeLogo({
919
+ title: config.title
920
+ })
921
+ }
922
+ ];
923
+ }
924
+ async function shouldGenerate(filePath) {
925
+ const content = await promises.readFile(filePath, 'utf8').catch(()=>null);
926
+ if (null === content) return true;
927
+ const [firstLine] = content.split('\n');
928
+ if (firstLine === GENERATED_MARKER) return true;
929
+ return false;
930
+ }
931
+ async function writeAsset(asset, publicDir) {
932
+ const filePath = node_path.resolve(publicDir, asset.filename);
933
+ const result = await promises.writeFile(filePath, asset.content, 'utf8').catch((error)=>error);
934
+ if (void 0 !== result) {
935
+ if (result instanceof Error) return [
936
+ assetError('write_failed', `Failed to write ${asset.filename}: ${result.message}`),
937
+ null
938
+ ];
939
+ return [
940
+ assetError('write_failed', `Failed to write ${asset.filename}: ${String(result)}`),
941
+ null
942
+ ];
943
+ }
944
+ return [
945
+ null,
946
+ asset.filename
947
+ ];
948
+ }
949
+ async function generateAssets(params) {
950
+ await promises.mkdir(params.publicDir, {
951
+ recursive: true
952
+ });
953
+ const generators = [
954
+ ()=>generateBannerSvg(params.config),
955
+ ()=>generateLogoSvg(params.config)
956
+ ];
957
+ const written = await generators.reduce(async (accPromise, generate)=>{
958
+ const acc = await accPromise;
959
+ const [err, asset] = generate();
960
+ if (err) return acc;
961
+ const filePath = node_path.resolve(params.publicDir, asset.filename);
962
+ const shouldWrite = await shouldGenerate(filePath);
963
+ if (!shouldWrite) return acc;
964
+ const [writeErr, filename] = await writeAsset(asset, params.publicDir);
965
+ if (writeErr) return acc;
966
+ return [
967
+ ...acc,
968
+ filename
969
+ ];
970
+ }, Promise.resolve([]));
971
+ return [
972
+ null,
973
+ written
974
+ ];
975
+ }
976
+ function buildSourceMap(params) {
977
+ return new Map(params.pages.flatMap((page)=>{
978
+ if (null === page.source || void 0 === page.source) return [];
979
+ const relSource = node_path.relative(params.repoRoot, page.source);
980
+ return [
981
+ [
982
+ relSource,
983
+ page.outputPath
984
+ ]
985
+ ];
986
+ }));
987
+ }
988
+ const PLACEHOLDER_PREFIX = '<!--ZPRESS_CODE_BLOCK_';
989
+ const PLACEHOLDER_SUFFIX = '-->';
990
+ const CODE_BLOCK_RE = /```[\s\S]*?```/g;
991
+ const LINK_RE = /(?<!!)\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
992
+ const PLACEHOLDER_RE = /<!--ZPRESS_CODE_BLOCK_(\d+)-->/g;
993
+ function rewriteLinks(params) {
994
+ const codeBlocks = [];
995
+ const withoutCode = params.content.replace(CODE_BLOCK_RE, (block)=>{
996
+ const idx = codeBlocks.length;
997
+ codeBlocks.push(block);
998
+ return `${PLACEHOLDER_PREFIX}${idx}${PLACEHOLDER_SUFFIX}`;
999
+ });
1000
+ const sourceDir = node_path.dirname(params.sourcePath);
1001
+ const outputDir = node_path.dirname(params.outputPath);
1002
+ const rewritten = withoutCode.replace(LINK_RE, (fullMatch, url)=>{
1003
+ const resolved = resolveLink({
1004
+ url,
1005
+ sourceDir,
1006
+ outputDir,
1007
+ sourceMap: params.sourceMap
1008
+ });
1009
+ if (null === resolved) return fullMatch;
1010
+ return fullMatch.replace(url, resolved);
1011
+ });
1012
+ return rewritten.replace(PLACEHOLDER_RE, (_, idx)=>codeBlocks[Number(idx)]);
1013
+ }
1014
+ function resolveLink(params) {
1015
+ if (isNonRewritableUrl(params.url)) return null;
1016
+ const { linkPath, fragment } = splitFragment(params.url);
1017
+ if (!linkPath.endsWith('.md') && !linkPath.endsWith('.mdx')) return null;
1018
+ const resolvedSource = node_path.normalize(node_path.join(params.sourceDir, linkPath));
1019
+ const targetOutput = params.sourceMap.get(resolvedSource);
1020
+ if (void 0 === targetOutput) return null;
1021
+ const relativePath = node_path.relative(params.outputDir, targetOutput);
1022
+ const normalized = relativePath.split(node_path.sep).join('/');
1023
+ return `${normalized}${fragment}`;
1024
+ }
1025
+ function isNonRewritableUrl(url) {
1026
+ return url.startsWith('/') || url.startsWith('#') || url.startsWith('http://') || url.startsWith('https://') || url.startsWith('mailto:');
1027
+ }
1028
+ function splitFragment(url) {
1029
+ const hashIdx = url.indexOf('#');
1030
+ return match(-1 !== hashIdx).with(true, ()=>({
1031
+ linkPath: url.slice(0, hashIdx),
1032
+ fragment: url.slice(hashIdx)
1033
+ })).otherwise(()=>({
1034
+ linkPath: url,
1035
+ fragment: ''
1036
+ }));
1037
+ }
1038
+ async function copyPage(page, ctx) {
1039
+ const outPath = node_path.resolve(ctx.outDir, page.outputPath);
1040
+ await promises.mkdir(node_path.dirname(outPath), {
1041
+ recursive: true
1042
+ });
1043
+ const content = await (async ()=>{
1044
+ if (page.source) {
1045
+ const raw = await promises.readFile(page.source, 'utf8');
1046
+ const rewritten = rewriteSourceLinks(raw, page, ctx);
1047
+ return injectFrontmatter(rewritten, page.frontmatter);
1048
+ }
1049
+ if (page.content) {
1050
+ const body = match(typeof page.content).with('function', async ()=>await page.content()).otherwise(()=>page.content);
1051
+ return injectFrontmatter(await body, page.frontmatter);
1052
+ }
1053
+ log.error(`[zpress] Page "${page.outputPath}" has neither source nor content`);
1054
+ return '';
1055
+ })();
1056
+ const contentHash = createHash('sha256').update(content).digest('hex');
1057
+ const relativeSource = (()=>{
1058
+ if (null !== page.source && void 0 !== page.source) return node_path.relative(ctx.repoRoot, page.source);
1059
+ })();
1060
+ const prev = (()=>{
1061
+ if (null !== ctx.previousManifest && void 0 !== ctx.previousManifest) return ctx.previousManifest.files[page.outputPath];
1062
+ })();
1063
+ async function resolveSourceMtime() {
1064
+ if (null !== page.source && void 0 !== page.source) {
1065
+ const stat = await promises.stat(page.source);
1066
+ return stat.mtimeMs;
1067
+ }
1068
+ }
1069
+ if (prev && prev.contentHash === contentHash) return {
1070
+ source: relativeSource,
1071
+ sourceMtime: await resolveSourceMtime(),
1072
+ contentHash,
1073
+ outputPath: page.outputPath
1074
+ };
1075
+ await promises.writeFile(outPath, content, 'utf8');
1076
+ return {
1077
+ source: relativeSource,
1078
+ sourceMtime: await resolveSourceMtime(),
1079
+ contentHash,
1080
+ outputPath: page.outputPath
1081
+ };
1082
+ }
1083
+ function rewriteSourceLinks(raw, page, ctx) {
1084
+ if (null === ctx.sourceMap || void 0 === ctx.sourceMap) return raw;
1085
+ if (null === page.source || void 0 === page.source) return raw;
1086
+ const sourcePath = node_path.relative(ctx.repoRoot, page.source);
1087
+ return rewriteLinks({
1088
+ content: raw,
1089
+ sourcePath,
1090
+ outputPath: page.outputPath,
1091
+ sourceMap: ctx.sourceMap
1092
+ });
1093
+ }
1094
+ function injectFrontmatter(raw, fm) {
1095
+ if (0 === Object.keys(fm).length) return raw;
1096
+ const parsed = gray_matter(raw);
1097
+ const merged = {
1098
+ ...fm,
1099
+ ...parsed.data
1100
+ };
1101
+ return gray_matter.stringify(parsed.content, merged);
1102
+ }
1103
+ const ICON_COLORS = [
1104
+ 'purple',
1105
+ 'blue',
1106
+ 'green',
1107
+ 'amber',
1108
+ 'cyan',
1109
+ 'red',
1110
+ 'pink',
1111
+ 'slate'
1112
+ ];
1113
+ async function generateLandingContent(sectionText, description, children, iconColor) {
1114
+ const visible = children.filter((c)=>!c.hidden && c.link);
1115
+ const useWorkspace = visible.some((c)=>c.card);
1116
+ const descLine = match(description).with(P.nonNullable, (d)=>`\n${d}\n`).otherwise(()=>'');
1117
+ const imports = "import { WorkspaceCard, WorkspaceGrid, SectionCard, SectionGrid } from '@zpress/ui/theme'\n\n";
1118
+ if (useWorkspace) {
1119
+ const cards = await Promise.all(visible.map((child)=>buildWorkspaceCard(child)));
1120
+ const grid = cards.join('\n');
1121
+ return `${imports}# ${sectionText}\n${descLine}\n<WorkspaceGrid>\n${grid}\n</WorkspaceGrid>\n`;
1122
+ }
1123
+ const cards = await Promise.all(visible.map((child)=>buildSectionCard(child, iconColor)));
1124
+ const grid = cards.join('\n');
1125
+ return `${imports}# ${sectionText}\n${descLine}\n<SectionGrid>\n${grid}\n</SectionGrid>\n`;
1126
+ }
1127
+ function landing_buildWorkspaceCardJsx(data) {
1128
+ const icon = data.icon ?? match(true === data.hasChildren).with(true, ()=>'pixelarticons:folder').otherwise(()=>'pixelarticons:file');
1129
+ const iconColor = data.iconColor ?? 'purple';
1130
+ const props = [
1131
+ `text="${escapeJsxProp(data.text)}"`,
1132
+ `href="${data.link}"`,
1133
+ `icon="${icon}"`,
1134
+ `iconColor="${iconColor}"`,
1135
+ ...maybeScopeProp(data.scope),
1136
+ ...maybeDescriptionProp(data.description),
1137
+ ...maybeTagsProp(data.tags),
1138
+ ...maybeBadgeProp(data.badge)
1139
+ ];
1140
+ return ` <WorkspaceCard ${props.join(' ')} />`;
1141
+ }
1142
+ async function buildWorkspaceCard(entry) {
1143
+ const card = entry.card ?? {};
1144
+ const description = card.description ?? await resolveDescription(entry);
1145
+ return landing_buildWorkspaceCardJsx({
1146
+ link: entry.link ?? '',
1147
+ text: entry.text,
1148
+ icon: card.icon,
1149
+ iconColor: card.iconColor,
1150
+ scope: card.scope,
1151
+ description,
1152
+ tags: card.tags,
1153
+ badge: card.badge,
1154
+ hasChildren: null !== entry.items && void 0 !== entry.items && entry.items.length > 0
1155
+ });
1156
+ }
1157
+ async function buildSectionCard(entry, iconColor) {
1158
+ const hasChildren = entry.items && entry.items.length > 0;
1159
+ const icon = match(hasChildren).with(true, ()=>'pixelarticons:folder').otherwise(()=>'pixelarticons:file');
1160
+ const description = await resolveDescription(entry);
1161
+ const props = [
1162
+ `href="${entry.link}"`,
1163
+ `title="${escapeJsxProp(entry.text)}"`,
1164
+ `icon="${icon}"`,
1165
+ `iconColor="${iconColor}"`
1166
+ ];
1167
+ if (description) props.push(`description="${escapeJsxProp(description)}"`);
1168
+ return ` <SectionCard ${props.join(' ')} />`;
1169
+ }
1170
+ async function resolveDescription(entry) {
1171
+ if (null !== entry.card && void 0 !== entry.card && entry.card.description) return entry.card.description;
1172
+ if (null !== entry.page && void 0 !== entry.page && entry.page.source) try {
1173
+ const desc = await extractDescription(entry.page.source);
1174
+ if (desc) return desc;
1175
+ } catch {}
1176
+ if (null !== entry.page && void 0 !== entry.page && null !== entry.page.frontmatter && void 0 !== entry.page.frontmatter && entry.page.frontmatter.description) return String(entry.page.frontmatter.description);
1177
+ }
1178
+ async function extractDescription(sourcePath) {
1179
+ const raw = await promises.readFile(sourcePath, 'utf8');
1180
+ const { data, content } = gray_matter(raw);
1181
+ if (data.description) return String(data.description);
1182
+ const lines = content.split('\n');
1183
+ const headingIdx = lines.findIndex((l)=>l.startsWith('#'));
1184
+ const para = resolveParagraph(lines, headingIdx);
1185
+ if (para.length > 0) return para.join(' ');
1186
+ }
1187
+ function escapeJsxProp(str) {
1188
+ return str.replaceAll('"', '&quot;').replaceAll('{', '&#123;').replaceAll('}', '&#125;');
1189
+ }
1190
+ function resolveParagraph(lines, headingIdx) {
1191
+ if (-1 === headingIdx) return [];
1192
+ return lines.slice(headingIdx + 1).reduce((acc, line)=>{
1193
+ if (acc.done) return acc;
1194
+ if (line.startsWith('#')) return {
1195
+ done: true,
1196
+ result: acc.result
1197
+ };
1198
+ const trimmed = line.trim();
1199
+ if ('' === trimmed && acc.result.length > 0) return {
1200
+ done: true,
1201
+ result: acc.result
1202
+ };
1203
+ if ('' === trimmed) return acc;
1204
+ if (trimmed.startsWith('<') || trimmed.startsWith('---')) return acc;
1205
+ return {
1206
+ done: false,
1207
+ result: [
1208
+ ...acc.result,
1209
+ trimmed
1210
+ ]
1211
+ };
1212
+ }, {
1213
+ done: false,
1214
+ result: []
1215
+ }).result;
1216
+ }
1217
+ function maybeScopeProp(scope) {
1218
+ if (scope) return [
1219
+ `scope="${escapeJsxProp(scope)}"`
1220
+ ];
1221
+ return [];
1222
+ }
1223
+ function maybeDescriptionProp(description) {
1224
+ if (description) return [
1225
+ `description="${escapeJsxProp(description)}"`
1226
+ ];
1227
+ return [];
1228
+ }
1229
+ function maybeTagsProp(tags) {
1230
+ if (tags && tags.length > 0) return [
1231
+ `tags={${JSON.stringify(tags)}}`
1232
+ ];
1233
+ return [];
1234
+ }
1235
+ function maybeBadgeProp(badge) {
1236
+ if (badge) return [
1237
+ `badge={${JSON.stringify(badge)}}`
1238
+ ];
1239
+ return [];
1240
+ }
1241
+ const DEFAULT_SECTION_DESCRIPTIONS = {
1242
+ guides: 'Step-by-step walkthroughs covering setup, workflows, and common tasks.',
1243
+ guide: 'Step-by-step walkthroughs covering setup, workflows, and common tasks.',
1244
+ standards: 'Code style, naming conventions, and engineering best practices for the team.',
1245
+ standard: 'Code style, naming conventions, and engineering best practices for the team.',
1246
+ security: 'Authentication, authorization, secrets management, and vulnerability policies.',
1247
+ architecture: 'System design, service boundaries, data flow, and infrastructure decisions.',
1248
+ 'getting started': 'Everything you need to set up your environment and start contributing.',
1249
+ introduction: 'Project overview, goals, and how the pieces fit together.',
1250
+ overview: 'High-level summary of the platform, key concepts, and navigation.',
1251
+ 'api reference': 'Endpoint contracts, request/response schemas, and usage examples.',
1252
+ api: 'Endpoint contracts, request/response schemas, and usage examples.',
1253
+ testing: 'Test strategy, tooling, coverage targets, and how to run the suite.',
1254
+ deployment: 'Build pipelines, release process, and environment configuration.',
1255
+ contributing: 'How to propose changes, open PRs, and follow the development workflow.',
1256
+ troubleshooting: 'Common issues, error explanations, and debugging techniques.',
1257
+ configuration: 'Available settings, environment variables, and how to customize behavior.',
1258
+ reference: 'Detailed technical reference covering APIs, types, and configuration options.'
1259
+ };
1260
+ async function generateDefaultHomePage(config, repoRoot) {
1261
+ const { tagline } = config;
1262
+ const title = config.title ?? 'Documentation';
1263
+ const description = config.description ?? title;
1264
+ const firstLink = findFirstLink(config.sections);
1265
+ const features = await match(config.features).with(P.nonNullable, buildExplicitFeatures).otherwise(()=>buildFeatures(config.sections, repoRoot));
1266
+ const frontmatterFeatures = buildFrontmatterFeatures(features);
1267
+ const workspaceResult = buildWorkspaceData(config);
1268
+ const heroConfig = {
1269
+ name: title,
1270
+ text: description,
1271
+ ...match(tagline).with(P.nonNullable, (t)=>({
1272
+ tagline: t
1273
+ })).otherwise(()=>({})),
1274
+ actions: [
1275
+ {
1276
+ theme: 'brand',
1277
+ text: 'Get Started',
1278
+ link: firstLink
1279
+ }
1280
+ ],
1281
+ image: {
1282
+ src: '/banner.svg',
1283
+ alt: title
1284
+ }
1285
+ };
1286
+ const frontmatterData = {
1287
+ pageType: 'home',
1288
+ hero: heroConfig,
1289
+ ...match(frontmatterFeatures.length > 0).with(true, ()=>({
1290
+ features: frontmatterFeatures
1291
+ })).otherwise(()=>({}))
1292
+ };
1293
+ const content = gray_matter.stringify('', frontmatterData);
1294
+ return {
1295
+ content,
1296
+ workspaces: workspaceResult.data
1297
+ };
1298
+ }
1299
+ function buildFrontmatterFeatures(features) {
1300
+ return features.map((f)=>({
1301
+ title: f.title,
1302
+ details: f.details,
1303
+ ...match(f.link).with(P.nonNullable, (l)=>({
1304
+ link: l
1305
+ })).otherwise(()=>({})),
1306
+ ...match(f.iconId).with(P.nonNullable, (id)=>({
1307
+ icon: id
1308
+ })).otherwise(()=>({})),
1309
+ iconColor: f.iconColor
1310
+ }));
1311
+ }
1312
+ function buildExplicitFeatures(features) {
1313
+ return Promise.resolve(features.map((f, index)=>({
1314
+ title: f.text,
1315
+ details: f.description,
1316
+ link: f.link,
1317
+ iconId: f.icon ?? null,
1318
+ iconColor: ICON_COLORS[index % ICON_COLORS.length]
1319
+ })));
1320
+ }
1321
+ function buildWorkspaceData(config) {
1322
+ const apps = config.apps ?? [];
1323
+ const packages = config.packages ?? [];
1324
+ const workspaceGroups = config.workspaces ?? [];
1325
+ const hasWorkspaceItems = apps.length > 0 || packages.length > 0 || workspaceGroups.length > 0;
1326
+ if (!hasWorkspaceItems) return {
1327
+ data: []
1328
+ };
1329
+ const appsResult = match(apps.length > 0).with(true, ()=>buildGroupData('apps', 'Apps', 'Deployable applications that make up the platform \u2014 each runs as an independent service.', apps, 'apps/')).otherwise(()=>null);
1330
+ const packagesResult = match(packages.length > 0).with(true, ()=>buildGroupData('packages', 'Packages', 'Shared libraries and utilities consumed across apps and services.', packages, 'packages/')).otherwise(()=>null);
1331
+ const groupResults = workspaceGroups.map((g)=>buildGroupData('workspaces', g.name, g.description, g.items, ''));
1332
+ const allResults = [
1333
+ appsResult,
1334
+ packagesResult,
1335
+ ...groupResults
1336
+ ].filter((r)=>null !== r);
1337
+ return {
1338
+ data: allResults.map((r)=>r.group)
1339
+ };
1340
+ }
1341
+ function buildGroupData(type, heading, description, items, scopePrefix) {
1342
+ const cards = items.map((item)=>({
1343
+ text: item.text,
1344
+ href: item.docsPrefix,
1345
+ icon: item.icon,
1346
+ iconColor: item.iconColor,
1347
+ scope: resolveScope(scopePrefix),
1348
+ description: item.description,
1349
+ tags: resolveTagLabels(item.tags),
1350
+ badge: item.badge
1351
+ }));
1352
+ return {
1353
+ group: {
1354
+ type,
1355
+ heading,
1356
+ description,
1357
+ cards
1358
+ }
1359
+ };
1360
+ }
1361
+ function findFirstLink(sections) {
1362
+ const [first] = sections;
1363
+ if (!first) return '/';
1364
+ return first.link ?? first.prefix ?? '/';
1365
+ }
1366
+ function buildFeatures(sections, repoRoot) {
1367
+ return Promise.all(sections.slice(0, 3).map(async (section, index)=>{
1368
+ const link = section.link ?? findFirstChildLink(section);
1369
+ const details = await extractSectionDescription(section, repoRoot);
1370
+ const iconId = match(section.icon).with(P.nonNullable, (id)=>id).otherwise(()=>null);
1371
+ const iconColor = ICON_COLORS[index % ICON_COLORS.length];
1372
+ return {
1373
+ title: section.text,
1374
+ details,
1375
+ link,
1376
+ iconId,
1377
+ iconColor
1378
+ };
1379
+ }));
1380
+ }
1381
+ function findFirstChildLink(section) {
1382
+ if (!section.items) return;
1383
+ const first = section.items.find((item)=>item.link);
1384
+ if (first) return first.link;
1385
+ const nested = section.items.find((item)=>findFirstChildLink(item));
1386
+ if (nested) return findFirstChildLink(nested);
1387
+ }
1388
+ async function extractSectionDescription(section, repoRoot) {
1389
+ if (section.from && !hasGlobChars(section.from)) {
1390
+ const description = await readFrontmatterDescription(node_path.resolve(repoRoot, section.from));
1391
+ if (description) return description;
1392
+ }
1393
+ if (null !== section.frontmatter && void 0 !== section.frontmatter && section.frontmatter.description) return String(section.frontmatter.description);
1394
+ const knownDesc = DEFAULT_SECTION_DESCRIPTIONS[section.text.toLowerCase()];
1395
+ if (knownDesc) return knownDesc;
1396
+ return section.text;
1397
+ }
1398
+ async function readFrontmatterDescription(filePath) {
1399
+ try {
1400
+ const raw = await promises.readFile(filePath, 'utf8');
1401
+ const { data } = gray_matter(raw);
1402
+ return match(data.description).with(P.nonNullable, String).otherwise(()=>void 0);
1403
+ } catch {
1404
+ return;
1405
+ }
1406
+ }
1407
+ function resolveTagLabels(tags) {
1408
+ if (!tags) return [];
1409
+ return [
1410
+ ...tags
1411
+ ];
1412
+ }
1413
+ function resolveScope(scopePrefix) {
1414
+ if (scopePrefix.length > 0) return scopePrefix;
1415
+ }
1416
+ const MANIFEST_FILE = '.generated/manifest.json';
1417
+ async function loadManifest(outDir) {
1418
+ const p = node_path.resolve(outDir, MANIFEST_FILE);
1419
+ try {
1420
+ const raw = await promises.readFile(p, 'utf8');
1421
+ return JSON.parse(raw);
1422
+ } catch {
1423
+ return null;
1424
+ }
1425
+ }
1426
+ async function saveManifest(outDir, manifest) {
1427
+ const p = node_path.resolve(outDir, MANIFEST_FILE);
1428
+ await promises.mkdir(node_path.dirname(p), {
1429
+ recursive: true
1430
+ });
1431
+ await promises.writeFile(p, JSON.stringify(manifest, null, 2), 'utf8');
1432
+ }
1433
+ async function cleanStaleFiles(outDir, oldManifest, newManifest) {
1434
+ const oldPaths = new Set(Object.keys(oldManifest.files));
1435
+ const newPaths = new Set(Object.keys(newManifest.files));
1436
+ const stalePaths = [
1437
+ ...oldPaths
1438
+ ].filter((p)=>!newPaths.has(p));
1439
+ await stalePaths.reduce(async (prev, oldPath)=>{
1440
+ await prev;
1441
+ const abs = node_path.resolve(outDir, oldPath);
1442
+ await promises.rm(abs, {
1443
+ force: true
1444
+ });
1445
+ await pruneEmptyDirs(node_path.dirname(abs), outDir);
1446
+ }, Promise.resolve());
1447
+ return stalePaths.length;
1448
+ }
1449
+ async function pruneEmptyDirs(dir, stopAt) {
1450
+ if (dir === stopAt || !dir.startsWith(stopAt)) return;
1451
+ try {
1452
+ const entries = await promises.readdir(dir);
1453
+ if (0 === entries.length) {
1454
+ await promises.rmdir(dir);
1455
+ await pruneEmptyDirs(node_path.dirname(dir), stopAt);
1456
+ }
1457
+ } catch {}
1458
+ }
1459
+ function linkToOutputPath(link, ext = '.md') {
1460
+ const clean = match(link.startsWith('/')).with(true, ()=>link.slice(1)).otherwise(()=>link);
1461
+ if ('' === clean || '/' === clean) return `index${ext}`;
1462
+ return `${clean}${ext}`;
1463
+ }
1464
+ function sourceExt(filePath) {
1465
+ return match(node_path.extname(filePath)).with('.mdx', ()=>'.mdx').otherwise(()=>'.md');
1466
+ }
1467
+ function extractBaseDir(globPattern) {
1468
+ const firstGlobChar = globPattern.search(/[*?{}[\]]/);
1469
+ if (-1 === firstGlobChar) return node_path.dirname(globPattern);
1470
+ const beforeGlob = globPattern.slice(0, firstGlobChar);
1471
+ return match(beforeGlob.endsWith('/')).with(true, ()=>beforeGlob.slice(0, -1)).otherwise(()=>node_path.dirname(beforeGlob));
1472
+ }
1473
+ function deriveText(sourcePath, slug, mode) {
1474
+ return match(mode).with('frontmatter', ()=>deriveFromFrontmatter(sourcePath, slug)).with('heading', ()=>deriveFromHeading(sourcePath, slug)).with('filename', ()=>Promise.resolve(kebabToTitle(slug))).exhaustive();
1475
+ }
1476
+ function kebabToTitle(slug) {
1477
+ return words(slug).map(capitalize).join(' ');
1478
+ }
1479
+ async function deriveFromFrontmatter(sourcePath, fallbackSlug) {
1480
+ const content = await promises.readFile(sourcePath, 'utf8');
1481
+ const parsed = gray_matter(content);
1482
+ return match(parsed.data.title).with(P.string.minLength(1), (title)=>title).otherwise(()=>extractHeading(parsed.content, fallbackSlug));
1483
+ }
1484
+ async function deriveFromHeading(sourcePath, fallbackSlug) {
1485
+ const content = await promises.readFile(sourcePath, 'utf8');
1486
+ const { content: body } = gray_matter(content);
1487
+ return extractHeading(body, fallbackSlug);
1488
+ }
1489
+ function extractHeading(body, fallbackSlug) {
1490
+ const heading = body.match(/^#\s+(.+)$/m);
1491
+ return match(heading).with(P.nonNullable, (h)=>h[1].trim()).otherwise(()=>kebabToTitle(fallbackSlug));
1492
+ }
1493
+ const PRESERVED_HTML_TAGS = new Set([
1494
+ 'details',
1495
+ 'summary',
1496
+ 'dialog',
1497
+ 'div',
1498
+ 'p',
1499
+ 'blockquote',
1500
+ 'pre',
1501
+ 'figure',
1502
+ 'figcaption',
1503
+ 'hr',
1504
+ 'br',
1505
+ 'h1',
1506
+ 'h2',
1507
+ 'h3',
1508
+ 'h4',
1509
+ 'h5',
1510
+ 'h6',
1511
+ 'span',
1512
+ 'a',
1513
+ 'em',
1514
+ 'strong',
1515
+ 'code',
1516
+ 'del',
1517
+ 'ins',
1518
+ 'mark',
1519
+ 'sub',
1520
+ 'sup',
1521
+ 'kbd',
1522
+ 'abbr',
1523
+ 'small',
1524
+ 'img',
1525
+ 'video',
1526
+ 'audio',
1527
+ 'source',
1528
+ 'picture',
1529
+ 'table',
1530
+ 'thead',
1531
+ 'tbody',
1532
+ 'tfoot',
1533
+ 'tr',
1534
+ 'th',
1535
+ 'td',
1536
+ 'caption',
1537
+ 'colgroup',
1538
+ 'col',
1539
+ 'ul',
1540
+ 'ol',
1541
+ 'li',
1542
+ 'dl',
1543
+ 'dt',
1544
+ 'dd'
1545
+ ]);
1546
+ function stripXmlTags(markdown) {
1547
+ const parts = markdown.split(/(```[\s\S]*?```)/g);
1548
+ const stripped = parts.map((part, i)=>{
1549
+ if (i % 2 === 1) return part;
1550
+ return part.replaceAll(/<\/?([a-zA-Z][\w-]*)[^>]*>/g, (tag, name)=>{
1551
+ if (PRESERVED_HTML_TAGS.has(name.toLowerCase())) return tag;
1552
+ return '';
1553
+ });
1554
+ }).join('');
1555
+ return stripped.replaceAll(/\n{3,}/g, '\n\n');
1556
+ }
1557
+ const PLANNING_DIR = '.planning';
1558
+ const PLANNING_PREFIX = '/planning';
1559
+ const PLANNING_FRONTMATTER = {
1560
+ sidebar: false,
1561
+ pageClass: 'planning-page'
1562
+ };
1563
+ async function discoverPlanningPages(ctx) {
1564
+ const planningDir = node_path.resolve(ctx.repoRoot, PLANNING_DIR);
1565
+ if (!existsSync(planningDir)) return [];
1566
+ const files = await fast_glob('**/*.md', {
1567
+ cwd: planningDir,
1568
+ onlyFiles: true,
1569
+ ignore: [
1570
+ '**/_*'
1571
+ ]
1572
+ });
1573
+ if (0 === files.length) return [];
1574
+ const docPages = files.map((relativePath)=>{
1575
+ const sourcePath = node_path.resolve(planningDir, relativePath);
1576
+ const slug = relativePath.replace(/\.md$/, '');
1577
+ return {
1578
+ content: async ()=>{
1579
+ const raw = await promises.readFile(sourcePath, 'utf8');
1580
+ return stripXmlTags(raw);
1581
+ },
1582
+ outputPath: linkToOutputPath(`${PLANNING_PREFIX}/${slug}`),
1583
+ frontmatter: PLANNING_FRONTMATTER
1584
+ };
1585
+ });
1586
+ const indexPage = {
1587
+ content: ()=>generatePlanningIndex(files, planningDir),
1588
+ outputPath: linkToOutputPath(PLANNING_PREFIX),
1589
+ frontmatter: PLANNING_FRONTMATTER
1590
+ };
1591
+ return [
1592
+ indexPage,
1593
+ ...docPages
1594
+ ];
1595
+ }
1596
+ async function generatePlanningIndex(files, planningDir) {
1597
+ const entries = await Promise.all(files.map(async (relativePath)=>{
1598
+ const slug = relativePath.replace(/\.md$/, '');
1599
+ const sourcePath = node_path.resolve(planningDir, relativePath);
1600
+ const text = await deriveText(sourcePath, node_path.basename(slug), 'heading');
1601
+ const segments = relativePath.split('/');
1602
+ const dirName = resolveDirName(segments);
1603
+ return {
1604
+ slug,
1605
+ text,
1606
+ dirName
1607
+ };
1608
+ }));
1609
+ const rootFiles = entries.filter((e)=>void 0 === e.dirName).toSorted((a, b)=>naturalCompare(a.slug, b.slug));
1610
+ const grouped = groupBy(entries.filter((e)=>void 0 !== e.dirName), (e)=>e.dirName);
1611
+ const dirs = Object.entries(grouped).toSorted(([a], [b])=>naturalCompare(a, b)).map(([dirName, dirEntries])=>({
1612
+ name: dirName,
1613
+ title: kebabToTitle(dirName),
1614
+ files: dirEntries.map((e)=>({
1615
+ slug: e.slug,
1616
+ text: e.text
1617
+ })).toSorted((a, b)=>naturalCompare(a.slug, b.slug))
1618
+ }));
1619
+ const rootSection = resolveRootSection(rootFiles);
1620
+ const dirSections = dirs.map((dir)=>{
1621
+ const heading = `## ${dir.title}\n`;
1622
+ const links = dir.files.map((e)=>`- [${e.text}](${PLANNING_PREFIX}/${e.slug})`).join('\n');
1623
+ return `${heading}\n${links}`;
1624
+ });
1625
+ const sections = [
1626
+ '# Planning\n\nInternal planning documents.\n',
1627
+ ...rootSection,
1628
+ ...dirSections
1629
+ ];
1630
+ return `${sections.join('\n\n')}\n`;
1631
+ }
1632
+ function naturalCompare(a, b) {
1633
+ const aParts = a.split(/(\d+)/);
1634
+ const bParts = b.split(/(\d+)/);
1635
+ const len = Math.min(aParts.length, bParts.length);
1636
+ const indices = Array.from({
1637
+ length: len
1638
+ }, (_, idx)=>idx);
1639
+ const result = indices.reduce((acc, idx)=>{
1640
+ if (null !== acc) return acc;
1641
+ const aPart = aParts[idx];
1642
+ const bPart = bParts[idx];
1643
+ if (/^\d+$/.test(aPart) && /^\d+$/.test(bPart)) {
1644
+ const diff = Number(aPart) - Number(bPart);
1645
+ if (0 !== diff) return diff;
1646
+ return null;
1647
+ }
1648
+ if (aPart < bPart) return -1;
1649
+ if (aPart > bPart) return 1;
1650
+ return null;
1651
+ }, null);
1652
+ if (null !== result) return result;
1653
+ return aParts.length - bParts.length;
1654
+ }
1655
+ function resolveDirName(segments) {
1656
+ if (segments.length > 1) return segments[0];
1657
+ }
1658
+ function resolveRootSection(rootFiles) {
1659
+ if (0 === rootFiles.length) return [];
1660
+ return [
1661
+ rootFiles.map((e)=>`- [${e.text}](${PLANNING_PREFIX}/${e.slug})`).join('\n')
1662
+ ];
1663
+ }
1664
+ function sectionFirst(a, b) {
1665
+ const aIsSection = (()=>{
1666
+ if (null !== a.items && void 0 !== a.items && a.items.length > 0) return 0;
1667
+ return 1;
1668
+ })();
1669
+ const bIsSection = (()=>{
1670
+ if (null !== b.items && void 0 !== b.items && b.items.length > 0) return 0;
1671
+ return 1;
1672
+ })();
1673
+ return aIsSection - bIsSection;
1674
+ }
1675
+ function sortEntries(entries, sort) {
1676
+ if (!sort) return [
1677
+ ...entries
1678
+ ];
1679
+ return match(sort).with('alpha', ()=>[
1680
+ ...entries
1681
+ ].toSorted((a, b)=>sectionFirst(a, b) || a.text.localeCompare(b.text))).with('filename', ()=>[
1682
+ ...entries
1683
+ ].toSorted((a, b)=>{
1684
+ const rank = sectionFirst(a, b);
1685
+ if (0 !== rank) return rank;
1686
+ const aKey = match(a.page).with(P.nonNullable, (p)=>p.outputPath).otherwise(()=>a.text);
1687
+ const bKey = match(b.page).with(P.nonNullable, (p)=>p.outputPath).otherwise(()=>b.text);
1688
+ return aKey.localeCompare(bKey);
1689
+ })).otherwise((comparator)=>[
1690
+ ...entries
1691
+ ].toSorted((a, b)=>comparator(toResolvedPage(a), toResolvedPage(b))));
1692
+ }
1693
+ function toResolvedPage(entry) {
1694
+ const source = (()=>{
1695
+ if (entry.page) return entry.page.source;
1696
+ })();
1697
+ const frontmatter = (()=>{
1698
+ if (entry.page) return entry.page.frontmatter;
1699
+ return {};
1700
+ })();
1701
+ return {
1702
+ text: entry.text,
1703
+ link: match(entry.link).with(P.nonNullable, (l)=>l).otherwise(()=>''),
1704
+ source,
1705
+ frontmatter
1706
+ };
1707
+ }
1708
+ async function resolveRecursiveGlob(entry, ctx, frontmatter, depth) {
1709
+ const ignore = [
1710
+ ...ctx.config.exclude ?? [],
1711
+ ...entry.exclude ?? []
1712
+ ];
1713
+ const indexFile = entry.indexFile ?? 'overview';
1714
+ if (null === entry.from || void 0 === entry.from) {
1715
+ log.error('[zpress] resolveRecursiveGlob called without entry.from');
1716
+ return [];
1717
+ }
1718
+ const files = await fast_glob(entry.from, {
1719
+ cwd: ctx.repoRoot,
1720
+ ignore,
1721
+ absolute: false,
1722
+ onlyFiles: true
1723
+ });
1724
+ if (0 === files.length) {
1725
+ if (!ctx.quiet) log.warn(`Glob "${entry.from}" matched 0 files for "${entry.text}"`);
1726
+ return [];
1727
+ }
1728
+ const baseDir = extractBaseDir(entry.from);
1729
+ const prefix = entry.prefix ?? '';
1730
+ const textFrom = entry.textFrom ?? 'filename';
1731
+ const root = buildDirTree(files, baseDir);
1732
+ return buildEntryTree({
1733
+ node: root,
1734
+ prefix,
1735
+ textFrom,
1736
+ textTransform: entry.textTransform,
1737
+ sort: entry.sort,
1738
+ collapsible: entry.collapsible,
1739
+ indexFile,
1740
+ ctx,
1741
+ frontmatter,
1742
+ depth
1743
+ });
1744
+ }
1745
+ function buildDirTree(files, baseDir) {
1746
+ const basePrefixLen = match(baseDir.length > 0).with(true, ()=>baseDir.length + 1).otherwise(()=>0);
1747
+ return files.reduce((tree, file)=>{
1748
+ const rel = file.slice(basePrefixLen);
1749
+ const segments = rel.split('/');
1750
+ const dirSegments = segments.slice(0, -1);
1751
+ const current = dirSegments.reduce((acc, seg)=>{
1752
+ if (!acc.subdirs.has(seg)) acc.subdirs.set(seg, {
1753
+ files: [],
1754
+ subdirs: new Map()
1755
+ });
1756
+ return acc.subdirs.get(seg);
1757
+ }, tree);
1758
+ current.files.push(file);
1759
+ return tree;
1760
+ }, {
1761
+ files: [],
1762
+ subdirs: new Map()
1763
+ });
1764
+ }
1765
+ async function buildEntryTree(params) {
1766
+ const { node, prefix, textFrom, textTransform, sort, collapsible, indexFile, ctx, frontmatter, depth } = params;
1767
+ const nonIndexFiles = node.files.filter((file)=>node_path.basename(file, node_path.extname(file)) !== indexFile);
1768
+ const fileEntries = await Promise.all(nonIndexFiles.map(async (file)=>{
1769
+ const ext = sourceExt(file);
1770
+ const slug = node_path.basename(file, node_path.extname(file));
1771
+ const link = `${prefix}/${slug}`;
1772
+ const sourcePath = node_path.resolve(ctx.repoRoot, file);
1773
+ const rawText = await deriveText(sourcePath, slug, textFrom);
1774
+ const text = match(textTransform).with(P.nonNullable, (t)=>t(rawText, slug)).otherwise(()=>rawText);
1775
+ return {
1776
+ text,
1777
+ link,
1778
+ page: {
1779
+ source: sourcePath,
1780
+ outputPath: linkToOutputPath(link, ext),
1781
+ frontmatter
1782
+ }
1783
+ };
1784
+ }));
1785
+ const subdirEntries = await Promise.all([
1786
+ ...node.subdirs
1787
+ ].map(async ([dirName, subNode])=>{
1788
+ const subPrefix = `${prefix}/${dirName}`;
1789
+ const indexFilePath = subNode.files.find((f)=>node_path.basename(f, node_path.extname(f)) === indexFile);
1790
+ const { sectionText, sectionPage } = await resolveSubdirSection({
1791
+ indexFilePath,
1792
+ dirName,
1793
+ subPrefix,
1794
+ indexFile,
1795
+ textFrom,
1796
+ textTransform,
1797
+ ctx,
1798
+ frontmatter
1799
+ });
1800
+ const children = await buildEntryTree({
1801
+ node: subNode,
1802
+ prefix: subPrefix,
1803
+ textFrom,
1804
+ textTransform,
1805
+ sort,
1806
+ collapsible,
1807
+ indexFile,
1808
+ ctx,
1809
+ frontmatter,
1810
+ depth: depth + 1
1811
+ });
1812
+ const sorted = sortEntries(children, sort);
1813
+ const autoEffectiveCollapsible = resolveAutoCollapsible(depth);
1814
+ const effectiveCollapsible = collapsible ?? autoEffectiveCollapsible;
1815
+ const sectionLink = resolveSectionLink(indexFilePath, subPrefix, indexFile);
1816
+ return {
1817
+ text: sectionText,
1818
+ link: sectionLink,
1819
+ collapsible: effectiveCollapsible,
1820
+ items: sorted,
1821
+ page: sectionPage
1822
+ };
1823
+ }));
1824
+ return sortEntries([
1825
+ ...fileEntries,
1826
+ ...subdirEntries
1827
+ ], sort);
1828
+ }
1829
+ async function resolveSubdirSection(params) {
1830
+ const { indexFilePath, dirName, subPrefix, indexFile, textFrom, textTransform, ctx, frontmatter } = params;
1831
+ if (indexFilePath) {
1832
+ const ext = sourceExt(indexFilePath);
1833
+ const sourcePath = node_path.resolve(ctx.repoRoot, indexFilePath);
1834
+ const rawText = await deriveText(sourcePath, dirName, textFrom);
1835
+ const sectionText = match(textTransform).with(P.nonNullable, (t)=>t(rawText, dirName)).otherwise(()=>rawText);
1836
+ const sectionPage = {
1837
+ source: sourcePath,
1838
+ outputPath: linkToOutputPath(`${subPrefix}/${indexFile}`, ext),
1839
+ frontmatter
1840
+ };
1841
+ return {
1842
+ sectionText,
1843
+ sectionPage
1844
+ };
1845
+ }
1846
+ const rawText = kebabToTitle(dirName);
1847
+ const sectionText = match(textTransform).with(P.nonNullable, (t)=>t(rawText, dirName)).otherwise(()=>rawText);
1848
+ return {
1849
+ sectionText,
1850
+ sectionPage: void 0
1851
+ };
1852
+ }
1853
+ function resolveAutoCollapsible(depth) {
1854
+ if (depth > 0) return true;
1855
+ }
1856
+ function resolveSectionLink(indexFilePath, subPrefix, indexFile) {
1857
+ if (null != indexFilePath) return `${subPrefix}/${indexFile}`;
1858
+ }
1859
+ async function resolveEntries(entries, ctx, inheritedFrontmatter = {}, depth = 0) {
1860
+ const results = await Promise.all(entries.map((entry)=>resolveEntry(entry, ctx, inheritedFrontmatter, depth)));
1861
+ const result = collectResults(results);
1862
+ const [err] = result;
1863
+ if (err) return [
1864
+ err,
1865
+ null
1866
+ ];
1867
+ const [, collected] = result;
1868
+ return [
1869
+ null,
1870
+ [
1871
+ ...collected
1872
+ ]
1873
+ ];
1874
+ }
1875
+ function resolveEntry(entry, ctx, inheritedFrontmatter, depth) {
1876
+ const mergedFm = {
1877
+ ...inheritedFrontmatter,
1878
+ ...entry.frontmatter
1879
+ };
1880
+ if (entry.from && !hasGlobChars(entry.from) && !entry.items) return Promise.resolve(resolveFilePage(entry, ctx, mergedFm));
1881
+ if (entry.content && entry.link) return Promise.resolve(resolveVirtualPage(entry, mergedFm));
1882
+ return resolveSection(entry, ctx, mergedFm, depth);
1883
+ }
1884
+ function resolveFilePage(entry, ctx, frontmatter) {
1885
+ if (null === entry.from || void 0 === entry.from) return [
1886
+ syncError('missing_from', 'resolveFilePage called without entry.from'),
1887
+ null
1888
+ ];
1889
+ const sourcePath = node_path.resolve(ctx.repoRoot, entry.from);
1890
+ if (!node_fs.existsSync(sourcePath)) return [
1891
+ syncError('file_not_found', `Source file not found: ${entry.from}`),
1892
+ null
1893
+ ];
1894
+ if (null === entry.link || void 0 === entry.link) return [
1895
+ syncError('missing_link', `resolveFilePage called without entry.link for: ${entry.from}`),
1896
+ null
1897
+ ];
1898
+ const ext = sourceExt(entry.from);
1899
+ return [
1900
+ null,
1901
+ {
1902
+ text: entry.text,
1903
+ link: entry.link,
1904
+ hidden: entry.hidden,
1905
+ card: entry.card,
1906
+ page: {
1907
+ source: sourcePath,
1908
+ outputPath: linkToOutputPath(entry.link, ext),
1909
+ frontmatter
1910
+ }
1911
+ }
1912
+ ];
1913
+ }
1914
+ function resolveVirtualPage(entry, frontmatter) {
1915
+ if (void 0 === entry.link || null === entry.link) return [
1916
+ syncError('missing_link', 'resolveVirtualPage called without entry.link'),
1917
+ null
1918
+ ];
1919
+ return [
1920
+ null,
1921
+ {
1922
+ text: entry.text,
1923
+ link: entry.link,
1924
+ hidden: entry.hidden,
1925
+ card: entry.card,
1926
+ page: {
1927
+ content: entry.content,
1928
+ outputPath: linkToOutputPath(entry.link),
1929
+ frontmatter
1930
+ }
1931
+ }
1932
+ ];
1933
+ }
1934
+ async function resolveSection(entry, ctx, mergedFm, depth) {
1935
+ const globbed = await (()=>{
1936
+ if (entry.from && hasGlobChars(entry.from)) {
1937
+ if (entry.recursive) return resolveRecursiveGlob(entry, ctx, mergedFm, depth + 1);
1938
+ return resolveGlob(entry, ctx, mergedFm);
1939
+ }
1940
+ return Promise.resolve([]);
1941
+ })();
1942
+ const explicitResult = await (()=>{
1943
+ if (entry.items) return resolveEntries(entry.items, ctx, mergedFm, depth + 1);
1944
+ return Promise.resolve([
1945
+ null,
1946
+ []
1947
+ ]);
1948
+ })();
1949
+ const [explicitErr, explicit] = explicitResult;
1950
+ if (explicitErr) return [
1951
+ explicitErr,
1952
+ null
1953
+ ];
1954
+ const children = [
1955
+ ...globbed,
1956
+ ...explicit
1957
+ ];
1958
+ const deduped = deduplicateByLink(children);
1959
+ const sorted = sortEntries(deduped, entry.sort);
1960
+ const sectionPage = resolveSectionPage(entry, ctx, mergedFm);
1961
+ const autoCollapsible = (()=>{
1962
+ if (depth > 0) return true;
1963
+ })();
1964
+ const collapsible = entry.collapsible ?? autoCollapsible;
1965
+ return [
1966
+ null,
1967
+ {
1968
+ text: entry.text,
1969
+ link: entry.link,
1970
+ collapsible,
1971
+ hidden: entry.hidden,
1972
+ card: entry.card,
1973
+ isolated: entry.isolated,
1974
+ items: sorted,
1975
+ page: sectionPage
1976
+ }
1977
+ ];
1978
+ }
1979
+ function resolveSectionPage(entry, ctx, mergedFm) {
1980
+ if (entry.link && entry.from && !hasGlobChars(entry.from)) {
1981
+ const sourcePath = node_path.resolve(ctx.repoRoot, entry.from);
1982
+ if (node_fs.existsSync(sourcePath)) {
1983
+ const ext = sourceExt(entry.from);
1984
+ return {
1985
+ source: sourcePath,
1986
+ outputPath: linkToOutputPath(entry.link, ext),
1987
+ frontmatter: mergedFm
1988
+ };
1989
+ }
1990
+ } else if (entry.link && entry.recursive && entry.from) {
1991
+ const baseDir = extractBaseDir(entry.from);
1992
+ const indexFile = entry.indexFile ?? 'overview';
1993
+ const mdPath = node_path.join(baseDir, `${indexFile}.md`);
1994
+ const mdxPath = node_path.join(baseDir, `${indexFile}.mdx`);
1995
+ const mdxExists = node_fs.existsSync(node_path.resolve(ctx.repoRoot, mdxPath));
1996
+ const indexPath = match(mdxExists).with(true, ()=>mdxPath).otherwise(()=>mdPath);
1997
+ const sourcePath = node_path.resolve(ctx.repoRoot, indexPath);
1998
+ if (node_fs.existsSync(sourcePath)) {
1999
+ const ext = sourceExt(indexPath);
2000
+ return {
2001
+ source: sourcePath,
2002
+ outputPath: linkToOutputPath(entry.link, ext),
2003
+ frontmatter: mergedFm
2004
+ };
2005
+ }
2006
+ }
2007
+ }
2008
+ async function resolveGlob(entry, ctx, frontmatter) {
2009
+ const ignore = [
2010
+ ...ctx.config.exclude ?? [],
2011
+ ...entry.exclude ?? []
2012
+ ];
2013
+ if (null === entry.from || void 0 === entry.from) {
2014
+ log.error('[zpress] resolveGlob called without entry.from');
2015
+ return [];
2016
+ }
2017
+ const files = await fast_glob(entry.from, {
2018
+ cwd: ctx.repoRoot,
2019
+ ignore,
2020
+ absolute: false,
2021
+ onlyFiles: true
2022
+ });
2023
+ if (0 === files.length) {
2024
+ if (!ctx.quiet) log.warn(`Glob "${entry.from}" matched 0 files for "${entry.text}"`);
2025
+ return [];
2026
+ }
2027
+ const prefix = entry.prefix ?? '';
2028
+ const textFrom = entry.textFrom ?? 'filename';
2029
+ const { textTransform } = entry;
2030
+ return Promise.all(files.map(async (file)=>{
2031
+ const ext = sourceExt(file);
2032
+ const slug = node_path.basename(file, node_path.extname(file));
2033
+ const link = `${prefix}/${slug}`;
2034
+ const sourcePath = node_path.resolve(ctx.repoRoot, file);
2035
+ const rawText = await deriveText(sourcePath, slug, textFrom);
2036
+ const text = match(textTransform).with(P.nonNullable, (t)=>t(rawText, slug)).otherwise(()=>rawText);
2037
+ return {
2038
+ text,
2039
+ link,
2040
+ page: {
2041
+ source: sourcePath,
2042
+ outputPath: linkToOutputPath(link, ext),
2043
+ frontmatter
2044
+ }
2045
+ };
2046
+ }));
2047
+ }
2048
+ function deduplicateByLink(entries) {
2049
+ const { result } = entries.reduce((acc, entry)=>{
2050
+ if (null === entry.link || void 0 === entry.link) return {
2051
+ seen: acc.seen,
2052
+ result: [
2053
+ ...acc.result,
2054
+ entry
2055
+ ]
2056
+ };
2057
+ const existing = acc.seen.get(entry.link);
2058
+ if (void 0 === existing) {
2059
+ acc.seen.set(entry.link, acc.result.length);
2060
+ return {
2061
+ seen: acc.seen,
2062
+ result: [
2063
+ ...acc.result,
2064
+ entry
2065
+ ]
2066
+ };
2067
+ }
2068
+ return {
2069
+ seen: acc.seen,
2070
+ result: acc.result.map((item, i)=>{
2071
+ if (i === existing) return entry;
2072
+ return item;
2073
+ })
2074
+ };
2075
+ }, {
2076
+ seen: new Map(),
2077
+ result: []
2078
+ });
2079
+ return result;
2080
+ }
2081
+ function buildSidebarEntry(entry, icon) {
2082
+ if (entry.items && entry.items.length > 0) return {
2083
+ text: entry.text,
2084
+ items: generateSidebar(entry.items),
2085
+ ...maybeCollapsed(entry.collapsible),
2086
+ ...maybeLink(entry.link),
2087
+ ...maybeIcon(icon)
2088
+ };
2089
+ if (null === entry.link || void 0 === entry.link) {
2090
+ log.error(`[zpress] Leaf entry "${entry.text}" has no link — skipping`);
2091
+ return {
2092
+ text: entry.text
2093
+ };
2094
+ }
2095
+ return {
2096
+ text: entry.text,
2097
+ link: entry.link,
2098
+ ...maybeIcon(icon)
2099
+ };
2100
+ }
2101
+ function generateSidebar(entries, icons) {
2102
+ const visible = entries.filter((e)=>!e.hidden);
2103
+ const pages = visible.filter((e)=>!e.items || 0 === e.items.length);
2104
+ const sections = visible.filter((e)=>e.items && e.items.length > 0);
2105
+ return [
2106
+ ...pages,
2107
+ ...sections
2108
+ ].map((entry)=>{
2109
+ const icon = resolveIcon(icons, entry.text);
2110
+ return buildSidebarEntry(entry, icon);
2111
+ });
2112
+ }
2113
+ function buildNavEntry(entry, icon) {
2114
+ const link = sidebar_resolveLink(entry);
2115
+ const children = resolveChildren(entry);
2116
+ return {
2117
+ text: entry.text,
2118
+ link,
2119
+ ...maybeIcon(icon),
2120
+ ...maybeChildren(children)
2121
+ };
2122
+ }
2123
+ function generateNav(config, resolved, icons) {
2124
+ if ('auto' !== config.nav && void 0 !== config.nav) return [
2125
+ ...config.nav
2126
+ ];
2127
+ const visible = resolved.filter((e)=>!e.hidden);
2128
+ const nonIsolated = visible.filter((e)=>!e.isolated).slice(0, 3);
2129
+ const isolated = visible.filter((e)=>e.isolated);
2130
+ return [
2131
+ ...nonIsolated,
2132
+ ...isolated
2133
+ ].map((entry)=>buildNavEntry(entry, icons.get(entry.text))).filter((item)=>void 0 !== item.link);
2134
+ }
2135
+ function sidebar_findFirstLink(entry) {
2136
+ if (entry.link) return entry.link;
2137
+ if (entry.items) {
2138
+ const mapped = entry.items.map(sidebar_findFirstLink);
2139
+ return mapped.find(Boolean);
2140
+ }
2141
+ }
2142
+ function maybeCollapsed(collapsible) {
2143
+ if (collapsible) return {
2144
+ collapsed: true
2145
+ };
2146
+ return {};
2147
+ }
2148
+ function maybeLink(link) {
2149
+ if (link) return {
2150
+ link
2151
+ };
2152
+ return {};
2153
+ }
2154
+ function maybeIcon(icon) {
2155
+ if (icon) return {
2156
+ icon
2157
+ };
2158
+ return {};
2159
+ }
2160
+ function resolveIcon(icons, text) {
2161
+ if (icons) return icons.get(text);
2162
+ }
2163
+ function sidebar_resolveLink(entry) {
2164
+ if (entry.link) return entry.link;
2165
+ return sidebar_findFirstLink(entry);
2166
+ }
2167
+ function resolveChildren(entry) {
2168
+ if (entry.isolated && entry.items && entry.items.length > 0) return entry.items.filter((child)=>!child.hidden).map((child)=>({
2169
+ text: child.text,
2170
+ link: resolveChildLink(child)
2171
+ })).filter((child)=>void 0 !== child.link);
2172
+ }
2173
+ function resolveChildLink(child) {
2174
+ if (child.link) return child.link;
2175
+ return sidebar_findFirstLink(child);
2176
+ }
2177
+ function maybeChildren(children) {
2178
+ if (children && children.length > 0) return {
2179
+ items: children
2180
+ };
2181
+ return {};
2182
+ }
2183
+ const OVERVIEW_SLUGS = [
2184
+ 'overview',
2185
+ 'index',
2186
+ 'readme'
2187
+ ];
2188
+ function promoteOverviewChild(entry) {
2189
+ if (!entry.link || !entry.items || 0 === entry.items.length || entry.page) return;
2190
+ const entryLink = entry.link;
2191
+ const { items } = entry;
2192
+ const promoted = OVERVIEW_SLUGS.map((slug)=>items.find((item)=>{
2193
+ if (!item.link || !item.page) return false;
2194
+ const lastSegment = item.link.split('/').at(-1);
2195
+ return lastSegment === slug;
2196
+ })).find((item)=>void 0 !== item);
2197
+ if (!promoted || !promoted.page) return;
2198
+ const childPage = promoted.page;
2199
+ const ext = resolveExt(childPage.source);
2200
+ entry.page = {
2201
+ source: childPage.source,
2202
+ content: childPage.content,
2203
+ outputPath: linkToOutputPath(entryLink, ext),
2204
+ frontmatter: childPage.frontmatter
2205
+ };
2206
+ entry.items = items.filter((item)=>item !== promoted);
2207
+ }
2208
+ function injectLandingPages(entries, configEntries, workspaceItems, colorIndex = {
2209
+ value: 0
2210
+ }) {
2211
+ entries.reduce((_, entry)=>{
2212
+ promoteOverviewChild(entry);
2213
+ if (entry.link && !entry.page) {
2214
+ const configEntry = findConfigEntry(configEntries, entry.link);
2215
+ const description = inject_resolveDescription(configEntry);
2216
+ const hasSelfLinkedChild = checkHasSelfLinkedChild(entry.items, entry.link);
2217
+ if (entry.items && entry.items.length > 0 && !hasSelfLinkedChild) {
2218
+ const color = ICON_COLORS[colorIndex.value % ICON_COLORS.length];
2219
+ colorIndex.value += 1;
2220
+ const children = entry.items;
2221
+ entry.page = {
2222
+ content: ()=>generateLandingContent(entry.text, description, children, color),
2223
+ outputPath: linkToOutputPath(entry.link).replace(/\.md$/, '.mdx'),
2224
+ frontmatter: {}
2225
+ };
2226
+ } else if (!entry.items || 0 === entry.items.length) {
2227
+ const matching = workspaceItems.filter((item)=>item.docsPrefix.startsWith(`${entry.link}/`));
2228
+ if (matching.length > 0) {
2229
+ const segments = entry.link.split('/');
2230
+ const lastSegment = segments.findLast((seg)=>seg.length > 0);
2231
+ const scope = `${lastSegment}/`;
2232
+ entry.page = {
2233
+ content: ()=>generateWorkspaceLandingPage(entry.text, description, matching, scope),
2234
+ outputPath: linkToOutputPath(entry.link).replace(/\.md$/, '.mdx'),
2235
+ frontmatter: {}
2236
+ };
2237
+ }
2238
+ if (0 === matching.length) {
2239
+ const entryLink = entry.link;
2240
+ const exact = workspaceItems.find((item)=>item.docsPrefix === entryLink);
2241
+ if (exact) entry.page = {
2242
+ content: ()=>`# ${exact.text}\n\n${exact.description}\n`,
2243
+ outputPath: linkToOutputPath(entryLink),
2244
+ frontmatter: {}
2245
+ };
2246
+ }
2247
+ }
2248
+ }
2249
+ if (entry.items) injectLandingPages(entry.items, configEntries, workspaceItems, colorIndex);
2250
+ }, void 0);
2251
+ }
2252
+ function generateWorkspaceLandingPage(heading, description, items, scopePrefix) {
2253
+ const imports = "import { WorkspaceCard, WorkspaceGrid } from '@zpress/ui/theme'\n\n";
2254
+ const cards = items.map((item)=>{
2255
+ const tags = resolveTags(item.tags);
2256
+ return landing_buildWorkspaceCardJsx({
2257
+ link: item.docsPrefix,
2258
+ text: item.text,
2259
+ icon: item.icon,
2260
+ iconColor: item.iconColor,
2261
+ scope: scopePrefix,
2262
+ description: item.description,
2263
+ tags,
2264
+ badge: item.badge
2265
+ });
2266
+ });
2267
+ const descLine = match(description).with(P.nonNullable, (d)=>`\n${d}\n`).otherwise(()=>'');
2268
+ return `${imports}# ${heading}\n${descLine}\n<WorkspaceGrid>\n${cards.join('\n')}\n</WorkspaceGrid>\n`;
2269
+ }
2270
+ function findConfigEntry(entries, link) {
2271
+ const direct = entries.find((entry)=>entry.link === link);
2272
+ if (direct) return direct;
2273
+ const nested = entries.filter((entry)=>null !== entry.items && void 0 !== entry.items).map((entry)=>findConfigEntry(entry.items, link)).find((result)=>null != result);
2274
+ return nested;
2275
+ }
2276
+ function resolveExt(source) {
2277
+ if (source) return sourceExt(source);
2278
+ return '.md';
2279
+ }
2280
+ function inject_resolveDescription(configEntry) {
2281
+ if (null != configEntry && null !== configEntry.frontmatter && void 0 !== configEntry.frontmatter) return configEntry.frontmatter.description;
2282
+ }
2283
+ function checkHasSelfLinkedChild(items, link) {
2284
+ if (items) return items.some((child)=>child.link === link);
2285
+ return false;
2286
+ }
2287
+ function resolveTags(tags) {
2288
+ if (tags) return [
2289
+ ...tags
2290
+ ];
2291
+ }
2292
+ function buildMultiSidebar(resolved, openapiSidebar, icons) {
2293
+ const rootEntries = resolved.filter((e)=>!e.isolated);
2294
+ const isolatedEntries = resolved.filter((e)=>e.isolated && e.link);
2295
+ const docsSidebar = generateSidebar(rootEntries, icons);
2296
+ const childrenByLink = new Map(isolatedEntries.map((entry)=>{
2297
+ const link = entry.link;
2298
+ const items = resolveEntryItems(entry.items);
2299
+ return [
2300
+ link,
2301
+ generateSidebar(items)
2302
+ ];
2303
+ }));
2304
+ const isolatedSidebar = Object.fromEntries(isolatedEntries.flatMap((entry)=>{
2305
+ const entryLink = entry.link;
2306
+ const children = resolveChildrenByLink(childrenByLink, entryLink);
2307
+ const icon = icons.get(entry.text);
2308
+ const parentLink = resolveParentLink(entryLink);
2309
+ const parentEntry = match(parentLink).with(P.nonNullable, (pl)=>isolatedEntries.find((e)=>e.link === pl)).otherwise(()=>{});
2310
+ const isChild = null != parentEntry && parentEntry !== entry;
2311
+ const landing = {
2312
+ text: entry.text,
2313
+ link: entryLink,
2314
+ ...multi_maybeIcon(icon)
2315
+ };
2316
+ const sidebarItems = match(isChild).with(true, ()=>{
2317
+ const pe = parentEntry;
2318
+ const peLink = pe.link;
2319
+ const parentIcon = icons.get(pe.text);
2320
+ const parentLanding = {
2321
+ text: pe.text,
2322
+ link: peLink,
2323
+ ...multi_maybeIcon(parentIcon)
2324
+ };
2325
+ const siblings = isolatedEntries.filter((sib)=>{
2326
+ const sibLink = sib.link;
2327
+ return sib.link !== peLink && sibLink.startsWith(`${peLink}/`);
2328
+ });
2329
+ const siblingGroups = siblings.map((sib)=>{
2330
+ const sibLink = sib.link;
2331
+ const sibChildren = resolveChildrenByLink(childrenByLink, sibLink);
2332
+ const isCurrent = sib.link === entry.link;
2333
+ return buildSidebarGroup(sib.text, sibLink, sibChildren, !isCurrent);
2334
+ });
2335
+ return [
2336
+ parentLanding,
2337
+ ...siblingGroups
2338
+ ];
2339
+ }).otherwise(()=>{
2340
+ const childGroups = match(0 === children.length).with(true, ()=>isolatedEntries.filter((child)=>{
2341
+ const childLink = child.link;
2342
+ return child.link !== entry.link && childLink.startsWith(`${entryLink}/`);
2343
+ }).map((child)=>{
2344
+ const childLink = child.link;
2345
+ const childItems = resolveChildrenByLink(childrenByLink, childLink);
2346
+ return buildSidebarGroup(child.text, childLink, childItems, true);
2347
+ })).otherwise(()=>[]);
2348
+ return [
2349
+ landing,
2350
+ ...children,
2351
+ ...childGroups
2352
+ ];
2353
+ });
2354
+ return [
2355
+ [
2356
+ `${entryLink}/`,
2357
+ sidebarItems
2358
+ ],
2359
+ [
2360
+ entryLink,
2361
+ sidebarItems
2362
+ ]
2363
+ ];
2364
+ }));
2365
+ const openapiEntries = buildOpenapiSidebarEntries(openapiSidebar);
2366
+ const sidebar = {
2367
+ '/': docsSidebar,
2368
+ ...isolatedSidebar,
2369
+ ...openapiEntries
2370
+ };
2371
+ const sortedKeys = Object.keys(sidebar).toSorted((a, b)=>b.length - a.length);
2372
+ return Object.fromEntries(sortedKeys.map((key)=>[
2373
+ key,
2374
+ sidebar[key]
2375
+ ]));
2376
+ }
2377
+ function resolveEntryItems(items) {
2378
+ if (items) return [
2379
+ ...items
2380
+ ];
2381
+ return [];
2382
+ }
2383
+ function resolveChildrenByLink(childrenByLink, link) {
2384
+ const got = childrenByLink.get(link);
2385
+ if (got) return got;
2386
+ return [];
2387
+ }
2388
+ function resolveParentLink(entryLink) {
2389
+ const segments = entryLink.split('/').slice(0, -1).join('/');
2390
+ if (segments) return segments;
2391
+ return null;
2392
+ }
2393
+ function multi_maybeIcon(icon) {
2394
+ if (icon) return {
2395
+ icon
2396
+ };
2397
+ return {};
2398
+ }
2399
+ function buildSidebarGroup(text, link, children, collapsed) {
2400
+ if (children.length > 0) return {
2401
+ text,
2402
+ link,
2403
+ collapsed,
2404
+ items: children
2405
+ };
2406
+ return {
2407
+ text,
2408
+ link
2409
+ };
2410
+ }
2411
+ function buildOpenapiSidebarEntries(_openapiSidebar) {
2412
+ return {};
2413
+ }
2414
+ function enrichWorkspaceCards(entries, config) {
2415
+ const workspaceGroupItems = (config.workspaces ?? []).flatMap((g)=>g.items);
2416
+ const items = [
2417
+ ...config.apps ?? [],
2418
+ ...config.packages ?? [],
2419
+ ...workspaceGroupItems
2420
+ ];
2421
+ if (0 === items.length) return [
2422
+ ...entries
2423
+ ];
2424
+ return enrichEntries(entries, items);
2425
+ }
2426
+ function synthesizeWorkspaceSections(config) {
2427
+ const existingLinks = collectAllLinks(config.sections);
2428
+ const apps = config.apps ?? [];
2429
+ const packages = config.packages ?? [];
2430
+ const groups = config.workspaces ?? [];
2431
+ const appsEntry = match(apps.length > 0 && !existingLinks.has('/apps')).with(true, ()=>({
2432
+ text: 'Apps',
2433
+ link: '/apps',
2434
+ isolated: true,
2435
+ icon: 'pixelarticons:device-laptop',
2436
+ frontmatter: {
2437
+ description: 'Deployable applications that make up the platform.'
2438
+ },
2439
+ items: apps.filter((item)=>!existingLinks.has(item.docsPrefix)).map((item)=>workspaceItemToEntry(item))
2440
+ })).otherwise(()=>null);
2441
+ const packagesEntry = match(packages.length > 0 && !existingLinks.has('/packages')).with(true, ()=>({
2442
+ text: 'Packages',
2443
+ link: '/packages',
2444
+ isolated: true,
2445
+ icon: 'pixelarticons:archive',
2446
+ frontmatter: {
2447
+ description: 'Shared libraries and utilities consumed across apps and services.'
2448
+ },
2449
+ items: packages.filter((item)=>!existingLinks.has(item.docsPrefix)).map((item)=>workspaceItemToEntry(item))
2450
+ })).otherwise(()=>null);
2451
+ const groupEntries = groups.map((group)=>{
2452
+ const link = group.link ?? `/${slugify(group.name)}`;
2453
+ if (existingLinks.has(link)) return null;
2454
+ return {
2455
+ text: group.name,
2456
+ link,
2457
+ isolated: true,
2458
+ icon: group.icon,
2459
+ frontmatter: {
2460
+ description: group.description
2461
+ },
2462
+ items: group.items.filter((item)=>!existingLinks.has(item.docsPrefix)).map((item)=>workspaceItemToEntry(item))
2463
+ };
2464
+ });
2465
+ return [
2466
+ appsEntry,
2467
+ packagesEntry,
2468
+ ...groupEntries
2469
+ ].filter((entry)=>null !== entry);
2470
+ }
2471
+ function collectAllLinks(sections) {
2472
+ return new Set(sections.flatMap((entry)=>{
2473
+ const self = collectSelfLinks(entry.link);
2474
+ const nested = collectNestedLinks(entry.items);
2475
+ return [
2476
+ ...self,
2477
+ ...nested
2478
+ ];
2479
+ }));
2480
+ }
2481
+ function slugify(text) {
2482
+ return text.toLowerCase().replaceAll(/[^a-z0-9]+/g, '-').replaceAll(/^-+|-+$/g, '');
2483
+ }
2484
+ function enrichEntries(entries, items) {
2485
+ return entries.map((entry)=>{
2486
+ const enrichedItems = resolveEnrichedItems(entry.items, items);
2487
+ if (entry.link && !entry.card) {
2488
+ const entryLink = entry.link;
2489
+ const matched = items.find((item)=>entryLink === item.docsPrefix);
2490
+ if (matched) {
2491
+ const scope = deriveScope(matched.docsPrefix);
2492
+ const tags = workspace_resolveTags(matched.tags);
2493
+ const badge = resolveBadge(matched.badge);
2494
+ return {
2495
+ ...entry,
2496
+ items: enrichedItems,
2497
+ card: {
2498
+ icon: matched.icon,
2499
+ iconColor: matched.iconColor,
2500
+ scope,
2501
+ description: matched.description,
2502
+ tags,
2503
+ badge
2504
+ }
2505
+ };
2506
+ }
2507
+ }
2508
+ if (enrichedItems) return {
2509
+ ...entry,
2510
+ items: enrichedItems
2511
+ };
2512
+ return entry;
2513
+ });
2514
+ }
2515
+ function deriveScope(docsPrefix) {
2516
+ const segments = docsPrefix.split('/').filter(Boolean);
2517
+ if (segments.length > 0) return `${segments[0]}/`;
2518
+ return '';
2519
+ }
2520
+ function workspaceItemToEntry(item) {
2521
+ const base = {
2522
+ text: item.text,
2523
+ link: item.docsPrefix
2524
+ };
2525
+ return applyOptionalFields(base, item);
2526
+ }
2527
+ function applyOptionalFields(base, item) {
2528
+ const fromPattern = item.from ?? 'docs/*.md';
2529
+ const basePath = item.docsPrefix.replace(/^\//, '');
2530
+ const resolvedFrom = `${basePath}/${fromPattern}`;
2531
+ return omitBy({
2532
+ ...base,
2533
+ from: resolvedFrom,
2534
+ prefix: item.docsPrefix,
2535
+ items: item.items,
2536
+ sort: item.sort,
2537
+ textFrom: item.textFrom,
2538
+ textTransform: item.textTransform,
2539
+ recursive: item.recursive,
2540
+ indexFile: resolveIndexFile(item.recursive, item.indexFile),
2541
+ exclude: resolveExclude(item.exclude),
2542
+ collapsible: item.collapsible,
2543
+ frontmatter: item.frontmatter
2544
+ }, isUndefined);
2545
+ }
2546
+ function collectSelfLinks(link) {
2547
+ if (null != link) return [
2548
+ link
2549
+ ];
2550
+ return [];
2551
+ }
2552
+ function collectNestedLinks(items) {
2553
+ if (items) return [
2554
+ ...collectAllLinks(items)
2555
+ ];
2556
+ return [];
2557
+ }
2558
+ function resolveEnrichedItems(items, workspaceItems) {
2559
+ if (items) return enrichEntries(items, workspaceItems);
2560
+ }
2561
+ function workspace_resolveTags(tags) {
2562
+ if (tags) return [
2563
+ ...tags
2564
+ ];
2565
+ }
2566
+ function resolveBadge(badge) {
2567
+ if (badge) return {
2568
+ src: badge.src,
2569
+ alt: badge.alt
2570
+ };
2571
+ }
2572
+ function resolveIndexFile(recursive, indexFile) {
2573
+ if (recursive) return indexFile;
2574
+ }
2575
+ function resolveExclude(exclude) {
2576
+ if (exclude) return [
2577
+ ...exclude
2578
+ ];
2579
+ }
2580
+ async function sync(config, options) {
2581
+ const start = performance.now();
2582
+ const quiet = resolveQuiet(options.quiet);
2583
+ const { repoRoot, contentDir: outDir } = options.paths;
2584
+ await promises.mkdir(outDir, {
2585
+ recursive: true
2586
+ });
2587
+ await promises.mkdir(node_path.resolve(outDir, '.generated'), {
2588
+ recursive: true
2589
+ });
2590
+ await seedDefaultAssets(options.paths.publicDir);
2591
+ const assetConfig = buildAssetConfig(config);
2592
+ if (assetConfig) await generateAssets({
2593
+ config: assetConfig,
2594
+ publicDir: options.paths.publicDir
2595
+ });
2596
+ await copyAll(options.paths.publicDir, node_path.resolve(outDir, 'public'));
2597
+ const previousManifest = await loadManifest(outDir);
2598
+ const ctx = {
2599
+ repoRoot,
2600
+ outDir,
2601
+ config,
2602
+ previousManifest,
2603
+ manifest: {
2604
+ files: {},
2605
+ timestamp: Date.now()
2606
+ },
2607
+ quiet
2608
+ };
2609
+ const workspaceSections = synthesizeWorkspaceSections(config);
2610
+ const allSections = [
2611
+ ...config.sections,
2612
+ ...workspaceSections
2613
+ ];
2614
+ const [resolveErr, rawResolved] = await resolveEntries(allSections, ctx);
2615
+ if (resolveErr) {
2616
+ log.error(`[zpress] ${resolveErr.message}`);
2617
+ return {
2618
+ pagesWritten: 0,
2619
+ pagesSkipped: 0,
2620
+ pagesRemoved: 0,
2621
+ elapsed: performance.now() - start
2622
+ };
2623
+ }
2624
+ const resolved = enrichWorkspaceCards(rawResolved, config);
2625
+ const workspaceGroupItems = (config.workspaces ?? []).flatMap((g)=>g.items);
2626
+ const workspaceItems = [
2627
+ ...config.apps ?? [],
2628
+ ...config.packages ?? [],
2629
+ ...workspaceGroupItems
2630
+ ];
2631
+ injectLandingPages(resolved, allSections, workspaceItems);
2632
+ const sectionPages = collectPages(resolved);
2633
+ const hasExplicitHome = sectionPages.some((p)=>'index.md' === p.outputPath);
2634
+ const homeResult = await match(hasExplicitHome).with(true, ()=>Promise.resolve(null)).otherwise(()=>generateDefaultHomePage(config, repoRoot));
2635
+ await match(homeResult).with(P.nonNullable, async (result)=>{
2636
+ await promises.writeFile(node_path.resolve(outDir, '.generated/workspaces.json'), JSON.stringify(result.workspaces, null, 2), 'utf8');
2637
+ }).otherwise(()=>Promise.resolve());
2638
+ const pages = match(homeResult).with(P.nonNullable, (result)=>[
2639
+ ...sectionPages,
2640
+ {
2641
+ content: result.content,
2642
+ outputPath: 'index.md',
2643
+ frontmatter: {}
2644
+ }
2645
+ ]).otherwise(()=>sectionPages);
2646
+ const planningPages = await discoverPlanningPages(ctx);
2647
+ const openapiSidebar = [];
2648
+ const allPages = [
2649
+ ...pages,
2650
+ ...planningPages
2651
+ ];
2652
+ const sourceMap = buildSourceMap({
2653
+ pages: allPages,
2654
+ repoRoot
2655
+ });
2656
+ const copyCtx = {
2657
+ ...ctx,
2658
+ sourceMap
2659
+ };
2660
+ const { written, skipped } = await allPages.reduce(async (accPromise, page)=>{
2661
+ const counts = await accPromise;
2662
+ const entry = await copyPage(page, copyCtx);
2663
+ const prevFile = match(previousManifest).with(P.nonNullable, (m)=>m.files[entry.outputPath]).otherwise(()=>{});
2664
+ const isNew = entry.contentHash !== match(prevFile).with(P.nonNullable, (p)=>p.contentHash).otherwise(()=>{});
2665
+ ctx.manifest.files[entry.outputPath] = entry;
2666
+ if (isNew) return {
2667
+ written: counts.written + 1,
2668
+ skipped: counts.skipped
2669
+ };
2670
+ return {
2671
+ written: counts.written,
2672
+ skipped: counts.skipped + 1
2673
+ };
2674
+ }, Promise.resolve({
2675
+ written: 0,
2676
+ skipped: 0
2677
+ }));
2678
+ const removed = await match(previousManifest).with(P.nonNullable, async (m)=>await cleanStaleFiles(outDir, m, ctx.manifest)).otherwise(()=>Promise.resolve(0));
2679
+ const icons = buildIconMap(allSections);
2680
+ const sortedSidebar = buildMultiSidebar(resolved, openapiSidebar, icons);
2681
+ const nav = generateNav(config, resolved, icons);
2682
+ await promises.writeFile(node_path.resolve(outDir, '.generated/sidebar.json'), JSON.stringify(sortedSidebar, null, 2), 'utf8');
2683
+ await promises.writeFile(node_path.resolve(outDir, '.generated/nav.json'), JSON.stringify(nav, null, 2), 'utf8');
2684
+ await saveManifest(outDir, ctx.manifest);
2685
+ await writeZpressReadme(options.paths.outputRoot);
2686
+ const elapsed = performance.now() - start;
2687
+ if (!quiet) log.success(`Sync complete: ${written} written, ${skipped} unchanged, ${removed} removed (${elapsed.toFixed(0)}ms)`);
2688
+ return {
2689
+ pagesWritten: written,
2690
+ pagesSkipped: skipped,
2691
+ pagesRemoved: removed,
2692
+ elapsed
2693
+ };
2694
+ }
2695
+ function collectPages(entries) {
2696
+ return entries.reduce((pages, entry)=>{
2697
+ const withPage = concatPage(pages, entry.page);
2698
+ if (entry.items) return [
2699
+ ...withPage,
2700
+ ...collectPages(entry.items)
2701
+ ];
2702
+ return withPage;
2703
+ }, []);
2704
+ }
2705
+ async function writeZpressReadme(outputRoot) {
2706
+ const readmePath = node_path.resolve(outputRoot, 'README.md');
2707
+ const content = `# .zpress
2708
+
2709
+ This directory is managed by zpress. It contains the
2710
+ materialized documentation site — synced content, build artifacts, and static assets.
2711
+
2712
+ | Directory | Description | Tracked |
2713
+ | ----------- | ---------------------------------------------- | ------- |
2714
+ | \`content/\` | Synced markdown pages and generated config | No |
2715
+ | \`public/\` | Static assets (logos, icons, banners) | Yes |
2716
+ | \`dist/\` | Build output | No |
2717
+ | \`cache/\` | Build cache | No |
2718
+
2719
+ ## Commands
2720
+
2721
+ \`\`\`bash
2722
+ zpress sync # Sync docs into content/
2723
+ zpress dev # Start dev server
2724
+ zpress build # Build static site
2725
+ \`\`\`
2726
+
2727
+ > **Do not edit files in \`content/\`** — they are regenerated on every sync.
2728
+ > Edit the source markdown in your workspace packages instead.
2729
+ `;
2730
+ await promises.writeFile(readmePath, content, 'utf8');
2731
+ }
2732
+ async function seedDefaultAssets(publicDir) {
2733
+ const defaultsDir = node_path.resolve(import.meta.dirname, '..', 'public');
2734
+ const exists = await promises.stat(defaultsDir).catch(()=>null);
2735
+ if (!exists) return;
2736
+ await copySeeded(defaultsDir, publicDir);
2737
+ }
2738
+ async function copySeeded(src, dest) {
2739
+ await promises.mkdir(dest, {
2740
+ recursive: true
2741
+ });
2742
+ const entries = await promises.readdir(src, {
2743
+ withFileTypes: true
2744
+ });
2745
+ await entries.reduce(async (prevPromise, entry)=>{
2746
+ await prevPromise;
2747
+ const srcPath = node_path.resolve(src, entry.name);
2748
+ const destPath = node_path.resolve(dest, entry.name);
2749
+ if (entry.isDirectory()) return void await copySeeded(srcPath, destPath);
2750
+ const shouldCopy = await isReplaceable(destPath);
2751
+ if (shouldCopy) await promises.copyFile(srcPath, destPath);
2752
+ }, Promise.resolve());
2753
+ }
2754
+ async function isReplaceable(filePath) {
2755
+ const content = await promises.readFile(filePath, 'utf8').catch(()=>null);
2756
+ if (null === content) return true;
2757
+ const [firstLine] = content.split('\n');
2758
+ return firstLine === GENERATED_MARKER;
2759
+ }
2760
+ async function copyAll(src, dest) {
2761
+ const exists = await promises.stat(src).catch(()=>null);
2762
+ if (!exists) return;
2763
+ await promises.mkdir(dest, {
2764
+ recursive: true
2765
+ });
2766
+ const entries = await promises.readdir(src, {
2767
+ withFileTypes: true
2768
+ });
2769
+ await entries.reduce(async (prevPromise, entry)=>{
2770
+ await prevPromise;
2771
+ const srcPath = node_path.resolve(src, entry.name);
2772
+ const destPath = node_path.resolve(dest, entry.name);
2773
+ if (entry.isDirectory()) await copyAll(srcPath, destPath);
2774
+ else await promises.copyFile(srcPath, destPath);
2775
+ }, Promise.resolve());
2776
+ }
2777
+ function buildIconMap(sections) {
2778
+ return new Map(sections.flatMap((section)=>{
2779
+ if (section.icon) return [
2780
+ [
2781
+ section.text,
2782
+ section.icon
2783
+ ]
2784
+ ];
2785
+ return [];
2786
+ }));
2787
+ }
2788
+ function resolveQuiet(quiet) {
2789
+ if (null != quiet) return quiet;
2790
+ return false;
2791
+ }
2792
+ function concatPage(pages, page) {
2793
+ if (page) return [
2794
+ ...pages,
2795
+ page
2796
+ ];
2797
+ return [
2798
+ ...pages
2799
+ ];
2800
+ }
2801
+ function buildAssetConfig(config) {
2802
+ if (!config.title) return null;
2803
+ return {
2804
+ title: config.title,
2805
+ tagline: config.tagline
2806
+ };
2807
+ }
2808
+ function createPaths(dir) {
2809
+ const repoRoot = node_path.resolve(dir);
2810
+ const outputRoot = node_path.resolve(repoRoot, '.zpress');
2811
+ return {
2812
+ repoRoot,
2813
+ outputRoot,
2814
+ contentDir: node_path.resolve(outputRoot, 'content'),
2815
+ publicDir: node_path.resolve(outputRoot, 'public'),
2816
+ distDir: node_path.resolve(outputRoot, 'dist'),
2817
+ cacheDir: node_path.resolve(outputRoot, 'cache')
2818
+ };
2819
+ }
2820
+ export { configError, config_loadConfig as loadConfig, createPaths, defineConfig, generateAssets, generateBannerSvg, generateLogoSvg, hasGlobChars, loadManifest, resolveEntries, sync, syncError };