@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Scantrix
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/scantrix-logo-light.svg" alt="Scantrix" width="420" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h3 align="center">
|
|
6
|
+
<em>Playwright</em> · <em>Cypress</em> ·<em> Selenium</em></br>
|
|
7
|
+
Find out why your tests keep failing without running a single one.
|
|
8
|
+
</h3>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<a href="#quick-start">Quick Start</a> ·
|
|
12
|
+
<a href="#output-formats">Output</a> ·
|
|
13
|
+
<a href="#cli-reference">CLI Reference</a> ·
|
|
14
|
+
<a href="#github-action">GitHub Action</a> ·
|
|
15
|
+
<a href="docs/RULES.md">Rules</a> ·
|
|
16
|
+
<a href="#license">License</a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="https://github.com/Scantrix/scantrix/actions/workflows/ci.yml">
|
|
21
|
+
<img src="https://img.shields.io/github/actions/workflow/status/Scantrix/scantrix/ci.yml?branch=main&label=build" alt="Build Status" />
|
|
22
|
+
</a>
|
|
23
|
+
<a href="https://www.npmjs.com/package/@scantrix/cli">
|
|
24
|
+
<img src="https://img.shields.io/npm/v/@scantrix/cli?color=cb3837&logo=npm&logoColor=white" alt="NPM Version" />
|
|
25
|
+
</a>
|
|
26
|
+
<a href="#license">
|
|
27
|
+
<img src="https://img.shields.io/badge/license-MIT-blue" alt="License: MIT" />
|
|
28
|
+
</a>
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
Most tools tell you a test failed. **Scantrix tells you why your entire approach is failing** and exactly how to fix it.
|
|
34
|
+
|
|
35
|
+
No test execution. No credentials. No file modifications. Just root causes.
|
|
36
|
+
|
|
37
|
+
### EXAMPLE FINDING
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
### KEY CAPABILITIES
|
|
41
|
+
>
|
|
42
|
+
>| Capability | Traditional Reporters | Scantrix |
|
|
43
|
+
>|-------------------|---------------------------|-----------------------------------|
|
|
44
|
+
>| Analysis | Runtime (Post Failure) | Static (Pre-emptive) |
|
|
45
|
+
>| Scope | Single Test Result | Repository wide Health |
|
|
46
|
+
>| Root Cause | Stack Trace only | Correlated Design Patterns |
|
|
47
|
+
>| Actionability | "Test Failed" | Step-by-step Fix Guidance |
|
|
48
|
+
>| CI Impact | Log bloat | CI Pipeline Optimization |
|
|
49
|
+
>| Security | Requires Tokens | Zero Footprint (Local Execution) |
|
|
50
|
+
|
|
51
|
+
</br>
|
|
52
|
+
|
|
53
|
+
### RISK OVERVIEW
|
|
54
|
+
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
### RECOMMENDED ACTIONS
|
|
58
|
+
|
|
59
|
+

|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## What Scantrix Diagnoses
|
|
63
|
+
|
|
64
|
+
**Flaky test root causes.** Not just which tests fail, but exactly why they fail and what will actually fix them.
|
|
65
|
+
|
|
66
|
+
**Framework-level design debt.** Structural patterns that silently destabilize entire test suites before anyone realizes something is wrong.
|
|
67
|
+
|
|
68
|
+
**CI configuration risks.** The environmental factors making your pipeline unpredictable run to run.
|
|
69
|
+
|
|
70
|
+
**Blast radius of bad patterns.** One anti-pattern can affect hundreds of tests. Scantrix quantifies that exposure so you know where to act first.
|
|
71
|
+
|
|
72
|
+
**Execution waste.** Where time is being burned in your pipeline and the precise changes that recover it.
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
## What Scantrix Does NOT Do
|
|
76
|
+
Scantrix operates as a zero-footprint, read-only analyzer, ensuring repository integrity while maintaining a strict security posture. <br>
|
|
77
|
+
- It does not execute your tests <br>
|
|
78
|
+
- It does not modify any files in the target repository <br>
|
|
79
|
+
- It does not make network connections beyond the local filesystem <br>
|
|
80
|
+
- It does not require credentials and access tokens of any kind </br>
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
## Quick Start
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Install dependencies
|
|
87
|
+
npm install
|
|
88
|
+
|
|
89
|
+
# Build
|
|
90
|
+
npm run build
|
|
91
|
+
|
|
92
|
+
# Scan a repository
|
|
93
|
+
node dist/cli.js /path/to/repo --out ./audit-out
|
|
94
|
+
|
|
95
|
+
# Or use the shorthand (builds + scans)
|
|
96
|
+
npm run audit -- /path/to/repo --out ./audit-out
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Output Formats
|
|
100
|
+
|
|
101
|
+
Every scan produces an **overall risk grade (A through F)** alongside severity-bucketed findings.
|
|
102
|
+
|
|
103
|
+
| Format | File | Description |
|
|
104
|
+
|--------|------|-------------|
|
|
105
|
+
| Markdown | `audit_summary.md` | Primary report with executive-grade findings and hotspots |
|
|
106
|
+
| HTML | `audit_summary.html` | Styled report for browser viewing |
|
|
107
|
+
| JSON | `findings.json` | Machine-readable findings array |
|
|
108
|
+
| SARIF | `audit.sarif` | Native GitHub Code Scanning and Azure DevOps support — findings appear directly in the Security tab |
|
|
109
|
+
| Email | `audit_email.html` | Email-ready HTML summary |
|
|
110
|
+
|
|
111
|
+
Use `--format` to select specific formats:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
node dist/cli.js /path/to/repo --out ./audit-out --format md,json,sarif
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
## CLI Reference
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
scantrix <repoPath> [options]
|
|
123
|
+
scantrix init [dir]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Scan Options
|
|
127
|
+
|
|
128
|
+
| Flag | Description | Default |
|
|
129
|
+
|------|-------------|---------|
|
|
130
|
+
| `--out <dir>` | Output directory for reports | `./audit-out` |
|
|
131
|
+
| `--format <list>` | Comma-separated formats: `md,html,json,sarif,email` | all |
|
|
132
|
+
| `--config <path>` | Path to `.auditrc.json` config file | auto-detected |
|
|
133
|
+
| `--updates` | Check npm registry for outdated dependencies | off |
|
|
134
|
+
| `--diff <path>` | Explicit baseline `findings.json` for comparison | auto |
|
|
135
|
+
| `--json-path <path>` | Write canonical `ScanResult` JSON to this path | — |
|
|
136
|
+
|
|
137
|
+
### Environment Variables
|
|
138
|
+
|
|
139
|
+
| Variable | Description |
|
|
140
|
+
|----------|-------------|
|
|
141
|
+
| `SCANTRIX_JSON_PATH` | Write canonical results JSON to this path |
|
|
142
|
+
|
|
143
|
+
### Init Subcommand
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Create a starter .auditrc.json in the target repo
|
|
147
|
+
node dist/cli.js init /path/to/repo
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Configuration
|
|
151
|
+
|
|
152
|
+
Create an `.auditrc.json` in the target repository to customize behavior:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
node dist/cli.js init /path/to/repo
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The config file supports rule overrides (disable rules, adjust severity) and scan options. See [CLI Architecture](docs/CLI_ARCHITECTURE.md) for details.
|
|
159
|
+
|
|
160
|
+
## GitHub Action
|
|
161
|
+
|
|
162
|
+
Use Scantrix in CI with the provided composite action:
|
|
163
|
+
|
|
164
|
+
```yaml
|
|
165
|
+
- uses: Scantrix/scantrix@v1
|
|
166
|
+
with:
|
|
167
|
+
repo-path: "."
|
|
168
|
+
output-dir: "./scantrix-out"
|
|
169
|
+
format: "md,html,json,sarif"
|
|
170
|
+
post-comment: "true" # Post summary on PRs
|
|
171
|
+
fail-on-high: "true" # Block merges with high-severity findings
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Action Inputs
|
|
175
|
+
|
|
176
|
+
| Input | Description | Default |
|
|
177
|
+
|-------|-------------|---------|
|
|
178
|
+
| `repo-path` | Path to repository to scan | `.` |
|
|
179
|
+
| `output-dir` | Directory for audit artifacts | `./scantrix-out` |
|
|
180
|
+
| `config-path` | Path to `.auditrc.json` | — |
|
|
181
|
+
| `format` | Output formats (comma-separated) | `md,html,json,sarif` |
|
|
182
|
+
| `check-updates` | Check for outdated dependencies | `false` |
|
|
183
|
+
| `post-comment` | Post summary comment on PRs | `true` |
|
|
184
|
+
| `fail-on-high` | Fail if high-severity findings exist | `false` |
|
|
185
|
+
|
|
186
|
+
### Action Outputs
|
|
187
|
+
|
|
188
|
+
| Output | Description |
|
|
189
|
+
|--------|-------------|
|
|
190
|
+
| `findings-count` | Total number of findings |
|
|
191
|
+
| `high-count` | Number of high-severity findings |
|
|
192
|
+
| `medium-count` | Number of medium-severity findings |
|
|
193
|
+
| `low-count` | Number of low-severity findings |
|
|
194
|
+
| `risk-grade` | Overall risk grade (A through F) |
|
|
195
|
+
| `report-path` | Path to generated report directory |
|
|
196
|
+
|
|
197
|
+
## Documentation
|
|
198
|
+
|
|
199
|
+
- [Rules Reference](docs/RULES.md) — all 87 finding rules with severity and description
|
|
200
|
+
- [CI Integration](docs/CI_INTEGRATION.md) — CI pipeline setup and quality gates
|
|
201
|
+
- [CLI Architecture](docs/CLI_ARCHITECTURE.md) — module diagram and data flow
|
|
202
|
+
- [Security](SECURITY.md) — security model and threat considerations
|
|
203
|
+
- [Finding Reference](docs/FINDING_REFERENCE.md) — complete finding ID catalog
|
|
204
|
+
|
|
205
|
+
## Contributing
|
|
206
|
+
|
|
207
|
+
Contributions are welcome. Please open an issue to discuss proposed changes before submitting a pull request.
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# Run the test suite
|
|
211
|
+
npm test
|
|
212
|
+
|
|
213
|
+
# Run tests in watch mode
|
|
214
|
+
npm run test:watch
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
[MIT](LICENSE) — See LICENSE file.</br>
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AST-based Playwright config parser using the TypeScript Compiler API.
|
|
4
|
+
*
|
|
5
|
+
* This module parses playwright.config.ts/js using a real TypeScript AST instead
|
|
6
|
+
* of regexes, handling:
|
|
7
|
+
* - Computed values: `60 * 1000`, `2 * 60 * 1000`
|
|
8
|
+
* - Spread operators: `...baseUse` (recorded as unknown)
|
|
9
|
+
* - Ternary expressions: `process.env.CI ? 2 : 0` (recorded as the raw source text)
|
|
10
|
+
* - Variable references within the same file
|
|
11
|
+
* - `satisfies` keyword (TS 4.9+)
|
|
12
|
+
* - Template literals (partial)
|
|
13
|
+
* - Array/object literals (recursed)
|
|
14
|
+
*
|
|
15
|
+
* Falls back gracefully when encountering unsupported patterns.
|
|
16
|
+
*/
|
|
17
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.parseConfigWithAst = parseConfigWithAst;
|
|
22
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
23
|
+
// ─── Public API ──────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Parse a Playwright config file using the TypeScript compiler API.
|
|
26
|
+
* Returns a structured PlaywrightConfigSummary, or undefined if parsing fails.
|
|
27
|
+
*/
|
|
28
|
+
function parseConfigWithAst(filePath, sourceText) {
|
|
29
|
+
try {
|
|
30
|
+
const sourceFile = typescript_1.default.createSourceFile(filePath, sourceText, typescript_1.default.ScriptTarget.Latest,
|
|
31
|
+
/* setParentNodes */ true);
|
|
32
|
+
// Build a simple scope of top-level `const x = ...` declarations
|
|
33
|
+
const scope = buildFileScope(sourceFile);
|
|
34
|
+
// Find the config object: `defineConfig({...})` or `export default {...}`
|
|
35
|
+
const configNode = findConfigObject(sourceFile);
|
|
36
|
+
if (!configNode)
|
|
37
|
+
return undefined;
|
|
38
|
+
const raw = evaluateObject(configNode, scope);
|
|
39
|
+
if (!raw || typeof raw !== "object")
|
|
40
|
+
return undefined;
|
|
41
|
+
return mapToSummary(filePath, raw, scope);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function buildFileScope(sourceFile) {
|
|
48
|
+
const scope = new Map();
|
|
49
|
+
for (const stmt of sourceFile.statements) {
|
|
50
|
+
// const FOO = expr;
|
|
51
|
+
if (typescript_1.default.isVariableStatement(stmt)) {
|
|
52
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
53
|
+
if (typescript_1.default.isIdentifier(decl.name) && decl.initializer) {
|
|
54
|
+
scope.set(decl.name.text, decl.initializer);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return scope;
|
|
60
|
+
}
|
|
61
|
+
// ─── Config Object Finder ────────────────────────────────────────────
|
|
62
|
+
function findConfigObject(sourceFile) {
|
|
63
|
+
let result;
|
|
64
|
+
function visit(node) {
|
|
65
|
+
// defineConfig({ ... })
|
|
66
|
+
if (typescript_1.default.isCallExpression(node) &&
|
|
67
|
+
typescript_1.default.isIdentifier(node.expression) &&
|
|
68
|
+
node.expression.text === "defineConfig" &&
|
|
69
|
+
node.arguments.length >= 1) {
|
|
70
|
+
const arg = node.arguments[0];
|
|
71
|
+
if (typescript_1.default.isObjectLiteralExpression(arg)) {
|
|
72
|
+
result = arg;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// export default { ... }
|
|
77
|
+
if (typescript_1.default.isExportAssignment(node) && !node.isExportEquals) {
|
|
78
|
+
let expr = node.expression;
|
|
79
|
+
// Handle: export default { ... } satisfies PlaywrightTestConfig
|
|
80
|
+
if (typescript_1.default.isSatisfiesExpression?.(expr)) {
|
|
81
|
+
expr = expr.expression;
|
|
82
|
+
}
|
|
83
|
+
// Handle: export default { ... } as PlaywrightTestConfig
|
|
84
|
+
if (typescript_1.default.isAsExpression(expr)) {
|
|
85
|
+
expr = expr.expression;
|
|
86
|
+
}
|
|
87
|
+
if (typescript_1.default.isObjectLiteralExpression(expr)) {
|
|
88
|
+
result = expr;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
typescript_1.default.forEachChild(node, visit);
|
|
93
|
+
}
|
|
94
|
+
visit(sourceFile);
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
// ─── AST Evaluator ───────────────────────────────────────────────────
|
|
98
|
+
// Tries to evaluate AST nodes into plain JS values.
|
|
99
|
+
// Returns undefined for unevaluable nodes, or the raw source text as a string.
|
|
100
|
+
function evaluateNode(node, scope) {
|
|
101
|
+
// Numeric literal
|
|
102
|
+
if (typescript_1.default.isNumericLiteral(node)) {
|
|
103
|
+
return Number(node.text);
|
|
104
|
+
}
|
|
105
|
+
// String literal
|
|
106
|
+
if (typescript_1.default.isStringLiteral(node) || typescript_1.default.isNoSubstitutionTemplateLiteral(node)) {
|
|
107
|
+
return node.text;
|
|
108
|
+
}
|
|
109
|
+
// Boolean
|
|
110
|
+
if (node.kind === typescript_1.default.SyntaxKind.TrueKeyword)
|
|
111
|
+
return true;
|
|
112
|
+
if (node.kind === typescript_1.default.SyntaxKind.FalseKeyword)
|
|
113
|
+
return false;
|
|
114
|
+
if (node.kind === typescript_1.default.SyntaxKind.UndefinedKeyword)
|
|
115
|
+
return undefined;
|
|
116
|
+
if (node.kind === typescript_1.default.SyntaxKind.NullKeyword)
|
|
117
|
+
return null;
|
|
118
|
+
// Identifier — look up in scope
|
|
119
|
+
if (typescript_1.default.isIdentifier(node)) {
|
|
120
|
+
const ref = scope.get(node.text);
|
|
121
|
+
if (ref)
|
|
122
|
+
return evaluateNode(ref, scope);
|
|
123
|
+
return `<ref:${node.text}>`;
|
|
124
|
+
}
|
|
125
|
+
// Binary expression (multiplication, addition)
|
|
126
|
+
if (typescript_1.default.isBinaryExpression(node)) {
|
|
127
|
+
const left = evaluateNode(node.left, scope);
|
|
128
|
+
const right = evaluateNode(node.right, scope);
|
|
129
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
130
|
+
switch (node.operatorToken.kind) {
|
|
131
|
+
case typescript_1.default.SyntaxKind.AsteriskToken: return left * right;
|
|
132
|
+
case typescript_1.default.SyntaxKind.PlusToken: return left + right;
|
|
133
|
+
case typescript_1.default.SyntaxKind.MinusToken: return left - right;
|
|
134
|
+
case typescript_1.default.SyntaxKind.SlashToken: return right !== 0 ? left / right : undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Logical OR for fallback patterns: `process.env.X || 'default'`
|
|
138
|
+
if (node.operatorToken.kind === typescript_1.default.SyntaxKind.BarBarToken) {
|
|
139
|
+
// Return the raw source expression as a string for display
|
|
140
|
+
return node.getText();
|
|
141
|
+
}
|
|
142
|
+
// Return raw expression text
|
|
143
|
+
return node.getText();
|
|
144
|
+
}
|
|
145
|
+
// Conditional (ternary) expression
|
|
146
|
+
if (typescript_1.default.isConditionalExpression(node)) {
|
|
147
|
+
// We can't evaluate process.env.CI at parse time, so return the raw text
|
|
148
|
+
return node.getText();
|
|
149
|
+
}
|
|
150
|
+
// Property access: process.env.CI, devices['Desktop Chrome']
|
|
151
|
+
if (typescript_1.default.isPropertyAccessExpression(node) || typescript_1.default.isElementAccessExpression(node)) {
|
|
152
|
+
return node.getText();
|
|
153
|
+
}
|
|
154
|
+
// Prefix unary: !condition, -number
|
|
155
|
+
if (typescript_1.default.isPrefixUnaryExpression(node)) {
|
|
156
|
+
const operand = evaluateNode(node.operand, scope);
|
|
157
|
+
if (node.operator === typescript_1.default.SyntaxKind.ExclamationToken && typeof operand === "boolean")
|
|
158
|
+
return !operand;
|
|
159
|
+
if (node.operator === typescript_1.default.SyntaxKind.MinusToken && typeof operand === "number")
|
|
160
|
+
return -operand;
|
|
161
|
+
return node.getText();
|
|
162
|
+
}
|
|
163
|
+
// Object literal
|
|
164
|
+
if (typescript_1.default.isObjectLiteralExpression(node)) {
|
|
165
|
+
return evaluateObject(node, scope);
|
|
166
|
+
}
|
|
167
|
+
// Array literal
|
|
168
|
+
if (typescript_1.default.isArrayLiteralExpression(node)) {
|
|
169
|
+
return node.elements.map((el) => evaluateNode(el, scope));
|
|
170
|
+
}
|
|
171
|
+
// Spread element
|
|
172
|
+
if (typescript_1.default.isSpreadElement(node)) {
|
|
173
|
+
return { __spread: node.expression.getText() };
|
|
174
|
+
}
|
|
175
|
+
// Call expression: require.resolve('...'), etc.
|
|
176
|
+
if (typescript_1.default.isCallExpression(node)) {
|
|
177
|
+
// Try to get the first string argument
|
|
178
|
+
if (node.arguments.length > 0 && typescript_1.default.isStringLiteral(node.arguments[0])) {
|
|
179
|
+
return node.arguments[0].text;
|
|
180
|
+
}
|
|
181
|
+
return node.getText();
|
|
182
|
+
}
|
|
183
|
+
// Template expression: `${process.env.BASE_URL}/api`
|
|
184
|
+
if (typescript_1.default.isTemplateExpression(node)) {
|
|
185
|
+
return node.getText();
|
|
186
|
+
}
|
|
187
|
+
// ParenthesizedExpression
|
|
188
|
+
if (typescript_1.default.isParenthesizedExpression(node)) {
|
|
189
|
+
return evaluateNode(node.expression, scope);
|
|
190
|
+
}
|
|
191
|
+
// `satisfies` expression (TS 4.9+)
|
|
192
|
+
if (typescript_1.default.isSatisfiesExpression?.(node)) {
|
|
193
|
+
return evaluateNode(node.expression, scope);
|
|
194
|
+
}
|
|
195
|
+
// `as` expression
|
|
196
|
+
if (typescript_1.default.isAsExpression(node)) {
|
|
197
|
+
return evaluateNode(node.expression, scope);
|
|
198
|
+
}
|
|
199
|
+
// Fallback: raw text
|
|
200
|
+
return node.getText();
|
|
201
|
+
}
|
|
202
|
+
function evaluateObject(node, scope) {
|
|
203
|
+
const result = {};
|
|
204
|
+
for (const prop of node.properties) {
|
|
205
|
+
// Spread assignment: ...baseConfig
|
|
206
|
+
if (typescript_1.default.isSpreadAssignment(prop)) {
|
|
207
|
+
result.__spreads = result.__spreads || [];
|
|
208
|
+
result.__spreads.push(prop.expression.getText());
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (typescript_1.default.isPropertyAssignment(prop)) {
|
|
212
|
+
const key = getPropertyName(prop);
|
|
213
|
+
if (key) {
|
|
214
|
+
result[key] = evaluateNode(prop.initializer, scope);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Shorthand property: { workers }
|
|
218
|
+
if (typescript_1.default.isShorthandPropertyAssignment(prop)) {
|
|
219
|
+
const key = prop.name.text;
|
|
220
|
+
const ref = scope.get(key);
|
|
221
|
+
result[key] = ref ? evaluateNode(ref, scope) : `<ref:${key}>`;
|
|
222
|
+
}
|
|
223
|
+
// Method declaration: ignored (not typical in config)
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
function getPropertyName(prop) {
|
|
228
|
+
const name = prop.name;
|
|
229
|
+
if (typescript_1.default.isIdentifier(name))
|
|
230
|
+
return name.text;
|
|
231
|
+
if (typescript_1.default.isStringLiteral(name))
|
|
232
|
+
return name.text;
|
|
233
|
+
if (typescript_1.default.isComputedPropertyName(name))
|
|
234
|
+
return undefined; // can't evaluate
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
// ─── Mapping to PlaywrightConfigSummary ──────────────────────────────
|
|
238
|
+
function asNumber(v) {
|
|
239
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
240
|
+
return v;
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
function asString(v) {
|
|
244
|
+
if (typeof v === "string")
|
|
245
|
+
return v;
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
function mapToSummary(filePath, raw, scope) {
|
|
249
|
+
const summary = { configPath: filePath };
|
|
250
|
+
summary.testDir = asString(raw.testDir);
|
|
251
|
+
summary.timeoutMs = asNumber(raw.timeout);
|
|
252
|
+
summary.workers = raw.workers !== undefined ? String(raw.workers) : undefined;
|
|
253
|
+
summary.retries = raw.retries !== undefined ? String(raw.retries) : undefined;
|
|
254
|
+
summary.globalSetup = asString(raw.globalSetup);
|
|
255
|
+
// expect block
|
|
256
|
+
if (raw.expect && typeof raw.expect === "object") {
|
|
257
|
+
summary.expectTimeoutMs = asNumber(raw.expect.timeout);
|
|
258
|
+
}
|
|
259
|
+
// use block
|
|
260
|
+
if (raw.use && typeof raw.use === "object") {
|
|
261
|
+
const u = raw.use;
|
|
262
|
+
summary.use = {
|
|
263
|
+
headless: u.headless !== undefined ? String(u.headless) : undefined,
|
|
264
|
+
actionTimeoutMs: asNumber(u.actionTimeout),
|
|
265
|
+
trace: asString(u.trace),
|
|
266
|
+
video: asString(u.video),
|
|
267
|
+
screenshot: asString(u.screenshot),
|
|
268
|
+
ignoreHTTPSErrors: u.ignoreHTTPSErrors === true ? true :
|
|
269
|
+
u.ignoreHTTPSErrors === false ? false : undefined,
|
|
270
|
+
baseURL: asString(u.baseURL),
|
|
271
|
+
};
|
|
272
|
+
// Parse launchOptions.args if present
|
|
273
|
+
if (u.launchOptions && typeof u.launchOptions === "object" && Array.isArray(u.launchOptions.args)) {
|
|
274
|
+
summary.use.launchArgs = u.launchOptions.args.filter((a) => typeof a === "string");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// reporters
|
|
278
|
+
if (Array.isArray(raw.reporter)) {
|
|
279
|
+
summary.reporters = raw.reporter
|
|
280
|
+
.map((r) => {
|
|
281
|
+
if (typeof r === "string")
|
|
282
|
+
return { name: r };
|
|
283
|
+
if (Array.isArray(r) && typeof r[0] === "string") {
|
|
284
|
+
return { name: r[0], details: r[1] && typeof r[1] === "object" ? r[1] : undefined };
|
|
285
|
+
}
|
|
286
|
+
return undefined;
|
|
287
|
+
})
|
|
288
|
+
.filter((x) => x != null);
|
|
289
|
+
}
|
|
290
|
+
// projects
|
|
291
|
+
if (Array.isArray(raw.projects)) {
|
|
292
|
+
summary.projects = raw.projects
|
|
293
|
+
.map((p) => {
|
|
294
|
+
if (!p || typeof p !== "object")
|
|
295
|
+
return undefined;
|
|
296
|
+
return {
|
|
297
|
+
name: asString(p.name) ?? "unknown",
|
|
298
|
+
grep: asString(p.grep),
|
|
299
|
+
browserName: asString(p.browserName),
|
|
300
|
+
isMobile: p.isMobile === true ? true : p.isMobile === false ? false : undefined,
|
|
301
|
+
};
|
|
302
|
+
})
|
|
303
|
+
.filter((x) => x != null);
|
|
304
|
+
}
|
|
305
|
+
// env hints — detect from raw source text patterns
|
|
306
|
+
// (these are derived from text patterns, not AST; we keep them for compatibility)
|
|
307
|
+
return summary;
|
|
308
|
+
}
|