@zpress/cli 0.3.4 → 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.
Files changed (3) hide show
  1. package/README.md +9 -7
  2. package/dist/145.mjs +282 -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,164 @@ 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);
591
+ return uniq([
592
+ ...dirs.map(toDirectory),
593
+ ...CONFIG_GLOBS
594
+ ]);
595
+ }
596
+ function flattenIncludePaths(sections) {
597
+ return sections.flatMap(flattenSection);
598
+ }
599
+ function flattenSection(section) {
600
+ const includes = normalizeInclude(section.include);
601
+ if (includes.length > 0 && section.items) return [
602
+ ...includes,
603
+ ...flattenIncludePaths(section.items)
604
+ ];
605
+ if (includes.length > 0) return includes;
606
+ if (section.items) return flattenIncludePaths(section.items);
607
+ return [];
608
+ }
609
+ function toDirectory(from) {
610
+ if (hasGlobChars(from)) {
611
+ const normalized = from.replaceAll('\\', '/');
612
+ const segments = normalized.split('/');
613
+ const dirSegments = segments.filter((s)=>!hasGlobChars(s));
614
+ return dirSegments.join('/') || '.';
615
+ }
616
+ return from;
617
+ }
618
+ const RENAME_SEPARATOR = ' -> ';
619
+ function gitChangedFiles(params) {
620
+ const [err, output] = execSilent({
621
+ file: 'git',
622
+ args: [
623
+ 'status',
624
+ '--short',
625
+ '--',
626
+ ...params.dirs
627
+ ],
628
+ cwd: params.repoRoot
629
+ });
630
+ if (err) return [
631
+ err,
632
+ null
633
+ ];
634
+ if (!output) return [
635
+ null,
636
+ []
637
+ ];
638
+ const files = output.split('\n').filter((line)=>line.length > 0).map(parseStatusLine).filter((p)=>p.length > 0);
639
+ return [
640
+ null,
641
+ files
642
+ ];
643
+ }
644
+ function parseStatusLine(line) {
645
+ const filePart = line.slice(3);
646
+ const renameIdx = filePart.indexOf(RENAME_SEPARATOR);
647
+ if (-1 !== renameIdx) return stripQuotes(filePart.slice(renameIdx + RENAME_SEPARATOR.length));
648
+ return stripQuotes(filePart);
649
+ }
650
+ function stripQuotes(value) {
651
+ const trimmed = value.trim();
652
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) return trimmed.slice(1, -1);
653
+ return trimmed;
654
+ }
655
+ function execSilent(params) {
656
+ try {
657
+ const output = execFileSync(params.file, [
658
+ ...params.args
659
+ ], {
660
+ cwd: params.cwd,
661
+ stdio: 'pipe',
662
+ encoding: 'utf8'
663
+ }).trimEnd();
664
+ return [
665
+ null,
666
+ output
667
+ ];
668
+ } catch (error) {
669
+ if (error instanceof Error) return [
670
+ error,
671
+ null
672
+ ];
673
+ return [
674
+ new Error(String(error)),
675
+ null
676
+ ];
677
+ }
678
+ }
461
679
  const registry = createRegistry();
462
680
  const draftCommand = command({
463
681
  description: 'Scaffold a new documentation file from a template',
@@ -557,9 +775,9 @@ function maybeHidden(hidden) {
557
775
  };
558
776
  return {};
559
777
  }
560
- function maybeIsolated(isolated) {
561
- if (isolated) return {
562
- isolated
778
+ function maybeStandalone(standalone) {
779
+ if (standalone) return {
780
+ standalone
563
781
  };
564
782
  return {};
565
783
  }
@@ -578,7 +796,7 @@ function buildDumpEntry(entry) {
578
796
  ...maybeLink(entry.link),
579
797
  ...maybeCollapsible(entry.collapsible),
580
798
  ...maybeHidden(entry.hidden),
581
- ...maybeIsolated(entry.isolated),
799
+ ...maybeStandalone(entry.standalone),
582
800
  ...maybeItems(entry.items)
583
801
  };
584
802
  }
@@ -629,7 +847,11 @@ function buildAssetConfig(config) {
629
847
  const serveCommand = command({
630
848
  description: 'Preview the built Rspress site',
631
849
  options: z.object({
632
- open: z.boolean().optional().default(true)
850
+ open: z.boolean().optional().default(true),
851
+ port: z.number().optional(),
852
+ theme: z.string().optional(),
853
+ colorMode: z.string().optional(),
854
+ vscode: z.boolean().optional().default(false)
633
855
  }),
634
856
  handler: async (ctx)=>{
635
857
  ctx.logger.intro('zpress serve');
@@ -643,11 +865,15 @@ const serveCommand = command({
643
865
  });
644
866
  process.exit(1);
645
867
  }
646
- if (ctx.args.open) setTimeout(()=>openBrowser(`http://localhost:${DEFAULT_PORT}`), 2000);
647
- await serveSite({
868
+ const port = await serveSite({
648
869
  config,
649
- paths
870
+ paths,
871
+ port: ctx.args.port,
872
+ theme: ctx.args.theme,
873
+ colorMode: ctx.args.colorMode,
874
+ vscode: ctx.args.vscode
650
875
  });
876
+ if (ctx.args.open) openBrowser(`http://localhost:${port}`);
651
877
  }
652
878
  });
653
879
  const CONFIG_FILENAME = 'zpress.config.ts';
@@ -666,6 +892,7 @@ const setupCommand = command({
666
892
  const title = deriveTitle(cwd);
667
893
  node_fs.writeFileSync(configPath, buildConfigTemplate(title), 'utf8');
668
894
  ctx.logger.success(`Created ${CONFIG_FILENAME} (title: "${title}")`);
895
+ await ensureGitignore(paths, ctx.logger);
669
896
  await promises.mkdir(paths.publicDir, {
670
897
  recursive: true
671
898
  });
@@ -685,7 +912,7 @@ const setupCommand = command({
685
912
  }
686
913
  });
687
914
  function extractGitRepoName(cwd) {
688
- const url = execSilent('git', [
915
+ const url = setup_execSilent('git', [
689
916
  'remote',
690
917
  'get-url',
691
918
  'origin'
@@ -695,7 +922,7 @@ function extractGitRepoName(cwd) {
695
922
  if (!match) return null;
696
923
  return match[1];
697
924
  }
698
- function execSilent(file, args, cwd) {
925
+ function setup_execSilent(file, args, cwd) {
699
926
  try {
700
927
  return execFileSync(file, [
701
928
  ...args
@@ -721,14 +948,43 @@ export default defineConfig({
721
948
  title: '${escaped}',
722
949
  sections: [
723
950
  {
724
- text: 'Getting Started',
725
- prefix: '/getting-started',
726
- from: 'docs/*.md',
951
+ title: 'Getting Started',
952
+ path: '/getting-started',
953
+ include: 'docs/*.md',
727
954
  },
728
955
  ],
729
956
  })
730
957
  `;
731
958
  }
959
+ const ZPRESS_GITIGNORE_ENTRY = '.zpress/';
960
+ const NESTED_GITIGNORE_CONTENT = `# managed by zpress — ignore everything by default
961
+ *
962
+
963
+ # to track custom assets (e.g. banners, logos), uncomment the following lines:
964
+ # !public/
965
+ # !public/**
966
+ `;
967
+ async function ensureGitignore(paths, logger) {
968
+ const rootGitignore = node_path.join(paths.repoRoot, '.gitignore');
969
+ if (node_fs.existsSync(rootGitignore)) {
970
+ const content = node_fs.readFileSync(rootGitignore, 'utf8');
971
+ const lines = content.split('\n');
972
+ const alreadyIgnored = lines.some((line)=>line.trim() === ZPRESS_GITIGNORE_ENTRY || '.zpress' === line.trim());
973
+ if (alreadyIgnored) return;
974
+ const suffix = (()=>{
975
+ if (content.endsWith('\n')) return '';
976
+ return '\n';
977
+ })();
978
+ node_fs.writeFileSync(rootGitignore, `${content}${suffix}\n# zpress\n${ZPRESS_GITIGNORE_ENTRY}\n`, 'utf8');
979
+ logger.success('Added .zpress/ to .gitignore');
980
+ return;
981
+ }
982
+ await promises.mkdir(paths.outputRoot, {
983
+ recursive: true
984
+ });
985
+ await promises.writeFile(node_path.join(paths.outputRoot, '.gitignore'), NESTED_GITIGNORE_CONTENT, 'utf8');
986
+ logger.success('Created .zpress/.gitignore');
987
+ }
732
988
  const syncCommand = command({
733
989
  description: 'Sync documentation sources into .zpress/',
734
990
  options: z.object({
@@ -756,11 +1012,12 @@ const syncCommand = command({
756
1012
  });
757
1013
  await cli({
758
1014
  name: 'zpress',
759
- version: "0.3.4",
1015
+ version: "0.4.0",
760
1016
  description: 'CLI for building and serving documentation',
761
1017
  commands: {
762
1018
  sync: syncCommand,
763
1019
  dev: devCommand,
1020
+ diff: diffCommand,
764
1021
  build: buildCommand,
765
1022
  check: checkCommand,
766
1023
  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.0",
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.0"
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
  }