@zenithbuild/cli 0.5.0-beta.2.6 → 0.6.2
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/README.md +5 -0
- package/dist/build.js +284 -126
- package/dist/dev-server.js +607 -55
- package/dist/index.js +84 -23
- package/dist/preview.js +332 -41
- package/dist/resolve-components.js +108 -0
- package/dist/server-contract.js +150 -11
- package/dist/ui/env.js +17 -1
- package/dist/ui/format.js +131 -54
- package/dist/ui/logger.js +239 -74
- package/package.json +4 -3
|
@@ -245,6 +245,9 @@ function findNextKnownTag(source, registry, startIndex) {
|
|
|
245
245
|
if (!registry.has(name)) {
|
|
246
246
|
continue;
|
|
247
247
|
}
|
|
248
|
+
if (isInsideExpressionScope(source, match.index)) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
248
251
|
return {
|
|
249
252
|
name,
|
|
250
253
|
start: match.index,
|
|
@@ -256,6 +259,111 @@ function findNextKnownTag(source, registry, startIndex) {
|
|
|
256
259
|
return null;
|
|
257
260
|
}
|
|
258
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Detect whether `index` is inside a `{ ... }` expression scope.
|
|
264
|
+
*
|
|
265
|
+
* This prevents component macro expansion inside embedded markup expressions,
|
|
266
|
+
* which must remain expression-local so the compiler can lower them safely.
|
|
267
|
+
*
|
|
268
|
+
* @param {string} source
|
|
269
|
+
* @param {number} index
|
|
270
|
+
* @returns {boolean}
|
|
271
|
+
*/
|
|
272
|
+
function isInsideExpressionScope(source, index) {
|
|
273
|
+
let depth = 0;
|
|
274
|
+
let mode = 'code';
|
|
275
|
+
let escaped = false;
|
|
276
|
+
const lower = source.toLowerCase();
|
|
277
|
+
|
|
278
|
+
for (let i = 0; i < index; i++) {
|
|
279
|
+
if (mode === 'code') {
|
|
280
|
+
if (lower.startsWith('<script', i)) {
|
|
281
|
+
const close = lower.indexOf('</script>', i + 7);
|
|
282
|
+
if (close < 0 || close >= index) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
i = close + '</script>'.length - 1;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (lower.startsWith('<style', i)) {
|
|
289
|
+
const close = lower.indexOf('</style>', i + 6);
|
|
290
|
+
if (close < 0 || close >= index) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
i = close + '</style>'.length - 1;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const ch = source[i];
|
|
299
|
+
const next = i + 1 < index ? source[i + 1] : '';
|
|
300
|
+
|
|
301
|
+
if (mode === 'line-comment') {
|
|
302
|
+
if (ch === '\n') {
|
|
303
|
+
mode = 'code';
|
|
304
|
+
}
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (mode === 'block-comment') {
|
|
308
|
+
if (ch === '*' && next === '/') {
|
|
309
|
+
mode = 'code';
|
|
310
|
+
i += 1;
|
|
311
|
+
}
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (mode === 'single-quote' || mode === 'double-quote' || mode === 'template') {
|
|
315
|
+
if (escaped) {
|
|
316
|
+
escaped = false;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (ch === '\\') {
|
|
320
|
+
escaped = true;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (
|
|
324
|
+
(mode === 'single-quote' && ch === "'") ||
|
|
325
|
+
(mode === 'double-quote' && ch === '"') ||
|
|
326
|
+
(mode === 'template' && ch === '`')
|
|
327
|
+
) {
|
|
328
|
+
mode = 'code';
|
|
329
|
+
}
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (ch === '/' && next === '/') {
|
|
334
|
+
mode = 'line-comment';
|
|
335
|
+
i += 1;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (ch === '/' && next === '*') {
|
|
339
|
+
mode = 'block-comment';
|
|
340
|
+
i += 1;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (ch === "'") {
|
|
344
|
+
mode = 'single-quote';
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (ch === '"') {
|
|
348
|
+
mode = 'double-quote';
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (ch === '`') {
|
|
352
|
+
mode = 'template';
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (ch === '{') {
|
|
356
|
+
depth += 1;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (ch === '}') {
|
|
360
|
+
depth = Math.max(0, depth - 1);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return depth > 0;
|
|
365
|
+
}
|
|
366
|
+
|
|
259
367
|
/**
|
|
260
368
|
* Find the matching </Name> for an opening tag, accounting for nested
|
|
261
369
|
* tags with the same name.
|
package/dist/server-contract.js
CHANGED
|
@@ -2,9 +2,73 @@
|
|
|
2
2
|
// ---------------------------------------------------------------------------
|
|
3
3
|
// Shared validation and payload resolution logic for <script server> blocks.
|
|
4
4
|
|
|
5
|
-
const NEW_KEYS = new Set(['data', 'load', 'prerender']);
|
|
5
|
+
const NEW_KEYS = new Set(['data', 'load', 'guard', 'prerender']);
|
|
6
6
|
const LEGACY_KEYS = new Set(['ssr_data', 'props', 'ssr', 'prerender']);
|
|
7
|
-
const ALLOWED_KEYS = new Set(['data', 'load', 'prerender', 'ssr_data', 'props', 'ssr']);
|
|
7
|
+
const ALLOWED_KEYS = new Set(['data', 'load', 'guard', 'prerender', 'ssr_data', 'props', 'ssr']);
|
|
8
|
+
|
|
9
|
+
const ROUTE_RESULT_KINDS = new Set(['allow', 'redirect', 'deny', 'data']);
|
|
10
|
+
|
|
11
|
+
export function allow() {
|
|
12
|
+
return { kind: 'allow' };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function redirect(location, status = 302) {
|
|
16
|
+
return {
|
|
17
|
+
kind: 'redirect',
|
|
18
|
+
location: String(location || ''),
|
|
19
|
+
status: Number.isInteger(status) ? status : 302
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function deny(status = 403, message = undefined) {
|
|
24
|
+
return {
|
|
25
|
+
kind: 'deny',
|
|
26
|
+
status: Number.isInteger(status) ? status : 403,
|
|
27
|
+
message: typeof message === 'string' ? message : undefined
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function data(payload) {
|
|
32
|
+
return { kind: 'data', data: payload };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isRouteResultLike(value) {
|
|
36
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const kind = value.kind;
|
|
40
|
+
return typeof kind === 'string' && ROUTE_RESULT_KINDS.has(kind);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function assertValidRouteResultShape(value, where, allowedKinds) {
|
|
44
|
+
if (!isRouteResultLike(value)) {
|
|
45
|
+
throw new Error(`[Zenith] ${where}: invalid route result. Expected object with kind.`);
|
|
46
|
+
}
|
|
47
|
+
const kind = value.kind;
|
|
48
|
+
if (!allowedKinds.has(kind)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`[Zenith] ${where}: kind "${kind}" is not allowed here (allowed: ${Array.from(allowedKinds).join(', ')}).`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (kind === 'redirect') {
|
|
55
|
+
if (typeof value.location !== 'string' || value.location.length === 0) {
|
|
56
|
+
throw new Error(`[Zenith] ${where}: redirect requires non-empty string location.`);
|
|
57
|
+
}
|
|
58
|
+
if (value.status !== undefined && (!Number.isInteger(value.status) || value.status < 300 || value.status > 399)) {
|
|
59
|
+
throw new Error(`[Zenith] ${where}: redirect status must be an integer 3xx.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (kind === 'deny') {
|
|
64
|
+
if (!Number.isInteger(value.status) || (value.status !== 401 && value.status !== 403)) {
|
|
65
|
+
throw new Error(`[Zenith] ${where}: deny status must be 401 or 403.`);
|
|
66
|
+
}
|
|
67
|
+
if (value.message !== undefined && typeof value.message !== 'string') {
|
|
68
|
+
throw new Error(`[Zenith] ${where}: deny message must be a string when provided.`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
8
72
|
|
|
9
73
|
export function validateServerExports({ exports, filePath }) {
|
|
10
74
|
const exportKeys = Object.keys(exports);
|
|
@@ -16,6 +80,7 @@ export function validateServerExports({ exports, filePath }) {
|
|
|
16
80
|
|
|
17
81
|
const hasData = 'data' in exports;
|
|
18
82
|
const hasLoad = 'load' in exports;
|
|
83
|
+
const hasGuard = 'guard' in exports;
|
|
19
84
|
|
|
20
85
|
const hasNew = hasData || hasLoad;
|
|
21
86
|
const hasLegacy = ('ssr_data' in exports) || ('props' in exports) || ('ssr' in exports);
|
|
@@ -47,6 +112,20 @@ export function validateServerExports({ exports, filePath }) {
|
|
|
47
112
|
throw new Error(`[Zenith] ${filePath}: "load(ctx)" must not contain rest parameters.`);
|
|
48
113
|
}
|
|
49
114
|
}
|
|
115
|
+
|
|
116
|
+
if (hasGuard && typeof exports.guard !== 'function') {
|
|
117
|
+
throw new Error(`[Zenith] ${filePath}: "guard" must be a function.`);
|
|
118
|
+
}
|
|
119
|
+
if (hasGuard) {
|
|
120
|
+
if (exports.guard.length !== 1) {
|
|
121
|
+
throw new Error(`[Zenith] ${filePath}: "guard(ctx)" must take exactly 1 argument.`);
|
|
122
|
+
}
|
|
123
|
+
const fnStr = exports.guard.toString();
|
|
124
|
+
const paramsMatch = fnStr.match(/^[^{=]+\(([^)]*)\)/);
|
|
125
|
+
if (paramsMatch && paramsMatch[1].includes('...')) {
|
|
126
|
+
throw new Error(`[Zenith] ${filePath}: "guard(ctx)" must not contain rest parameters.`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
50
129
|
}
|
|
51
130
|
|
|
52
131
|
export function assertJsonSerializable(value, where = 'payload') {
|
|
@@ -110,37 +189,97 @@ export function assertJsonSerializable(value, where = 'payload') {
|
|
|
110
189
|
walk(value, '$');
|
|
111
190
|
}
|
|
112
191
|
|
|
113
|
-
export async function
|
|
192
|
+
export async function resolveRouteResult({ exports, ctx, filePath, guardOnly = false }) {
|
|
114
193
|
validateServerExports({ exports, filePath });
|
|
115
194
|
|
|
195
|
+
const trace = {
|
|
196
|
+
guard: 'none',
|
|
197
|
+
load: 'none'
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if ('guard' in exports) {
|
|
201
|
+
const guardRaw = await exports.guard(ctx);
|
|
202
|
+
const guardResult = guardRaw == null ? allow() : guardRaw;
|
|
203
|
+
if (guardResult.kind === 'data') {
|
|
204
|
+
throw new Error(`[Zenith] ${filePath}: guard(ctx) returned data(payload) which is a critical invariant violation. guard() can only return allow(), redirect(), or deny(). Use load(ctx) for data injection.`);
|
|
205
|
+
}
|
|
206
|
+
assertValidRouteResultShape(
|
|
207
|
+
guardResult,
|
|
208
|
+
`${filePath}: guard(ctx) return`,
|
|
209
|
+
new Set(['allow', 'redirect', 'deny'])
|
|
210
|
+
);
|
|
211
|
+
trace.guard = guardResult.kind;
|
|
212
|
+
if (guardResult.kind === 'redirect' || guardResult.kind === 'deny') {
|
|
213
|
+
return { result: guardResult, trace };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (guardOnly) {
|
|
218
|
+
return { result: allow(), trace };
|
|
219
|
+
}
|
|
220
|
+
|
|
116
221
|
let payload;
|
|
117
222
|
if ('load' in exports) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
223
|
+
const loadRaw = await exports.load(ctx);
|
|
224
|
+
let loadResult = null;
|
|
225
|
+
if (isRouteResultLike(loadRaw)) {
|
|
226
|
+
loadResult = loadRaw;
|
|
227
|
+
assertValidRouteResultShape(
|
|
228
|
+
loadResult,
|
|
229
|
+
`${filePath}: load(ctx) return`,
|
|
230
|
+
new Set(['data', 'redirect', 'deny'])
|
|
231
|
+
);
|
|
232
|
+
} else {
|
|
233
|
+
assertJsonSerializable(loadRaw, `${filePath}: load(ctx) return`);
|
|
234
|
+
loadResult = data(loadRaw);
|
|
235
|
+
}
|
|
236
|
+
trace.load = loadResult.kind;
|
|
237
|
+
return { result: loadResult, trace };
|
|
121
238
|
}
|
|
122
239
|
if ('data' in exports) {
|
|
123
240
|
payload = exports.data;
|
|
124
241
|
assertJsonSerializable(payload, `${filePath}: data export`);
|
|
125
|
-
|
|
242
|
+
trace.load = 'data';
|
|
243
|
+
return { result: data(payload), trace };
|
|
126
244
|
}
|
|
127
245
|
|
|
128
246
|
// legacy fallback
|
|
129
247
|
if ('ssr_data' in exports) {
|
|
130
248
|
payload = exports.ssr_data;
|
|
131
249
|
assertJsonSerializable(payload, `${filePath}: ssr_data export`);
|
|
132
|
-
|
|
250
|
+
trace.load = 'data';
|
|
251
|
+
return { result: data(payload), trace };
|
|
133
252
|
}
|
|
134
253
|
if ('props' in exports) {
|
|
135
254
|
payload = exports.props;
|
|
136
255
|
assertJsonSerializable(payload, `${filePath}: props export`);
|
|
137
|
-
|
|
256
|
+
trace.load = 'data';
|
|
257
|
+
return { result: data(payload), trace };
|
|
138
258
|
}
|
|
139
259
|
if ('ssr' in exports) {
|
|
140
260
|
payload = exports.ssr;
|
|
141
261
|
assertJsonSerializable(payload, `${filePath}: ssr export`);
|
|
142
|
-
|
|
262
|
+
trace.load = 'data';
|
|
263
|
+
return { result: data(payload), trace };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return { result: data({}), trace };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export async function resolveServerPayload({ exports, ctx, filePath }) {
|
|
270
|
+
const resolved = await resolveRouteResult({ exports, ctx, filePath });
|
|
271
|
+
if (!resolved || !resolved.result || typeof resolved.result !== 'object') {
|
|
272
|
+
return {};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (resolved.result.kind === 'data') {
|
|
276
|
+
return resolved.result.data;
|
|
277
|
+
}
|
|
278
|
+
if (resolved.result.kind === 'allow') {
|
|
279
|
+
return {};
|
|
143
280
|
}
|
|
144
281
|
|
|
145
|
-
|
|
282
|
+
throw new Error(
|
|
283
|
+
`[Zenith] ${filePath}: resolveServerPayload() expected data but received ${resolved.result.kind}. Use resolveRouteResult() for guard/load flows.`
|
|
284
|
+
);
|
|
146
285
|
}
|
package/dist/ui/env.js
CHANGED
|
@@ -10,6 +10,14 @@ function flagEnabled(value) {
|
|
|
10
10
|
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function parseLogLevel(value) {
|
|
14
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
15
|
+
if (normalized === 'quiet' || normalized === 'verbose') {
|
|
16
|
+
return normalized;
|
|
17
|
+
}
|
|
18
|
+
return 'normal';
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
/**
|
|
14
22
|
* @param {{ env?: Record<string, string | undefined>, stdout?: { isTTY?: boolean } }} runtime
|
|
15
23
|
*/
|
|
@@ -21,10 +29,17 @@ export function getUiMode(runtime = process) {
|
|
|
21
29
|
const noColor = env.NO_COLOR !== undefined && String(env.NO_COLOR).length >= 0;
|
|
22
30
|
const forceColor = flagEnabled(env.FORCE_COLOR);
|
|
23
31
|
const debug = flagEnabled(env.ZENITH_DEBUG);
|
|
32
|
+
let logLevel = parseLogLevel(env.ZENITH_LOG_LEVEL);
|
|
24
33
|
|
|
25
34
|
const plain = noUi || ci || !tty;
|
|
26
35
|
const color = !plain && !noColor && (forceColor || tty);
|
|
27
36
|
const spinner = tty && !plain && !ci;
|
|
37
|
+
if (flagEnabled(env.ZENITH_DEV_TRACE)) {
|
|
38
|
+
logLevel = 'verbose';
|
|
39
|
+
}
|
|
40
|
+
if (debug && logLevel !== 'quiet') {
|
|
41
|
+
logLevel = 'verbose';
|
|
42
|
+
}
|
|
28
43
|
|
|
29
44
|
return {
|
|
30
45
|
plain,
|
|
@@ -32,7 +47,8 @@ export function getUiMode(runtime = process) {
|
|
|
32
47
|
tty,
|
|
33
48
|
ci,
|
|
34
49
|
spinner,
|
|
35
|
-
debug
|
|
50
|
+
debug,
|
|
51
|
+
logLevel
|
|
36
52
|
};
|
|
37
53
|
}
|
|
38
54
|
|
package/dist/ui/format.js
CHANGED
|
@@ -1,50 +1,78 @@
|
|
|
1
|
-
|
|
2
|
-
* Deterministic text formatters for CLI UX.
|
|
3
|
-
*/
|
|
4
|
-
|
|
1
|
+
import pc from 'picocolors';
|
|
5
2
|
import { relative, sep } from 'node:path';
|
|
6
3
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
const DEFAULT_PHASE = 'cli';
|
|
5
|
+
const DEFAULT_FILE = '.';
|
|
6
|
+
const DEFAULT_HINT_BASE = 'https://github.com/zenithbuild/zenith/blob/main/zenith-cli/CLI_CONTRACT.md';
|
|
7
|
+
const PREFIX = '[zenith]';
|
|
8
|
+
const TAG_WIDTH = 6;
|
|
9
|
+
|
|
10
|
+
const TAG_COLORS = {
|
|
11
|
+
DEV: (colors, value) => colors.cyan(value),
|
|
12
|
+
BUILD: (colors, value) => colors.blue(value),
|
|
13
|
+
HMR: (colors, value) => colors.magenta(value),
|
|
14
|
+
ROUTER: (colors, value) => colors.cyan(value),
|
|
15
|
+
CSS: (colors, value) => colors.yellow(value),
|
|
16
|
+
OK: (colors, value) => colors.green(value),
|
|
17
|
+
WARN: (colors, value) => colors.bold(colors.yellow(value)),
|
|
18
|
+
ERR: (colors, value) => colors.bold(colors.red(value))
|
|
15
19
|
};
|
|
16
20
|
|
|
17
|
-
function
|
|
21
|
+
function getColors(mode) {
|
|
22
|
+
return pc.createColors(Boolean(mode?.color));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function formatPrefix(mode) {
|
|
26
|
+
return mode.color ? getColors(mode).dim(PREFIX) : PREFIX;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function colorizeTag(mode, tag) {
|
|
30
|
+
const padded = String(tag || '').padEnd(TAG_WIDTH, ' ');
|
|
18
31
|
if (!mode.color) {
|
|
19
|
-
return
|
|
32
|
+
return padded;
|
|
20
33
|
}
|
|
21
|
-
|
|
34
|
+
const colors = getColors(mode);
|
|
35
|
+
const colorizer = TAG_COLORS[tag] || ((_colors, value) => colors.white(value));
|
|
36
|
+
return colorizer(colors, padded);
|
|
22
37
|
}
|
|
23
38
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
39
|
+
function colorizeGlyph(mode, glyph, tag) {
|
|
40
|
+
if (!mode.color) {
|
|
41
|
+
return glyph;
|
|
42
|
+
}
|
|
43
|
+
const colors = getColors(mode);
|
|
44
|
+
const colorizer = TAG_COLORS[tag] || ((_colors, value) => value);
|
|
45
|
+
return colorizer(colors, glyph);
|
|
27
46
|
}
|
|
28
47
|
|
|
29
|
-
export function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return
|
|
48
|
+
export function formatLine(mode, { glyph = '•', tag = 'DEV', text = '' }) {
|
|
49
|
+
return `${formatPrefix(mode)} ${colorizeGlyph(mode, glyph, tag)} ${colorizeTag(mode, tag)} ${String(text || '')}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatStep(mode, text, tag = 'BUILD') {
|
|
53
|
+
return formatLine(mode, { glyph: '•', tag, text });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function formatHint(mode, text) {
|
|
57
|
+
const body = ` hint: ${String(text || '').trim()}`;
|
|
58
|
+
return mode.color ? getColors(mode).dim(body) : body;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function formatHeading(mode, text) {
|
|
62
|
+
const label = mode.color ? getColors(mode).bold('Zenith CLI') : 'Zenith CLI';
|
|
63
|
+
return `${label} ${String(text || '').trim()}`.trim();
|
|
35
64
|
}
|
|
36
65
|
|
|
37
|
-
export function formatSummaryTable(mode, rows) {
|
|
66
|
+
export function formatSummaryTable(mode, rows, tag = 'BUILD') {
|
|
38
67
|
if (!Array.isArray(rows) || rows.length === 0) {
|
|
39
68
|
return '';
|
|
40
69
|
}
|
|
41
|
-
const maxLabel = rows.reduce((acc, row) => Math.max(acc, String(row.label || '').length), 0);
|
|
42
70
|
return rows
|
|
43
|
-
.map((row) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
})
|
|
71
|
+
.map((row) => formatLine(mode, {
|
|
72
|
+
glyph: '•',
|
|
73
|
+
tag,
|
|
74
|
+
text: `${String(row.label || '')}: ${String(row.value || '')}`
|
|
75
|
+
}))
|
|
48
76
|
.join('\n');
|
|
49
77
|
}
|
|
50
78
|
|
|
@@ -62,21 +90,53 @@ function normalizeFileLinePath(line) {
|
|
|
62
90
|
|
|
63
91
|
const prefix = match[1];
|
|
64
92
|
const filePath = match[2].trim();
|
|
65
|
-
|
|
66
|
-
|
|
93
|
+
const normalized = normalizePathForDisplay(filePath);
|
|
94
|
+
return `${prefix}${normalized}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizePathForDisplay(filePath) {
|
|
98
|
+
const value = String(filePath || '').trim();
|
|
99
|
+
if (!value) {
|
|
100
|
+
return DEFAULT_FILE;
|
|
101
|
+
}
|
|
102
|
+
if (!value.startsWith('/') && !/^[A-Za-z]:\\/.test(value)) {
|
|
103
|
+
return value;
|
|
67
104
|
}
|
|
68
105
|
|
|
69
106
|
const cwd = process.cwd();
|
|
70
107
|
const cwdWithSep = cwd.endsWith(sep) ? cwd : `${cwd}${sep}`;
|
|
71
|
-
if (
|
|
72
|
-
return
|
|
108
|
+
if (value === cwd) {
|
|
109
|
+
return DEFAULT_FILE;
|
|
73
110
|
}
|
|
74
|
-
if (
|
|
75
|
-
const relativePath = relative(cwd,
|
|
76
|
-
return
|
|
111
|
+
if (value.startsWith(cwdWithSep)) {
|
|
112
|
+
const relativePath = relative(cwd, value).replaceAll('\\', '/');
|
|
113
|
+
return relativePath || DEFAULT_FILE;
|
|
77
114
|
}
|
|
78
115
|
|
|
79
|
-
return
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function inferPhaseFromArgv() {
|
|
120
|
+
const knownPhases = new Set(['build', 'dev', 'preview']);
|
|
121
|
+
for (const arg of process.argv.slice(2)) {
|
|
122
|
+
if (knownPhases.has(arg)) {
|
|
123
|
+
return arg;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return DEFAULT_PHASE;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function extractFileFromMessage(message) {
|
|
130
|
+
const match = String(message || '').match(/\bFile:\s+([^\n]+)/);
|
|
131
|
+
return match ? match[1].trim() : '';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatHintUrl(code) {
|
|
135
|
+
const slug = String(code || 'CLI_ERROR')
|
|
136
|
+
.toLowerCase()
|
|
137
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
138
|
+
.replace(/^-+|-+$/g, '');
|
|
139
|
+
return `${DEFAULT_HINT_BASE}#${slug || 'cli-error'}`;
|
|
80
140
|
}
|
|
81
141
|
|
|
82
142
|
export function normalizeErrorMessagePaths(message) {
|
|
@@ -96,32 +156,49 @@ export function normalizeError(err) {
|
|
|
96
156
|
return new Error(sanitizeErrorMessage(err));
|
|
97
157
|
}
|
|
98
158
|
|
|
159
|
+
function firstMeaningfulLine(text) {
|
|
160
|
+
return String(text || '')
|
|
161
|
+
.split('\n')
|
|
162
|
+
.map((line) => line.trim())
|
|
163
|
+
.find((line) => line.length > 0) || '';
|
|
164
|
+
}
|
|
165
|
+
|
|
99
166
|
/**
|
|
100
167
|
* @param {unknown} err
|
|
101
|
-
* @param {{ plain: boolean, color: boolean, debug
|
|
168
|
+
* @param {{ plain: boolean, color: boolean, debug?: boolean, logLevel?: string }} mode
|
|
102
169
|
*/
|
|
103
170
|
export function formatErrorBlock(err, mode) {
|
|
104
171
|
const normalized = normalizeError(err);
|
|
105
|
-
const maybe = /** @type {{ code?: unknown, phase?: unknown, kind?: unknown }} */ (normalized);
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
|
|
172
|
+
const maybe = /** @type {{ code?: unknown, phase?: unknown, kind?: unknown, file?: unknown, hint?: unknown }} */ (normalized);
|
|
173
|
+
const phase = maybe.phase ? sanitizeErrorMessage(maybe.phase) : inferPhaseFromArgv();
|
|
174
|
+
const code = maybe.code
|
|
175
|
+
? sanitizeErrorMessage(maybe.code)
|
|
176
|
+
: `${phase.toUpperCase().replace(/[^A-Z0-9]+/g, '_') || 'CLI'}_FAILED`;
|
|
109
177
|
const rawMessage = sanitizeErrorMessage(normalized.message || String(normalized));
|
|
110
178
|
const message = normalizeErrorMessagePaths(rawMessage);
|
|
179
|
+
const compactMessage = firstMeaningfulLine(message) || 'Command failed';
|
|
180
|
+
const file = normalizePathForDisplay(
|
|
181
|
+
sanitizeErrorMessage(maybe.file || extractFileFromMessage(message) || DEFAULT_FILE)
|
|
182
|
+
);
|
|
183
|
+
const hint = sanitizeErrorMessage(maybe.hint || formatHintUrl(code));
|
|
184
|
+
|
|
185
|
+
if (mode.logLevel !== 'verbose' && !mode.debug) {
|
|
186
|
+
return [
|
|
187
|
+
formatLine(mode, { glyph: '✖', tag: 'ERR', text: compactMessage }),
|
|
188
|
+
formatHint(mode, hint)
|
|
189
|
+
].join('\n');
|
|
190
|
+
}
|
|
111
191
|
|
|
112
192
|
const lines = [];
|
|
113
|
-
lines.push('
|
|
114
|
-
lines.push(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
lines.push(`[zenith] Code: ${code}`);
|
|
120
|
-
}
|
|
121
|
-
lines.push(`[zenith] Message: ${message}`);
|
|
193
|
+
lines.push(formatLine(mode, { glyph: '✖', tag: 'ERR', text: compactMessage }));
|
|
194
|
+
lines.push(formatHint(mode, hint || formatHintUrl(code)));
|
|
195
|
+
lines.push(`${formatPrefix(mode)} code: ${code || 'CLI_FAILED'}`);
|
|
196
|
+
lines.push(`${formatPrefix(mode)} phase: ${phase || DEFAULT_PHASE}`);
|
|
197
|
+
lines.push(`${formatPrefix(mode)} file: ${file || DEFAULT_FILE}`);
|
|
198
|
+
lines.push(`${formatPrefix(mode)} detail: ${message}`);
|
|
122
199
|
|
|
123
200
|
if (mode.debug && normalized.stack) {
|
|
124
|
-
lines.push(
|
|
201
|
+
lines.push(`${formatPrefix(mode)} stack:`);
|
|
125
202
|
lines.push(...String(normalized.stack).split('\n').slice(0, 20));
|
|
126
203
|
}
|
|
127
204
|
|