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