@andy2639/jest-context 1.0.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.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/bin/console-context.cjs +685 -0
- package/dist/bin/console-context.cjs.map +1 -0
- package/dist/bin/console-context.d.mts +1 -0
- package/dist/bin/console-context.d.ts +1 -0
- package/dist/bin/console-context.mjs +12 -0
- package/dist/bin/console-context.mjs.map +1 -0
- package/dist/bin/coverage-context.cjs +496 -0
- package/dist/bin/coverage-context.cjs.map +1 -0
- package/dist/bin/coverage-context.d.mts +1 -0
- package/dist/bin/coverage-context.d.ts +1 -0
- package/dist/bin/coverage-context.mjs +12 -0
- package/dist/bin/coverage-context.mjs.map +1 -0
- package/dist/bin/test-context.cjs +585 -0
- package/dist/bin/test-context.cjs.map +1 -0
- package/dist/bin/test-context.d.mts +1 -0
- package/dist/bin/test-context.d.ts +1 -0
- package/dist/bin/test-context.mjs +12 -0
- package/dist/bin/test-context.mjs.map +1 -0
- package/dist/chunk-DEJBEL4M.mjs +168 -0
- package/dist/chunk-DEJBEL4M.mjs.map +1 -0
- package/dist/chunk-DUQBPBV4.mjs +252 -0
- package/dist/chunk-DUQBPBV4.mjs.map +1 -0
- package/dist/chunk-WPFTKCAT.mjs +169 -0
- package/dist/chunk-WPFTKCAT.mjs.map +1 -0
- package/dist/chunk-YTFA3KPD.mjs +513 -0
- package/dist/chunk-YTFA3KPD.mjs.map +1 -0
- package/dist/index.cjs +1112 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +141 -0
- package/dist/index.d.ts +141 -0
- package/dist/index.mjs +99 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +57 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
CONSOLE_LOG_TYPES: () => CONSOLE_LOG_TYPES,
|
|
34
|
+
JEST_PATTERNS: () => JEST_PATTERNS,
|
|
35
|
+
VALID_EXPORT_FORMATS: () => VALID_EXPORT_FORMATS,
|
|
36
|
+
VALID_MODES: () => VALID_MODES,
|
|
37
|
+
VALID_TEST_EXTENSIONS: () => VALID_TEST_EXTENSIONS,
|
|
38
|
+
buildJestArgsForMode: () => buildJestArgsForMode,
|
|
39
|
+
buildTimestampedPath: () => buildTimestampedPath,
|
|
40
|
+
createProgressBar: () => createProgressBar,
|
|
41
|
+
createSpinner: () => createSpinner,
|
|
42
|
+
detectPackageManager: () => detectPackageManager,
|
|
43
|
+
displayBanner: () => displayBanner,
|
|
44
|
+
displayHelp: () => displayHelp,
|
|
45
|
+
execCommand: () => execCommand,
|
|
46
|
+
exitWithError: () => exitWithError,
|
|
47
|
+
exportToFile: () => exportToFile,
|
|
48
|
+
extractConsoleType: () => extractConsoleType,
|
|
49
|
+
extractFailures: () => extractFailures,
|
|
50
|
+
filterTestFiles: () => filterTestFiles,
|
|
51
|
+
formatAsMarkdown: () => formatAsMarkdown,
|
|
52
|
+
getDisplayTimestamp: () => getDisplayTimestamp,
|
|
53
|
+
getFilenameTimestamp: () => getFilenameTimestamp,
|
|
54
|
+
getJestCommand: () => getJestCommand,
|
|
55
|
+
getPlatform: () => getPlatform,
|
|
56
|
+
getStagedFiles: () => getStagedFiles,
|
|
57
|
+
handleExportOrDisplay: () => handleExportOrDisplay,
|
|
58
|
+
hasGit: () => hasGit,
|
|
59
|
+
isCodeSnippetLine: () => isCodeSnippetLine,
|
|
60
|
+
isConsoleTypeLine: () => isConsoleTypeLine,
|
|
61
|
+
isLinux: () => isLinux,
|
|
62
|
+
isMac: () => isMac,
|
|
63
|
+
isStackTraceLine: () => isStackTraceLine,
|
|
64
|
+
isWindows: () => isWindows,
|
|
65
|
+
logInfo: () => logInfo,
|
|
66
|
+
logSuccess: () => logSuccess,
|
|
67
|
+
logWarning: () => logWarning,
|
|
68
|
+
parseExportCliArgs: () => parseExportCliArgs,
|
|
69
|
+
resolveCommand: () => resolveCommand,
|
|
70
|
+
runConsoleContext: () => runConsoleContext,
|
|
71
|
+
runCoverageContext: () => runCoverageContext,
|
|
72
|
+
runJest: () => runJest,
|
|
73
|
+
runTestContext: () => runTestContext,
|
|
74
|
+
shouldShowUI: () => shouldShowUI,
|
|
75
|
+
stripAnsi: () => stripAnsi,
|
|
76
|
+
validateCliArgs: () => validateCliArgs
|
|
77
|
+
});
|
|
78
|
+
module.exports = __toCommonJS(index_exports);
|
|
79
|
+
|
|
80
|
+
// src/core/constants.ts
|
|
81
|
+
var VALID_MODES = ["--all", "--related", "--tests"];
|
|
82
|
+
var VALID_EXPORT_FORMATS = ["txt", "md"];
|
|
83
|
+
var VALID_TEST_EXTENSIONS = /\.(test|spec)\.(ts|tsx|js|jsx)$/;
|
|
84
|
+
var CONSOLE_LOG_TYPES = {
|
|
85
|
+
ERROR: "console.error",
|
|
86
|
+
WARN: "console.warn",
|
|
87
|
+
LOG: "console.log",
|
|
88
|
+
DEBUG: "console.debug"
|
|
89
|
+
};
|
|
90
|
+
var JEST_PATTERNS = {
|
|
91
|
+
FAIL_LINE: /^FAIL\s+/,
|
|
92
|
+
PASS_LINE: /^PASS\s+/,
|
|
93
|
+
TEST_FILE: /^(PASS|FAIL)\s+(.+\.(test|spec)\.tsx?)/,
|
|
94
|
+
COVERAGE_LINE: /^(.+?)\s+\|\s+([\d.]+)\s+\|\s+([\d.]+)\s+\|\s+([\d.]+)\s+\|\s+([\d.]+)(?:\s+\|\s+(.+))?$/,
|
|
95
|
+
BULLET_POINT: /^●\s+/,
|
|
96
|
+
CODE_LINE: /^>?\s*\d*\s*\|/,
|
|
97
|
+
STACK_TRACE: /^\s+at\s+/
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// src/core/platform.ts
|
|
101
|
+
function getPlatform() {
|
|
102
|
+
return process.platform;
|
|
103
|
+
}
|
|
104
|
+
function isWindows() {
|
|
105
|
+
return process.platform === "win32";
|
|
106
|
+
}
|
|
107
|
+
function isMac() {
|
|
108
|
+
return process.platform === "darwin";
|
|
109
|
+
}
|
|
110
|
+
function isLinux() {
|
|
111
|
+
return process.platform === "linux";
|
|
112
|
+
}
|
|
113
|
+
function resolveCommand(cmd) {
|
|
114
|
+
if (!isWindows()) return cmd;
|
|
115
|
+
const windowsCommands = {
|
|
116
|
+
git: "git.exe",
|
|
117
|
+
node: "node.exe",
|
|
118
|
+
npm: "npm.cmd",
|
|
119
|
+
npx: "npx.cmd",
|
|
120
|
+
yarn: "yarn.cmd",
|
|
121
|
+
pnpm: "pnpm.cmd"
|
|
122
|
+
};
|
|
123
|
+
return windowsCommands[cmd] ?? cmd;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/core/package-manager.ts
|
|
127
|
+
function detectPackageManager() {
|
|
128
|
+
if (process.env.npm_execpath?.includes("yarn")) return "yarn";
|
|
129
|
+
if (process.env.npm_execpath?.includes("pnpm")) return "pnpm";
|
|
130
|
+
return "npm";
|
|
131
|
+
}
|
|
132
|
+
function getJestCommand(packageManager = null) {
|
|
133
|
+
const pm = packageManager ?? detectPackageManager();
|
|
134
|
+
switch (pm) {
|
|
135
|
+
case "yarn":
|
|
136
|
+
return ["yarn", "jest"];
|
|
137
|
+
case "pnpm":
|
|
138
|
+
return ["pnpm", "jest"];
|
|
139
|
+
default:
|
|
140
|
+
return ["npx", "jest"];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/core/exec.ts
|
|
145
|
+
var import_node_child_process = require("child_process");
|
|
146
|
+
async function execCommand(cmd, args = [], options = {}) {
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
const resolvedCmd = resolveCommand(cmd);
|
|
149
|
+
const child = (0, import_node_child_process.spawn)(resolvedCmd, args, {
|
|
150
|
+
shell: options.shell ?? process.platform === "win32"
|
|
151
|
+
});
|
|
152
|
+
let stdout = "";
|
|
153
|
+
let stderr = "";
|
|
154
|
+
child.stdout?.on("data", (d) => {
|
|
155
|
+
stdout += d.toString();
|
|
156
|
+
});
|
|
157
|
+
child.stderr?.on("data", (d) => {
|
|
158
|
+
stderr += d.toString();
|
|
159
|
+
});
|
|
160
|
+
child.on("close", (code) => {
|
|
161
|
+
const result = {
|
|
162
|
+
stdout,
|
|
163
|
+
stderr,
|
|
164
|
+
output: `${stdout}
|
|
165
|
+
${stderr}`,
|
|
166
|
+
code
|
|
167
|
+
};
|
|
168
|
+
if (code !== 0 && !options.ignoreErrors) {
|
|
169
|
+
reject(new Error(`Command failed with code ${code}: ${cmd} ${args.join(" ")}`));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
resolve(result);
|
|
173
|
+
});
|
|
174
|
+
child.on("error", (error) => {
|
|
175
|
+
reject(new Error(`Failed to execute command: ${error.message}`));
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/core/git.ts
|
|
181
|
+
var cachedGitAvailability = null;
|
|
182
|
+
var cachedStagedFiles = null;
|
|
183
|
+
var cachedStagedFilesAt = 0;
|
|
184
|
+
var STAGED_FILES_CACHE_TTL_MS = 1e3;
|
|
185
|
+
async function hasGit() {
|
|
186
|
+
if (cachedGitAvailability !== null) {
|
|
187
|
+
return cachedGitAvailability;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const result = await execCommand("git", ["--version"], { ignoreErrors: true });
|
|
191
|
+
cachedGitAvailability = result.code === 0;
|
|
192
|
+
return cachedGitAvailability;
|
|
193
|
+
} catch {
|
|
194
|
+
cachedGitAvailability = false;
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function getStagedFiles() {
|
|
199
|
+
const now = Date.now();
|
|
200
|
+
if (cachedStagedFiles && now - cachedStagedFilesAt <= STAGED_FILES_CACHE_TTL_MS) {
|
|
201
|
+
return cachedStagedFiles;
|
|
202
|
+
}
|
|
203
|
+
const gitAvailable = await hasGit();
|
|
204
|
+
if (!gitAvailable) {
|
|
205
|
+
throw new Error("Git is not installed or not available in PATH");
|
|
206
|
+
}
|
|
207
|
+
const result = await execCommand("git", ["diff", "--name-only", "--cached"], {
|
|
208
|
+
ignoreErrors: true
|
|
209
|
+
});
|
|
210
|
+
const files = result.stdout.split("\n").map((f) => f.trim()).filter(Boolean);
|
|
211
|
+
cachedStagedFiles = files;
|
|
212
|
+
cachedStagedFilesAt = now;
|
|
213
|
+
return files;
|
|
214
|
+
}
|
|
215
|
+
function filterTestFiles(files) {
|
|
216
|
+
return files.filter((file) => VALID_TEST_EXTENSIONS.test(file));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/core/jest.ts
|
|
220
|
+
var import_node_child_process2 = require("child_process");
|
|
221
|
+
async function runJest(args = [], options = {}) {
|
|
222
|
+
const {
|
|
223
|
+
verbose = false,
|
|
224
|
+
coverage = false,
|
|
225
|
+
packageManager = null,
|
|
226
|
+
ignoreErrors = true,
|
|
227
|
+
disableVerbose = false
|
|
228
|
+
} = options;
|
|
229
|
+
const jestCmd = getJestCommand(packageManager);
|
|
230
|
+
const jestArgs = [...jestCmd.slice(1)];
|
|
231
|
+
if (verbose && !disableVerbose) {
|
|
232
|
+
jestArgs.push("--verbose");
|
|
233
|
+
}
|
|
234
|
+
if (coverage) {
|
|
235
|
+
jestArgs.push("--coverage");
|
|
236
|
+
if (options.coverageReporters) {
|
|
237
|
+
for (const reporter of options.coverageReporters) {
|
|
238
|
+
jestArgs.push(`--coverageReporters=${reporter}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (isWindows()) {
|
|
243
|
+
jestArgs.push("--no-watchman");
|
|
244
|
+
}
|
|
245
|
+
jestArgs.push(...args);
|
|
246
|
+
return new Promise((resolve, reject) => {
|
|
247
|
+
const child = (0, import_node_child_process2.spawn)(jestCmd[0], jestArgs, {
|
|
248
|
+
shell: isWindows()
|
|
249
|
+
});
|
|
250
|
+
let stdout = "";
|
|
251
|
+
let stderr = "";
|
|
252
|
+
child.stdout.on("data", (d) => {
|
|
253
|
+
stdout += d.toString();
|
|
254
|
+
});
|
|
255
|
+
child.stderr.on("data", (d) => {
|
|
256
|
+
stderr += d.toString();
|
|
257
|
+
});
|
|
258
|
+
child.on("close", (code) => {
|
|
259
|
+
const result = {
|
|
260
|
+
stdout,
|
|
261
|
+
stderr,
|
|
262
|
+
output: `${stdout}
|
|
263
|
+
${stderr}`,
|
|
264
|
+
code
|
|
265
|
+
};
|
|
266
|
+
if (code !== 0 && !ignoreErrors) {
|
|
267
|
+
reject(new Error(`Jest failed with code ${code}`));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
resolve(result);
|
|
271
|
+
});
|
|
272
|
+
child.on("error", reject);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
async function buildJestArgsForMode(mode) {
|
|
276
|
+
if (mode === "all") {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
const staged = await getStagedFiles();
|
|
280
|
+
if (staged.length === 0) {
|
|
281
|
+
throw new Error(`No staged files found for mode: ${mode}`);
|
|
282
|
+
}
|
|
283
|
+
if (mode === "tests") {
|
|
284
|
+
const testFiles = filterTestFiles(staged);
|
|
285
|
+
if (testFiles.length === 0) {
|
|
286
|
+
throw new Error("No test files found in staged files");
|
|
287
|
+
}
|
|
288
|
+
return testFiles;
|
|
289
|
+
}
|
|
290
|
+
return ["--findRelatedTests", ...staged];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/core/parsing.ts
|
|
294
|
+
function stripAnsi(text) {
|
|
295
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
296
|
+
}
|
|
297
|
+
function isCodeSnippetLine(line) {
|
|
298
|
+
return JEST_PATTERNS.CODE_LINE.test(stripAnsi(line).trim());
|
|
299
|
+
}
|
|
300
|
+
function isStackTraceLine(line) {
|
|
301
|
+
return JEST_PATTERNS.STACK_TRACE.test(stripAnsi(line));
|
|
302
|
+
}
|
|
303
|
+
function isConsoleTypeLine(line) {
|
|
304
|
+
const clean = stripAnsi(line).trim();
|
|
305
|
+
return Object.values(CONSOLE_LOG_TYPES).includes(clean);
|
|
306
|
+
}
|
|
307
|
+
function extractConsoleType(line) {
|
|
308
|
+
const clean = stripAnsi(line).trim();
|
|
309
|
+
return Object.values(CONSOLE_LOG_TYPES).find((type) => type === clean) ?? null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/core/export.ts
|
|
313
|
+
var import_node_fs = require("fs");
|
|
314
|
+
var import_node_path = __toESM(require("path"));
|
|
315
|
+
function getDisplayTimestamp(locale = "en-US") {
|
|
316
|
+
return (/* @__PURE__ */ new Date()).toLocaleString(locale, {
|
|
317
|
+
year: "numeric",
|
|
318
|
+
month: "2-digit",
|
|
319
|
+
day: "2-digit",
|
|
320
|
+
hour: "2-digit",
|
|
321
|
+
minute: "2-digit",
|
|
322
|
+
second: "2-digit",
|
|
323
|
+
hour12: false
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function getFilenameTimestamp() {
|
|
327
|
+
const now = /* @__PURE__ */ new Date();
|
|
328
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
329
|
+
return [now.getFullYear(), pad(now.getMonth() + 1), pad(now.getDate())].join("-") + "_" + [pad(now.getHours()), pad(now.getMinutes()), pad(now.getSeconds())].join("-");
|
|
330
|
+
}
|
|
331
|
+
function buildTimestampedPath(prefix, extension, dir = process.cwd()) {
|
|
332
|
+
const timestamp = getFilenameTimestamp();
|
|
333
|
+
const filename = `${prefix}-${timestamp}.${extension}`;
|
|
334
|
+
return import_node_path.default.resolve(dir, filename);
|
|
335
|
+
}
|
|
336
|
+
function formatAsMarkdown(content, title, timestamp = null) {
|
|
337
|
+
const lines = [`# ${title}`, ""];
|
|
338
|
+
if (timestamp) {
|
|
339
|
+
lines.push(`Generated: ${timestamp}`, "");
|
|
340
|
+
}
|
|
341
|
+
lines.push("```text", content.trimEnd(), "```", "");
|
|
342
|
+
return lines.join("\n");
|
|
343
|
+
}
|
|
344
|
+
function exportToFile(content, options) {
|
|
345
|
+
const { prefix, format, title, dir = process.cwd() } = options;
|
|
346
|
+
const filePath = buildTimestampedPath(prefix, format, dir);
|
|
347
|
+
let finalContent = content;
|
|
348
|
+
if (format === "md" && title) {
|
|
349
|
+
finalContent = formatAsMarkdown(content, title, getDisplayTimestamp());
|
|
350
|
+
}
|
|
351
|
+
(0, import_node_fs.writeFileSync)(filePath, finalContent, "utf-8");
|
|
352
|
+
return filePath;
|
|
353
|
+
}
|
|
354
|
+
function parseExportCliArgs(args) {
|
|
355
|
+
let exportFormat = null;
|
|
356
|
+
const exportArgs = [];
|
|
357
|
+
for (const arg of args) {
|
|
358
|
+
if (arg === "--export") {
|
|
359
|
+
exportFormat = "txt";
|
|
360
|
+
exportArgs.push(arg);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (!arg.startsWith("--export=")) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const value = arg.slice("--export=".length).trim().toLowerCase();
|
|
367
|
+
if (!value || !VALID_EXPORT_FORMATS.includes(value)) {
|
|
368
|
+
throw new Error(`Invalid value for --export: ${value || "(empty)"}. Valid values: txt, md`);
|
|
369
|
+
}
|
|
370
|
+
exportFormat = value;
|
|
371
|
+
exportArgs.push(arg);
|
|
372
|
+
}
|
|
373
|
+
return { exportFormat, exportArgs };
|
|
374
|
+
}
|
|
375
|
+
function handleExportOrDisplay(content, config) {
|
|
376
|
+
const { exportFormat, prefix, title } = config;
|
|
377
|
+
if (!exportFormat) {
|
|
378
|
+
console.log(content);
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
const outputPath = exportToFile(content, {
|
|
382
|
+
prefix,
|
|
383
|
+
format: exportFormat,
|
|
384
|
+
title
|
|
385
|
+
});
|
|
386
|
+
console.log(`Output exported to: ${outputPath}`);
|
|
387
|
+
return outputPath;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/core/cli.ts
|
|
391
|
+
function validateCliArgs(args, schema) {
|
|
392
|
+
const parsed = {};
|
|
393
|
+
const unknownArgs = [];
|
|
394
|
+
for (const [key, config] of Object.entries(schema)) {
|
|
395
|
+
if (config.default !== void 0) {
|
|
396
|
+
parsed[key] = config.default;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
for (const arg of args) {
|
|
400
|
+
let matched = false;
|
|
401
|
+
for (const [key, config] of Object.entries(schema)) {
|
|
402
|
+
if (config.values?.includes(arg)) {
|
|
403
|
+
if (config.exclusive && parsed[key] !== void 0 && parsed[key] !== config.default) {
|
|
404
|
+
throw new Error(`Multiple values provided for ${key}: ${parsed[key]} and ${arg}`);
|
|
405
|
+
}
|
|
406
|
+
parsed[key] = arg;
|
|
407
|
+
matched = true;
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
if (config.prefix && arg.startsWith(config.prefix)) {
|
|
411
|
+
const value = arg.slice(config.prefix.length).trim().toLowerCase();
|
|
412
|
+
if (!value) {
|
|
413
|
+
parsed[key] = "true";
|
|
414
|
+
matched = true;
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
if (config.allowedValues && !config.allowedValues.includes(value)) {
|
|
418
|
+
throw new Error(
|
|
419
|
+
`Invalid value for ${config.prefix}: ${value}. Valid values: ${config.allowedValues.join(", ")}`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
parsed[key] = value;
|
|
423
|
+
matched = true;
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (!matched && arg.startsWith("--")) {
|
|
428
|
+
unknownArgs.push(arg);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return { parsed, unknownArgs };
|
|
432
|
+
}
|
|
433
|
+
function displayHelp(title, content) {
|
|
434
|
+
console.log(`
|
|
435
|
+
${title}
|
|
436
|
+
`);
|
|
437
|
+
if (content.description) {
|
|
438
|
+
console.log(`${content.description}
|
|
439
|
+
`);
|
|
440
|
+
}
|
|
441
|
+
if (content.usage?.length) {
|
|
442
|
+
console.log("Usage:");
|
|
443
|
+
content.usage.forEach((line) => console.log(` ${line}`));
|
|
444
|
+
console.log("");
|
|
445
|
+
}
|
|
446
|
+
if (content.options?.length) {
|
|
447
|
+
console.log("Options:");
|
|
448
|
+
content.options.forEach((line) => console.log(` ${line}`));
|
|
449
|
+
console.log("");
|
|
450
|
+
}
|
|
451
|
+
if (content.examples?.length) {
|
|
452
|
+
console.log("Examples:");
|
|
453
|
+
content.examples.forEach((line) => console.log(` ${line}`));
|
|
454
|
+
console.log("");
|
|
455
|
+
}
|
|
456
|
+
process.exit(0);
|
|
457
|
+
}
|
|
458
|
+
function exitWithError(message, code = 1) {
|
|
459
|
+
console.error(`\u274C ${message}`);
|
|
460
|
+
process.exit(code);
|
|
461
|
+
}
|
|
462
|
+
function logWarning(message) {
|
|
463
|
+
console.warn(`\u26A0\uFE0F ${message}`);
|
|
464
|
+
}
|
|
465
|
+
function logInfo(message) {
|
|
466
|
+
console.log(`\u2139\uFE0F ${message}`);
|
|
467
|
+
}
|
|
468
|
+
function logSuccess(message) {
|
|
469
|
+
console.log(`\u2705 ${message}`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/core/ui.ts
|
|
473
|
+
function shouldShowUI(exportMode, noBanner = false) {
|
|
474
|
+
return !exportMode && !noBanner && Boolean(process.stdout.isTTY);
|
|
475
|
+
}
|
|
476
|
+
function displayBanner(config) {
|
|
477
|
+
const { text, subtitle, info = {}, colors = ["cyan"], font = "block" } = config;
|
|
478
|
+
const bannerText = [text, subtitle].filter(Boolean).join(" ").trim();
|
|
479
|
+
const cfonts = require("cfonts");
|
|
480
|
+
const boxenModule = require("boxen");
|
|
481
|
+
const boxen = typeof boxenModule === "function" ? boxenModule : boxenModule.default;
|
|
482
|
+
const renderedBanner = cfonts.render(bannerText, {
|
|
483
|
+
font,
|
|
484
|
+
colors,
|
|
485
|
+
align: "left",
|
|
486
|
+
background: "transparent",
|
|
487
|
+
letterSpacing: 1,
|
|
488
|
+
lineHeight: 1,
|
|
489
|
+
space: false,
|
|
490
|
+
maxLength: "0",
|
|
491
|
+
env: "node"
|
|
492
|
+
});
|
|
493
|
+
const infoLine = Object.entries(info).map(([key, value]) => `${key}: ${value}`).join(" | ");
|
|
494
|
+
const contentLines = [renderedBanner.string.trimEnd()];
|
|
495
|
+
if (infoLine) {
|
|
496
|
+
contentLines.push("", ` ${infoLine}`);
|
|
497
|
+
}
|
|
498
|
+
contentLines.push(` Date: ${getDisplayTimestamp()}`);
|
|
499
|
+
const boxed = boxen(contentLines.join("\n"), {
|
|
500
|
+
padding: {
|
|
501
|
+
top: 0,
|
|
502
|
+
right: 1,
|
|
503
|
+
bottom: 0,
|
|
504
|
+
left: 1
|
|
505
|
+
},
|
|
506
|
+
margin: {
|
|
507
|
+
top: 0,
|
|
508
|
+
right: 0,
|
|
509
|
+
bottom: 1,
|
|
510
|
+
left: 0
|
|
511
|
+
},
|
|
512
|
+
borderStyle: "round",
|
|
513
|
+
borderColor: colors[0] ?? "cyan"
|
|
514
|
+
});
|
|
515
|
+
console.log(boxed);
|
|
516
|
+
}
|
|
517
|
+
function createSpinner(text, color = "cyan") {
|
|
518
|
+
const ora = require("ora");
|
|
519
|
+
return ora({ text, color, spinner: "dots" });
|
|
520
|
+
}
|
|
521
|
+
function createProgressBar(total = 100, task = "Progress") {
|
|
522
|
+
const cliProgress = require("cli-progress");
|
|
523
|
+
const bar = new cliProgress.SingleBar(
|
|
524
|
+
{
|
|
525
|
+
format: "{task} [{bar}] {percentage}% | {status}",
|
|
526
|
+
barCompleteChar: "\u2588",
|
|
527
|
+
barIncompleteChar: "\u2591",
|
|
528
|
+
hideCursor: true,
|
|
529
|
+
barsize: 24
|
|
530
|
+
},
|
|
531
|
+
cliProgress.Presets.shades_classic
|
|
532
|
+
);
|
|
533
|
+
bar.start(total, 0, {
|
|
534
|
+
task,
|
|
535
|
+
status: "Starting..."
|
|
536
|
+
});
|
|
537
|
+
return {
|
|
538
|
+
update: (value, payload) => bar.update(value, payload),
|
|
539
|
+
stop: () => bar.stop()
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/commands/test-context.ts
|
|
544
|
+
function extractFailures(raw) {
|
|
545
|
+
const lines = raw.split("\n");
|
|
546
|
+
const temp = [];
|
|
547
|
+
let current = null;
|
|
548
|
+
for (const line of lines) {
|
|
549
|
+
if (JEST_PATTERNS.FAIL_LINE.test(line)) {
|
|
550
|
+
if (current) {
|
|
551
|
+
temp.push(current);
|
|
552
|
+
}
|
|
553
|
+
current = {
|
|
554
|
+
file: line.replace("FAIL ", "").trim(),
|
|
555
|
+
tests: []
|
|
556
|
+
};
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (JEST_PATTERNS.BULLET_POINT.test(line.trim())) {
|
|
560
|
+
current?.tests.push(line.trim().slice(1).trim());
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (current) {
|
|
564
|
+
temp.push(current);
|
|
565
|
+
}
|
|
566
|
+
const merged = {};
|
|
567
|
+
for (const item of temp) {
|
|
568
|
+
if (!merged[item.file]) {
|
|
569
|
+
merged[item.file] = {
|
|
570
|
+
file: item.file,
|
|
571
|
+
tests: []
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
merged[item.file].tests.push(...item.tests);
|
|
575
|
+
}
|
|
576
|
+
for (const value of Object.values(merged)) {
|
|
577
|
+
value.tests = Array.from(new Set(value.tests));
|
|
578
|
+
}
|
|
579
|
+
return Object.values(merged);
|
|
580
|
+
}
|
|
581
|
+
async function runTestContext(argv = process.argv.slice(2)) {
|
|
582
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
583
|
+
displayHelp("Test Context Script", {
|
|
584
|
+
description: "Run Jest and print failures in LLM-friendly format.",
|
|
585
|
+
usage: [
|
|
586
|
+
"test-context -- --all",
|
|
587
|
+
"test-context -- --related",
|
|
588
|
+
"test-context -- --tests"
|
|
589
|
+
],
|
|
590
|
+
options: [
|
|
591
|
+
"--all Run all tests (default)",
|
|
592
|
+
"--related Run tests related to staged files",
|
|
593
|
+
"--tests Run only staged test files",
|
|
594
|
+
"--export Export output as txt",
|
|
595
|
+
"--export=txt|md Export output in selected format",
|
|
596
|
+
"--no-banner Disable fancy terminal UI",
|
|
597
|
+
"-h, --help Show this help"
|
|
598
|
+
],
|
|
599
|
+
examples: ["test-context -- --related --export=md"]
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
const noBanner = argv.includes("--no-banner");
|
|
603
|
+
const args = argv.filter((arg) => arg !== "--no-banner");
|
|
604
|
+
let exportFormat;
|
|
605
|
+
let exportArgs;
|
|
606
|
+
let parsed;
|
|
607
|
+
let unknownArgs;
|
|
608
|
+
try {
|
|
609
|
+
({ exportFormat, exportArgs } = parseExportCliArgs(args));
|
|
610
|
+
({ parsed, unknownArgs } = validateCliArgs(args, {
|
|
611
|
+
mode: {
|
|
612
|
+
values: [...VALID_MODES],
|
|
613
|
+
default: "--all",
|
|
614
|
+
exclusive: true
|
|
615
|
+
}
|
|
616
|
+
}));
|
|
617
|
+
} catch (error) {
|
|
618
|
+
exitWithError(error.message);
|
|
619
|
+
}
|
|
620
|
+
const exportArgSet = new Set(exportArgs);
|
|
621
|
+
const filteredUnknownArgs = unknownArgs.filter((arg) => !exportArgSet.has(arg));
|
|
622
|
+
if (filteredUnknownArgs.length > 0) {
|
|
623
|
+
logWarning(`Unknown arguments ignored: ${filteredUnknownArgs.join(", ")}`);
|
|
624
|
+
}
|
|
625
|
+
const modeArg = parsed.mode ?? "--all";
|
|
626
|
+
const mode = modeArg === "--related" ? "related" : modeArg === "--tests" ? "tests" : "all";
|
|
627
|
+
const showUI = shouldShowUI(exportFormat, noBanner);
|
|
628
|
+
if (showUI) {
|
|
629
|
+
displayBanner({
|
|
630
|
+
text: "TEST",
|
|
631
|
+
subtitle: "CONTEXT",
|
|
632
|
+
info: {
|
|
633
|
+
Mode: mode
|
|
634
|
+
},
|
|
635
|
+
colors: ["cyan", "blue"]
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
let jestArgs = [];
|
|
639
|
+
try {
|
|
640
|
+
jestArgs = await buildJestArgsForMode(mode);
|
|
641
|
+
} catch (error) {
|
|
642
|
+
exitWithError(error.message);
|
|
643
|
+
}
|
|
644
|
+
const spinner = showUI ? createSpinner("Running Jest tests...") : null;
|
|
645
|
+
spinner?.start();
|
|
646
|
+
const result = await runJest(jestArgs, {
|
|
647
|
+
verbose: true,
|
|
648
|
+
ignoreErrors: true
|
|
649
|
+
});
|
|
650
|
+
const output = stripAnsi(result.output);
|
|
651
|
+
const hadFailures = output.includes("FAIL") || output.includes("\u25CF");
|
|
652
|
+
if (spinner) {
|
|
653
|
+
if (hadFailures) {
|
|
654
|
+
spinner.warn("Tests completed with failures");
|
|
655
|
+
} else {
|
|
656
|
+
spinner.succeed("All tests passed!");
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (!hadFailures) {
|
|
660
|
+
const passMessage = "All tests passed!";
|
|
661
|
+
handleExportOrDisplay(passMessage, {
|
|
662
|
+
exportFormat,
|
|
663
|
+
prefix: "export-test-context",
|
|
664
|
+
title: "Test Context"
|
|
665
|
+
});
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const failures = extractFailures(output);
|
|
669
|
+
const lines = ["=== LLM TEST CONTEXT BEGIN ===", ""];
|
|
670
|
+
if (failures.length === 0) {
|
|
671
|
+
lines.push("No test failures could be parsed from Jest output.");
|
|
672
|
+
lines.push("This may happen if Jest failed before running tests.", "");
|
|
673
|
+
}
|
|
674
|
+
for (const fail of failures) {
|
|
675
|
+
lines.push(`File: ${fail.file}`);
|
|
676
|
+
lines.push("Failures:");
|
|
677
|
+
for (const test of fail.tests) {
|
|
678
|
+
lines.push(`- ${test}`);
|
|
679
|
+
}
|
|
680
|
+
lines.push("");
|
|
681
|
+
}
|
|
682
|
+
lines.push("=== LLM TEST CONTEXT END ===");
|
|
683
|
+
handleExportOrDisplay(lines.join("\n"), {
|
|
684
|
+
exportFormat,
|
|
685
|
+
prefix: "export-test-context",
|
|
686
|
+
title: "Test Context"
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/commands/coverage-context.ts
|
|
691
|
+
var import_node_path2 = __toESM(require("path"));
|
|
692
|
+
function parseCoverage(text) {
|
|
693
|
+
const lines = text.split("\n");
|
|
694
|
+
const rows = [];
|
|
695
|
+
let currentFolder = null;
|
|
696
|
+
for (const line of lines) {
|
|
697
|
+
const match = line.match(JEST_PATTERNS.COVERAGE_LINE);
|
|
698
|
+
if (!match) continue;
|
|
699
|
+
const [, rawFile, stmts, branch, funcs, linesPct, uncovered] = match;
|
|
700
|
+
const normalized = rawFile.trim();
|
|
701
|
+
if (normalized === "All files") continue;
|
|
702
|
+
const isFile = /\.(ts|tsx)$/.test(normalized);
|
|
703
|
+
if (!isFile) {
|
|
704
|
+
currentFolder = normalized;
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (/index\.(ts|tsx)$/.test(normalized)) continue;
|
|
708
|
+
if (!currentFolder) currentFolder = import_node_path2.default.dirname(normalized);
|
|
709
|
+
rows.push({
|
|
710
|
+
folder: currentFolder,
|
|
711
|
+
file: import_node_path2.default.basename(normalized),
|
|
712
|
+
stmts: Number(stmts),
|
|
713
|
+
branch: Number(branch),
|
|
714
|
+
funcs: Number(funcs),
|
|
715
|
+
lines: Number(linesPct),
|
|
716
|
+
uncovered: uncovered?.trim()
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
return rows;
|
|
720
|
+
}
|
|
721
|
+
function filterIncomplete(rows, threshold = 100) {
|
|
722
|
+
return rows.filter(
|
|
723
|
+
(r) => !(r.stmts >= threshold && r.branch >= threshold && r.funcs >= threshold && r.lines >= threshold)
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
function groupByFolder(rows) {
|
|
727
|
+
const grouped = {};
|
|
728
|
+
for (const row of rows) {
|
|
729
|
+
const { folder, file, ...rest } = row;
|
|
730
|
+
if (!grouped[folder]) grouped[folder] = [];
|
|
731
|
+
grouped[folder].push({ file, coverage: rest });
|
|
732
|
+
}
|
|
733
|
+
return grouped;
|
|
734
|
+
}
|
|
735
|
+
function formatCoverageOutput(grouped) {
|
|
736
|
+
let output = "";
|
|
737
|
+
for (const folder of Object.keys(grouped)) {
|
|
738
|
+
output += `\u{1F4C1} Folder: ${folder}
|
|
739
|
+
`;
|
|
740
|
+
for (const item of grouped[folder]) {
|
|
741
|
+
const { file, coverage } = item;
|
|
742
|
+
output += ` \u{1F4C4} File: ${file}
|
|
743
|
+
`;
|
|
744
|
+
output += `- Coverage:
|
|
745
|
+
`;
|
|
746
|
+
output += ` - Stmts: ${coverage.stmts}
|
|
747
|
+
`;
|
|
748
|
+
output += ` - Branch: ${coverage.branch}
|
|
749
|
+
`;
|
|
750
|
+
output += ` - Funcs: ${coverage.funcs}
|
|
751
|
+
`;
|
|
752
|
+
output += ` - Lines: ${coverage.lines}
|
|
753
|
+
`;
|
|
754
|
+
output += ` - Uncovered: ${coverage.uncovered || "N/A"}
|
|
755
|
+
|
|
756
|
+
`;
|
|
757
|
+
}
|
|
758
|
+
output += "\n";
|
|
759
|
+
}
|
|
760
|
+
return output.trim();
|
|
761
|
+
}
|
|
762
|
+
async function runCoverageContext(argv = process.argv.slice(2)) {
|
|
763
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
764
|
+
displayHelp("Coverage Context Script", {
|
|
765
|
+
description: "Run Jest coverage and print files below threshold.",
|
|
766
|
+
usage: ["coverage-context", "coverage-context --threshold=90"],
|
|
767
|
+
options: [
|
|
768
|
+
"--threshold=0..100 Minimum percentage for stmts/branch/funcs/lines (default: 100)",
|
|
769
|
+
"--export Export output as txt",
|
|
770
|
+
"--export=txt|md Export output in selected format",
|
|
771
|
+
"--no-banner Disable fancy terminal UI",
|
|
772
|
+
"-h, --help Show this help"
|
|
773
|
+
],
|
|
774
|
+
examples: ["coverage-context --threshold=90 --export=md"]
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
const noBanner = argv.includes("--no-banner");
|
|
778
|
+
const args = argv.filter((arg) => arg !== "--no-banner");
|
|
779
|
+
const { exportFormat, exportArgs } = parseExportCliArgs(args);
|
|
780
|
+
const { parsed, unknownArgs } = validateCliArgs(args, {
|
|
781
|
+
threshold: {
|
|
782
|
+
prefix: "--threshold=",
|
|
783
|
+
allowedValues: Array.from({ length: 101 }, (_, i) => String(i))
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
const exportArgSet = new Set(exportArgs);
|
|
787
|
+
const filteredUnknownArgs = unknownArgs.filter((arg) => !exportArgSet.has(arg));
|
|
788
|
+
if (filteredUnknownArgs.length > 0) {
|
|
789
|
+
logWarning(`Unknown arguments ignored: ${filteredUnknownArgs.join(", ")}`);
|
|
790
|
+
}
|
|
791
|
+
const threshold = parsed.threshold ? Number(parsed.threshold) : 100;
|
|
792
|
+
const showUI = shouldShowUI(exportFormat, noBanner);
|
|
793
|
+
if (showUI) {
|
|
794
|
+
displayBanner({
|
|
795
|
+
text: "COVERAGE",
|
|
796
|
+
info: {
|
|
797
|
+
Threshold: `>=${threshold}%`
|
|
798
|
+
},
|
|
799
|
+
colors: ["magenta", "blue"]
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
const bar = showUI ? createProgressBar(100, "Running coverage") : null;
|
|
803
|
+
bar?.update(10, { status: "Running Jest..." });
|
|
804
|
+
const result = await runJest([], {
|
|
805
|
+
coverage: true,
|
|
806
|
+
coverageReporters: ["text", "text-summary"],
|
|
807
|
+
ignoreErrors: true
|
|
808
|
+
});
|
|
809
|
+
bar?.update(70, { status: "Parsing output..." });
|
|
810
|
+
const rows = parseCoverage(result.output);
|
|
811
|
+
if (rows.length === 0) {
|
|
812
|
+
bar?.update(100, { status: "Done" });
|
|
813
|
+
bar?.stop();
|
|
814
|
+
handleExportOrDisplay("No coverage rows parsed from Jest output.", {
|
|
815
|
+
exportFormat,
|
|
816
|
+
prefix: "export-coverage-context",
|
|
817
|
+
title: "Coverage Context"
|
|
818
|
+
});
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const incomplete = filterIncomplete(rows, threshold);
|
|
822
|
+
const grouped = groupByFolder(incomplete);
|
|
823
|
+
bar?.update(100, { status: "Done" });
|
|
824
|
+
bar?.stop();
|
|
825
|
+
if (Object.keys(grouped).length === 0) {
|
|
826
|
+
handleExportOrDisplay(`All files meet threshold >= ${threshold}%`, {
|
|
827
|
+
exportFormat,
|
|
828
|
+
prefix: "export-coverage-context",
|
|
829
|
+
title: "Coverage Context"
|
|
830
|
+
});
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const formatted = formatCoverageOutput(grouped);
|
|
834
|
+
handleExportOrDisplay(formatted, {
|
|
835
|
+
exportFormat,
|
|
836
|
+
prefix: "export-coverage-context",
|
|
837
|
+
title: "Coverage Context"
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// src/commands/console-context.ts
|
|
842
|
+
var import_node_path3 = __toESM(require("path"));
|
|
843
|
+
function parseConsoleWarnings(rawOutput) {
|
|
844
|
+
const lines = rawOutput.split("\n");
|
|
845
|
+
const warnings = [];
|
|
846
|
+
const state = {
|
|
847
|
+
currentTestFile: null,
|
|
848
|
+
currentWarning: null,
|
|
849
|
+
pendingWarnings: []
|
|
850
|
+
};
|
|
851
|
+
const flushCurrentWarning = () => {
|
|
852
|
+
if (!state.currentWarning) return;
|
|
853
|
+
if (state.currentWarning.testFile === "unknown" && state.currentTestFile) {
|
|
854
|
+
state.currentWarning.testFile = state.currentTestFile;
|
|
855
|
+
}
|
|
856
|
+
warnings.push(state.currentWarning);
|
|
857
|
+
state.pendingWarnings.push(state.currentWarning);
|
|
858
|
+
state.currentWarning = null;
|
|
859
|
+
};
|
|
860
|
+
for (const line of lines) {
|
|
861
|
+
const trimmed = line.trim();
|
|
862
|
+
if (JEST_PATTERNS.TEST_FILE.test(trimmed)) {
|
|
863
|
+
const match = trimmed.match(JEST_PATTERNS.TEST_FILE);
|
|
864
|
+
if (match) {
|
|
865
|
+
state.currentTestFile = import_node_path3.default.basename(match[2]);
|
|
866
|
+
for (const warning of state.pendingWarnings) {
|
|
867
|
+
if (warning.testFile === "unknown") warning.testFile = state.currentTestFile;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
flushCurrentWarning();
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
if (trimmed === "\u25CF Console") {
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
if (isConsoleTypeLine(line)) {
|
|
877
|
+
flushCurrentWarning();
|
|
878
|
+
state.currentWarning = {
|
|
879
|
+
type: extractConsoleType(line) ?? "console.warn",
|
|
880
|
+
testFile: state.currentTestFile ?? "unknown",
|
|
881
|
+
messages: []
|
|
882
|
+
};
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
if (!state.currentWarning) continue;
|
|
886
|
+
if (isCodeSnippetLine(line)) continue;
|
|
887
|
+
if (isStackTraceLine(line)) continue;
|
|
888
|
+
const cleanMessage = stripAnsi(line).trim();
|
|
889
|
+
if (cleanMessage.length > 0) {
|
|
890
|
+
state.currentWarning.messages.push(cleanMessage);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
flushCurrentWarning();
|
|
894
|
+
return warnings;
|
|
895
|
+
}
|
|
896
|
+
function filterWarningsByType(warnings, filter) {
|
|
897
|
+
if (filter === "errors") {
|
|
898
|
+
return warnings.filter((w) => w.type === CONSOLE_LOG_TYPES.ERROR);
|
|
899
|
+
}
|
|
900
|
+
if (filter === "warns") {
|
|
901
|
+
return warnings.filter((w) => w.type === CONSOLE_LOG_TYPES.WARN);
|
|
902
|
+
}
|
|
903
|
+
return warnings;
|
|
904
|
+
}
|
|
905
|
+
function groupByTestFile(warnings) {
|
|
906
|
+
const grouped = {};
|
|
907
|
+
for (const warning of warnings) {
|
|
908
|
+
const file = warning.testFile;
|
|
909
|
+
if (!grouped[file]) grouped[file] = [];
|
|
910
|
+
grouped[file].push(warning);
|
|
911
|
+
}
|
|
912
|
+
return grouped;
|
|
913
|
+
}
|
|
914
|
+
function sortFilesByWarningCount(grouped) {
|
|
915
|
+
const entries = Object.entries(grouped);
|
|
916
|
+
entries.sort((a, b) => b[1].length - a[1].length);
|
|
917
|
+
return entries;
|
|
918
|
+
}
|
|
919
|
+
function formatWarningsOutput(grouped, timestamp, stats) {
|
|
920
|
+
const lines = [];
|
|
921
|
+
lines.push("=== CONSOLE WARNINGS CONTEXT BEGIN ===", "", `\u{1F4C5} Run Date: ${timestamp}`, "");
|
|
922
|
+
const sortedEntries = sortFilesByWarningCount(grouped);
|
|
923
|
+
const totalFiles = sortedEntries.length;
|
|
924
|
+
if (totalFiles === 0) {
|
|
925
|
+
lines.push("\u2705 No console warnings found!");
|
|
926
|
+
} else {
|
|
927
|
+
lines.push(
|
|
928
|
+
`\u26A0\uFE0F Found ${stats.total} warning(s) in ${totalFiles} test file(s)`,
|
|
929
|
+
"",
|
|
930
|
+
"\u{1F4CA} Summary by Type:",
|
|
931
|
+
` - console.error: ${stats.byType[CONSOLE_LOG_TYPES.ERROR] ?? 0} occurrence(s)`,
|
|
932
|
+
` - console.warn: ${stats.byType[CONSOLE_LOG_TYPES.WARN] ?? 0} occurrence(s)`,
|
|
933
|
+
"",
|
|
934
|
+
"=".repeat(60),
|
|
935
|
+
""
|
|
936
|
+
);
|
|
937
|
+
for (const [file, warnings] of sortedEntries) {
|
|
938
|
+
lines.push(`\u{1F4C4} File: ${file}`, ` Total Warnings: ${warnings.length}`, "");
|
|
939
|
+
warnings.forEach((warning, idx) => {
|
|
940
|
+
lines.push(` ${idx + 1}. ${warning.type}`, "");
|
|
941
|
+
if (warning.messages.length > 0) {
|
|
942
|
+
lines.push(" \u{1F4DD} Message:");
|
|
943
|
+
warning.messages.forEach((msg) => lines.push(` ${msg}`));
|
|
944
|
+
lines.push("");
|
|
945
|
+
}
|
|
946
|
+
if (idx < warnings.length - 1) {
|
|
947
|
+
lines.push(" " + "-".repeat(50), "");
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
lines.push("=".repeat(60), "");
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
lines.push("=== CONSOLE WARNINGS CONTEXT END ===");
|
|
954
|
+
return `${lines.join("\n")}
|
|
955
|
+
`;
|
|
956
|
+
}
|
|
957
|
+
function calculateStatistics(warnings) {
|
|
958
|
+
const stats = {
|
|
959
|
+
total: warnings.length,
|
|
960
|
+
byType: {
|
|
961
|
+
[CONSOLE_LOG_TYPES.ERROR]: 0,
|
|
962
|
+
[CONSOLE_LOG_TYPES.WARN]: 0
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
for (const warning of warnings) {
|
|
966
|
+
if (warning.type === CONSOLE_LOG_TYPES.ERROR) {
|
|
967
|
+
stats.byType[CONSOLE_LOG_TYPES.ERROR]++;
|
|
968
|
+
} else if (warning.type === CONSOLE_LOG_TYPES.WARN) {
|
|
969
|
+
stats.byType[CONSOLE_LOG_TYPES.WARN]++;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return stats;
|
|
973
|
+
}
|
|
974
|
+
async function runConsoleContext(argv = process.argv.slice(2)) {
|
|
975
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
976
|
+
displayHelp("Console Context Script", {
|
|
977
|
+
description: "Capture and format console warnings/errors from Jest output.",
|
|
978
|
+
usage: [
|
|
979
|
+
"console-context -- --all",
|
|
980
|
+
"console-context -- --related",
|
|
981
|
+
"console-context -- --tests"
|
|
982
|
+
],
|
|
983
|
+
options: [
|
|
984
|
+
"--all Run all tests (default)",
|
|
985
|
+
"--related Run tests related to staged files",
|
|
986
|
+
"--tests Run only staged test files",
|
|
987
|
+
"--only-errors Show only console.error warnings",
|
|
988
|
+
"--only-warns Show only console.warn warnings",
|
|
989
|
+
"--export Export output as txt",
|
|
990
|
+
"--export=txt|md Export output in selected format",
|
|
991
|
+
"--no-banner Disable fancy terminal UI",
|
|
992
|
+
"-h, --help Show this help"
|
|
993
|
+
],
|
|
994
|
+
examples: [
|
|
995
|
+
"console-context -- --related --only-errors",
|
|
996
|
+
"console-context -- --all --export=md"
|
|
997
|
+
]
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
const noBanner = argv.includes("--no-banner");
|
|
1001
|
+
const args = argv.filter((arg) => arg !== "--no-banner");
|
|
1002
|
+
const { exportFormat, exportArgs } = parseExportCliArgs(args);
|
|
1003
|
+
const { parsed, unknownArgs } = validateCliArgs(args, {
|
|
1004
|
+
mode: {
|
|
1005
|
+
values: [...VALID_MODES],
|
|
1006
|
+
default: "--all",
|
|
1007
|
+
exclusive: true
|
|
1008
|
+
},
|
|
1009
|
+
filter: {
|
|
1010
|
+
values: ["--only-errors", "--only-warns"],
|
|
1011
|
+
exclusive: true
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
const exportArgSet = new Set(exportArgs);
|
|
1015
|
+
const filteredUnknownArgs = unknownArgs.filter((arg) => !exportArgSet.has(arg));
|
|
1016
|
+
if (filteredUnknownArgs.length > 0) {
|
|
1017
|
+
logWarning(`Unknown arguments ignored: ${filteredUnknownArgs.join(", ")}`);
|
|
1018
|
+
}
|
|
1019
|
+
const mode = parsed.mode ?? "--all";
|
|
1020
|
+
const filter = parsed.filter === "--only-errors" ? "errors" : parsed.filter === "--only-warns" ? "warns" : null;
|
|
1021
|
+
const showUI = shouldShowUI(exportFormat, noBanner);
|
|
1022
|
+
if (showUI) {
|
|
1023
|
+
displayBanner({
|
|
1024
|
+
text: "CONSOLE",
|
|
1025
|
+
subtitle: "WARNINGS",
|
|
1026
|
+
info: {
|
|
1027
|
+
Mode: mode,
|
|
1028
|
+
Filter: filter ?? "none"
|
|
1029
|
+
},
|
|
1030
|
+
colors: ["yellow", "red"]
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
const modeKey = mode.replace("--", "");
|
|
1034
|
+
let jestArgs = [];
|
|
1035
|
+
try {
|
|
1036
|
+
jestArgs = await buildJestArgsForMode(modeKey);
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
exitWithError(error.message);
|
|
1039
|
+
}
|
|
1040
|
+
const spinner = showUI ? createSpinner("Capturing console warnings...", "yellow") : null;
|
|
1041
|
+
spinner?.start();
|
|
1042
|
+
const result = await runJest(jestArgs, {
|
|
1043
|
+
disableVerbose: true,
|
|
1044
|
+
ignoreErrors: true
|
|
1045
|
+
});
|
|
1046
|
+
let warnings = parseConsoleWarnings(result.output);
|
|
1047
|
+
warnings = filterWarningsByType(warnings, filter);
|
|
1048
|
+
if (spinner) {
|
|
1049
|
+
if (warnings.length > 0) {
|
|
1050
|
+
spinner.warn(`Found ${warnings.length} warning(s)`);
|
|
1051
|
+
} else {
|
|
1052
|
+
spinner.succeed("No console warnings found");
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const grouped = groupByTestFile(warnings);
|
|
1056
|
+
const stats = calculateStatistics(warnings);
|
|
1057
|
+
const timestamp = getDisplayTimestamp();
|
|
1058
|
+
const formatted = formatWarningsOutput(grouped, timestamp, stats);
|
|
1059
|
+
handleExportOrDisplay(formatted, {
|
|
1060
|
+
exportFormat,
|
|
1061
|
+
prefix: "export-console-context",
|
|
1062
|
+
title: "Console Warnings Context"
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1066
|
+
0 && (module.exports = {
|
|
1067
|
+
CONSOLE_LOG_TYPES,
|
|
1068
|
+
JEST_PATTERNS,
|
|
1069
|
+
VALID_EXPORT_FORMATS,
|
|
1070
|
+
VALID_MODES,
|
|
1071
|
+
VALID_TEST_EXTENSIONS,
|
|
1072
|
+
buildJestArgsForMode,
|
|
1073
|
+
buildTimestampedPath,
|
|
1074
|
+
createProgressBar,
|
|
1075
|
+
createSpinner,
|
|
1076
|
+
detectPackageManager,
|
|
1077
|
+
displayBanner,
|
|
1078
|
+
displayHelp,
|
|
1079
|
+
execCommand,
|
|
1080
|
+
exitWithError,
|
|
1081
|
+
exportToFile,
|
|
1082
|
+
extractConsoleType,
|
|
1083
|
+
extractFailures,
|
|
1084
|
+
filterTestFiles,
|
|
1085
|
+
formatAsMarkdown,
|
|
1086
|
+
getDisplayTimestamp,
|
|
1087
|
+
getFilenameTimestamp,
|
|
1088
|
+
getJestCommand,
|
|
1089
|
+
getPlatform,
|
|
1090
|
+
getStagedFiles,
|
|
1091
|
+
handleExportOrDisplay,
|
|
1092
|
+
hasGit,
|
|
1093
|
+
isCodeSnippetLine,
|
|
1094
|
+
isConsoleTypeLine,
|
|
1095
|
+
isLinux,
|
|
1096
|
+
isMac,
|
|
1097
|
+
isStackTraceLine,
|
|
1098
|
+
isWindows,
|
|
1099
|
+
logInfo,
|
|
1100
|
+
logSuccess,
|
|
1101
|
+
logWarning,
|
|
1102
|
+
parseExportCliArgs,
|
|
1103
|
+
resolveCommand,
|
|
1104
|
+
runConsoleContext,
|
|
1105
|
+
runCoverageContext,
|
|
1106
|
+
runJest,
|
|
1107
|
+
runTestContext,
|
|
1108
|
+
shouldShowUI,
|
|
1109
|
+
stripAnsi,
|
|
1110
|
+
validateCliArgs
|
|
1111
|
+
});
|
|
1112
|
+
//# sourceMappingURL=index.cjs.map
|