@zpress/cli 0.3.4 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +9 -7
  2. package/dist/145.mjs +289 -25
  3. package/package.json +7 -6
package/README.md CHANGED
@@ -1,12 +1,14 @@
1
- <div align="center">
2
- <img src="https://raw.githubusercontent.com/joggrdocs/zpress/main/assets/banner.svg" alt="zpress" width="90%" />
3
- <p><strong>CLI for building and serving zpress documentation sites.</strong></p>
1
+ # @zpress/cli
4
2
 
5
- <a href="https://github.com/joggrdocs/zpress/actions/workflows/ci.yml"><img src="https://github.com/joggrdocs/zpress/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI" /></a>
6
- <a href="https://www.npmjs.com/package/@zpress/cli"><img src="https://img.shields.io/npm/v/@zpress/cli" alt="npm version" /></a>
7
- <a href="https://github.com/joggrdocs/zpress/blob/main/LICENSE"><img src="https://img.shields.io/github/license/joggrdocs/zpress" alt="License" /></a>
3
+ CLI for building and serving zpress documentation sites.
8
4
 
9
- </div>
5
+ <span class="zp-badge">
6
+
7
+ [![CI](https://github.com/joggrdocs/zpress/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/joggrdocs/zpress/actions/workflows/ci.yml)
8
+ [![npm version](https://img.shields.io/npm/v/@zpress/cli)](https://www.npmjs.com/package/@zpress/cli)
9
+ [![License](https://img.shields.io/github/license/joggrdocs/zpress)](https://github.com/joggrdocs/zpress/blob/main/LICENSE)
10
+
11
+ </span>
10
12
 
11
13
  ## Install
12
14
 
package/dist/145.mjs CHANGED
@@ -1,28 +1,42 @@
1
+ import { createRequire } from "node:module";
1
2
  import { cli, command } from "@kidd-cli/core";
2
- import { configError, createPaths, generateAssets, loadConfig, loadManifest, resolveEntries, sync } from "@zpress/core";
3
+ import { configError, createPaths, generateAssets, hasGlobChars, loadConfig, loadManifest, normalizeInclude, resolveEntries, sync } from "@zpress/core";
3
4
  import { z } from "zod";
4
5
  import node_path from "node:path";
5
6
  import { execFileSync, spawn } from "node:child_process";
7
+ import { once } from "node:events";
8
+ import { Server } from "node:http";
6
9
  import { platform } from "node:os";
7
10
  import { build, dev, serve } from "@rspress/core";
8
11
  import { createRspressConfig } from "@zpress/ui";
12
+ import get_port, { portNumbers } from "get-port";
9
13
  import { P, match as external_ts_pattern_match } from "ts-pattern";
10
14
  import promises from "node:fs/promises";
11
- import { compact } from "es-toolkit";
15
+ import { compact, uniq } from "es-toolkit";
12
16
  import { createRegistry, render, toSlug } from "@zpress/templates";
13
17
  import node_fs from "node:fs";
18
+ globalThis.require = globalThis.require ?? createRequire(import.meta.url);
14
19
  function toError(error) {
15
20
  if (error instanceof Error) return error;
16
21
  return new Error(String(error));
17
22
  }
18
- const DEFAULT_PORT = 6174;
23
+ const DEV_PORT = 6174;
24
+ const DEV_PORT_RANGE = 5;
25
+ const SERVE_PORT = 8080;
19
26
  async function startDevServer(options) {
20
27
  const { paths } = options;
28
+ const preferred = options.port ?? DEV_PORT;
29
+ const port = await get_port({
30
+ port: portNumbers(preferred, preferred + DEV_PORT_RANGE)
31
+ });
21
32
  let serverInstance = null;
22
33
  async function startServer(config) {
23
34
  const rspressConfig = createRspressConfig({
24
35
  config,
25
- paths
36
+ paths,
37
+ vscode: options.vscode,
38
+ themeOverride: options.theme,
39
+ colorModeOverride: options.colorMode
26
40
  });
27
41
  try {
28
42
  serverInstance = await dev({
@@ -32,7 +46,8 @@ async function startDevServer(options) {
32
46
  configFilePath: '',
33
47
  extraBuilderConfig: {
34
48
  server: {
35
- port: DEFAULT_PORT
49
+ port,
50
+ strictPort: true
36
51
  }
37
52
  }
38
53
  });
@@ -47,11 +62,20 @@ async function startDevServer(options) {
47
62
  return async (newConfig)=>{
48
63
  process.stdout.write('\nšŸ”„ Config changed — restarting dev server...\n');
49
64
  if (serverInstance) {
65
+ const httpServer = getHttpServer(serverInstance);
66
+ const closeEvent = createCloseEvent(httpServer);
50
67
  try {
51
68
  await serverInstance.close();
52
69
  } catch (error) {
53
70
  process.stderr.write(`Error closing server: ${toError(error).message}\n`);
54
71
  }
72
+ if (closeEvent) {
73
+ const PORT_RELEASE_TIMEOUT = 5000;
74
+ await Promise.race([
75
+ closeEvent,
76
+ new Promise((resolve)=>setTimeout(resolve, PORT_RELEASE_TIMEOUT))
77
+ ]);
78
+ }
55
79
  serverInstance = null;
56
80
  }
57
81
  const restarted = await startServer(newConfig);
@@ -60,7 +84,10 @@ async function startDevServer(options) {
60
84
  };
61
85
  }
62
86
  async function buildSite(options) {
63
- const rspressConfig = createRspressConfig(options);
87
+ const rspressConfig = createRspressConfig({
88
+ config: options.config,
89
+ paths: options.paths
90
+ });
64
91
  await build({
65
92
  docDirectory: options.paths.contentDir,
66
93
  config: rspressConfig,
@@ -68,7 +95,10 @@ async function buildSite(options) {
68
95
  });
69
96
  }
70
97
  async function buildSiteForCheck(options) {
71
- const rspressConfig = createRspressConfig(options);
98
+ const rspressConfig = createRspressConfig({
99
+ config: options.config,
100
+ paths: options.paths
101
+ });
72
102
  await build({
73
103
  docDirectory: options.paths.contentDir,
74
104
  config: rspressConfig,
@@ -76,12 +106,23 @@ async function buildSiteForCheck(options) {
76
106
  });
77
107
  }
78
108
  async function serveSite(options) {
79
- const rspressConfig = createRspressConfig(options);
109
+ const rspressConfig = createRspressConfig({
110
+ config: options.config,
111
+ paths: options.paths,
112
+ vscode: options.vscode,
113
+ themeOverride: options.theme,
114
+ colorModeOverride: options.colorMode
115
+ });
116
+ const preferredPort = options.port ?? SERVE_PORT;
117
+ const port = await get_port({
118
+ port: portNumbers(preferredPort, preferredPort + DEV_PORT_RANGE)
119
+ });
80
120
  await serve({
81
121
  config: rspressConfig,
82
122
  configFilePath: '',
83
- port: DEFAULT_PORT
123
+ port
84
124
  });
125
+ return port;
85
126
  }
86
127
  function openBrowser(url) {
87
128
  const os = platform();
@@ -108,6 +149,17 @@ function openBrowser(url) {
108
149
  detached: true
109
150
  }).unref();
110
151
  }
152
+ function createCloseEvent(httpServer) {
153
+ if (null === httpServer) return null;
154
+ if (!httpServer.listening) return null;
155
+ return once(httpServer, 'close');
156
+ }
157
+ function getHttpServer(instance) {
158
+ const record = instance;
159
+ const value = record['httpServer'];
160
+ if (value instanceof Server) return value;
161
+ return null;
162
+ }
111
163
  const ANSI_PATTERN = /\u001B\[[0-9;]*m/g;
112
164
  const RED = '\u001B[31m';
113
165
  const DIM = '\u001B[2m';
@@ -422,7 +474,11 @@ const devCommand = command({
422
474
  description: 'Run sync + watcher and start Rspress dev server',
423
475
  options: z.object({
424
476
  quiet: z.boolean().optional().default(false),
425
- clean: z.boolean().optional().default(false)
477
+ clean: z.boolean().optional().default(false),
478
+ port: z.number().optional(),
479
+ theme: z.string().optional(),
480
+ colorMode: z.string().optional(),
481
+ vscode: z.boolean().optional().default(false)
426
482
  }),
427
483
  handler: async (ctx)=>{
428
484
  const { quiet } = ctx.args;
@@ -447,7 +503,11 @@ const devCommand = command({
447
503
  });
448
504
  const onConfigReload = await startDevServer({
449
505
  config,
450
- paths
506
+ paths,
507
+ port: ctx.args.port,
508
+ theme: ctx.args.theme,
509
+ colorMode: ctx.args.colorMode,
510
+ vscode: ctx.args.vscode
451
511
  });
452
512
  const { createWatcher } = await import("./watcher.mjs");
453
513
  const watcher = createWatcher(config, paths, onConfigReload);
@@ -458,6 +518,171 @@ const devCommand = command({
458
518
  process.on('SIGTERM', cleanup);
459
519
  }
460
520
  });
521
+ const CONFIG_GLOBS = [
522
+ 'zpress.config.ts',
523
+ 'zpress.config.mts',
524
+ 'zpress.config.cts',
525
+ 'zpress.config.js',
526
+ 'zpress.config.mjs',
527
+ 'zpress.config.cjs',
528
+ 'zpress.config.json'
529
+ ];
530
+ const diffCommand = command({
531
+ description: 'Show changed files in configured source directories',
532
+ options: z.object({
533
+ pretty: z.boolean().optional().default(false)
534
+ }),
535
+ handler: async (ctx)=>{
536
+ const { pretty } = ctx.args;
537
+ const paths = createPaths(process.cwd());
538
+ const [configErr, config] = await loadConfig(paths.repoRoot);
539
+ if (configErr) {
540
+ if (pretty) {
541
+ ctx.logger.intro('zpress diff');
542
+ ctx.logger.error(configErr.message);
543
+ if (configErr.errors && configErr.errors.length > 0) configErr.errors.forEach((err)=>{
544
+ const p = err.path.join('.');
545
+ ctx.logger.error(` ${p}: ${err.message}`);
546
+ });
547
+ }
548
+ process.exit(1);
549
+ }
550
+ const dirs = collectWatchPaths(config);
551
+ if (0 === dirs.length) {
552
+ if (pretty) {
553
+ ctx.logger.intro('zpress diff');
554
+ ctx.logger.warn('No source directories found in config');
555
+ ctx.logger.outro('Done');
556
+ }
557
+ return;
558
+ }
559
+ const [gitErr, changed] = gitChangedFiles({
560
+ repoRoot: paths.repoRoot,
561
+ dirs
562
+ });
563
+ if (gitErr) {
564
+ if (pretty) {
565
+ ctx.logger.intro('zpress diff');
566
+ ctx.logger.error(`Git failed: ${gitErr.message}`);
567
+ ctx.logger.outro('Done');
568
+ }
569
+ process.exit(1);
570
+ }
571
+ if (0 === changed.length) {
572
+ if (pretty) {
573
+ ctx.logger.intro('zpress diff');
574
+ ctx.logger.success('No changes detected');
575
+ ctx.logger.outro('Done');
576
+ }
577
+ return;
578
+ }
579
+ if (pretty) {
580
+ ctx.logger.intro('zpress diff');
581
+ ctx.logger.step(`Watching ${dirs.length} path(s)`);
582
+ ctx.logger.note(changed.join('\n'), `${changed.length} changed file(s)`);
583
+ ctx.logger.outro('Done');
584
+ return;
585
+ }
586
+ process.stdout.write(`${changed.join(' ')}\n`);
587
+ }
588
+ });
589
+ function collectWatchPaths(config) {
590
+ const dirs = flattenIncludePaths(config.sections).map(toDirectory);
591
+ const roots = dirs.map(toTopLevelRoot).filter((r)=>r.length > 0);
592
+ return uniq([
593
+ ...dirs,
594
+ ...roots,
595
+ ...CONFIG_GLOBS
596
+ ]);
597
+ }
598
+ function flattenIncludePaths(sections) {
599
+ return sections.flatMap(flattenSection);
600
+ }
601
+ function flattenSection(section) {
602
+ const includes = normalizeInclude(section.include);
603
+ if (includes.length > 0 && section.items) return [
604
+ ...includes,
605
+ ...flattenIncludePaths(section.items)
606
+ ];
607
+ if (includes.length > 0) return includes;
608
+ if (section.items) return flattenIncludePaths(section.items);
609
+ return [];
610
+ }
611
+ function toDirectory(from) {
612
+ if (hasGlobChars(from)) {
613
+ const normalized = from.replaceAll('\\', '/');
614
+ const segments = normalized.split('/');
615
+ const dirSegments = segments.filter((s)=>!hasGlobChars(s));
616
+ return dirSegments.join('/') || '.';
617
+ }
618
+ return from;
619
+ }
620
+ function toTopLevelRoot(dir) {
621
+ const idx = dir.indexOf('/');
622
+ if (-1 === idx) return '';
623
+ return dir.slice(0, idx + 1);
624
+ }
625
+ const RENAME_SEPARATOR = ' -> ';
626
+ function gitChangedFiles(params) {
627
+ const [err, output] = execSilent({
628
+ file: 'git',
629
+ args: [
630
+ 'status',
631
+ '--short',
632
+ '--',
633
+ ...params.dirs
634
+ ],
635
+ cwd: params.repoRoot
636
+ });
637
+ if (err) return [
638
+ err,
639
+ null
640
+ ];
641
+ if (!output) return [
642
+ null,
643
+ []
644
+ ];
645
+ const files = output.split('\n').filter((line)=>line.length > 0).map(parseStatusLine).filter((p)=>p.length > 0);
646
+ return [
647
+ null,
648
+ files
649
+ ];
650
+ }
651
+ function parseStatusLine(line) {
652
+ const filePart = line.slice(3);
653
+ const renameIdx = filePart.indexOf(RENAME_SEPARATOR);
654
+ if (-1 !== renameIdx) return stripQuotes(filePart.slice(renameIdx + RENAME_SEPARATOR.length));
655
+ return stripQuotes(filePart);
656
+ }
657
+ function stripQuotes(value) {
658
+ const trimmed = value.trim();
659
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) return trimmed.slice(1, -1);
660
+ return trimmed;
661
+ }
662
+ function execSilent(params) {
663
+ try {
664
+ const output = execFileSync(params.file, [
665
+ ...params.args
666
+ ], {
667
+ cwd: params.cwd,
668
+ stdio: 'pipe',
669
+ encoding: 'utf8'
670
+ }).trimEnd();
671
+ return [
672
+ null,
673
+ output
674
+ ];
675
+ } catch (error) {
676
+ if (error instanceof Error) return [
677
+ error,
678
+ null
679
+ ];
680
+ return [
681
+ new Error(String(error)),
682
+ null
683
+ ];
684
+ }
685
+ }
461
686
  const registry = createRegistry();
462
687
  const draftCommand = command({
463
688
  description: 'Scaffold a new documentation file from a template',
@@ -557,9 +782,9 @@ function maybeHidden(hidden) {
557
782
  };
558
783
  return {};
559
784
  }
560
- function maybeIsolated(isolated) {
561
- if (isolated) return {
562
- isolated
785
+ function maybeStandalone(standalone) {
786
+ if (standalone) return {
787
+ standalone
563
788
  };
564
789
  return {};
565
790
  }
@@ -578,7 +803,7 @@ function buildDumpEntry(entry) {
578
803
  ...maybeLink(entry.link),
579
804
  ...maybeCollapsible(entry.collapsible),
580
805
  ...maybeHidden(entry.hidden),
581
- ...maybeIsolated(entry.isolated),
806
+ ...maybeStandalone(entry.standalone),
582
807
  ...maybeItems(entry.items)
583
808
  };
584
809
  }
@@ -629,7 +854,11 @@ function buildAssetConfig(config) {
629
854
  const serveCommand = command({
630
855
  description: 'Preview the built Rspress site',
631
856
  options: z.object({
632
- open: z.boolean().optional().default(true)
857
+ open: z.boolean().optional().default(true),
858
+ port: z.number().optional(),
859
+ theme: z.string().optional(),
860
+ colorMode: z.string().optional(),
861
+ vscode: z.boolean().optional().default(false)
633
862
  }),
634
863
  handler: async (ctx)=>{
635
864
  ctx.logger.intro('zpress serve');
@@ -643,11 +872,15 @@ const serveCommand = command({
643
872
  });
644
873
  process.exit(1);
645
874
  }
646
- if (ctx.args.open) setTimeout(()=>openBrowser(`http://localhost:${DEFAULT_PORT}`), 2000);
647
- await serveSite({
875
+ const port = await serveSite({
648
876
  config,
649
- paths
877
+ paths,
878
+ port: ctx.args.port,
879
+ theme: ctx.args.theme,
880
+ colorMode: ctx.args.colorMode,
881
+ vscode: ctx.args.vscode
650
882
  });
883
+ if (ctx.args.open) openBrowser(`http://localhost:${port}`);
651
884
  }
652
885
  });
653
886
  const CONFIG_FILENAME = 'zpress.config.ts';
@@ -666,6 +899,7 @@ const setupCommand = command({
666
899
  const title = deriveTitle(cwd);
667
900
  node_fs.writeFileSync(configPath, buildConfigTemplate(title), 'utf8');
668
901
  ctx.logger.success(`Created ${CONFIG_FILENAME} (title: "${title}")`);
902
+ await ensureGitignore(paths, ctx.logger);
669
903
  await promises.mkdir(paths.publicDir, {
670
904
  recursive: true
671
905
  });
@@ -685,7 +919,7 @@ const setupCommand = command({
685
919
  }
686
920
  });
687
921
  function extractGitRepoName(cwd) {
688
- const url = execSilent('git', [
922
+ const url = setup_execSilent('git', [
689
923
  'remote',
690
924
  'get-url',
691
925
  'origin'
@@ -695,7 +929,7 @@ function extractGitRepoName(cwd) {
695
929
  if (!match) return null;
696
930
  return match[1];
697
931
  }
698
- function execSilent(file, args, cwd) {
932
+ function setup_execSilent(file, args, cwd) {
699
933
  try {
700
934
  return execFileSync(file, [
701
935
  ...args
@@ -721,14 +955,43 @@ export default defineConfig({
721
955
  title: '${escaped}',
722
956
  sections: [
723
957
  {
724
- text: 'Getting Started',
725
- prefix: '/getting-started',
726
- from: 'docs/*.md',
958
+ title: 'Getting Started',
959
+ path: '/getting-started',
960
+ include: 'docs/*.md',
727
961
  },
728
962
  ],
729
963
  })
730
964
  `;
731
965
  }
966
+ const ZPRESS_GITIGNORE_ENTRY = '.zpress/';
967
+ const NESTED_GITIGNORE_CONTENT = `# managed by zpress — ignore everything by default
968
+ *
969
+
970
+ # to track custom assets (e.g. banners, logos), uncomment the following lines:
971
+ # !public/
972
+ # !public/**
973
+ `;
974
+ async function ensureGitignore(paths, logger) {
975
+ const rootGitignore = node_path.join(paths.repoRoot, '.gitignore');
976
+ if (node_fs.existsSync(rootGitignore)) {
977
+ const content = node_fs.readFileSync(rootGitignore, 'utf8');
978
+ const lines = content.split('\n');
979
+ const alreadyIgnored = lines.some((line)=>line.trim() === ZPRESS_GITIGNORE_ENTRY || '.zpress' === line.trim());
980
+ if (alreadyIgnored) return;
981
+ const suffix = (()=>{
982
+ if (content.endsWith('\n')) return '';
983
+ return '\n';
984
+ })();
985
+ node_fs.writeFileSync(rootGitignore, `${content}${suffix}\n# zpress\n${ZPRESS_GITIGNORE_ENTRY}\n`, 'utf8');
986
+ logger.success('Added .zpress/ to .gitignore');
987
+ return;
988
+ }
989
+ await promises.mkdir(paths.outputRoot, {
990
+ recursive: true
991
+ });
992
+ await promises.writeFile(node_path.join(paths.outputRoot, '.gitignore'), NESTED_GITIGNORE_CONTENT, 'utf8');
993
+ logger.success('Created .zpress/.gitignore');
994
+ }
732
995
  const syncCommand = command({
733
996
  description: 'Sync documentation sources into .zpress/',
734
997
  options: z.object({
@@ -756,11 +1019,12 @@ const syncCommand = command({
756
1019
  });
757
1020
  await cli({
758
1021
  name: 'zpress',
759
- version: "0.3.4",
1022
+ version: "0.4.1",
760
1023
  description: 'CLI for building and serving documentation',
761
1024
  commands: {
762
1025
  sync: syncCommand,
763
1026
  dev: devCommand,
1027
+ diff: diffCommand,
764
1028
  build: buildCommand,
765
1029
  check: checkCommand,
766
1030
  draft: draftCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zpress/cli",
3
- "version": "0.3.4",
3
+ "version": "0.4.1",
4
4
  "description": "CLI for building and serving zpress documentation sites",
5
5
  "keywords": [
6
6
  "cli",
@@ -34,14 +34,15 @@
34
34
  "provenance": true
35
35
  },
36
36
  "dependencies": {
37
- "@kidd-cli/core": "^0.7.0",
38
- "@rspress/core": "^2.0.5",
37
+ "@kidd-cli/core": "^0.10.0",
38
+ "@rspress/core": "^2.0.6",
39
39
  "es-toolkit": "^1.45.1",
40
+ "get-port": "^7.1.0",
40
41
  "ts-pattern": "^5.9.0",
41
42
  "zod": "^4.3.6",
42
- "@zpress/core": "0.7.0",
43
+ "@zpress/core": "0.7.1",
43
44
  "@zpress/templates": "0.1.1",
44
- "@zpress/ui": "0.7.0"
45
+ "@zpress/ui": "0.8.1"
45
46
  },
46
47
  "devDependencies": {
47
48
  "@rslib/core": "^0.20.0",
@@ -55,6 +56,6 @@
55
56
  "build": "rslib build",
56
57
  "dev": "node ./dist/index.mjs dev --cwd ../..",
57
58
  "test": "vitest run",
58
- "typecheck": "tsc --noEmit"
59
+ "typecheck": "tsgo --noEmit"
59
60
  }
60
61
  }