@scantrix/cli 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/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/astConfigParser.js +308 -0
- package/dist/astRuleHelpers.js +1451 -0
- package/dist/auditConfig.js +81 -0
- package/dist/ciExtractor.js +327 -0
- package/dist/cli.js +156 -0
- package/dist/configExtractor.js +261 -0
- package/dist/cypressExtractor.js +217 -0
- package/dist/diffTracker.js +310 -0
- package/dist/report.js +1904 -0
- package/dist/sarifFormatter.js +88 -0
- package/dist/scanResult.js +45 -0
- package/dist/scanner.js +3519 -0
- package/dist/scoring.js +206 -0
- package/dist/sinks/index.js +29 -0
- package/dist/sinks/jsonSink.js +28 -0
- package/dist/sinks/types.js +2 -0
- package/docs/high-res-icon.svg +26 -0
- package/docs/scantrix-logo-light.svg +64 -0
- package/docs/scantrix-logo.svg +64 -0
- package/package.json +55 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.evaluateNumericExpression = evaluateNumericExpression;
|
|
7
|
+
exports.extractPlaywrightConfig = extractPlaywrightConfig;
|
|
8
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const astConfigParser_1 = require("./astConfigParser");
|
|
11
|
+
// small helpers
|
|
12
|
+
function matchFirst(content, re) {
|
|
13
|
+
const m = content.match(re);
|
|
14
|
+
return m?.[1];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Evaluate common numeric expressions found in Playwright configs.
|
|
18
|
+
*
|
|
19
|
+
* Handles:
|
|
20
|
+
* - Plain integers: 60000
|
|
21
|
+
* - Underscore notation: 60_000
|
|
22
|
+
* - Scientific notation: 6e4
|
|
23
|
+
* - Simple multiplication: 60 * 1000, 1000 * 60, 2 * 60 * 1000
|
|
24
|
+
*
|
|
25
|
+
* Does NOT handle:
|
|
26
|
+
* - Ternary expressions: process.env.CI ? 30000 : 60000
|
|
27
|
+
* - Variable references: MY_TIMEOUT
|
|
28
|
+
* - Function calls: getTimeout()
|
|
29
|
+
* - Template literals or string concatenation
|
|
30
|
+
*
|
|
31
|
+
* Returns undefined for expressions it cannot evaluate.
|
|
32
|
+
*/
|
|
33
|
+
function evaluateNumericExpression(expr) {
|
|
34
|
+
if (!expr)
|
|
35
|
+
return undefined;
|
|
36
|
+
const cleaned = expr
|
|
37
|
+
.trim()
|
|
38
|
+
.replace(/_/g, "") // 60_000 -> 60000
|
|
39
|
+
.replace(/\s+/g, " "); // normalise whitespace
|
|
40
|
+
// Direct number (handles 60000, 6e4, 0x... etc.)
|
|
41
|
+
const direct = Number(cleaned);
|
|
42
|
+
if (Number.isFinite(direct))
|
|
43
|
+
return direct;
|
|
44
|
+
// Simple multiplication chain: N * M [ * P ... ]
|
|
45
|
+
const parts = cleaned.split(/\s*\*\s*/);
|
|
46
|
+
if (parts.length >= 2 && parts.every((p) => /^\d+(?:\.\d+)?(?:e\d+)?$/.test(p))) {
|
|
47
|
+
const result = parts.reduce((acc, p) => acc * Number(p), 1);
|
|
48
|
+
if (Number.isFinite(result))
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
// very small “object block” extraction: finds `key: { ... }` and returns {...} as text
|
|
54
|
+
function extractBlock(content, key) {
|
|
55
|
+
const idx = content.indexOf(`${key}:`);
|
|
56
|
+
if (idx < 0)
|
|
57
|
+
return undefined;
|
|
58
|
+
const braceStart = content.indexOf("{", idx);
|
|
59
|
+
if (braceStart < 0)
|
|
60
|
+
return undefined;
|
|
61
|
+
let depth = 0;
|
|
62
|
+
for (let i = braceStart; i < content.length; i++) {
|
|
63
|
+
const ch = content[i];
|
|
64
|
+
if (ch === "{")
|
|
65
|
+
depth++;
|
|
66
|
+
if (ch === "}")
|
|
67
|
+
depth--;
|
|
68
|
+
if (depth === 0) {
|
|
69
|
+
return content.slice(braceStart, i + 1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
function extractArrayBlock(content, key) {
|
|
75
|
+
const idx = content.indexOf(`${key}:`);
|
|
76
|
+
if (idx < 0)
|
|
77
|
+
return undefined;
|
|
78
|
+
const arrStart = content.indexOf("[", idx);
|
|
79
|
+
if (arrStart < 0)
|
|
80
|
+
return undefined;
|
|
81
|
+
let depth = 0;
|
|
82
|
+
for (let i = arrStart; i < content.length; i++) {
|
|
83
|
+
const ch = content[i];
|
|
84
|
+
if (ch === "[")
|
|
85
|
+
depth++;
|
|
86
|
+
if (ch === "]")
|
|
87
|
+
depth--;
|
|
88
|
+
if (depth === 0) {
|
|
89
|
+
return content.slice(arrStart, i + 1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
function parseLaunchArgs(useBlock) {
|
|
95
|
+
if (!useBlock)
|
|
96
|
+
return undefined;
|
|
97
|
+
const launchBlock = extractBlock(useBlock, "launchOptions");
|
|
98
|
+
if (!launchBlock)
|
|
99
|
+
return undefined;
|
|
100
|
+
// args: ['--a','--b']
|
|
101
|
+
const m = launchBlock.match(/\bargs\s*:\s*\[([\s\S]*?)\]/);
|
|
102
|
+
if (!m)
|
|
103
|
+
return undefined;
|
|
104
|
+
const inside = m[1];
|
|
105
|
+
const args = [...inside.matchAll(/['"`]([^'"`]+)['"`]/g)].map((x) => x[1]);
|
|
106
|
+
return args.length ? args : undefined;
|
|
107
|
+
}
|
|
108
|
+
function parseReporters(content) {
|
|
109
|
+
// We only need to detect common reporters + key fields.
|
|
110
|
+
// Find occurrences like ['html', { outputFolder: 'playwright-report', open: 'never' }]
|
|
111
|
+
const reporters = [];
|
|
112
|
+
// html
|
|
113
|
+
const htmlMatches = [...content.matchAll(/\[\s*['"`]html['"`]\s*,\s*\{([\s\S]*?)\}\s*\]/g)];
|
|
114
|
+
for (const m of htmlMatches) {
|
|
115
|
+
const block = m[1];
|
|
116
|
+
const outputFolder = matchFirst(block, /\boutputFolder\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
117
|
+
const open = matchFirst(block, /\bopen\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
118
|
+
reporters.push({ name: "html", details: { outputFolder, open } });
|
|
119
|
+
}
|
|
120
|
+
// junit
|
|
121
|
+
const junitMatches = [...content.matchAll(/\[\s*['"`]junit['"`]\s*,\s*\{([\s\S]*?)\}\s*\]/g)];
|
|
122
|
+
for (const m of junitMatches) {
|
|
123
|
+
const block = m[1];
|
|
124
|
+
const outputFile = matchFirst(block, /\boutputFile\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
125
|
+
reporters.push({ name: "junit", details: { outputFile } });
|
|
126
|
+
}
|
|
127
|
+
// list
|
|
128
|
+
if (/\[\s*['"`]list['"`]\s*\]/.test(content) || /['"`]list['"`]/.test(content)) {
|
|
129
|
+
reporters.push({ name: "list" });
|
|
130
|
+
}
|
|
131
|
+
// azure reporter
|
|
132
|
+
if (/@alex_neo\/playwright-azure-reporter/.test(content)) {
|
|
133
|
+
const orgUrl = matchFirst(content, /\borgUrl\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
134
|
+
const projectName = matchFirst(content, /\bprojectName\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
135
|
+
const uploadAttachments = /uploadAttachments\s*:\s*true/.test(content);
|
|
136
|
+
const attachmentsType = [...content.matchAll(/\battachmentsType\s*:\s*\[([\s\S]*?)\]/g)]
|
|
137
|
+
.flatMap((m) => [...m[1].matchAll(/['"`]([^'"`]+)['"`]/g)].map((x) => x[1]));
|
|
138
|
+
reporters.push({
|
|
139
|
+
name: "@alex_neo/playwright-azure-reporter",
|
|
140
|
+
details: {
|
|
141
|
+
orgUrl,
|
|
142
|
+
projectName,
|
|
143
|
+
uploadAttachments,
|
|
144
|
+
attachmentsType: attachmentsType.length ? attachmentsType : undefined,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return reporters.length ? reporters : undefined;
|
|
149
|
+
}
|
|
150
|
+
function parseProjects(projectsBlock) {
|
|
151
|
+
if (!projectsBlock)
|
|
152
|
+
return undefined;
|
|
153
|
+
const projects = [];
|
|
154
|
+
// very lightweight: find "name: '...'" occurrences and nearby grep/browserName/isMobile text
|
|
155
|
+
const entries = projectsBlock.split(/\}\s*,\s*\{/g).map((s) => s.replace(/^\[/, "{").replace(/\]$/, "}"));
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
const name = matchFirst(entry, /\bname\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
158
|
+
if (!name)
|
|
159
|
+
continue;
|
|
160
|
+
const grep = matchFirst(entry, /\bgrep\s*:\s*(\/[^\/]+\/[gimsuy]*)/);
|
|
161
|
+
const browserName = matchFirst(entry, /\bbrowserName\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
162
|
+
const isMobile = /\bisMobile\s*:\s*true/.test(entry) ? true : /\bisMobile\s*:\s*false/.test(entry) ? false : undefined;
|
|
163
|
+
projects.push({ name, grep, browserName, isMobile });
|
|
164
|
+
}
|
|
165
|
+
return projects.length ? projects : undefined;
|
|
166
|
+
}
|
|
167
|
+
async function extractPlaywrightConfig(repoPath,
|
|
168
|
+
/** Optional direct path to the config file (skips root-level search). */
|
|
169
|
+
configFilePath) {
|
|
170
|
+
// Find config file — prefer the explicitly provided path, then search repo root
|
|
171
|
+
let configPath = configFilePath;
|
|
172
|
+
if (!configPath) {
|
|
173
|
+
const candidates = [
|
|
174
|
+
path_1.default.join(repoPath, "playwright.config.ts"),
|
|
175
|
+
path_1.default.join(repoPath, "playwright.config.js"),
|
|
176
|
+
path_1.default.join(repoPath, "playwright.config.mjs"),
|
|
177
|
+
path_1.default.join(repoPath, "playwright.config.cjs"),
|
|
178
|
+
];
|
|
179
|
+
for (const c of candidates) {
|
|
180
|
+
try {
|
|
181
|
+
await promises_1.default.access(c);
|
|
182
|
+
configPath = c;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// ignore
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!configPath)
|
|
191
|
+
return undefined;
|
|
192
|
+
const content = await promises_1.default.readFile(configPath, "utf8");
|
|
193
|
+
// ── Strategy 1: AST-based parsing (handles computed values, spreads, etc.) ──
|
|
194
|
+
const astResult = (0, astConfigParser_1.parseConfigWithAst)(configPath, content);
|
|
195
|
+
if (astResult) {
|
|
196
|
+
// Augment with env hints (regex-based, cheap, and AST doesn't do these)
|
|
197
|
+
astResult.envHints = {
|
|
198
|
+
headlessEnvVar: content.includes("HEADLESS_MODE") ? "HEADLESS_MODE" : undefined,
|
|
199
|
+
workersEnvVar: content.includes("process.env.WORKERS") ? "WORKERS" : undefined,
|
|
200
|
+
ciEnvVar: content.includes("process.env.CI") ? "CI" : undefined,
|
|
201
|
+
azureTokenEnvVar: content.includes("AZURE_TOKEN") ? "AZURE_TOKEN" : undefined,
|
|
202
|
+
planIdEnvVar: content.includes("PLAN_ID") ? "PLAN_ID" : undefined,
|
|
203
|
+
};
|
|
204
|
+
// Augment with reporters if AST didn't capture them well (complex array syntax)
|
|
205
|
+
if (!astResult.reporters?.length) {
|
|
206
|
+
astResult.reporters = parseReporters(content);
|
|
207
|
+
}
|
|
208
|
+
return astResult;
|
|
209
|
+
}
|
|
210
|
+
// ── Strategy 2: Regex fallback (legacy, handles simpler configs) ──
|
|
211
|
+
const summary = { configPath };
|
|
212
|
+
// top-level
|
|
213
|
+
summary.testDir = matchFirst(content, /\btestDir\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
214
|
+
// Timeout: extract the raw expression after `timeout:` and evaluate it.
|
|
215
|
+
// Supports: 60000, 60_000, 6e4, 60 * 1000, 1000 * 60, 2 * 60 * 1000.
|
|
216
|
+
const rawTimeout = matchFirst(content, /(?<![.\w])timeout\s*:\s*([^,\n{}]+)/);
|
|
217
|
+
summary.timeoutMs = evaluateNumericExpression(rawTimeout);
|
|
218
|
+
// expect.timeout (often written as 20 * 1000)
|
|
219
|
+
const expectBlock = extractBlock(content, "expect");
|
|
220
|
+
if (expectBlock) {
|
|
221
|
+
const rawExpectTimeout = matchFirst(expectBlock, /\btimeout\s*:\s*([^,\n{}]+)/);
|
|
222
|
+
summary.expectTimeoutMs = evaluateNumericExpression(rawExpectTimeout);
|
|
223
|
+
}
|
|
224
|
+
summary.workers = matchFirst(content, /\bworkers\s*:\s*([^,\n]+)/)?.trim();
|
|
225
|
+
summary.retries = matchFirst(content, /\bretries\s*:\s*([^,\n]+)/)?.trim();
|
|
226
|
+
summary.globalSetup = matchFirst(content, /\bglobalSetup\s*:\s*require\.resolve\(\s*['"`]([^'"`]+)['"`]\s*\)/);
|
|
227
|
+
// use block
|
|
228
|
+
const useBlock = extractBlock(content, "use");
|
|
229
|
+
if (useBlock) {
|
|
230
|
+
const headlessExpr = matchFirst(useBlock, /\bheadless\s*:\s*([^,\n]+)/)?.trim();
|
|
231
|
+
const rawActionTimeout = matchFirst(useBlock, /\bactionTimeout\s*:\s*([^,\n{}]+)/);
|
|
232
|
+
const traceExpr = matchFirst(useBlock, /\btrace\s*:\s*([^,\n]+)/)?.trim();
|
|
233
|
+
const videoExpr = matchFirst(useBlock, /\bvideo\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
234
|
+
const screenshotExpr = matchFirst(useBlock, /\bscreenshot\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
235
|
+
const ignoreHttps = /\bignoreHTTPSErrors\s*:\s*true/.test(useBlock) ? true : /\bignoreHTTPSErrors\s*:\s*false/.test(useBlock) ? false : undefined;
|
|
236
|
+
const launchArgs = parseLaunchArgs(useBlock);
|
|
237
|
+
const baseURLExpr = matchFirst(useBlock, /\bbaseURL\s*:\s*(['"`][^'"`]+['"`]|[^,\n]+)/)?.trim();
|
|
238
|
+
summary.use = {
|
|
239
|
+
headless: headlessExpr,
|
|
240
|
+
actionTimeoutMs: evaluateNumericExpression(rawActionTimeout),
|
|
241
|
+
trace: traceExpr,
|
|
242
|
+
video: videoExpr,
|
|
243
|
+
screenshot: screenshotExpr,
|
|
244
|
+
ignoreHTTPSErrors: ignoreHttps,
|
|
245
|
+
launchArgs,
|
|
246
|
+
baseURL: baseURLExpr,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
// reporters + projects
|
|
250
|
+
summary.reporters = parseReporters(content);
|
|
251
|
+
summary.projects = parseProjects(extractArrayBlock(content, "projects"));
|
|
252
|
+
// env hinting (we can detect usage by simple matches)
|
|
253
|
+
summary.envHints = {
|
|
254
|
+
headlessEnvVar: content.includes("HEADLESS_MODE") ? "HEADLESS_MODE" : undefined,
|
|
255
|
+
workersEnvVar: content.includes("process.env.WORKERS") ? "WORKERS" : undefined,
|
|
256
|
+
ciEnvVar: content.includes("process.env.CI") ? "CI" : undefined,
|
|
257
|
+
azureTokenEnvVar: content.includes("AZURE_TOKEN") ? "AZURE_TOKEN" : undefined,
|
|
258
|
+
planIdEnvVar: content.includes("PLAN_ID") ? "PLAN_ID" : undefined,
|
|
259
|
+
};
|
|
260
|
+
return summary;
|
|
261
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cypress configuration extractor.
|
|
4
|
+
*
|
|
5
|
+
* Detects Cypress projects and parses their configuration from:
|
|
6
|
+
* - cypress.config.ts / cypress.config.js (Cypress 10+)
|
|
7
|
+
* - cypress.json (Cypress <10, legacy)
|
|
8
|
+
*
|
|
9
|
+
* Also detects the Cypress directory structure (cypress/e2e, cypress/integration).
|
|
10
|
+
*/
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.extractCypressConfig = extractCypressConfig;
|
|
16
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
19
|
+
async function extractCypressConfig(repoPath) {
|
|
20
|
+
// Priority order: TS > JS > JSON
|
|
21
|
+
const candidates = [
|
|
22
|
+
{ file: "cypress.config.ts", format: "ts" },
|
|
23
|
+
{ file: "cypress.config.js", format: "js" },
|
|
24
|
+
{ file: "cypress.config.mjs", format: "js" },
|
|
25
|
+
{ file: "cypress.json", format: "json" },
|
|
26
|
+
];
|
|
27
|
+
let configPath;
|
|
28
|
+
let configFormat = "ts";
|
|
29
|
+
for (const c of candidates) {
|
|
30
|
+
const full = path_1.default.join(repoPath, c.file);
|
|
31
|
+
try {
|
|
32
|
+
await promises_1.default.access(full);
|
|
33
|
+
configPath = full;
|
|
34
|
+
configFormat = c.format;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// not found
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!configPath) {
|
|
42
|
+
// Also check if cypress/ directory exists without config (very old projects)
|
|
43
|
+
try {
|
|
44
|
+
const cypressDir = path_1.default.join(repoPath, "cypress");
|
|
45
|
+
const stat = await promises_1.default.stat(cypressDir);
|
|
46
|
+
if (stat.isDirectory()) {
|
|
47
|
+
return {
|
|
48
|
+
configPath: cypressDir,
|
|
49
|
+
configFormat: "json",
|
|
50
|
+
testDir: "cypress",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// no cypress directory either
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const content = await promises_1.default.readFile(configPath, "utf8");
|
|
60
|
+
if (configFormat === "json") {
|
|
61
|
+
return parseJsonConfig(configPath, content);
|
|
62
|
+
}
|
|
63
|
+
return parseTsJsConfig(configPath, content, configFormat);
|
|
64
|
+
}
|
|
65
|
+
// ─── JSON Config (Cypress <10) ───────────────────────────────────────
|
|
66
|
+
function parseJsonConfig(configPath, content) {
|
|
67
|
+
try {
|
|
68
|
+
const json = JSON.parse(content);
|
|
69
|
+
return {
|
|
70
|
+
configPath,
|
|
71
|
+
configFormat: "json",
|
|
72
|
+
baseUrl: json.baseUrl,
|
|
73
|
+
defaultCommandTimeout: asNum(json.defaultCommandTimeout),
|
|
74
|
+
requestTimeout: asNum(json.requestTimeout),
|
|
75
|
+
responseTimeout: asNum(json.responseTimeout),
|
|
76
|
+
pageLoadTimeout: asNum(json.pageLoadTimeout),
|
|
77
|
+
viewportWidth: asNum(json.viewportWidth),
|
|
78
|
+
viewportHeight: asNum(json.viewportHeight),
|
|
79
|
+
video: asBool(json.video),
|
|
80
|
+
screenshotOnRunFailure: asBool(json.screenshotOnRunFailure),
|
|
81
|
+
retries: parseRetries(json.retries),
|
|
82
|
+
testDir: json.integrationFolder ?? "cypress/integration",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// ─── TS/JS Config (Cypress 10+) ─────────────────────────────────────
|
|
90
|
+
function parseTsJsConfig(configPath, content, format) {
|
|
91
|
+
try {
|
|
92
|
+
const sourceFile = typescript_1.default.createSourceFile(configPath, content, typescript_1.default.ScriptTarget.Latest, true);
|
|
93
|
+
const configObj = findDefineConfigOrDefault(sourceFile);
|
|
94
|
+
if (!configObj)
|
|
95
|
+
return undefined;
|
|
96
|
+
const raw = evalObj(configObj);
|
|
97
|
+
// Cypress 10+ configs nest most settings under `e2e:` or `component:`
|
|
98
|
+
const e2e = raw.e2e ?? raw;
|
|
99
|
+
return {
|
|
100
|
+
configPath,
|
|
101
|
+
configFormat: format,
|
|
102
|
+
baseUrl: asStr(e2e.baseUrl),
|
|
103
|
+
specPattern: asStr(e2e.specPattern),
|
|
104
|
+
defaultCommandTimeout: asNum(e2e.defaultCommandTimeout),
|
|
105
|
+
requestTimeout: asNum(e2e.requestTimeout),
|
|
106
|
+
responseTimeout: asNum(e2e.responseTimeout),
|
|
107
|
+
pageLoadTimeout: asNum(e2e.pageLoadTimeout),
|
|
108
|
+
viewportWidth: asNum(e2e.viewportWidth ?? raw.viewportWidth),
|
|
109
|
+
viewportHeight: asNum(e2e.viewportHeight ?? raw.viewportHeight),
|
|
110
|
+
video: asBool(e2e.video ?? raw.video),
|
|
111
|
+
screenshotOnRunFailure: asBool(e2e.screenshotOnRunFailure ?? raw.screenshotOnRunFailure),
|
|
112
|
+
retries: parseRetries(e2e.retries ?? raw.retries),
|
|
113
|
+
testDir: asStr(e2e.specPattern)
|
|
114
|
+
? path_1.default.dirname(asStr(e2e.specPattern).split("*")[0]) || "cypress/e2e"
|
|
115
|
+
: "cypress/e2e",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function findDefineConfigOrDefault(sourceFile) {
|
|
123
|
+
let result;
|
|
124
|
+
function visit(node) {
|
|
125
|
+
if (result)
|
|
126
|
+
return;
|
|
127
|
+
if (typescript_1.default.isCallExpression(node) &&
|
|
128
|
+
typescript_1.default.isIdentifier(node.expression) &&
|
|
129
|
+
node.expression.text === "defineConfig" &&
|
|
130
|
+
node.arguments.length >= 1 &&
|
|
131
|
+
typescript_1.default.isObjectLiteralExpression(node.arguments[0])) {
|
|
132
|
+
result = node.arguments[0];
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (typescript_1.default.isExportAssignment(node) && !node.isExportEquals) {
|
|
136
|
+
let expr = node.expression;
|
|
137
|
+
if (typescript_1.default.isAsExpression(expr))
|
|
138
|
+
expr = expr.expression;
|
|
139
|
+
if (typescript_1.default.isObjectLiteralExpression(expr)) {
|
|
140
|
+
result = expr;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
typescript_1.default.forEachChild(node, visit);
|
|
145
|
+
}
|
|
146
|
+
visit(sourceFile);
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
// ─── Lightweight AST evaluator ───────────────────────────────────────
|
|
150
|
+
function evalNode(node) {
|
|
151
|
+
if (typescript_1.default.isNumericLiteral(node))
|
|
152
|
+
return Number(node.text);
|
|
153
|
+
if (typescript_1.default.isStringLiteral(node))
|
|
154
|
+
return node.text;
|
|
155
|
+
if (node.kind === typescript_1.default.SyntaxKind.TrueKeyword)
|
|
156
|
+
return true;
|
|
157
|
+
if (node.kind === typescript_1.default.SyntaxKind.FalseKeyword)
|
|
158
|
+
return false;
|
|
159
|
+
if (typescript_1.default.isObjectLiteralExpression(node))
|
|
160
|
+
return evalObj(node);
|
|
161
|
+
if (typescript_1.default.isArrayLiteralExpression(node))
|
|
162
|
+
return node.elements.map(evalNode);
|
|
163
|
+
if (typescript_1.default.isBinaryExpression(node)) {
|
|
164
|
+
const l = evalNode(node.left);
|
|
165
|
+
const r = evalNode(node.right);
|
|
166
|
+
if (typeof l === "number" && typeof r === "number") {
|
|
167
|
+
if (node.operatorToken.kind === typescript_1.default.SyntaxKind.AsteriskToken)
|
|
168
|
+
return l * r;
|
|
169
|
+
if (node.operatorToken.kind === typescript_1.default.SyntaxKind.PlusToken)
|
|
170
|
+
return l + r;
|
|
171
|
+
}
|
|
172
|
+
return node.getText();
|
|
173
|
+
}
|
|
174
|
+
if (typescript_1.default.isConditionalExpression(node))
|
|
175
|
+
return node.getText();
|
|
176
|
+
if (typescript_1.default.isPropertyAccessExpression(node))
|
|
177
|
+
return node.getText();
|
|
178
|
+
if (typescript_1.default.isParenthesizedExpression(node))
|
|
179
|
+
return evalNode(node.expression);
|
|
180
|
+
return node.getText();
|
|
181
|
+
}
|
|
182
|
+
function evalObj(node) {
|
|
183
|
+
const result = {};
|
|
184
|
+
for (const prop of node.properties) {
|
|
185
|
+
if (typescript_1.default.isPropertyAssignment(prop) && typescript_1.default.isIdentifier(prop.name)) {
|
|
186
|
+
result[prop.name.text] = evalNode(prop.initializer);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
192
|
+
function asNum(v) {
|
|
193
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
194
|
+
return v;
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
function asStr(v) {
|
|
198
|
+
if (typeof v === "string")
|
|
199
|
+
return v;
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
function asBool(v) {
|
|
203
|
+
if (typeof v === "boolean")
|
|
204
|
+
return v;
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
function parseRetries(v) {
|
|
208
|
+
if (typeof v === "number")
|
|
209
|
+
return { runMode: v, openMode: 0 };
|
|
210
|
+
if (v && typeof v === "object") {
|
|
211
|
+
return {
|
|
212
|
+
runMode: asNum(v.runMode),
|
|
213
|
+
openMode: asNum(v.openMode),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|