@lark-apaas/fullstack-cli 1.1.8 → 1.1.9-alpha.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/commands/gen-db-schema.js +4 -4
- package/dist/commands/read-logs/client-std.d.ts +2 -0
- package/dist/commands/read-logs/client-std.js +98 -0
- package/dist/commands/read-logs/json-lines.d.ts +3 -0
- package/dist/commands/read-logs/json-lines.js +217 -0
- package/dist/commands/read-logs/server-std.d.ts +1 -0
- package/dist/commands/read-logs/server-std.js +25 -0
- package/dist/commands/read-logs/std-utils.d.ts +5 -0
- package/dist/commands/read-logs/std-utils.js +61 -0
- package/dist/commands/read-logs/tail.d.ts +2 -0
- package/dist/commands/read-logs/tail.js +47 -0
- package/dist/commands/read-logs.d.ts +8 -1
- package/dist/commands/read-logs.js +112 -327
- package/dist/commands/read-logs.test.js +131 -1
- package/package.json +2 -2
- package/templates/scripts/dev.sh +231 -31
|
@@ -1,26 +1,125 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
1
|
import path from 'node:path';
|
|
2
|
+
import { readServerStdSegment } from './read-logs/server-std.js';
|
|
3
|
+
import { readClientStdSegment } from './read-logs/client-std.js';
|
|
4
|
+
import { readJsonLinesByTraceId, readJsonLinesLastPid, readJsonLinesTail } from './read-logs/json-lines.js';
|
|
5
|
+
import { fileExists, readFileTailLines } from './read-logs/tail.js';
|
|
6
|
+
function hasErrorInStdLines(lines) {
|
|
7
|
+
const combined = lines.join('\n');
|
|
8
|
+
if (!combined)
|
|
9
|
+
return false;
|
|
10
|
+
const strong = [
|
|
11
|
+
/compiled with errors/i,
|
|
12
|
+
/failed to compile/i,
|
|
13
|
+
/error: \w+/i,
|
|
14
|
+
/uncaught/i,
|
|
15
|
+
/unhandled/i,
|
|
16
|
+
/eaddrinuse/i,
|
|
17
|
+
/cannot find module/i,
|
|
18
|
+
/module not found/i,
|
|
19
|
+
/ts\d{3,5}:/i,
|
|
20
|
+
];
|
|
21
|
+
if (strong.some((re) => re.test(combined)))
|
|
22
|
+
return true;
|
|
23
|
+
const weakLine = /\b(error|fatal|exception)\b/i;
|
|
24
|
+
const ignorePatterns = [
|
|
25
|
+
/\b0\s+errors?\b/i,
|
|
26
|
+
/Server Error \d{3}/i,
|
|
27
|
+
];
|
|
28
|
+
return lines.some((line) => {
|
|
29
|
+
const text = line.trim();
|
|
30
|
+
if (!text)
|
|
31
|
+
return false;
|
|
32
|
+
if (ignorePatterns.some((re) => re.test(text)))
|
|
33
|
+
return false;
|
|
34
|
+
return weakLine.test(text);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function hasErrorInLogObject(value) {
|
|
38
|
+
if (!value || typeof value !== 'object')
|
|
39
|
+
return false;
|
|
40
|
+
const obj = value;
|
|
41
|
+
const level = typeof obj.level === 'string' ? obj.level.toLowerCase() : '';
|
|
42
|
+
if (level === 'error' || level === 'fatal')
|
|
43
|
+
return true;
|
|
44
|
+
if (level === 'err')
|
|
45
|
+
return true;
|
|
46
|
+
if (level === 'warn' && typeof obj.stack === 'string' && obj.stack.length > 0)
|
|
47
|
+
return true;
|
|
48
|
+
if (typeof obj.stack === 'string' && obj.stack.length > 0)
|
|
49
|
+
return true;
|
|
50
|
+
const statusCode = obj.statusCode;
|
|
51
|
+
const status_code = obj.status_code;
|
|
52
|
+
const meta = obj.meta;
|
|
53
|
+
const metaObj = meta && typeof meta === 'object' ? meta : null;
|
|
54
|
+
const metaStatusCode = metaObj?.statusCode;
|
|
55
|
+
const metaStatus_code = metaObj?.status_code;
|
|
56
|
+
const candidates = [statusCode, status_code, metaStatusCode, metaStatus_code];
|
|
57
|
+
for (const candidate of candidates) {
|
|
58
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate >= 400) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const message = typeof obj.message === 'string' ? obj.message : '';
|
|
63
|
+
if (message && hasErrorInStdLines([message]))
|
|
64
|
+
return true;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
3
67
|
export async function readLatestLogLines(options) {
|
|
4
68
|
const maxLines = options.maxLines ?? 200;
|
|
5
69
|
const filePath = resolveLogFilePath(options.logDir, options.type);
|
|
6
70
|
if (!fileExists(filePath)) {
|
|
7
71
|
throw new Error(`Log file not found: ${filePath}`);
|
|
8
72
|
}
|
|
9
|
-
let lines = readFileTailLines(filePath, maxLines * 5);
|
|
10
73
|
if (options.type === 'server-std') {
|
|
11
|
-
|
|
74
|
+
return readServerStdSegment(filePath, maxLines);
|
|
75
|
+
}
|
|
76
|
+
if (options.type === 'client-std') {
|
|
77
|
+
return readClientStdSegment(filePath, maxLines);
|
|
12
78
|
}
|
|
13
|
-
|
|
14
|
-
|
|
79
|
+
const traceId = typeof options.traceId === 'string' ? options.traceId.trim() : '';
|
|
80
|
+
if (traceId) {
|
|
81
|
+
return readJsonLinesByTraceId(filePath, traceId, maxLines);
|
|
15
82
|
}
|
|
16
|
-
|
|
83
|
+
let lines = readFileTailLines(filePath, maxLines * 5);
|
|
84
|
+
if (options.type === 'server' || options.type === 'trace') {
|
|
17
85
|
lines = readJsonLinesLastPid(filePath, maxLines);
|
|
18
86
|
}
|
|
19
|
-
else if (options.type === 'browser'
|
|
20
|
-
lines = readJsonLinesTail(
|
|
87
|
+
else if (options.type === 'browser') {
|
|
88
|
+
lines = readJsonLinesTail(lines, maxLines);
|
|
21
89
|
}
|
|
22
90
|
return lines;
|
|
23
91
|
}
|
|
92
|
+
export async function readLogsJsonResult(options) {
|
|
93
|
+
const lines = await readLatestLogLines(options);
|
|
94
|
+
if (options.type === 'server-std' || options.type === 'client-std') {
|
|
95
|
+
return {
|
|
96
|
+
hasError: hasErrorInStdLines(lines),
|
|
97
|
+
message: lines.join('\n'),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const logs = [];
|
|
101
|
+
let hasError = false;
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
if (!hasError && hasErrorInStdLines([line])) {
|
|
104
|
+
hasError = true;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(line);
|
|
108
|
+
logs.push(parsed);
|
|
109
|
+
if (!hasError && hasErrorInLogObject(parsed)) {
|
|
110
|
+
hasError = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
hasError,
|
|
119
|
+
message: '',
|
|
120
|
+
logs,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
24
123
|
function resolveLogFilePath(logDir, type) {
|
|
25
124
|
const base = path.isAbsolute(logDir) ? logDir : path.join(process.cwd(), logDir);
|
|
26
125
|
if (type === 'server') {
|
|
@@ -38,331 +137,17 @@ function resolveLogFilePath(logDir, type) {
|
|
|
38
137
|
if (type === 'browser') {
|
|
39
138
|
return path.join(base, 'browser.log');
|
|
40
139
|
}
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
function fileExists(filePath) {
|
|
44
|
-
try {
|
|
45
|
-
fs.accessSync(filePath, fs.constants.F_OK | fs.constants.R_OK);
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function readFileTailLines(filePath, maxLines) {
|
|
53
|
-
const stat = fs.statSync(filePath);
|
|
54
|
-
if (stat.size === 0) {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
const fd = fs.openSync(filePath, 'r');
|
|
58
|
-
const chunkSize = 64 * 1024;
|
|
59
|
-
const chunks = [];
|
|
60
|
-
let position = stat.size;
|
|
61
|
-
let collectedLines = 0;
|
|
62
|
-
try {
|
|
63
|
-
while (position > 0 && collectedLines <= maxLines) {
|
|
64
|
-
const length = Math.min(chunkSize, position);
|
|
65
|
-
const buffer = Buffer.alloc(length);
|
|
66
|
-
position -= length;
|
|
67
|
-
fs.readSync(fd, buffer, 0, length, position);
|
|
68
|
-
chunks.unshift(buffer.toString('utf8'));
|
|
69
|
-
const chunkLines = buffer.toString('utf8').split('\n').length - 1;
|
|
70
|
-
collectedLines += chunkLines;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
finally {
|
|
74
|
-
fs.closeSync(fd);
|
|
75
|
-
}
|
|
76
|
-
const content = chunks.join('');
|
|
77
|
-
const allLines = content.split('\n');
|
|
78
|
-
if (allLines.length === 0) {
|
|
79
|
-
return [];
|
|
80
|
-
}
|
|
81
|
-
if (allLines[allLines.length - 1] === '') {
|
|
82
|
-
allLines.pop();
|
|
83
|
-
}
|
|
84
|
-
if (allLines.length <= maxLines) {
|
|
85
|
-
return allLines;
|
|
86
|
-
}
|
|
87
|
-
return allLines.slice(allLines.length - maxLines);
|
|
88
|
-
}
|
|
89
|
-
function stripPrefixFromStdLine(line) {
|
|
90
|
-
const match = line.match(/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] \[(server|client)\] (.*)$/);
|
|
91
|
-
if (!match) {
|
|
92
|
-
return line;
|
|
93
|
-
}
|
|
94
|
-
return match[3] || '';
|
|
95
|
-
}
|
|
96
|
-
function findLastSegmentByMarkers(lines, markers) {
|
|
97
|
-
let startIndex = -1;
|
|
98
|
-
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
99
|
-
const line = lines[i];
|
|
100
|
-
if (markers.start(line)) {
|
|
101
|
-
startIndex = i;
|
|
102
|
-
break;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
if (startIndex === -1) {
|
|
106
|
-
return lines;
|
|
107
|
-
}
|
|
108
|
-
let endIndex = lines.length;
|
|
109
|
-
if (markers.reset) {
|
|
110
|
-
for (let i = startIndex + 1; i < lines.length; i += 1) {
|
|
111
|
-
if (markers.reset(lines[i])) {
|
|
112
|
-
endIndex = i;
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return lines.slice(startIndex, endIndex);
|
|
118
|
-
}
|
|
119
|
-
function extractServerStdSegment(lines, maxLines) {
|
|
120
|
-
const bodyLines = lines.map(stripPrefixFromStdLine);
|
|
121
|
-
let startIndex = -1;
|
|
122
|
-
for (let i = bodyLines.length - 1; i >= 0; i -= 1) {
|
|
123
|
-
const line = bodyLines[i];
|
|
124
|
-
if (!line)
|
|
125
|
-
continue;
|
|
126
|
-
if (/\bdev:server\b/.test(line)) {
|
|
127
|
-
startIndex = i;
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (startIndex !== -1) {
|
|
132
|
-
const segment = bodyLines.slice(startIndex);
|
|
133
|
-
if (segment.length <= maxLines) {
|
|
134
|
-
return segment;
|
|
135
|
-
}
|
|
136
|
-
return segment.slice(segment.length - maxLines);
|
|
137
|
-
}
|
|
138
|
-
const markers = {
|
|
139
|
-
start: (line) => {
|
|
140
|
-
if (!line)
|
|
141
|
-
return false;
|
|
142
|
-
if (line.includes('Starting compilation in watch mode'))
|
|
143
|
-
return true;
|
|
144
|
-
if (line.includes('File change detected. Starting incremental compilation'))
|
|
145
|
-
return true;
|
|
146
|
-
if (line.includes('Starting Nest application'))
|
|
147
|
-
return true;
|
|
148
|
-
if (line.includes('Nest application successfully started'))
|
|
149
|
-
return true;
|
|
150
|
-
return false;
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
const segment = findLastSegmentByMarkers(bodyLines, markers);
|
|
154
|
-
if (segment.length === 0) {
|
|
155
|
-
return [];
|
|
156
|
-
}
|
|
157
|
-
if (segment.length <= maxLines) {
|
|
158
|
-
return segment;
|
|
159
|
-
}
|
|
160
|
-
return segment.slice(segment.length - maxLines);
|
|
161
|
-
}
|
|
162
|
-
function extractClientStdSegment(lines, maxLines) {
|
|
163
|
-
const bodyLines = lines.map(stripPrefixFromStdLine);
|
|
164
|
-
const hotStartMarkers = [
|
|
165
|
-
/file change detected\..*incremental compilation/i,
|
|
166
|
-
/starting incremental compilation/i,
|
|
167
|
-
/starting compilation/i,
|
|
168
|
-
/\bcompiling\b/i,
|
|
169
|
-
/\brecompil/i,
|
|
170
|
-
];
|
|
171
|
-
const hotEndMarkers = [
|
|
172
|
-
/file change detected\..*incremental compilation/i,
|
|
173
|
-
/\bwebpack compiled\b/i,
|
|
174
|
-
/compiled successfully/i,
|
|
175
|
-
/compiled with warnings/i,
|
|
176
|
-
/compiled with errors/i,
|
|
177
|
-
/failed to compile/i,
|
|
178
|
-
/fast refresh/i,
|
|
179
|
-
/\bhmr\b/i,
|
|
180
|
-
/hot update/i,
|
|
181
|
-
/\bhot reload\b/i,
|
|
182
|
-
/\bhmr update\b/i,
|
|
183
|
-
];
|
|
184
|
-
const compiledMarkers = [
|
|
185
|
-
/\bwebpack compiled\b/i,
|
|
186
|
-
/compiled successfully/i,
|
|
187
|
-
/compiled with warnings/i,
|
|
188
|
-
/compiled with errors/i,
|
|
189
|
-
/failed to compile/i,
|
|
190
|
-
];
|
|
191
|
-
let startIndex = -1;
|
|
192
|
-
for (let i = bodyLines.length - 1; i >= 0; i -= 1) {
|
|
193
|
-
const line = bodyLines[i];
|
|
194
|
-
if (!line)
|
|
195
|
-
continue;
|
|
196
|
-
if (hotStartMarkers.some((re) => re.test(line))) {
|
|
197
|
-
startIndex = i;
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
if (startIndex === -1) {
|
|
202
|
-
let pivotIndex = -1;
|
|
203
|
-
for (let i = bodyLines.length - 1; i >= 0; i -= 1) {
|
|
204
|
-
const line = bodyLines[i];
|
|
205
|
-
if (!line)
|
|
206
|
-
continue;
|
|
207
|
-
if (hotEndMarkers.some((re) => re.test(line))) {
|
|
208
|
-
pivotIndex = i;
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
if (pivotIndex !== -1) {
|
|
213
|
-
if (compiledMarkers.some((re) => re.test(bodyLines[pivotIndex] ?? ''))) {
|
|
214
|
-
startIndex = pivotIndex;
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
const searchLimit = 80;
|
|
218
|
-
const from = Math.max(0, pivotIndex - searchLimit);
|
|
219
|
-
for (let i = pivotIndex; i >= from; i -= 1) {
|
|
220
|
-
const line = bodyLines[i];
|
|
221
|
-
if (!line)
|
|
222
|
-
continue;
|
|
223
|
-
if (compiledMarkers.some((re) => re.test(line))) {
|
|
224
|
-
startIndex = i;
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (startIndex === -1) {
|
|
229
|
-
startIndex = pivotIndex;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
if (startIndex === -1) {
|
|
235
|
-
for (let i = bodyLines.length - 1; i >= 0; i -= 1) {
|
|
236
|
-
const line = bodyLines[i];
|
|
237
|
-
if (!line)
|
|
238
|
-
continue;
|
|
239
|
-
if (/\bdev:client\b/.test(line)) {
|
|
240
|
-
startIndex = i;
|
|
241
|
-
break;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
const segment = startIndex === -1 ? bodyLines : bodyLines.slice(startIndex);
|
|
246
|
-
if (segment.length === 0) {
|
|
247
|
-
return [];
|
|
248
|
-
}
|
|
249
|
-
if (segment.length <= maxLines) {
|
|
250
|
-
return segment;
|
|
251
|
-
}
|
|
252
|
-
return segment.slice(segment.length - maxLines);
|
|
253
|
-
}
|
|
254
|
-
function normalizePid(value) {
|
|
255
|
-
if (typeof value === 'number') {
|
|
256
|
-
return String(value);
|
|
257
|
-
}
|
|
258
|
-
if (typeof value === 'string' && value.length > 0) {
|
|
259
|
-
return value;
|
|
260
|
-
}
|
|
261
|
-
return 'unknown';
|
|
262
|
-
}
|
|
263
|
-
function readJsonLinesLastPid(filePath, maxLines) {
|
|
264
|
-
const stat = fs.statSync(filePath);
|
|
265
|
-
if (stat.size === 0) {
|
|
266
|
-
return [];
|
|
267
|
-
}
|
|
268
|
-
const fd = fs.openSync(filePath, 'r');
|
|
269
|
-
const chunkSize = 64 * 1024;
|
|
270
|
-
let position = stat.size;
|
|
271
|
-
let remainder = '';
|
|
272
|
-
let targetPid = null;
|
|
273
|
-
let finished = false;
|
|
274
|
-
const collected = [];
|
|
275
|
-
try {
|
|
276
|
-
while (position > 0 && !finished) {
|
|
277
|
-
const length = Math.min(chunkSize, position);
|
|
278
|
-
position -= length;
|
|
279
|
-
const buffer = Buffer.alloc(length);
|
|
280
|
-
fs.readSync(fd, buffer, 0, length, position);
|
|
281
|
-
let chunk = buffer.toString('utf8');
|
|
282
|
-
if (remainder) {
|
|
283
|
-
chunk += remainder;
|
|
284
|
-
remainder = '';
|
|
285
|
-
}
|
|
286
|
-
const parts = chunk.split('\n');
|
|
287
|
-
remainder = parts.shift() ?? '';
|
|
288
|
-
for (let i = parts.length - 1; i >= 0; i -= 1) {
|
|
289
|
-
const line = parts[i].trim();
|
|
290
|
-
if (!line)
|
|
291
|
-
continue;
|
|
292
|
-
let parsed = null;
|
|
293
|
-
try {
|
|
294
|
-
parsed = JSON.parse(line);
|
|
295
|
-
}
|
|
296
|
-
catch {
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
const pid = normalizePid(parsed?.pid);
|
|
300
|
-
if (targetPid === null) {
|
|
301
|
-
targetPid = pid;
|
|
302
|
-
}
|
|
303
|
-
if (pid !== targetPid) {
|
|
304
|
-
finished = true;
|
|
305
|
-
break;
|
|
306
|
-
}
|
|
307
|
-
collected.push(line);
|
|
308
|
-
if (collected.length >= maxLines * 5) {
|
|
309
|
-
finished = true;
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
if (!finished && remainder) {
|
|
315
|
-
const line = remainder.trim();
|
|
316
|
-
if (line) {
|
|
317
|
-
try {
|
|
318
|
-
const parsed = JSON.parse(line);
|
|
319
|
-
const pid = normalizePid(parsed?.pid);
|
|
320
|
-
if (targetPid === null) {
|
|
321
|
-
targetPid = pid;
|
|
322
|
-
}
|
|
323
|
-
if (pid === targetPid) {
|
|
324
|
-
collected.push(line);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
catch {
|
|
328
|
-
return [];
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
finally {
|
|
334
|
-
fs.closeSync(fd);
|
|
335
|
-
}
|
|
336
|
-
const reversed = collected.reverse();
|
|
337
|
-
if (reversed.length <= maxLines) {
|
|
338
|
-
return reversed;
|
|
339
|
-
}
|
|
340
|
-
return reversed.slice(reversed.length - maxLines);
|
|
341
|
-
}
|
|
342
|
-
function readJsonLinesTail(filePath, maxLines) {
|
|
343
|
-
const lines = readFileTailLines(filePath, maxLines);
|
|
344
|
-
const result = [];
|
|
345
|
-
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
346
|
-
const line = lines[i].trim();
|
|
347
|
-
if (!line)
|
|
348
|
-
continue;
|
|
349
|
-
result.push(line);
|
|
350
|
-
if (result.length >= maxLines) {
|
|
351
|
-
break;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return result.reverse();
|
|
140
|
+
throw new Error(`Unsupported log type: ${type}`);
|
|
355
141
|
}
|
|
356
142
|
export async function run(options) {
|
|
357
143
|
try {
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
process.stdout.write(line + '\n');
|
|
361
|
-
}
|
|
144
|
+
const result = await readLogsJsonResult(options);
|
|
145
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
362
146
|
}
|
|
363
147
|
catch (error) {
|
|
364
148
|
const message = error instanceof Error ? error.message : String(error);
|
|
365
|
-
|
|
149
|
+
const result = { hasError: true, message };
|
|
150
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
366
151
|
process.exitCode = 1;
|
|
367
152
|
}
|
|
368
153
|
}
|
|
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { readLatestLogLines } from './read-logs';
|
|
5
|
+
import { readLatestLogLines, readLogsJsonResult } from './read-logs';
|
|
6
6
|
async function writeFile(dir, fileName, content) {
|
|
7
7
|
await fs.promises.mkdir(dir, { recursive: true });
|
|
8
8
|
await fs.promises.writeFile(path.join(dir, fileName), content, 'utf8');
|
|
@@ -31,6 +31,31 @@ describe('readLatestLogLines', () => {
|
|
|
31
31
|
});
|
|
32
32
|
expect(lines).toEqual(['> app@1.0.0 dev:server', 'started', 'ready']);
|
|
33
33
|
});
|
|
34
|
+
it('returns only the last server-std segment by latest marker', async () => {
|
|
35
|
+
const content = [
|
|
36
|
+
'[2026-01-05 10:00:00] [server] > app@1.0.0 dev:server',
|
|
37
|
+
'[2026-01-05 10:00:01] [server] [10:00:01] Starting compilation in watch mode...',
|
|
38
|
+
'[2026-01-05 10:00:02] [server] other',
|
|
39
|
+
'[2026-01-05 10:00:03] [server] [10:00:03] File change detected. Starting incremental compilation...',
|
|
40
|
+
'[2026-01-05 10:00:04] [server] other2',
|
|
41
|
+
'[2026-01-05 10:00:05] [server] [Nest] 1 - 2026/01/05 10:00:05 LOG [NestFactory] Starting Nest application...',
|
|
42
|
+
'[2026-01-05 10:00:06] [server] after-old',
|
|
43
|
+
'[2026-01-05 10:01:00] [server] [10:01:00] File change detected. Starting incremental compilation...',
|
|
44
|
+
'[2026-01-05 10:01:01] [server] tail-1',
|
|
45
|
+
'[2026-01-05 10:01:02] [server] tail-2',
|
|
46
|
+
].join('\n');
|
|
47
|
+
await writeFile(tmpDir, 'server.std.log', content);
|
|
48
|
+
const lines = await readLatestLogLines({
|
|
49
|
+
logDir: tmpDir,
|
|
50
|
+
type: 'server-std',
|
|
51
|
+
maxLines: 200,
|
|
52
|
+
});
|
|
53
|
+
expect(lines).toEqual([
|
|
54
|
+
'[10:01:00] File change detected. Starting incremental compilation...',
|
|
55
|
+
'tail-1',
|
|
56
|
+
'tail-2',
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
34
59
|
it('returns only the last client-std segment by hot reload markers', async () => {
|
|
35
60
|
const content = [
|
|
36
61
|
'[2026-01-05 10:00:00] [client] > app@1.0.0 dev:client',
|
|
@@ -66,4 +91,109 @@ describe('readLatestLogLines', () => {
|
|
|
66
91
|
JSON.stringify({ pid: 200, time: '2026-01-05T11:00:01.000Z', level: 'INFO', message: 'new2' }),
|
|
67
92
|
]);
|
|
68
93
|
});
|
|
94
|
+
it('filters server.log by traceId when provided', async () => {
|
|
95
|
+
const content = [
|
|
96
|
+
JSON.stringify({ pid: 200, trace_id: 't1', level: 'INFO', message: 'a' }),
|
|
97
|
+
JSON.stringify({ pid: 200, trace_id: 't2', level: 'INFO', message: 'b' }),
|
|
98
|
+
JSON.stringify({ pid: 200, trace_id: 't2', level: 'INFO', message: 'c' }),
|
|
99
|
+
JSON.stringify({ pid: 200, trace_id: 't3', level: 'INFO', message: 'd' }),
|
|
100
|
+
].join('\n');
|
|
101
|
+
await writeFile(tmpDir, 'server.log', content);
|
|
102
|
+
const result = await readLogsJsonResult({
|
|
103
|
+
logDir: tmpDir,
|
|
104
|
+
type: 'server',
|
|
105
|
+
maxLines: 200,
|
|
106
|
+
traceId: 't2',
|
|
107
|
+
});
|
|
108
|
+
expect(result).toEqual({
|
|
109
|
+
hasError: false,
|
|
110
|
+
message: '',
|
|
111
|
+
logs: [
|
|
112
|
+
{ pid: 200, trace_id: 't2', level: 'INFO', message: 'b' },
|
|
113
|
+
{ pid: 200, trace_id: 't2', level: 'INFO', message: 'c' },
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
it('filters browser.log by traceId when provided', async () => {
|
|
118
|
+
const content = [
|
|
119
|
+
JSON.stringify({ level: 'info', message: 'x', trace_id: 't1' }),
|
|
120
|
+
JSON.stringify({ level: 'info', message: 'y', trace_id: 't2' }),
|
|
121
|
+
JSON.stringify({ level: 'info', message: 'z', trace_id: 't2' }),
|
|
122
|
+
].join('\n');
|
|
123
|
+
await writeFile(tmpDir, 'browser.log', content);
|
|
124
|
+
const result = await readLogsJsonResult({
|
|
125
|
+
logDir: tmpDir,
|
|
126
|
+
type: 'browser',
|
|
127
|
+
maxLines: 200,
|
|
128
|
+
traceId: 't2',
|
|
129
|
+
});
|
|
130
|
+
expect(result).toEqual({
|
|
131
|
+
hasError: false,
|
|
132
|
+
message: '',
|
|
133
|
+
logs: [
|
|
134
|
+
{ level: 'info', message: 'y', trace_id: 't2' },
|
|
135
|
+
{ level: 'info', message: 'z', trace_id: 't2' },
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
it('returns JSON result for server-std as message', async () => {
|
|
140
|
+
const content = [
|
|
141
|
+
'[2026-01-05 10:00:00] [server] > app@1.0.0 dev:server',
|
|
142
|
+
'[2026-01-05 10:00:01] [server] booting',
|
|
143
|
+
].join('\n');
|
|
144
|
+
await writeFile(tmpDir, 'server.std.log', content);
|
|
145
|
+
const result = await readLogsJsonResult({
|
|
146
|
+
logDir: tmpDir,
|
|
147
|
+
type: 'server-std',
|
|
148
|
+
maxLines: 200,
|
|
149
|
+
});
|
|
150
|
+
expect(result).toEqual({
|
|
151
|
+
hasError: false,
|
|
152
|
+
message: '> app@1.0.0 dev:server\nbooting',
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
it('returns JSON result for server as logs', async () => {
|
|
156
|
+
const content = [
|
|
157
|
+
JSON.stringify({ pid: 100, message: 'old' }),
|
|
158
|
+
JSON.stringify({ pid: 200, message: 'new' }),
|
|
159
|
+
JSON.stringify({ pid: 200, message: 'new2' }),
|
|
160
|
+
].join('\n');
|
|
161
|
+
await writeFile(tmpDir, 'server.log', content);
|
|
162
|
+
const result = await readLogsJsonResult({
|
|
163
|
+
logDir: tmpDir,
|
|
164
|
+
type: 'server',
|
|
165
|
+
maxLines: 200,
|
|
166
|
+
});
|
|
167
|
+
expect(result).toEqual({
|
|
168
|
+
hasError: false,
|
|
169
|
+
message: '',
|
|
170
|
+
logs: [{ pid: 200, message: 'new' }, { pid: 200, message: 'new2' }],
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
it('sets hasError=true for server-std error keywords', async () => {
|
|
174
|
+
const content = [
|
|
175
|
+
'[2026-01-05 10:00:00] [server] > app@1.0.0 dev:server',
|
|
176
|
+
'[2026-01-05 10:00:01] [server] ERROR something bad happened',
|
|
177
|
+
].join('\n');
|
|
178
|
+
await writeFile(tmpDir, 'server.std.log', content);
|
|
179
|
+
const result = await readLogsJsonResult({
|
|
180
|
+
logDir: tmpDir,
|
|
181
|
+
type: 'server-std',
|
|
182
|
+
maxLines: 200,
|
|
183
|
+
});
|
|
184
|
+
expect(result.hasError).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
it('sets hasError=true for server JSON logs with error level', async () => {
|
|
187
|
+
const content = [
|
|
188
|
+
JSON.stringify({ pid: 200, level: 'INFO', message: 'ok' }),
|
|
189
|
+
JSON.stringify({ pid: 200, level: 'ERROR', message: 'failed' }),
|
|
190
|
+
].join('\n');
|
|
191
|
+
await writeFile(tmpDir, 'server.log', content);
|
|
192
|
+
const result = await readLogsJsonResult({
|
|
193
|
+
logDir: tmpDir,
|
|
194
|
+
type: 'server',
|
|
195
|
+
maxLines: 200,
|
|
196
|
+
});
|
|
197
|
+
expect(result.hasError).toBe(true);
|
|
198
|
+
});
|
|
69
199
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/fullstack-cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.9-alpha.1",
|
|
4
4
|
"description": "CLI tool for fullstack template management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@lark-apaas/devtool-kits": "
|
|
34
|
+
"@lark-apaas/devtool-kits": "1.2.12-alpha.0",
|
|
35
35
|
"@vercel/nft": "^0.30.3",
|
|
36
36
|
"cac": "^6.7.14",
|
|
37
37
|
"dotenv": "^16.0.0",
|