@veraxhq/verax 0.1.0 → 0.2.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/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +14 -36
- package/src/cli/commands/default.js +523 -0
- package/src/cli/commands/doctor.js +165 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +402 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +296 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +34 -0
- package/src/cli/util/expectation-extractor.js +378 -0
- package/src/cli/util/findings-writer.js +31 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +366 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +29 -0
- package/src/cli/util/project-discovery.js +277 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/summary-writer.js +32 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +101 -0
- package/src/verax/cli/wizard.js +98 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +403 -0
- package/src/verax/core/incremental-store.js +237 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +521 -0
- package/src/verax/detect/comparison.js +2 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +177 -0
- package/src/verax/detect/expectation-model.js +194 -172
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +44 -8
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +172 -286
- package/src/verax/detect/interactive-findings.js +613 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/verdict-engine.js +563 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/index.js +90 -14
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +579 -0
- package/src/verax/intel/vue-router-extractor.js +323 -0
- package/src/verax/learn/action-contract-extractor.js +335 -101
- package/src/verax/learn/ast-contract-extractor.js +95 -5
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/manifest-writer.js +97 -47
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +27 -96
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +112 -4
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +10 -5
- package/src/verax/observe/console-sensor.js +1 -17
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +512 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +643 -275
- package/src/verax/observe/index.js +908 -27
- package/src/verax/observe/index.js.backup +1 -0
- package/src/verax/observe/interaction-discovery.js +365 -14
- package/src/verax/observe/interaction-runner.js +563 -198
- package/src/verax/observe/loading-sensor.js +139 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +37 -17
- package/src/verax/observe/state-sensor.js +389 -0
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +61 -20
- package/src/verax/observe/ui-signal-sensor.js +136 -17
- package/src/verax/scan-summary-writer.js +77 -15
- package/src/verax/shared/artifact-manager.js +110 -8
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +170 -0
- package/src/verax/shared/dynamic-route-utils.js +218 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +14 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +65 -0
- package/src/verax/validate/context-validator.js +244 -0
- package/src/verax/validate/context-validator.js.bak +0 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CODE INTELLIGENCE v1 — Vue Navigation Promise Extraction (AST-based)
|
|
3
|
+
*
|
|
4
|
+
* Extracts navigation promises from Vue components:
|
|
5
|
+
* - <router-link to="/path">
|
|
6
|
+
* - <RouterLink :to="{ path: '/path' }">
|
|
7
|
+
* - router.push('/path'), router.replace('/path')
|
|
8
|
+
* - Dynamic targets: router.push(`/users/${id}`)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import ts from 'typescript';
|
|
12
|
+
import { parseFile, findNodes, getStringLiteral, getNodeLocation } from './ts-program.js';
|
|
13
|
+
import { readFileSync, existsSync } from 'fs';
|
|
14
|
+
import { resolve, extname } from 'path';
|
|
15
|
+
import { normalizeDynamicRoute, normalizeTemplateLiteral } from '../shared/dynamic-route-utils.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extract navigation promises from Vue components.
|
|
19
|
+
*
|
|
20
|
+
* @param {Object} program - TypeScript program
|
|
21
|
+
* @param {string} projectRoot - Project root
|
|
22
|
+
* @returns {Promise<Array>} - Array of navigation expectation objects
|
|
23
|
+
*/
|
|
24
|
+
export async function extractVueNavigationPromises(program, projectRoot) {
|
|
25
|
+
const expectations = [];
|
|
26
|
+
|
|
27
|
+
if (!program || !program.program) return expectations;
|
|
28
|
+
|
|
29
|
+
// Find Vue component files (.vue, .ts, .js, .tsx, .jsx)
|
|
30
|
+
const vueFiles = [];
|
|
31
|
+
const sourceFiles = program.getSourceFiles ? program.getSourceFiles() : (program.sourceFiles || []);
|
|
32
|
+
|
|
33
|
+
for (const sourceFile of sourceFiles) {
|
|
34
|
+
// sourceFiles can be either file paths (strings) or SourceFile objects
|
|
35
|
+
const filePath = typeof sourceFile === 'string' ? sourceFile : (sourceFile.fileName || sourceFile);
|
|
36
|
+
if (!filePath) continue;
|
|
37
|
+
const ext = extname(filePath);
|
|
38
|
+
if (['.vue', '.ts', '.js', '.tsx', '.jsx'].includes(ext)) {
|
|
39
|
+
vueFiles.push(filePath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Also explicitly glob for .vue files since TypeScript might not include them
|
|
44
|
+
try {
|
|
45
|
+
const { glob } = await import('glob');
|
|
46
|
+
const vueGlobPattern = resolve(projectRoot, '**/*.vue');
|
|
47
|
+
const vueFilesFromGlob = await glob(vueGlobPattern, { cwd: projectRoot });
|
|
48
|
+
for (const filePath of vueFilesFromGlob) {
|
|
49
|
+
const absPath = resolve(projectRoot, filePath);
|
|
50
|
+
if (!vueFiles.includes(absPath) && !vueFiles.find(f => f.endsWith(filePath))) {
|
|
51
|
+
vueFiles.push(absPath);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
// glob not available, skip
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const filePath of vueFiles) {
|
|
59
|
+
const fileExpectations = extractFromFile(filePath, projectRoot, program);
|
|
60
|
+
expectations.push(...fileExpectations);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return expectations;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extract navigation promises from a single file.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} filePath - File path
|
|
70
|
+
* @param {string} projectRoot - Project root
|
|
71
|
+
* @param {Object} program - TypeScript program
|
|
72
|
+
* @returns {Array} - Navigation expectations
|
|
73
|
+
*/
|
|
74
|
+
function extractFromFile(filePath, projectRoot, program) {
|
|
75
|
+
const expectations = [];
|
|
76
|
+
|
|
77
|
+
// Check if it's a .vue file (SFC)
|
|
78
|
+
if (extname(filePath) === '.vue') {
|
|
79
|
+
const sfcExpectations = extractFromVueSFC(filePath, projectRoot);
|
|
80
|
+
expectations.push(...sfcExpectations);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Extract from script section (TypeScript/JavaScript)
|
|
84
|
+
const ast = parseFile(filePath, true);
|
|
85
|
+
if (!ast) return expectations;
|
|
86
|
+
|
|
87
|
+
// Extract router.push/replace calls
|
|
88
|
+
const routerCalls = findRouterCalls(ast);
|
|
89
|
+
for (const call of routerCalls) {
|
|
90
|
+
const expectation = extractFromRouterCall(call, ast, projectRoot);
|
|
91
|
+
if (expectation) {
|
|
92
|
+
expectations.push(expectation);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Extract RouterLink JSX/TSX components
|
|
97
|
+
const routerLinks = findRouterLinkElements(ast);
|
|
98
|
+
for (const element of routerLinks) {
|
|
99
|
+
const expectation = extractFromRouterLink(element, ast, projectRoot);
|
|
100
|
+
if (expectation) {
|
|
101
|
+
expectations.push(expectation);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return expectations;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Extract navigation promises from Vue SFC (.vue file).
|
|
110
|
+
*
|
|
111
|
+
* @param {string} filePath - File path
|
|
112
|
+
* @param {string} projectRoot - Project root
|
|
113
|
+
* @returns {Array} - Navigation expectations
|
|
114
|
+
*/
|
|
115
|
+
function extractFromVueSFC(filePath, projectRoot) {
|
|
116
|
+
const expectations = [];
|
|
117
|
+
const { relative } = require('path');
|
|
118
|
+
|
|
119
|
+
if (!existsSync(filePath)) return expectations;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
123
|
+
|
|
124
|
+
// Extract template section
|
|
125
|
+
const templateMatch = content.match(/<template[^>]*>([\s\S]*?)<\/template>/);
|
|
126
|
+
if (templateMatch) {
|
|
127
|
+
const templateContent = templateMatch[1];
|
|
128
|
+
|
|
129
|
+
// Pattern 1: <router-link to="/path">
|
|
130
|
+
const routerLinkRegex = /<router-link[^>]*\s+to=["']([^"']+)["'][^>]*>/g;
|
|
131
|
+
let match;
|
|
132
|
+
while ((match = routerLinkRegex.exec(templateContent)) !== null) {
|
|
133
|
+
const path = match[1];
|
|
134
|
+
if (path && path.startsWith('/')) {
|
|
135
|
+
const normalized = normalizeDynamicRoute(path);
|
|
136
|
+
const line = (templateContent.substring(0, match.index).match(/\n/g) || []).length + 1;
|
|
137
|
+
|
|
138
|
+
if (normalized) {
|
|
139
|
+
// Dynamic route
|
|
140
|
+
expectations.push({
|
|
141
|
+
type: 'spa_navigation',
|
|
142
|
+
targetPath: normalized.examplePath,
|
|
143
|
+
originalPattern: normalized.originalPattern,
|
|
144
|
+
isDynamic: true,
|
|
145
|
+
exampleExecution: true,
|
|
146
|
+
matchAttribute: 'to',
|
|
147
|
+
proof: 'PROVEN_EXPECTATION',
|
|
148
|
+
sourceRef: `${relative(projectRoot, filePath).replace(/\\/g, '/')}:${line}`,
|
|
149
|
+
selectorHint: 'router-link',
|
|
150
|
+
metadata: {
|
|
151
|
+
elementFile: relative(projectRoot, filePath).replace(/\\/g, '/'),
|
|
152
|
+
elementLine: line,
|
|
153
|
+
eventType: 'click'
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
// Static route
|
|
158
|
+
expectations.push({
|
|
159
|
+
type: 'spa_navigation',
|
|
160
|
+
targetPath: path,
|
|
161
|
+
matchAttribute: 'to',
|
|
162
|
+
proof: 'PROVEN_EXPECTATION',
|
|
163
|
+
sourceRef: `${relative(projectRoot, filePath).replace(/\\/g, '/')}:${line}`,
|
|
164
|
+
selectorHint: 'router-link',
|
|
165
|
+
metadata: {
|
|
166
|
+
elementFile: relative(projectRoot, filePath).replace(/\\/g, '/'),
|
|
167
|
+
elementLine: line,
|
|
168
|
+
eventType: 'click'
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Pattern 2: <RouterLink :to="{ path: '/path' }">
|
|
176
|
+
const routerLinkBindingRegex = /<RouterLink\s+[^>]*:to=["']\{[^}]*path:\s*["']([^"']+)["'][^}]*\}[^>]*>/g;
|
|
177
|
+
while ((match = routerLinkBindingRegex.exec(templateContent)) !== null) {
|
|
178
|
+
const path = match[1];
|
|
179
|
+
if (path && path.startsWith('/')) {
|
|
180
|
+
const normalized = normalizeDynamicRoute(path);
|
|
181
|
+
const line = (templateContent.substring(0, match.index).match(/\n/g) || []).length + 1;
|
|
182
|
+
|
|
183
|
+
if (normalized) {
|
|
184
|
+
// Dynamic route
|
|
185
|
+
expectations.push({
|
|
186
|
+
type: 'spa_navigation',
|
|
187
|
+
targetPath: normalized.examplePath,
|
|
188
|
+
originalPattern: normalized.originalPattern,
|
|
189
|
+
isDynamic: true,
|
|
190
|
+
exampleExecution: true,
|
|
191
|
+
matchAttribute: 'to',
|
|
192
|
+
proof: 'PROVEN_EXPECTATION',
|
|
193
|
+
sourceRef: `${relative(projectRoot, filePath).replace(/\\/g, '/')}:${line}`,
|
|
194
|
+
selectorHint: 'RouterLink',
|
|
195
|
+
metadata: {
|
|
196
|
+
elementFile: relative(projectRoot, filePath).replace(/\\/g, '/'),
|
|
197
|
+
elementLine: line,
|
|
198
|
+
eventType: 'click'
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
} else {
|
|
202
|
+
// Static route
|
|
203
|
+
expectations.push({
|
|
204
|
+
type: 'spa_navigation',
|
|
205
|
+
targetPath: path,
|
|
206
|
+
matchAttribute: 'to',
|
|
207
|
+
proof: 'PROVEN_EXPECTATION',
|
|
208
|
+
sourceRef: `${relative(projectRoot, filePath).replace(/\\/g, '/')}:${line}`,
|
|
209
|
+
selectorHint: 'RouterLink',
|
|
210
|
+
metadata: {
|
|
211
|
+
elementFile: relative(projectRoot, filePath).replace(/\\/g, '/'),
|
|
212
|
+
elementLine: line,
|
|
213
|
+
eventType: 'click'
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Extract script section for router.push/replace
|
|
222
|
+
const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
|
|
223
|
+
if (scriptMatch) {
|
|
224
|
+
const scriptContent = scriptMatch[1];
|
|
225
|
+
const routerPushRegex = /router\.(push|replace)\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
226
|
+
while ((match = routerPushRegex.exec(scriptContent)) !== null) {
|
|
227
|
+
const method = match[1];
|
|
228
|
+
const path = match[2];
|
|
229
|
+
if (path && path.startsWith('/')) {
|
|
230
|
+
const normalized = normalizeDynamicRoute(path);
|
|
231
|
+
const line = (scriptContent.substring(0, match.index).match(/\n/g) || []).length + 1;
|
|
232
|
+
|
|
233
|
+
if (normalized) {
|
|
234
|
+
// Dynamic route
|
|
235
|
+
expectations.push({
|
|
236
|
+
type: 'spa_navigation',
|
|
237
|
+
targetPath: normalized.examplePath,
|
|
238
|
+
originalPattern: normalized.originalPattern,
|
|
239
|
+
isDynamic: true,
|
|
240
|
+
exampleExecution: true,
|
|
241
|
+
navigationMethod: method,
|
|
242
|
+
proof: 'PROVEN_EXPECTATION',
|
|
243
|
+
sourceRef: `${relative(projectRoot, filePath).replace(/\\/g, '/')}:${line}`,
|
|
244
|
+
selectorHint: null,
|
|
245
|
+
metadata: {
|
|
246
|
+
elementFile: relative(projectRoot, filePath).replace(/\\/g, '/'),
|
|
247
|
+
elementLine: line,
|
|
248
|
+
handlerName: `router.${method}`,
|
|
249
|
+
eventType: 'programmatic'
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
} else {
|
|
253
|
+
// Static route
|
|
254
|
+
expectations.push({
|
|
255
|
+
type: 'spa_navigation',
|
|
256
|
+
targetPath: path,
|
|
257
|
+
navigationMethod: method,
|
|
258
|
+
proof: 'PROVEN_EXPECTATION',
|
|
259
|
+
sourceRef: `${relative(projectRoot, filePath).replace(/\\/g, '/')}:${line}`,
|
|
260
|
+
selectorHint: null,
|
|
261
|
+
metadata: {
|
|
262
|
+
elementFile: relative(projectRoot, filePath).replace(/\\/g, '/'),
|
|
263
|
+
elementLine: line,
|
|
264
|
+
handlerName: `router.${method}`,
|
|
265
|
+
eventType: 'programmatic'
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch (err) {
|
|
273
|
+
// Skip if file can't be parsed
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return expectations;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Find router.push/replace calls in AST.
|
|
281
|
+
*
|
|
282
|
+
* @param {ts.SourceFile} ast - Source file
|
|
283
|
+
* @returns {Array} - Call expression nodes
|
|
284
|
+
*/
|
|
285
|
+
function findRouterCalls(ast) {
|
|
286
|
+
const calls = [];
|
|
287
|
+
|
|
288
|
+
const callExpressions = findNodes(ast, node => {
|
|
289
|
+
if (!ts.isCallExpression(node)) return false;
|
|
290
|
+
const expr = node.expression;
|
|
291
|
+
if (!ts.isPropertyAccessExpression(expr)) return false;
|
|
292
|
+
|
|
293
|
+
const obj = expr.expression;
|
|
294
|
+
const prop = expr.name;
|
|
295
|
+
|
|
296
|
+
if (!ts.isIdentifier(obj) || !ts.isIdentifier(prop)) return false;
|
|
297
|
+
|
|
298
|
+
return obj.text === 'router' && (prop.text === 'push' || prop.text === 'replace');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
return callExpressions;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Extract expectation from router call.
|
|
306
|
+
*
|
|
307
|
+
* @param {ts.CallExpression} call - Call expression
|
|
308
|
+
* @param {ts.SourceFile} ast - Source file
|
|
309
|
+
* @param {string} projectRoot - Project root
|
|
310
|
+
* @returns {Object|null} - Expectation or null
|
|
311
|
+
*/
|
|
312
|
+
function extractFromRouterCall(call, ast, projectRoot) {
|
|
313
|
+
const expr = call.expression;
|
|
314
|
+
if (!ts.isPropertyAccessExpression(expr)) return null;
|
|
315
|
+
|
|
316
|
+
const prop = expr.name;
|
|
317
|
+
const method = prop.text; // 'push' or 'replace'
|
|
318
|
+
|
|
319
|
+
if (call.arguments.length === 0) return null;
|
|
320
|
+
|
|
321
|
+
const arg = call.arguments[0];
|
|
322
|
+
const location = getNodeLocation(ast, call, projectRoot);
|
|
323
|
+
|
|
324
|
+
// String literal: router.push('/path')
|
|
325
|
+
const path = getStringLiteral(arg);
|
|
326
|
+
if (path && path.startsWith('/')) {
|
|
327
|
+
// Normalize dynamic routes
|
|
328
|
+
const normalized = normalizeDynamicRoute(path);
|
|
329
|
+
if (normalized) {
|
|
330
|
+
return {
|
|
331
|
+
type: 'spa_navigation',
|
|
332
|
+
targetPath: normalized.examplePath,
|
|
333
|
+
originalPattern: normalized.originalPattern,
|
|
334
|
+
isDynamic: true,
|
|
335
|
+
exampleExecution: true,
|
|
336
|
+
navigationMethod: method,
|
|
337
|
+
proof: 'PROVEN_EXPECTATION',
|
|
338
|
+
sourceRef: location.sourceRef,
|
|
339
|
+
selectorHint: null,
|
|
340
|
+
metadata: {
|
|
341
|
+
elementFile: location.file,
|
|
342
|
+
elementLine: location.line,
|
|
343
|
+
handlerName: `router.${method}`,
|
|
344
|
+
eventType: 'programmatic'
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
// Static route
|
|
349
|
+
return {
|
|
350
|
+
type: 'spa_navigation',
|
|
351
|
+
targetPath: path,
|
|
352
|
+
navigationMethod: method,
|
|
353
|
+
proof: 'PROVEN_EXPECTATION',
|
|
354
|
+
sourceRef: location.sourceRef,
|
|
355
|
+
selectorHint: null,
|
|
356
|
+
metadata: {
|
|
357
|
+
elementFile: location.file,
|
|
358
|
+
elementLine: location.line,
|
|
359
|
+
handlerName: `router.${method}`,
|
|
360
|
+
eventType: 'programmatic'
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Template literal: router.push(`/users/${id}`)
|
|
366
|
+
if (ts.isTemplateExpression(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
|
|
367
|
+
let templateText = null;
|
|
368
|
+
if (ts.isTemplateExpression(arg)) {
|
|
369
|
+
// Build template string with ${} placeholders
|
|
370
|
+
let text = arg.head?.text || '';
|
|
371
|
+
for (const span of arg.templateSpans || []) {
|
|
372
|
+
const expr = span.expression;
|
|
373
|
+
if (ts.isIdentifier(expr)) {
|
|
374
|
+
text += '${' + expr.text + '}';
|
|
375
|
+
} else {
|
|
376
|
+
// Complex expression - cannot normalize safely
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
text += span.literal?.text || '';
|
|
380
|
+
}
|
|
381
|
+
templateText = text;
|
|
382
|
+
} else if (ts.isNoSubstitutionTemplateLiteral(arg)) {
|
|
383
|
+
templateText = arg.text;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (templateText && templateText.startsWith('/')) {
|
|
387
|
+
// Normalize template literal
|
|
388
|
+
const normalized = normalizeTemplateLiteral(templateText) || normalizeDynamicRoute(templateText);
|
|
389
|
+
if (normalized) {
|
|
390
|
+
return {
|
|
391
|
+
type: 'spa_navigation',
|
|
392
|
+
targetPath: normalized.examplePath,
|
|
393
|
+
originalPattern: normalized.originalPattern,
|
|
394
|
+
isDynamic: true,
|
|
395
|
+
exampleExecution: true,
|
|
396
|
+
navigationMethod: method,
|
|
397
|
+
proof: 'PROVEN_EXPECTATION',
|
|
398
|
+
sourceRef: location.sourceRef,
|
|
399
|
+
selectorHint: null,
|
|
400
|
+
metadata: {
|
|
401
|
+
elementFile: location.file,
|
|
402
|
+
elementLine: location.line,
|
|
403
|
+
handlerName: `router.${method}`,
|
|
404
|
+
eventType: 'programmatic'
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Object literal: router.push({ path: '/path' })
|
|
412
|
+
if (ts.isObjectLiteralExpression(arg)) {
|
|
413
|
+
for (const prop of arg.properties) {
|
|
414
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
415
|
+
const name = prop.name;
|
|
416
|
+
if (!ts.isIdentifier(name)) continue;
|
|
417
|
+
if (name.text !== 'path') continue;
|
|
418
|
+
|
|
419
|
+
const pathValue = getStringLiteral(prop.initializer);
|
|
420
|
+
if (pathValue && pathValue.startsWith('/')) {
|
|
421
|
+
// Normalize dynamic routes
|
|
422
|
+
const normalized = normalizeDynamicRoute(pathValue);
|
|
423
|
+
if (normalized) {
|
|
424
|
+
return {
|
|
425
|
+
type: 'spa_navigation',
|
|
426
|
+
targetPath: normalized.examplePath,
|
|
427
|
+
originalPattern: normalized.originalPattern,
|
|
428
|
+
isDynamic: true,
|
|
429
|
+
exampleExecution: true,
|
|
430
|
+
navigationMethod: method,
|
|
431
|
+
proof: 'PROVEN_EXPECTATION',
|
|
432
|
+
sourceRef: location.sourceRef,
|
|
433
|
+
selectorHint: null,
|
|
434
|
+
metadata: {
|
|
435
|
+
elementFile: location.file,
|
|
436
|
+
elementLine: location.line,
|
|
437
|
+
handlerName: `router.${method}`,
|
|
438
|
+
eventType: 'programmatic'
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
// Static route
|
|
443
|
+
return {
|
|
444
|
+
type: 'spa_navigation',
|
|
445
|
+
targetPath: pathValue,
|
|
446
|
+
navigationMethod: method,
|
|
447
|
+
proof: 'PROVEN_EXPECTATION',
|
|
448
|
+
sourceRef: location.sourceRef,
|
|
449
|
+
selectorHint: null,
|
|
450
|
+
metadata: {
|
|
451
|
+
elementFile: location.file,
|
|
452
|
+
elementLine: location.line,
|
|
453
|
+
handlerName: `router.${method}`,
|
|
454
|
+
eventType: 'programmatic'
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Find RouterLink JSX elements.
|
|
466
|
+
*
|
|
467
|
+
* @param {ts.SourceFile} ast - Source file
|
|
468
|
+
* @returns {Array} - JSX element nodes
|
|
469
|
+
*/
|
|
470
|
+
function findRouterLinkElements(ast) {
|
|
471
|
+
const elements = [];
|
|
472
|
+
|
|
473
|
+
const jsxElements = findNodes(ast, node => {
|
|
474
|
+
if (!ts.isJsxOpeningElement(node) && !ts.isJsxSelfClosingElement(node)) return false;
|
|
475
|
+
|
|
476
|
+
const tagName = node.tagName;
|
|
477
|
+
if (!ts.isIdentifier(tagName)) return false;
|
|
478
|
+
|
|
479
|
+
return tagName.text === 'RouterLink' || tagName.text === 'router-link';
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
return jsxElements;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Extract expectation from RouterLink element.
|
|
487
|
+
*
|
|
488
|
+
* @param {ts.Node} element - JSX element
|
|
489
|
+
* @param {ts.SourceFile} ast - Source file
|
|
490
|
+
* @param {string} projectRoot - Project root
|
|
491
|
+
* @returns {Object|null} - Expectation or null
|
|
492
|
+
*/
|
|
493
|
+
function extractFromRouterLink(element, ast, projectRoot) {
|
|
494
|
+
const attributes = element.attributes;
|
|
495
|
+
if (!attributes || !attributes.properties) return null;
|
|
496
|
+
|
|
497
|
+
let targetPath = null;
|
|
498
|
+
|
|
499
|
+
for (const attr of attributes.properties) {
|
|
500
|
+
if (!ts.isJsxAttribute(attr)) continue;
|
|
501
|
+
|
|
502
|
+
const name = attr.name;
|
|
503
|
+
if (!ts.isIdentifier(name)) continue;
|
|
504
|
+
|
|
505
|
+
if (name.text === 'to') {
|
|
506
|
+
const initializer = attr.initializer;
|
|
507
|
+
|
|
508
|
+
// String literal: to="/path"
|
|
509
|
+
if (ts.isStringLiteral(initializer)) {
|
|
510
|
+
targetPath = initializer.text;
|
|
511
|
+
}
|
|
512
|
+
// JSX expression: to={"/path"} or to={{ path: "/path" }}
|
|
513
|
+
else if (ts.isJsxExpression(initializer)) {
|
|
514
|
+
const expr = initializer.expression;
|
|
515
|
+
|
|
516
|
+
// String literal in expression
|
|
517
|
+
if (ts.isStringLiteral(expr)) {
|
|
518
|
+
targetPath = expr.text;
|
|
519
|
+
}
|
|
520
|
+
// Object literal: to={{ path: "/path" }}
|
|
521
|
+
else if (ts.isObjectLiteralExpression(expr)) {
|
|
522
|
+
for (const prop of expr.properties) {
|
|
523
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
524
|
+
const propName = prop.name;
|
|
525
|
+
if (!ts.isIdentifier(propName)) continue;
|
|
526
|
+
if (propName.text !== 'path') continue;
|
|
527
|
+
|
|
528
|
+
const pathValue = getStringLiteral(prop.initializer);
|
|
529
|
+
if (pathValue) {
|
|
530
|
+
targetPath = pathValue;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (targetPath && targetPath.startsWith('/')) {
|
|
539
|
+
const location = getNodeLocation(ast, element, projectRoot);
|
|
540
|
+
const normalized = normalizeDynamicRoute(targetPath);
|
|
541
|
+
|
|
542
|
+
if (normalized) {
|
|
543
|
+
// Dynamic route
|
|
544
|
+
return {
|
|
545
|
+
type: 'spa_navigation',
|
|
546
|
+
targetPath: normalized.examplePath,
|
|
547
|
+
originalPattern: normalized.originalPattern,
|
|
548
|
+
isDynamic: true,
|
|
549
|
+
exampleExecution: true,
|
|
550
|
+
matchAttribute: 'to',
|
|
551
|
+
proof: 'PROVEN_EXPECTATION',
|
|
552
|
+
sourceRef: location.sourceRef,
|
|
553
|
+
selectorHint: 'RouterLink',
|
|
554
|
+
metadata: {
|
|
555
|
+
elementFile: location.file,
|
|
556
|
+
elementLine: location.line,
|
|
557
|
+
eventType: 'click'
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Static route
|
|
563
|
+
return {
|
|
564
|
+
type: 'spa_navigation',
|
|
565
|
+
targetPath: targetPath,
|
|
566
|
+
matchAttribute: 'to',
|
|
567
|
+
proof: 'PROVEN_EXPECTATION',
|
|
568
|
+
sourceRef: location.sourceRef,
|
|
569
|
+
selectorHint: 'RouterLink',
|
|
570
|
+
metadata: {
|
|
571
|
+
elementFile: location.file,
|
|
572
|
+
elementLine: location.line,
|
|
573
|
+
eventType: 'click'
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return null;
|
|
579
|
+
}
|