@zpress/cli 0.1.4 → 0.2.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.
- package/dist/index.mjs +261 -13
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { cli, command } from "@kidd-cli/core";
|
|
3
|
-
import { createPaths, generateAssets, loadConfig, loadManifest, resolveEntries, sync } from "@zpress/core";
|
|
3
|
+
import { configError, createPaths, generateAssets, loadConfig, loadManifest, resolveEntries, sync } from "@zpress/core";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import node_path from "node:path";
|
|
5
6
|
import { execFileSync, spawn } from "node:child_process";
|
|
6
7
|
import { platform } from "node:os";
|
|
7
8
|
import { build, dev, serve } from "@rspress/core";
|
|
@@ -9,7 +10,6 @@ import { createRspressConfig } from "@zpress/ui";
|
|
|
9
10
|
import { match as external_ts_pattern_match } from "ts-pattern";
|
|
10
11
|
import promises from "node:fs/promises";
|
|
11
12
|
import node_fs from "node:fs";
|
|
12
|
-
import node_path from "node:path";
|
|
13
13
|
const DEFAULT_PORT = 6174;
|
|
14
14
|
async function startDevServer(options) {
|
|
15
15
|
const rspressConfig = createRspressConfig(options);
|
|
@@ -33,6 +33,14 @@ async function buildSite(options) {
|
|
|
33
33
|
configFilePath: ''
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
+
async function buildSiteForCheck(options) {
|
|
37
|
+
const rspressConfig = createRspressConfig(options);
|
|
38
|
+
await build({
|
|
39
|
+
docDirectory: options.paths.contentDir,
|
|
40
|
+
config: rspressConfig,
|
|
41
|
+
configFilePath: ''
|
|
42
|
+
});
|
|
43
|
+
}
|
|
36
44
|
async function serveSite(options) {
|
|
37
45
|
const rspressConfig = createRspressConfig(options);
|
|
38
46
|
await serve({
|
|
@@ -66,6 +74,170 @@ function openBrowser(url) {
|
|
|
66
74
|
detached: true
|
|
67
75
|
}).unref();
|
|
68
76
|
}
|
|
77
|
+
const ANSI_PATTERN = /\u001B\[[0-9;]*m/g;
|
|
78
|
+
function stripAnsi(text) {
|
|
79
|
+
return text.replace(ANSI_PATTERN, '');
|
|
80
|
+
}
|
|
81
|
+
function runConfigCheck(params) {
|
|
82
|
+
const { config, loadError } = params;
|
|
83
|
+
if (loadError) return {
|
|
84
|
+
passed: false,
|
|
85
|
+
errors: [
|
|
86
|
+
loadError
|
|
87
|
+
]
|
|
88
|
+
};
|
|
89
|
+
if (!config) return {
|
|
90
|
+
passed: false,
|
|
91
|
+
errors: [
|
|
92
|
+
configError('empty_sections', 'Config is missing')
|
|
93
|
+
]
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
passed: true,
|
|
97
|
+
errors: []
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function chunkToString(chunk) {
|
|
101
|
+
if ('string' == typeof chunk) return chunk;
|
|
102
|
+
return Buffer.from(chunk).toString('utf8');
|
|
103
|
+
}
|
|
104
|
+
function toError(value) {
|
|
105
|
+
if (value instanceof Error) return value;
|
|
106
|
+
return new Error(String(value));
|
|
107
|
+
}
|
|
108
|
+
function createInterceptor(chunks) {
|
|
109
|
+
return function(chunk, encodingOrCb, maybeCb) {
|
|
110
|
+
const text = chunkToString(chunk);
|
|
111
|
+
chunks.push(text);
|
|
112
|
+
if ('function' == typeof encodingOrCb) encodingOrCb();
|
|
113
|
+
else if ('function' == typeof maybeCb) maybeCb();
|
|
114
|
+
return true;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async function captureOutput(fn) {
|
|
118
|
+
const chunks = [];
|
|
119
|
+
const originalStdoutWrite = process.stdout.write;
|
|
120
|
+
const originalStderrWrite = process.stderr.write;
|
|
121
|
+
process.stdout.write = createInterceptor(chunks);
|
|
122
|
+
process.stderr.write = createInterceptor(chunks);
|
|
123
|
+
try {
|
|
124
|
+
const result = await fn();
|
|
125
|
+
return {
|
|
126
|
+
result,
|
|
127
|
+
error: null,
|
|
128
|
+
captured: chunks.join('')
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return {
|
|
132
|
+
result: null,
|
|
133
|
+
error: toError(error),
|
|
134
|
+
captured: chunks.join('')
|
|
135
|
+
};
|
|
136
|
+
} finally{
|
|
137
|
+
process.stdout.write = originalStdoutWrite;
|
|
138
|
+
process.stderr.write = originalStderrWrite;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function flushGroup(results, file, links) {
|
|
142
|
+
if (file && links.length > 0) return [
|
|
143
|
+
...results,
|
|
144
|
+
{
|
|
145
|
+
file,
|
|
146
|
+
links
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
return results;
|
|
150
|
+
}
|
|
151
|
+
function parseDeadlinks(stderr) {
|
|
152
|
+
const clean = stripAnsi(stderr);
|
|
153
|
+
const lines = clean.split('\n');
|
|
154
|
+
const headerPattern = /Dead links found in (.+?):\s*$/;
|
|
155
|
+
const linkPattern = /"\[\.\.]\(([^)]+)\)"/;
|
|
156
|
+
const acc = lines.reduce((state, line)=>{
|
|
157
|
+
const headerMatch = headerPattern.exec(line);
|
|
158
|
+
if (headerMatch) {
|
|
159
|
+
const file = headerMatch[1] ?? '';
|
|
160
|
+
return {
|
|
161
|
+
results: flushGroup(state.results, state.currentFile, state.currentLinks),
|
|
162
|
+
currentFile: file,
|
|
163
|
+
currentLinks: []
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const linkMatch = linkPattern.exec(line);
|
|
167
|
+
if (linkMatch && state.currentFile) {
|
|
168
|
+
const link = linkMatch[1] ?? '';
|
|
169
|
+
return {
|
|
170
|
+
...state,
|
|
171
|
+
currentLinks: [
|
|
172
|
+
...state.currentLinks,
|
|
173
|
+
link
|
|
174
|
+
]
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return state;
|
|
178
|
+
}, {
|
|
179
|
+
results: [],
|
|
180
|
+
currentFile: null,
|
|
181
|
+
currentLinks: []
|
|
182
|
+
});
|
|
183
|
+
return flushGroup(acc.results, acc.currentFile, acc.currentLinks);
|
|
184
|
+
}
|
|
185
|
+
async function runBuildCheck(params) {
|
|
186
|
+
const { error, captured } = await captureOutput(()=>buildSiteForCheck({
|
|
187
|
+
config: params.config,
|
|
188
|
+
paths: params.paths
|
|
189
|
+
}));
|
|
190
|
+
if (error) {
|
|
191
|
+
const { repoRoot } = params.paths;
|
|
192
|
+
const deadlinks = parseDeadlinks(captured).map((info)=>({
|
|
193
|
+
file: node_path.relative(repoRoot, info.file),
|
|
194
|
+
links: info.links
|
|
195
|
+
}));
|
|
196
|
+
if (deadlinks.length > 0) return {
|
|
197
|
+
status: 'failed',
|
|
198
|
+
deadlinks
|
|
199
|
+
};
|
|
200
|
+
return {
|
|
201
|
+
status: 'error',
|
|
202
|
+
message: error.message
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
status: 'passed'
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const RED = '\u001B[31m';
|
|
210
|
+
const DIM = '\u001B[2m';
|
|
211
|
+
const RESET = '\u001B[0m';
|
|
212
|
+
function formatDeadlinkGroup(info) {
|
|
213
|
+
const header = ` ${RED}✖${RESET} ${info.file}`;
|
|
214
|
+
const links = info.links.map((link)=>` ${DIM}→${RESET} ${link}`);
|
|
215
|
+
return [
|
|
216
|
+
header,
|
|
217
|
+
...links
|
|
218
|
+
].join('\n');
|
|
219
|
+
}
|
|
220
|
+
function presentResults(params) {
|
|
221
|
+
const { configResult, buildResult, logger } = params;
|
|
222
|
+
if (configResult.passed) logger.success('Config valid');
|
|
223
|
+
else {
|
|
224
|
+
logger.error('Config validation failed:');
|
|
225
|
+
configResult.errors.map((err)=>{
|
|
226
|
+
logger.message(` ${err.message}`);
|
|
227
|
+
return null;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
if ('passed' === buildResult.status) logger.success('No broken links');
|
|
231
|
+
else if ('skipped' === buildResult.status) ;
|
|
232
|
+
else if ('error' === buildResult.status) logger.error(`Build failed: ${buildResult.message}`);
|
|
233
|
+
else {
|
|
234
|
+
const totalLinks = buildResult.deadlinks.reduce((sum, info)=>sum + info.links.length, 0);
|
|
235
|
+
logger.error(`Found ${totalLinks} broken link(s):`);
|
|
236
|
+
const block = buildResult.deadlinks.map(formatDeadlinkGroup).join('\n');
|
|
237
|
+
logger.message(block);
|
|
238
|
+
}
|
|
239
|
+
return configResult.passed && 'passed' === buildResult.status;
|
|
240
|
+
}
|
|
69
241
|
function cleanTargets(paths) {
|
|
70
242
|
return [
|
|
71
243
|
{
|
|
@@ -82,7 +254,7 @@ function cleanTargets(paths) {
|
|
|
82
254
|
}
|
|
83
255
|
];
|
|
84
256
|
}
|
|
85
|
-
async function
|
|
257
|
+
async function clean_clean(paths) {
|
|
86
258
|
const results = await Promise.all(cleanTargets(paths).map(async ({ dir, label })=>{
|
|
87
259
|
const exists = await promises.stat(dir).catch(()=>null);
|
|
88
260
|
if (exists) {
|
|
@@ -101,7 +273,7 @@ const cleanCommand = command({
|
|
|
101
273
|
handler: async (ctx)=>{
|
|
102
274
|
const paths = createPaths(process.cwd());
|
|
103
275
|
ctx.logger.intro('zpress clean');
|
|
104
|
-
const removed = await
|
|
276
|
+
const removed = await clean_clean(paths);
|
|
105
277
|
if (removed.length > 0) ctx.logger.success(`Removed: ${removed.join(', ')}`);
|
|
106
278
|
else ctx.logger.info('Nothing to clean');
|
|
107
279
|
ctx.logger.outro('Done');
|
|
@@ -111,14 +283,15 @@ const buildCommand = command({
|
|
|
111
283
|
description: 'Run sync and build the Rspress site',
|
|
112
284
|
args: z.object({
|
|
113
285
|
quiet: z.boolean().optional().default(false),
|
|
114
|
-
clean: z.boolean().optional().default(false)
|
|
286
|
+
clean: z.boolean().optional().default(false),
|
|
287
|
+
check: z.boolean().optional().default(true)
|
|
115
288
|
}),
|
|
116
289
|
handler: async (ctx)=>{
|
|
117
|
-
const { quiet } = ctx.args;
|
|
290
|
+
const { quiet, check } = ctx.args;
|
|
118
291
|
const paths = createPaths(process.cwd());
|
|
119
292
|
ctx.logger.intro('zpress build');
|
|
120
293
|
if (ctx.args.clean) {
|
|
121
|
-
const removed = await
|
|
294
|
+
const removed = await clean_clean(paths);
|
|
122
295
|
if (removed.length > 0 && !quiet) ctx.logger.info(`Cleaned: ${removed.join(', ')}`);
|
|
123
296
|
}
|
|
124
297
|
const [configErr, config] = await loadConfig(paths.repoRoot);
|
|
@@ -126,15 +299,89 @@ const buildCommand = command({
|
|
|
126
299
|
ctx.logger.error(configErr.message);
|
|
127
300
|
process.exit(1);
|
|
128
301
|
}
|
|
129
|
-
|
|
302
|
+
if (check) {
|
|
303
|
+
ctx.logger.step('Validating config...');
|
|
304
|
+
const configResult = runConfigCheck({
|
|
305
|
+
config,
|
|
306
|
+
loadError: configErr
|
|
307
|
+
});
|
|
308
|
+
ctx.logger.step('Syncing content...');
|
|
309
|
+
await sync(config, {
|
|
310
|
+
paths,
|
|
311
|
+
quiet: true
|
|
312
|
+
});
|
|
313
|
+
ctx.logger.step('Building & checking for broken links...');
|
|
314
|
+
const buildResult = await runBuildCheck({
|
|
315
|
+
config,
|
|
316
|
+
paths
|
|
317
|
+
});
|
|
318
|
+
const passed = presentResults({
|
|
319
|
+
configResult,
|
|
320
|
+
buildResult,
|
|
321
|
+
logger: ctx.logger
|
|
322
|
+
});
|
|
323
|
+
if (!passed) {
|
|
324
|
+
ctx.logger.outro('Build failed');
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
ctx.logger.outro('Done');
|
|
328
|
+
} else {
|
|
329
|
+
await sync(config, {
|
|
330
|
+
paths,
|
|
331
|
+
quiet
|
|
332
|
+
});
|
|
333
|
+
await buildSite({
|
|
334
|
+
config,
|
|
335
|
+
paths
|
|
336
|
+
});
|
|
337
|
+
ctx.logger.outro('Done');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
const checkCommand = command({
|
|
342
|
+
description: 'Validate config and check for broken links',
|
|
343
|
+
handler: async (ctx)=>{
|
|
344
|
+
const paths = createPaths(process.cwd());
|
|
345
|
+
ctx.logger.intro('zpress check');
|
|
346
|
+
ctx.logger.step('Validating config...');
|
|
347
|
+
const [configErr, config] = await loadConfig(paths.repoRoot);
|
|
348
|
+
const configResult = runConfigCheck({
|
|
349
|
+
config,
|
|
350
|
+
loadError: configErr
|
|
351
|
+
});
|
|
352
|
+
if (configErr || !config) {
|
|
353
|
+
const buildResult = {
|
|
354
|
+
status: 'skipped'
|
|
355
|
+
};
|
|
356
|
+
presentResults({
|
|
357
|
+
configResult,
|
|
358
|
+
buildResult,
|
|
359
|
+
logger: ctx.logger
|
|
360
|
+
});
|
|
361
|
+
ctx.logger.outro('Checks failed');
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
ctx.logger.step('Syncing content...');
|
|
365
|
+
const syncResult = await sync(config, {
|
|
130
366
|
paths,
|
|
131
|
-
quiet
|
|
367
|
+
quiet: true
|
|
132
368
|
});
|
|
133
|
-
|
|
369
|
+
ctx.logger.success(`Synced (${syncResult.pagesWritten} written, ${syncResult.pagesSkipped} unchanged)`);
|
|
370
|
+
ctx.logger.step('Checking for broken links...');
|
|
371
|
+
const buildResult = await runBuildCheck({
|
|
134
372
|
config,
|
|
135
373
|
paths
|
|
136
374
|
});
|
|
137
|
-
|
|
375
|
+
const passed = presentResults({
|
|
376
|
+
configResult,
|
|
377
|
+
buildResult,
|
|
378
|
+
logger: ctx.logger
|
|
379
|
+
});
|
|
380
|
+
if (passed) ctx.logger.outro('All checks passed');
|
|
381
|
+
else {
|
|
382
|
+
ctx.logger.outro('Checks failed');
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
138
385
|
}
|
|
139
386
|
});
|
|
140
387
|
const devCommand = command({
|
|
@@ -148,7 +395,7 @@ const devCommand = command({
|
|
|
148
395
|
const paths = createPaths(process.cwd());
|
|
149
396
|
ctx.logger.intro('zpress dev');
|
|
150
397
|
if (ctx.args.clean) {
|
|
151
|
-
const removed = await
|
|
398
|
+
const removed = await clean_clean(paths);
|
|
152
399
|
if (removed.length > 0 && !quiet) ctx.logger.info(`Cleaned: ${removed.join(', ')}`);
|
|
153
400
|
}
|
|
154
401
|
const [configErr, config] = await loadConfig(paths.repoRoot);
|
|
@@ -408,12 +655,13 @@ const syncCommand = command({
|
|
|
408
655
|
});
|
|
409
656
|
await cli({
|
|
410
657
|
name: 'zpress',
|
|
411
|
-
version: "0.1
|
|
658
|
+
version: "0.2.1",
|
|
412
659
|
description: 'CLI for building and serving documentation',
|
|
413
660
|
commands: {
|
|
414
661
|
sync: syncCommand,
|
|
415
662
|
dev: devCommand,
|
|
416
663
|
build: buildCommand,
|
|
664
|
+
check: checkCommand,
|
|
417
665
|
serve: serveCommand,
|
|
418
666
|
clean: cleanCommand,
|
|
419
667
|
dump: dumpCommand,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zpress/cli",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "CLI for building and serving zpress documentation sites",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"es-toolkit": "^1.45.1",
|
|
41
41
|
"ts-pattern": "^5.9.0",
|
|
42
42
|
"zod": "^4.3.6",
|
|
43
|
-
"@zpress/
|
|
44
|
-
"@zpress/
|
|
43
|
+
"@zpress/core": "0.5.0",
|
|
44
|
+
"@zpress/ui": "0.4.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@rslib/core": "^0.20.0",
|