@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.
- package/LICENSE +21 -0
- package/dist/index.mjs +2820 -0
- 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('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"');
|
|
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">█</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('"', '"').replaceAll('{', '{').replaceAll('}', '}');
|
|
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 };
|