@zpress/cli 0.3.3 ā 0.3.4
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/dist/145.mjs +774 -0
- package/dist/index.mjs +1 -725
- package/dist/watcher.mjs +8 -11
- package/package.json +7 -5
package/dist/145.mjs
ADDED
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
import { cli, command } from "@kidd-cli/core";
|
|
2
|
+
import { configError, createPaths, generateAssets, loadConfig, loadManifest, resolveEntries, sync } from "@zpress/core";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import node_path from "node:path";
|
|
5
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
6
|
+
import { platform } from "node:os";
|
|
7
|
+
import { build, dev, serve } from "@rspress/core";
|
|
8
|
+
import { createRspressConfig } from "@zpress/ui";
|
|
9
|
+
import { P, match as external_ts_pattern_match } from "ts-pattern";
|
|
10
|
+
import promises from "node:fs/promises";
|
|
11
|
+
import { compact } from "es-toolkit";
|
|
12
|
+
import { createRegistry, render, toSlug } from "@zpress/templates";
|
|
13
|
+
import node_fs from "node:fs";
|
|
14
|
+
function toError(error) {
|
|
15
|
+
if (error instanceof Error) return error;
|
|
16
|
+
return new Error(String(error));
|
|
17
|
+
}
|
|
18
|
+
const DEFAULT_PORT = 6174;
|
|
19
|
+
async function startDevServer(options) {
|
|
20
|
+
const { paths } = options;
|
|
21
|
+
let serverInstance = null;
|
|
22
|
+
async function startServer(config) {
|
|
23
|
+
const rspressConfig = createRspressConfig({
|
|
24
|
+
config,
|
|
25
|
+
paths
|
|
26
|
+
});
|
|
27
|
+
try {
|
|
28
|
+
serverInstance = await dev({
|
|
29
|
+
appDirectory: paths.repoRoot,
|
|
30
|
+
docDirectory: paths.contentDir,
|
|
31
|
+
config: rspressConfig,
|
|
32
|
+
configFilePath: '',
|
|
33
|
+
extraBuilderConfig: {
|
|
34
|
+
server: {
|
|
35
|
+
port: DEFAULT_PORT
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return true;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
process.stderr.write(`Dev server error: ${toError(error).message}\n`);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const started = await startServer(options.config);
|
|
46
|
+
if (!started) process.exit(1);
|
|
47
|
+
return async (newConfig)=>{
|
|
48
|
+
process.stdout.write('\nš Config changed ā restarting dev server...\n');
|
|
49
|
+
if (serverInstance) {
|
|
50
|
+
try {
|
|
51
|
+
await serverInstance.close();
|
|
52
|
+
} catch (error) {
|
|
53
|
+
process.stderr.write(`Error closing server: ${toError(error).message}\n`);
|
|
54
|
+
}
|
|
55
|
+
serverInstance = null;
|
|
56
|
+
}
|
|
57
|
+
const restarted = await startServer(newConfig);
|
|
58
|
+
if (restarted) process.stdout.write('ā
Dev server restarted\n\n');
|
|
59
|
+
else process.stderr.write('ā ļø Dev server failed to restart ā fix the config and save again\n\n');
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function buildSite(options) {
|
|
63
|
+
const rspressConfig = createRspressConfig(options);
|
|
64
|
+
await build({
|
|
65
|
+
docDirectory: options.paths.contentDir,
|
|
66
|
+
config: rspressConfig,
|
|
67
|
+
configFilePath: ''
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async function buildSiteForCheck(options) {
|
|
71
|
+
const rspressConfig = createRspressConfig(options);
|
|
72
|
+
await build({
|
|
73
|
+
docDirectory: options.paths.contentDir,
|
|
74
|
+
config: rspressConfig,
|
|
75
|
+
configFilePath: ''
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async function serveSite(options) {
|
|
79
|
+
const rspressConfig = createRspressConfig(options);
|
|
80
|
+
await serve({
|
|
81
|
+
config: rspressConfig,
|
|
82
|
+
configFilePath: '',
|
|
83
|
+
port: DEFAULT_PORT
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function openBrowser(url) {
|
|
87
|
+
const os = platform();
|
|
88
|
+
const { cmd, args } = external_ts_pattern_match(os).with('darwin', ()=>({
|
|
89
|
+
cmd: 'open',
|
|
90
|
+
args: [
|
|
91
|
+
url
|
|
92
|
+
]
|
|
93
|
+
})).with('win32', ()=>({
|
|
94
|
+
cmd: 'cmd',
|
|
95
|
+
args: [
|
|
96
|
+
'/c',
|
|
97
|
+
'start',
|
|
98
|
+
url
|
|
99
|
+
]
|
|
100
|
+
})).otherwise(()=>({
|
|
101
|
+
cmd: 'xdg-open',
|
|
102
|
+
args: [
|
|
103
|
+
url
|
|
104
|
+
]
|
|
105
|
+
}));
|
|
106
|
+
spawn(cmd, args, {
|
|
107
|
+
stdio: 'ignore',
|
|
108
|
+
detached: true
|
|
109
|
+
}).unref();
|
|
110
|
+
}
|
|
111
|
+
const ANSI_PATTERN = /\u001B\[[0-9;]*m/g;
|
|
112
|
+
const RED = '\u001B[31m';
|
|
113
|
+
const DIM = '\u001B[2m';
|
|
114
|
+
const RESET = '\u001B[0m';
|
|
115
|
+
function runConfigCheck(params) {
|
|
116
|
+
const { config, loadError } = params;
|
|
117
|
+
if (loadError) return {
|
|
118
|
+
passed: false,
|
|
119
|
+
errors: [
|
|
120
|
+
loadError
|
|
121
|
+
]
|
|
122
|
+
};
|
|
123
|
+
if (!config) return {
|
|
124
|
+
passed: false,
|
|
125
|
+
errors: [
|
|
126
|
+
configError('empty_sections', 'Config is missing')
|
|
127
|
+
]
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
passed: true,
|
|
131
|
+
errors: []
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async function runBuildCheck(params) {
|
|
135
|
+
const { error, captured } = await captureOutput(()=>buildSiteForCheck({
|
|
136
|
+
config: params.config,
|
|
137
|
+
paths: params.paths
|
|
138
|
+
}));
|
|
139
|
+
if (error) {
|
|
140
|
+
const { repoRoot } = params.paths;
|
|
141
|
+
const deadlinks = parseDeadlinks(captured).map((info)=>({
|
|
142
|
+
file: node_path.relative(repoRoot, info.file),
|
|
143
|
+
links: info.links
|
|
144
|
+
}));
|
|
145
|
+
if (deadlinks.length > 0) return {
|
|
146
|
+
status: 'failed',
|
|
147
|
+
deadlinks
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
status: 'error',
|
|
151
|
+
message: error.message
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
status: 'passed'
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function presentResults(params) {
|
|
159
|
+
const { configResult, buildResult, logger } = params;
|
|
160
|
+
if (configResult.passed) logger.success('Config valid');
|
|
161
|
+
else {
|
|
162
|
+
logger.error('Config validation failed:');
|
|
163
|
+
configResult.errors.map((err)=>{
|
|
164
|
+
logger.message(` ${err.message}`);
|
|
165
|
+
return null;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if ('passed' === buildResult.status) logger.success('No broken links');
|
|
169
|
+
else if ('skipped' === buildResult.status) ;
|
|
170
|
+
else if ('error' === buildResult.status) logger.error(`Build failed: ${buildResult.message}`);
|
|
171
|
+
else {
|
|
172
|
+
const totalLinks = buildResult.deadlinks.reduce((sum, info)=>sum + info.links.length, 0);
|
|
173
|
+
logger.error(`Found ${totalLinks} broken link(s):`);
|
|
174
|
+
const block = buildResult.deadlinks.map(formatDeadlinkGroup).join('\n');
|
|
175
|
+
logger.message(block);
|
|
176
|
+
}
|
|
177
|
+
return configResult.passed && 'passed' === buildResult.status;
|
|
178
|
+
}
|
|
179
|
+
function stripAnsi(text) {
|
|
180
|
+
return text.replace(ANSI_PATTERN, '');
|
|
181
|
+
}
|
|
182
|
+
function chunkToString(chunk) {
|
|
183
|
+
if ('string' == typeof chunk) return chunk;
|
|
184
|
+
return Buffer.from(chunk).toString('utf8');
|
|
185
|
+
}
|
|
186
|
+
function createInterceptor(chunks) {
|
|
187
|
+
return function(chunk, encodingOrCb, maybeCb) {
|
|
188
|
+
const text = chunkToString(chunk);
|
|
189
|
+
chunks.push(text);
|
|
190
|
+
if ('function' == typeof encodingOrCb) encodingOrCb();
|
|
191
|
+
else if ('function' == typeof maybeCb) maybeCb();
|
|
192
|
+
return true;
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async function captureOutput(fn) {
|
|
196
|
+
const chunks = [];
|
|
197
|
+
const originalStdoutWrite = process.stdout.write;
|
|
198
|
+
const originalStderrWrite = process.stderr.write;
|
|
199
|
+
process.stdout.write = createInterceptor(chunks);
|
|
200
|
+
process.stderr.write = createInterceptor(chunks);
|
|
201
|
+
try {
|
|
202
|
+
const result = await fn();
|
|
203
|
+
return {
|
|
204
|
+
result,
|
|
205
|
+
error: null,
|
|
206
|
+
captured: chunks.join('')
|
|
207
|
+
};
|
|
208
|
+
} catch (error) {
|
|
209
|
+
return {
|
|
210
|
+
result: null,
|
|
211
|
+
error: toError(error),
|
|
212
|
+
captured: chunks.join('')
|
|
213
|
+
};
|
|
214
|
+
} finally{
|
|
215
|
+
process.stdout.write = originalStdoutWrite;
|
|
216
|
+
process.stderr.write = originalStderrWrite;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function flushGroup(results, file, links) {
|
|
220
|
+
if (file && links.length > 0) return [
|
|
221
|
+
...results,
|
|
222
|
+
{
|
|
223
|
+
file,
|
|
224
|
+
links
|
|
225
|
+
}
|
|
226
|
+
];
|
|
227
|
+
return results;
|
|
228
|
+
}
|
|
229
|
+
function parseDeadlinks(stderr) {
|
|
230
|
+
const clean = stripAnsi(stderr);
|
|
231
|
+
const lines = clean.split('\n');
|
|
232
|
+
const headerPattern = /Dead links found in (.+?):\s*$/;
|
|
233
|
+
const linkPattern = /"\[\.\.]\(([^)]+)\)"/;
|
|
234
|
+
const acc = lines.reduce((state, line)=>{
|
|
235
|
+
const headerMatch = headerPattern.exec(line);
|
|
236
|
+
if (headerMatch) {
|
|
237
|
+
const file = headerMatch[1] ?? '';
|
|
238
|
+
return {
|
|
239
|
+
results: flushGroup(state.results, state.currentFile, state.currentLinks),
|
|
240
|
+
currentFile: file,
|
|
241
|
+
currentLinks: []
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const linkMatch = linkPattern.exec(line);
|
|
245
|
+
if (linkMatch && state.currentFile) {
|
|
246
|
+
const link = linkMatch[1] ?? '';
|
|
247
|
+
return {
|
|
248
|
+
...state,
|
|
249
|
+
currentLinks: [
|
|
250
|
+
...state.currentLinks,
|
|
251
|
+
link
|
|
252
|
+
]
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return state;
|
|
256
|
+
}, {
|
|
257
|
+
results: [],
|
|
258
|
+
currentFile: null,
|
|
259
|
+
currentLinks: []
|
|
260
|
+
});
|
|
261
|
+
return flushGroup(acc.results, acc.currentFile, acc.currentLinks);
|
|
262
|
+
}
|
|
263
|
+
function formatDeadlinkGroup(info) {
|
|
264
|
+
const header = ` ${RED}ā${RESET} ${info.file}`;
|
|
265
|
+
const links = info.links.map((link)=>` ${DIM}ā${RESET} ${link}`);
|
|
266
|
+
return [
|
|
267
|
+
header,
|
|
268
|
+
...links
|
|
269
|
+
].join('\n');
|
|
270
|
+
}
|
|
271
|
+
async function clean_clean(paths) {
|
|
272
|
+
const results = await Promise.all(cleanTargets(paths).map(async ({ dir, label })=>{
|
|
273
|
+
const exists = await promises.stat(dir).catch(()=>null);
|
|
274
|
+
if (exists) {
|
|
275
|
+
await promises.rm(dir, {
|
|
276
|
+
recursive: true,
|
|
277
|
+
force: true
|
|
278
|
+
});
|
|
279
|
+
return label;
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}));
|
|
283
|
+
return compact(results);
|
|
284
|
+
}
|
|
285
|
+
const cleanCommand = command({
|
|
286
|
+
description: 'Remove build artifacts, synced content, and build cache',
|
|
287
|
+
handler: async (ctx)=>{
|
|
288
|
+
const paths = createPaths(process.cwd());
|
|
289
|
+
ctx.logger.intro('zpress clean');
|
|
290
|
+
const removed = await clean_clean(paths);
|
|
291
|
+
if (removed.length > 0) ctx.logger.success(`Removed: ${removed.join(', ')}`);
|
|
292
|
+
else ctx.logger.info('Nothing to clean');
|
|
293
|
+
ctx.logger.outro('Done');
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
function cleanTargets(paths) {
|
|
297
|
+
return [
|
|
298
|
+
{
|
|
299
|
+
dir: paths.cacheDir,
|
|
300
|
+
label: 'cache'
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
dir: paths.contentDir,
|
|
304
|
+
label: 'content'
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
dir: paths.distDir,
|
|
308
|
+
label: 'dist'
|
|
309
|
+
}
|
|
310
|
+
];
|
|
311
|
+
}
|
|
312
|
+
const buildCommand = command({
|
|
313
|
+
description: 'Run sync and build the Rspress site',
|
|
314
|
+
options: z.object({
|
|
315
|
+
quiet: z.boolean().optional().default(false),
|
|
316
|
+
clean: z.boolean().optional().default(false),
|
|
317
|
+
check: z.boolean().optional().default(true)
|
|
318
|
+
}),
|
|
319
|
+
handler: async (ctx)=>{
|
|
320
|
+
const { quiet, check } = ctx.args;
|
|
321
|
+
const paths = createPaths(process.cwd());
|
|
322
|
+
ctx.logger.intro('zpress build');
|
|
323
|
+
if (ctx.args.clean) {
|
|
324
|
+
const removed = await clean_clean(paths);
|
|
325
|
+
if (removed.length > 0 && !quiet) ctx.logger.info(`Cleaned: ${removed.join(', ')}`);
|
|
326
|
+
}
|
|
327
|
+
const [configErr, config] = await loadConfig(paths.repoRoot);
|
|
328
|
+
if (configErr) {
|
|
329
|
+
ctx.logger.error(configErr.message);
|
|
330
|
+
if (configErr.errors && configErr.errors.length > 0) configErr.errors.map((err)=>{
|
|
331
|
+
const path = err.path.join('.');
|
|
332
|
+
return ctx.logger.error(` ${path}: ${err.message}`);
|
|
333
|
+
});
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
if (check) {
|
|
337
|
+
ctx.logger.step('Validating config...');
|
|
338
|
+
const configResult = runConfigCheck({
|
|
339
|
+
config,
|
|
340
|
+
loadError: configErr
|
|
341
|
+
});
|
|
342
|
+
ctx.logger.step('Syncing content...');
|
|
343
|
+
await sync(config, {
|
|
344
|
+
paths,
|
|
345
|
+
quiet: true
|
|
346
|
+
});
|
|
347
|
+
ctx.logger.step('Building & checking for broken links...');
|
|
348
|
+
const buildResult = await runBuildCheck({
|
|
349
|
+
config,
|
|
350
|
+
paths
|
|
351
|
+
});
|
|
352
|
+
const passed = presentResults({
|
|
353
|
+
configResult,
|
|
354
|
+
buildResult,
|
|
355
|
+
logger: ctx.logger
|
|
356
|
+
});
|
|
357
|
+
if (!passed) {
|
|
358
|
+
ctx.logger.outro('Build failed');
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
ctx.logger.outro('Done');
|
|
362
|
+
} else {
|
|
363
|
+
await sync(config, {
|
|
364
|
+
paths,
|
|
365
|
+
quiet
|
|
366
|
+
});
|
|
367
|
+
await buildSite({
|
|
368
|
+
config,
|
|
369
|
+
paths
|
|
370
|
+
});
|
|
371
|
+
ctx.logger.outro('Done');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
const checkCommand = command({
|
|
376
|
+
description: 'Validate config and check for broken links',
|
|
377
|
+
handler: async (ctx)=>{
|
|
378
|
+
const paths = createPaths(process.cwd());
|
|
379
|
+
ctx.logger.intro('zpress check');
|
|
380
|
+
ctx.logger.step('Validating config...');
|
|
381
|
+
const [configErr, config] = await loadConfig(paths.repoRoot);
|
|
382
|
+
const configResult = runConfigCheck({
|
|
383
|
+
config,
|
|
384
|
+
loadError: configErr
|
|
385
|
+
});
|
|
386
|
+
if (configErr || !config) {
|
|
387
|
+
const buildResult = {
|
|
388
|
+
status: 'skipped'
|
|
389
|
+
};
|
|
390
|
+
presentResults({
|
|
391
|
+
configResult,
|
|
392
|
+
buildResult,
|
|
393
|
+
logger: ctx.logger
|
|
394
|
+
});
|
|
395
|
+
ctx.logger.outro('Checks failed');
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
ctx.logger.step('Syncing content...');
|
|
399
|
+
const syncResult = await sync(config, {
|
|
400
|
+
paths,
|
|
401
|
+
quiet: true
|
|
402
|
+
});
|
|
403
|
+
ctx.logger.success(`Synced (${syncResult.pagesWritten} written, ${syncResult.pagesSkipped} unchanged)`);
|
|
404
|
+
ctx.logger.step('Checking for broken links...');
|
|
405
|
+
const buildResult = await runBuildCheck({
|
|
406
|
+
config,
|
|
407
|
+
paths
|
|
408
|
+
});
|
|
409
|
+
const passed = presentResults({
|
|
410
|
+
configResult,
|
|
411
|
+
buildResult,
|
|
412
|
+
logger: ctx.logger
|
|
413
|
+
});
|
|
414
|
+
if (passed) ctx.logger.outro('All checks passed');
|
|
415
|
+
else {
|
|
416
|
+
ctx.logger.outro('Checks failed');
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
const devCommand = command({
|
|
422
|
+
description: 'Run sync + watcher and start Rspress dev server',
|
|
423
|
+
options: z.object({
|
|
424
|
+
quiet: z.boolean().optional().default(false),
|
|
425
|
+
clean: z.boolean().optional().default(false)
|
|
426
|
+
}),
|
|
427
|
+
handler: async (ctx)=>{
|
|
428
|
+
const { quiet } = ctx.args;
|
|
429
|
+
const paths = createPaths(process.cwd());
|
|
430
|
+
ctx.logger.intro('zpress dev');
|
|
431
|
+
if (ctx.args.clean) {
|
|
432
|
+
const removed = await clean_clean(paths);
|
|
433
|
+
if (removed.length > 0 && !quiet) ctx.logger.info(`Cleaned: ${removed.join(', ')}`);
|
|
434
|
+
}
|
|
435
|
+
const [configErr, config] = await loadConfig(paths.repoRoot);
|
|
436
|
+
if (configErr) {
|
|
437
|
+
ctx.logger.error(configErr.message);
|
|
438
|
+
if (configErr.errors && configErr.errors.length > 0) configErr.errors.forEach((err)=>{
|
|
439
|
+
const path = err.path.join('.');
|
|
440
|
+
ctx.logger.error(` ${path}: ${err.message}`);
|
|
441
|
+
});
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
await sync(config, {
|
|
445
|
+
paths,
|
|
446
|
+
quiet
|
|
447
|
+
});
|
|
448
|
+
const onConfigReload = await startDevServer({
|
|
449
|
+
config,
|
|
450
|
+
paths
|
|
451
|
+
});
|
|
452
|
+
const { createWatcher } = await import("./watcher.mjs");
|
|
453
|
+
const watcher = createWatcher(config, paths, onConfigReload);
|
|
454
|
+
function cleanup() {
|
|
455
|
+
watcher.close();
|
|
456
|
+
}
|
|
457
|
+
process.on('SIGINT', cleanup);
|
|
458
|
+
process.on('SIGTERM', cleanup);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
const registry = createRegistry();
|
|
462
|
+
const draftCommand = command({
|
|
463
|
+
description: 'Scaffold a new documentation file from a template',
|
|
464
|
+
options: z.object({
|
|
465
|
+
type: z.string().optional(),
|
|
466
|
+
title: z.string().optional(),
|
|
467
|
+
out: z.string().optional().default('.')
|
|
468
|
+
}),
|
|
469
|
+
handler: async (ctx)=>{
|
|
470
|
+
ctx.logger.intro('zpress draft');
|
|
471
|
+
const typeArg = ctx.args.type;
|
|
472
|
+
const hasValidType = external_ts_pattern_match(typeArg).with(P.string.minLength(1), (t)=>registry.has(t)).otherwise(()=>false);
|
|
473
|
+
const selectedType = await external_ts_pattern_match(hasValidType).with(true, ()=>Promise.resolve(typeArg)).otherwise(()=>ctx.prompts.select({
|
|
474
|
+
message: 'Select a doc type',
|
|
475
|
+
options: registry.list().map((t)=>({
|
|
476
|
+
value: t.type,
|
|
477
|
+
label: t.label,
|
|
478
|
+
hint: t.hint
|
|
479
|
+
}))
|
|
480
|
+
}));
|
|
481
|
+
const template = registry.get(selectedType);
|
|
482
|
+
if (!template) return void ctx.logger.error(`Unknown template type: ${selectedType}`);
|
|
483
|
+
const title = await external_ts_pattern_match(ctx.args.title).with(P.string.minLength(1), (t)=>Promise.resolve(t)).otherwise(()=>ctx.prompts.text({
|
|
484
|
+
message: 'Document title',
|
|
485
|
+
placeholder: 'e.g. Authentication',
|
|
486
|
+
validate: (value)=>{
|
|
487
|
+
if (!value || 0 === value.trim().length) return 'Title is required';
|
|
488
|
+
}
|
|
489
|
+
}));
|
|
490
|
+
const slug = toSlug(title);
|
|
491
|
+
if (0 === slug.length) return void ctx.logger.error('Title must include at least one letter or number');
|
|
492
|
+
const content = render(template, {
|
|
493
|
+
title
|
|
494
|
+
});
|
|
495
|
+
const filename = `${slug}.md`;
|
|
496
|
+
const outDir = node_path.resolve(process.cwd(), ctx.args.out);
|
|
497
|
+
const filePath = node_path.join(outDir, filename);
|
|
498
|
+
const exists = await promises.access(filePath).then(()=>true).catch(()=>false);
|
|
499
|
+
if (exists) return void ctx.logger.error(`File already exists: ${node_path.relative(process.cwd(), filePath)}`);
|
|
500
|
+
await promises.mkdir(outDir, {
|
|
501
|
+
recursive: true
|
|
502
|
+
});
|
|
503
|
+
await promises.writeFile(filePath, content, 'utf8');
|
|
504
|
+
ctx.logger.success(`Created ${node_path.relative(process.cwd(), filePath)}`);
|
|
505
|
+
ctx.logger.outro('Done');
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
const dumpCommand = command({
|
|
509
|
+
description: 'Resolve and print the full entry tree as JSON',
|
|
510
|
+
handler: async (ctx)=>{
|
|
511
|
+
const paths = createPaths(process.cwd());
|
|
512
|
+
const [configErr, config] = await loadConfig(paths.repoRoot);
|
|
513
|
+
if (configErr) {
|
|
514
|
+
ctx.logger.error(configErr.message);
|
|
515
|
+
if (configErr.errors && configErr.errors.length > 0) configErr.errors.map((err)=>{
|
|
516
|
+
const path = err.path.join('.');
|
|
517
|
+
return ctx.logger.error(` ${path}: ${err.message}`);
|
|
518
|
+
});
|
|
519
|
+
process.exit(1);
|
|
520
|
+
}
|
|
521
|
+
const previousManifest = await loadManifest(paths.contentDir);
|
|
522
|
+
const syncCtx = {
|
|
523
|
+
repoRoot: paths.repoRoot,
|
|
524
|
+
outDir: paths.contentDir,
|
|
525
|
+
config,
|
|
526
|
+
previousManifest,
|
|
527
|
+
manifest: {
|
|
528
|
+
files: {},
|
|
529
|
+
timestamp: Date.now()
|
|
530
|
+
},
|
|
531
|
+
quiet: true
|
|
532
|
+
};
|
|
533
|
+
const [resolveErr, resolved] = await resolveEntries(config.sections, syncCtx);
|
|
534
|
+
if (resolveErr) {
|
|
535
|
+
ctx.logger.error(resolveErr.message);
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
const tree = toTree(resolved);
|
|
539
|
+
process.stdout.write(`${JSON.stringify(tree, null, 2)}\n`);
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
function maybeLink(link) {
|
|
543
|
+
if (link) return {
|
|
544
|
+
link
|
|
545
|
+
};
|
|
546
|
+
return {};
|
|
547
|
+
}
|
|
548
|
+
function maybeCollapsible(collapsible) {
|
|
549
|
+
if (collapsible) return {
|
|
550
|
+
collapsible
|
|
551
|
+
};
|
|
552
|
+
return {};
|
|
553
|
+
}
|
|
554
|
+
function maybeHidden(hidden) {
|
|
555
|
+
if (hidden) return {
|
|
556
|
+
hidden
|
|
557
|
+
};
|
|
558
|
+
return {};
|
|
559
|
+
}
|
|
560
|
+
function maybeIsolated(isolated) {
|
|
561
|
+
if (isolated) return {
|
|
562
|
+
isolated
|
|
563
|
+
};
|
|
564
|
+
return {};
|
|
565
|
+
}
|
|
566
|
+
function maybeItems(items) {
|
|
567
|
+
if (items && items.length > 0) return {
|
|
568
|
+
items: toTree(items)
|
|
569
|
+
};
|
|
570
|
+
return {};
|
|
571
|
+
}
|
|
572
|
+
function toTree(entries) {
|
|
573
|
+
return entries.map(buildDumpEntry);
|
|
574
|
+
}
|
|
575
|
+
function buildDumpEntry(entry) {
|
|
576
|
+
return {
|
|
577
|
+
text: entry.title,
|
|
578
|
+
...maybeLink(entry.link),
|
|
579
|
+
...maybeCollapsible(entry.collapsible),
|
|
580
|
+
...maybeHidden(entry.hidden),
|
|
581
|
+
...maybeIsolated(entry.isolated),
|
|
582
|
+
...maybeItems(entry.items)
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
const generateCommand = command({
|
|
586
|
+
description: 'Generate banner, logo, and icon SVG assets from project title',
|
|
587
|
+
handler: async (ctx)=>{
|
|
588
|
+
ctx.logger.intro('zpress generate');
|
|
589
|
+
const paths = createPaths(process.cwd());
|
|
590
|
+
const [configErr, config] = await loadConfig(paths.repoRoot);
|
|
591
|
+
if (configErr) {
|
|
592
|
+
ctx.logger.error(configErr.message);
|
|
593
|
+
if (configErr.errors && configErr.errors.length > 0) configErr.errors.map((err)=>{
|
|
594
|
+
const path = err.path.join('.');
|
|
595
|
+
return ctx.logger.error(` ${path}: ${err.message}`);
|
|
596
|
+
});
|
|
597
|
+
process.exit(1);
|
|
598
|
+
}
|
|
599
|
+
const assetConfig = buildAssetConfig(config);
|
|
600
|
+
if (!assetConfig) {
|
|
601
|
+
ctx.logger.warn('No title configured ā skipping asset generation');
|
|
602
|
+
ctx.logger.outro('Done');
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
await promises.mkdir(paths.publicDir, {
|
|
606
|
+
recursive: true
|
|
607
|
+
});
|
|
608
|
+
const [err, written] = await generateAssets({
|
|
609
|
+
config: assetConfig,
|
|
610
|
+
publicDir: paths.publicDir
|
|
611
|
+
});
|
|
612
|
+
if (err) {
|
|
613
|
+
ctx.logger.error(err.message);
|
|
614
|
+
ctx.logger.outro('Failed');
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (0 === written.length) ctx.logger.info('All assets are user-customized ā nothing to generate');
|
|
618
|
+
else ctx.logger.success(`Generated ${written.join(', ')}`);
|
|
619
|
+
ctx.logger.outro('Done');
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
function buildAssetConfig(config) {
|
|
623
|
+
if (!config.title) return null;
|
|
624
|
+
return {
|
|
625
|
+
title: config.title,
|
|
626
|
+
tagline: config.tagline
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
const serveCommand = command({
|
|
630
|
+
description: 'Preview the built Rspress site',
|
|
631
|
+
options: z.object({
|
|
632
|
+
open: z.boolean().optional().default(true)
|
|
633
|
+
}),
|
|
634
|
+
handler: async (ctx)=>{
|
|
635
|
+
ctx.logger.intro('zpress serve');
|
|
636
|
+
const paths = createPaths(process.cwd());
|
|
637
|
+
const [configErr, config] = await loadConfig(paths.repoRoot);
|
|
638
|
+
if (configErr) {
|
|
639
|
+
ctx.logger.error(configErr.message);
|
|
640
|
+
if (configErr.errors && configErr.errors.length > 0) configErr.errors.map((err)=>{
|
|
641
|
+
const path = err.path.join('.');
|
|
642
|
+
return ctx.logger.error(` ${path}: ${err.message}`);
|
|
643
|
+
});
|
|
644
|
+
process.exit(1);
|
|
645
|
+
}
|
|
646
|
+
if (ctx.args.open) setTimeout(()=>openBrowser(`http://localhost:${DEFAULT_PORT}`), 2000);
|
|
647
|
+
await serveSite({
|
|
648
|
+
config,
|
|
649
|
+
paths
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
const CONFIG_FILENAME = 'zpress.config.ts';
|
|
654
|
+
const setupCommand = command({
|
|
655
|
+
description: 'Initialize a zpress config in the current project',
|
|
656
|
+
handler: async (ctx)=>{
|
|
657
|
+
const cwd = process.cwd();
|
|
658
|
+
const paths = createPaths(cwd);
|
|
659
|
+
const configPath = node_path.join(paths.repoRoot, CONFIG_FILENAME);
|
|
660
|
+
ctx.logger.intro('zpress setup');
|
|
661
|
+
if (node_fs.existsSync(configPath)) {
|
|
662
|
+
ctx.logger.warn(`${CONFIG_FILENAME} already exists ā skipping`);
|
|
663
|
+
ctx.logger.outro('Done');
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const title = deriveTitle(cwd);
|
|
667
|
+
node_fs.writeFileSync(configPath, buildConfigTemplate(title), 'utf8');
|
|
668
|
+
ctx.logger.success(`Created ${CONFIG_FILENAME} (title: "${title}")`);
|
|
669
|
+
await promises.mkdir(paths.publicDir, {
|
|
670
|
+
recursive: true
|
|
671
|
+
});
|
|
672
|
+
const [assetErr, written] = await generateAssets({
|
|
673
|
+
config: {
|
|
674
|
+
title,
|
|
675
|
+
tagline: void 0
|
|
676
|
+
},
|
|
677
|
+
publicDir: paths.publicDir
|
|
678
|
+
});
|
|
679
|
+
if (assetErr) {
|
|
680
|
+
ctx.logger.error(`Asset generation failed: ${assetErr.message}`);
|
|
681
|
+
process.exit(1);
|
|
682
|
+
}
|
|
683
|
+
if (written.length > 0) ctx.logger.success(`Generated ${written.join(', ')}`);
|
|
684
|
+
ctx.logger.outro('Done');
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
function extractGitRepoName(cwd) {
|
|
688
|
+
const url = execSilent('git', [
|
|
689
|
+
'remote',
|
|
690
|
+
'get-url',
|
|
691
|
+
'origin'
|
|
692
|
+
], cwd);
|
|
693
|
+
if (!url) return null;
|
|
694
|
+
const match = url.match(/[/:]([^/:]+?)(?:\.git)?$/);
|
|
695
|
+
if (!match) return null;
|
|
696
|
+
return match[1];
|
|
697
|
+
}
|
|
698
|
+
function execSilent(file, args, cwd) {
|
|
699
|
+
try {
|
|
700
|
+
return execFileSync(file, [
|
|
701
|
+
...args
|
|
702
|
+
], {
|
|
703
|
+
cwd,
|
|
704
|
+
stdio: 'pipe',
|
|
705
|
+
encoding: 'utf8'
|
|
706
|
+
}).trim();
|
|
707
|
+
} catch {
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function deriveTitle(cwd) {
|
|
712
|
+
const repoName = extractGitRepoName(cwd);
|
|
713
|
+
if (repoName) return repoName;
|
|
714
|
+
return node_path.basename(cwd);
|
|
715
|
+
}
|
|
716
|
+
function buildConfigTemplate(title) {
|
|
717
|
+
const escaped = title.replaceAll("'", String.raw`\'`);
|
|
718
|
+
return `import { defineConfig } from '@zpress/kit'
|
|
719
|
+
|
|
720
|
+
export default defineConfig({
|
|
721
|
+
title: '${escaped}',
|
|
722
|
+
sections: [
|
|
723
|
+
{
|
|
724
|
+
text: 'Getting Started',
|
|
725
|
+
prefix: '/getting-started',
|
|
726
|
+
from: 'docs/*.md',
|
|
727
|
+
},
|
|
728
|
+
],
|
|
729
|
+
})
|
|
730
|
+
`;
|
|
731
|
+
}
|
|
732
|
+
const syncCommand = command({
|
|
733
|
+
description: 'Sync documentation sources into .zpress/',
|
|
734
|
+
options: z.object({
|
|
735
|
+
quiet: z.boolean().optional().default(false)
|
|
736
|
+
}),
|
|
737
|
+
handler: async (ctx)=>{
|
|
738
|
+
const { quiet } = ctx.args;
|
|
739
|
+
const paths = createPaths(process.cwd());
|
|
740
|
+
if (!quiet) ctx.logger.intro('zpress sync');
|
|
741
|
+
const [configErr, config] = await loadConfig(paths.repoRoot);
|
|
742
|
+
if (configErr) {
|
|
743
|
+
ctx.logger.error(configErr.message);
|
|
744
|
+
if (configErr.errors && configErr.errors.length > 0) configErr.errors.map((err)=>{
|
|
745
|
+
const path = err.path.join('.');
|
|
746
|
+
return ctx.logger.error(` ${path}: ${err.message}`);
|
|
747
|
+
});
|
|
748
|
+
process.exit(1);
|
|
749
|
+
}
|
|
750
|
+
await sync(config, {
|
|
751
|
+
paths,
|
|
752
|
+
quiet
|
|
753
|
+
});
|
|
754
|
+
if (!quiet) ctx.logger.outro('Done');
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
await cli({
|
|
758
|
+
name: 'zpress',
|
|
759
|
+
version: "0.3.4",
|
|
760
|
+
description: 'CLI for building and serving documentation',
|
|
761
|
+
commands: {
|
|
762
|
+
sync: syncCommand,
|
|
763
|
+
dev: devCommand,
|
|
764
|
+
build: buildCommand,
|
|
765
|
+
check: checkCommand,
|
|
766
|
+
draft: draftCommand,
|
|
767
|
+
serve: serveCommand,
|
|
768
|
+
clean: cleanCommand,
|
|
769
|
+
dump: dumpCommand,
|
|
770
|
+
setup: setupCommand,
|
|
771
|
+
generate: generateCommand
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
export { toError };
|