@appilots/cli 0.3.0 → 0.10.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/dist/cli/index.js +3576 -404
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +276 -20
- package/dist/index.d.ts +276 -20
- package/dist/index.js +2419 -223
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2407 -218
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -2
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs, { readFile, mkdir, writeFile } from 'fs/promises';
|
|
2
|
-
import * as
|
|
3
|
-
import
|
|
2
|
+
import * as path8 from 'path';
|
|
3
|
+
import path8__default, { join, resolve } from 'path';
|
|
4
4
|
import traverse4 from '@babel/traverse';
|
|
5
5
|
import * as BabelTypes from '@babel/types';
|
|
6
6
|
import glob from 'fast-glob';
|
|
@@ -56,6 +56,45 @@ function hasJsxAttribute(element, name) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// src/ast/jsx/classify.ts
|
|
59
|
+
var NON_INTERACTION_HANDLERS = /* @__PURE__ */ new Set([
|
|
60
|
+
"onLayout",
|
|
61
|
+
"onScroll",
|
|
62
|
+
"onScrollBeginDrag",
|
|
63
|
+
"onScrollEndDrag",
|
|
64
|
+
"onMomentumScrollBegin",
|
|
65
|
+
"onMomentumScrollEnd",
|
|
66
|
+
"onContentSizeChange",
|
|
67
|
+
"onEndReached",
|
|
68
|
+
"onViewableItemsChanged",
|
|
69
|
+
"onLoad",
|
|
70
|
+
"onLoadStart",
|
|
71
|
+
"onLoadEnd",
|
|
72
|
+
"onError",
|
|
73
|
+
"onProgress",
|
|
74
|
+
"onFocus",
|
|
75
|
+
"onBlur"
|
|
76
|
+
]);
|
|
77
|
+
var VALUE_HANDLERS = /* @__PURE__ */ new Set([
|
|
78
|
+
"onChange",
|
|
79
|
+
"onChangeText",
|
|
80
|
+
"onValueChange",
|
|
81
|
+
"onSelect",
|
|
82
|
+
"onSelectionChange",
|
|
83
|
+
"onSubmitEditing"
|
|
84
|
+
]);
|
|
85
|
+
function pressHandlerProp(element) {
|
|
86
|
+
let fallback;
|
|
87
|
+
for (const attr of element.attributes) {
|
|
88
|
+
if (attr.type !== "JSXAttribute" || attr.name.type !== "JSXIdentifier") continue;
|
|
89
|
+
const name = attr.name.name;
|
|
90
|
+
if (!/^on[A-Z]/.test(name)) continue;
|
|
91
|
+
if (attr.value?.type !== "JSXExpressionContainer") continue;
|
|
92
|
+
if (name === "onPress") return name;
|
|
93
|
+
if (NON_INTERACTION_HANDLERS.has(name) || VALUE_HANDLERS.has(name)) continue;
|
|
94
|
+
fallback ??= name;
|
|
95
|
+
}
|
|
96
|
+
return fallback;
|
|
97
|
+
}
|
|
59
98
|
var VIEW_COMPONENTS = /* @__PURE__ */ new Set(["View", "ScrollView", "SafeAreaView", "KeyboardAvoidingView"]);
|
|
60
99
|
var INPUT_COMPONENTS = /* @__PURE__ */ new Set(["TextInput", "Input", "HocInput"]);
|
|
61
100
|
var BUTTON_COMPONENTS = /* @__PURE__ */ new Set(["TouchableOpacity", "Pressable", "Button"]);
|
|
@@ -70,17 +109,16 @@ function classifyJsxComponent(name, element) {
|
|
|
70
109
|
if (INPUT_COMPONENTS.has(name)) return "input";
|
|
71
110
|
if (BUTTON_COMPONENTS.has(name)) return "button";
|
|
72
111
|
if (element) {
|
|
73
|
-
const hasOptions = hasJsxAttribute(element, "options");
|
|
112
|
+
const hasOptions = hasJsxAttribute(element, "options") || hasJsxAttribute(element, "segments") || hasJsxAttribute(element, "choices") || hasJsxAttribute(element, "tabs");
|
|
74
113
|
const hasValue = hasJsxAttribute(element, "value");
|
|
75
114
|
const hasChecked = hasJsxAttribute(element, "checked") || hasJsxAttribute(element, "selected");
|
|
76
115
|
const hasOnChange = hasJsxAttribute(element, "onChange") || hasJsxAttribute(element, "onValueChange") || hasJsxAttribute(element, "onChangeText");
|
|
77
|
-
const hasOnPress = hasJsxAttribute(element, "onPress");
|
|
78
116
|
if (hasOptions && (hasValue || hasOnChange)) return "select";
|
|
79
117
|
if (hasChecked && hasOnChange) return "toggle";
|
|
80
118
|
if (hasOnChange && (hasJsxAttribute(element, "label") || hasJsxAttribute(element, "placeholder"))) {
|
|
81
119
|
return "input";
|
|
82
120
|
}
|
|
83
|
-
if (
|
|
121
|
+
if (pressHandlerProp(element)) return "button";
|
|
84
122
|
if (getStringAttr(element, "visible") || getExpressionIdentifierAttr(element, "visible")) {
|
|
85
123
|
return "modal";
|
|
86
124
|
}
|
|
@@ -392,12 +430,12 @@ var ScreenAnalyzer = class {
|
|
|
392
430
|
cwd: this.config.rootDir,
|
|
393
431
|
ignore: exclude
|
|
394
432
|
});
|
|
395
|
-
screenPatternFiles = new Set(matched.map((f) =>
|
|
433
|
+
screenPatternFiles = new Set(matched.map((f) => path8__default.resolve(this.config.rootDir, f)));
|
|
396
434
|
}
|
|
397
435
|
const screens = [];
|
|
398
436
|
this.screensFilteredOut = 0;
|
|
399
437
|
for (const file of files) {
|
|
400
|
-
const filePath =
|
|
438
|
+
const filePath = path8__default.resolve(this.config.rootDir, file);
|
|
401
439
|
try {
|
|
402
440
|
const descriptor = await this.analyzeFile(filePath);
|
|
403
441
|
if (!descriptor) continue;
|
|
@@ -418,12 +456,17 @@ var ScreenAnalyzer = class {
|
|
|
418
456
|
}
|
|
419
457
|
} catch (error) {
|
|
420
458
|
if (this.verbose) {
|
|
421
|
-
console.warn(
|
|
459
|
+
console.warn(
|
|
460
|
+
`[ScreenAnalyzer] Failed to parse ${file}:`,
|
|
461
|
+
error instanceof Error ? error.message : error
|
|
462
|
+
);
|
|
422
463
|
}
|
|
423
464
|
}
|
|
424
465
|
}
|
|
425
466
|
if (this.verbose && this.strictScreens) {
|
|
426
|
-
console.log(
|
|
467
|
+
console.log(
|
|
468
|
+
`[ScreenAnalyzer] Strict mode: ${screens.length} included, ${this.screensFilteredOut} filtered out`
|
|
469
|
+
);
|
|
427
470
|
}
|
|
428
471
|
return screens;
|
|
429
472
|
}
|
|
@@ -711,7 +754,11 @@ var ScreenAnalyzer = class {
|
|
|
711
754
|
for (const form of secondary) {
|
|
712
755
|
const existing = out.find((candidate) => candidate.id === form.id) ?? this.findFormWithSharedFields(out, form);
|
|
713
756
|
if (!existing) {
|
|
714
|
-
out.push({
|
|
757
|
+
out.push({
|
|
758
|
+
...form,
|
|
759
|
+
fields: [...form.fields],
|
|
760
|
+
submitAction: this.namedSubmitAction(form.submitAction)
|
|
761
|
+
});
|
|
715
762
|
continue;
|
|
716
763
|
}
|
|
717
764
|
for (const field of form.fields) {
|
|
@@ -730,9 +777,12 @@ var ScreenAnalyzer = class {
|
|
|
730
777
|
findEquivalentField(fields, incoming) {
|
|
731
778
|
return fields.find((field) => {
|
|
732
779
|
if (field.name && incoming.name && field.name === incoming.name) return true;
|
|
733
|
-
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
734
|
-
|
|
735
|
-
if (field.
|
|
780
|
+
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
781
|
+
return true;
|
|
782
|
+
if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
|
|
783
|
+
return true;
|
|
784
|
+
if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
|
|
785
|
+
return true;
|
|
736
786
|
if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
|
|
737
787
|
return true;
|
|
738
788
|
}
|
|
@@ -892,9 +942,11 @@ var ScreenAnalyzer = class {
|
|
|
892
942
|
source: field.locator?.source ?? "accessibilityLabel"
|
|
893
943
|
});
|
|
894
944
|
} else if (attrName === "keyboardType") {
|
|
895
|
-
if (attr.value.value === "email-address" || attr.value.value === "email")
|
|
945
|
+
if (attr.value.value === "email-address" || attr.value.value === "email")
|
|
946
|
+
field.type = "email";
|
|
896
947
|
if (attr.value.value === "phone-pad") field.type = "phone";
|
|
897
|
-
if (attr.value.value === "numeric" || attr.value.value === "number-pad")
|
|
948
|
+
if (attr.value.value === "numeric" || attr.value.value === "number-pad")
|
|
949
|
+
field.type = "number";
|
|
898
950
|
}
|
|
899
951
|
} else if (BabelTypes.isJSXExpressionContainer(attr.value)) {
|
|
900
952
|
if (attrName === "value" && BabelTypes.isIdentifier(attr.value.expression)) {
|
|
@@ -1060,8 +1112,10 @@ var ScreenAnalyzer = class {
|
|
|
1060
1112
|
};
|
|
1061
1113
|
if (handler.nativeConfirmationExpected) action.nativeConfirmationExpected = true;
|
|
1062
1114
|
if (handler.targetScreen && !action.targetScreen) action.targetScreen = handler.targetScreen;
|
|
1063
|
-
if (handler.successSignal && !action.successSignal)
|
|
1064
|
-
|
|
1115
|
+
if (handler.successSignal && !action.successSignal)
|
|
1116
|
+
action.successSignal = handler.successSignal;
|
|
1117
|
+
if (handler.failureSignal && !action.failureSignal)
|
|
1118
|
+
action.failureSignal = handler.failureSignal;
|
|
1065
1119
|
if (handler.opensModal && !action.opensModal) action.opensModal = handler.opensModal;
|
|
1066
1120
|
if (handler.destructive || action.destructive === true || action.requiresConfirmation === true || action.effect === "destructive" || action.riskLevel === "high") {
|
|
1067
1121
|
action.destructive = true;
|
|
@@ -1079,6 +1133,7 @@ var ScreenAnalyzer = class {
|
|
|
1079
1133
|
if (!BabelTypes.isJSXIdentifier(element.name)) return;
|
|
1080
1134
|
const componentName = element.name.name;
|
|
1081
1135
|
if (classifyJsxComponent(componentName, element) !== "button") return;
|
|
1136
|
+
const pressProp = pressHandlerProp(element);
|
|
1082
1137
|
let label;
|
|
1083
1138
|
let handler;
|
|
1084
1139
|
for (const attr of element.attributes) {
|
|
@@ -1088,7 +1143,7 @@ var ScreenAnalyzer = class {
|
|
|
1088
1143
|
if ((attrName === "title" || attrName === "accessibilityLabel" || attrName === "label") && BabelTypes.isStringLiteral(attr.value)) {
|
|
1089
1144
|
label = attr.value.value;
|
|
1090
1145
|
}
|
|
1091
|
-
if (attrName ===
|
|
1146
|
+
if (attrName === pressProp && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
|
|
1092
1147
|
handler = attr.value.expression.name;
|
|
1093
1148
|
}
|
|
1094
1149
|
}
|
|
@@ -1154,10 +1209,13 @@ var ScreenAnalyzer = class {
|
|
|
1154
1209
|
}
|
|
1155
1210
|
};
|
|
1156
1211
|
if (fn.body) {
|
|
1157
|
-
traverse4(
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1212
|
+
traverse4(
|
|
1213
|
+
fn.body,
|
|
1214
|
+
{
|
|
1215
|
+
noScope: true,
|
|
1216
|
+
enter: (nodePath) => inspectNode(nodePath.node)
|
|
1217
|
+
}
|
|
1218
|
+
);
|
|
1161
1219
|
}
|
|
1162
1220
|
const expectedOutcome = targetScreen ? "navigation" : hasStateSetter ? "inline-feedback" : hasAwait || hasThen ? "mixed" : "none";
|
|
1163
1221
|
return {
|
|
@@ -1186,7 +1244,7 @@ var ScreenAnalyzer = class {
|
|
|
1186
1244
|
* heuristic must hit OR the JSDoc tag must be present.
|
|
1187
1245
|
*/
|
|
1188
1246
|
isElementDestructive(element, handlerName, actionId) {
|
|
1189
|
-
const
|
|
1247
|
+
const DESTRUCTIVE_VERB3 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
|
|
1190
1248
|
for (const attr of element.attributes) {
|
|
1191
1249
|
if (!BabelTypes.isJSXAttribute(attr)) continue;
|
|
1192
1250
|
const attrName = BabelTypes.isJSXIdentifier(attr.name) ? attr.name.name : null;
|
|
@@ -1201,8 +1259,8 @@ var ScreenAnalyzer = class {
|
|
|
1201
1259
|
}
|
|
1202
1260
|
}
|
|
1203
1261
|
}
|
|
1204
|
-
if (handlerName &&
|
|
1205
|
-
if (actionId &&
|
|
1262
|
+
if (handlerName && DESTRUCTIVE_VERB3.test(handlerName)) return true;
|
|
1263
|
+
if (actionId && DESTRUCTIVE_VERB3.test(actionId)) return true;
|
|
1206
1264
|
return false;
|
|
1207
1265
|
}
|
|
1208
1266
|
/**
|
|
@@ -1215,6 +1273,7 @@ var ScreenAnalyzer = class {
|
|
|
1215
1273
|
};
|
|
1216
1274
|
const componentName = BabelTypes.isJSXIdentifier(element.name) ? element.name.name : void 0;
|
|
1217
1275
|
action.sourceComponent = componentName;
|
|
1276
|
+
const pressProp = pressHandlerProp(element) ?? "onPress";
|
|
1218
1277
|
let handlerName;
|
|
1219
1278
|
for (const attr of element.attributes) {
|
|
1220
1279
|
if (!BabelTypes.isJSXAttribute(attr)) continue;
|
|
@@ -1249,7 +1308,7 @@ var ScreenAnalyzer = class {
|
|
|
1249
1308
|
}
|
|
1250
1309
|
}
|
|
1251
1310
|
} else if (BabelTypes.isJSXExpressionContainer(attr.value)) {
|
|
1252
|
-
if (attrName ===
|
|
1311
|
+
if (attrName === pressProp && BabelTypes.isIdentifier(attr.value.expression)) {
|
|
1253
1312
|
handlerName = attr.value.expression.name.toLowerCase();
|
|
1254
1313
|
action.handler = attr.value.expression.name;
|
|
1255
1314
|
if (handlerName.includes("submit")) {
|
|
@@ -1257,7 +1316,7 @@ var ScreenAnalyzer = class {
|
|
|
1257
1316
|
} else if (handlerName.includes("navigate")) {
|
|
1258
1317
|
action.type = "navigation";
|
|
1259
1318
|
}
|
|
1260
|
-
} else if (attrName ===
|
|
1319
|
+
} else if (attrName === pressProp) {
|
|
1261
1320
|
const inline = this.extractInlineOnPressMetadata(attr.value.expression);
|
|
1262
1321
|
if (inline.handler) {
|
|
1263
1322
|
handlerName = inline.handler.toLowerCase();
|
|
@@ -1281,7 +1340,11 @@ var ScreenAnalyzer = class {
|
|
|
1281
1340
|
}
|
|
1282
1341
|
}
|
|
1283
1342
|
if (!action.locator && action.id) {
|
|
1284
|
-
action.locator = {
|
|
1343
|
+
action.locator = {
|
|
1344
|
+
id: action.id,
|
|
1345
|
+
label: action.label,
|
|
1346
|
+
source: action.label ? "label" : "inferred"
|
|
1347
|
+
};
|
|
1285
1348
|
}
|
|
1286
1349
|
if (this.isElementDestructive(element, handlerName, action.id)) {
|
|
1287
1350
|
action.destructive = true;
|
|
@@ -1423,24 +1486,27 @@ var ScreenAnalyzer = class {
|
|
|
1423
1486
|
}
|
|
1424
1487
|
extractRowAction(fn) {
|
|
1425
1488
|
let action;
|
|
1426
|
-
traverse4(
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1489
|
+
traverse4(
|
|
1490
|
+
fn.body,
|
|
1491
|
+
{
|
|
1492
|
+
noScope: true,
|
|
1493
|
+
CallExpression: (nodePath) => {
|
|
1494
|
+
const node = nodePath.node;
|
|
1495
|
+
if (!BabelTypes.isMemberExpression(node.callee) || !BabelTypes.isIdentifier(node.callee.object) || node.callee.object.name !== "navigation" || !BabelTypes.isIdentifier(node.callee.property) || !["navigate", "push", "replace"].includes(node.callee.property.name)) {
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
const firstArg = node.arguments[0];
|
|
1499
|
+
if (!BabelTypes.isStringLiteral(firstArg)) return;
|
|
1500
|
+
const params = this.extractNavigationParams(node.arguments[1]);
|
|
1501
|
+
action = {
|
|
1502
|
+
type: "navigation",
|
|
1503
|
+
targetScreen: firstArg.value,
|
|
1504
|
+
...Object.keys(params).length > 0 ? { params } : {},
|
|
1505
|
+
description: `Pressing a row opens ${firstArg.value}`
|
|
1506
|
+
};
|
|
1432
1507
|
}
|
|
1433
|
-
const firstArg = node.arguments[0];
|
|
1434
|
-
if (!BabelTypes.isStringLiteral(firstArg)) return;
|
|
1435
|
-
const params = this.extractNavigationParams(node.arguments[1]);
|
|
1436
|
-
action = {
|
|
1437
|
-
type: "navigation",
|
|
1438
|
-
targetScreen: firstArg.value,
|
|
1439
|
-
...Object.keys(params).length > 0 ? { params } : {},
|
|
1440
|
-
description: `Pressing a row opens ${firstArg.value}`
|
|
1441
|
-
};
|
|
1442
1508
|
}
|
|
1443
|
-
|
|
1509
|
+
);
|
|
1444
1510
|
return action;
|
|
1445
1511
|
}
|
|
1446
1512
|
extractNavigationParams(arg) {
|
|
@@ -1473,15 +1539,18 @@ var ScreenAnalyzer = class {
|
|
|
1473
1539
|
} else if (BabelTypes.isIdentifier(firstParam)) {
|
|
1474
1540
|
itemNames.add(firstParam.name);
|
|
1475
1541
|
}
|
|
1476
|
-
traverse4(
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1542
|
+
traverse4(
|
|
1543
|
+
fn.body,
|
|
1544
|
+
{
|
|
1545
|
+
noScope: true,
|
|
1546
|
+
MemberExpression: (nodePath) => {
|
|
1547
|
+
const node = nodePath.node;
|
|
1548
|
+
if (BabelTypes.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes.isIdentifier(node.property)) {
|
|
1549
|
+
fields.add(node.property.name);
|
|
1550
|
+
}
|
|
1482
1551
|
}
|
|
1483
1552
|
}
|
|
1484
|
-
|
|
1553
|
+
);
|
|
1485
1554
|
return Array.from(fields).sort();
|
|
1486
1555
|
}
|
|
1487
1556
|
inferItemType(dataSource, renderItem, displayFields) {
|
|
@@ -1493,11 +1562,19 @@ var ScreenAnalyzer = class {
|
|
|
1493
1562
|
if (displayFields.length > 0) return "Item";
|
|
1494
1563
|
return void 0;
|
|
1495
1564
|
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Identity comes from how a field is NAMED, not from what the app
|
|
1567
|
+
* sells: `id`/`uuid`/`key`/`slug` are conventions any codebase uses,
|
|
1568
|
+
* `name`/`title`/`email` are how any row introduces itself. `plate`
|
|
1569
|
+
* used to sit in this list and read like one of them — but it is the
|
|
1570
|
+
* example app's schema, and no other tenant ever got its equivalent
|
|
1571
|
+
* (`mrn`, `trackingNumber`, `sku`) added here.
|
|
1572
|
+
*/
|
|
1496
1573
|
inferIdentityFields(keyField, displayFields) {
|
|
1497
1574
|
const out = /* @__PURE__ */ new Set();
|
|
1498
1575
|
if (keyField) out.add(keyField);
|
|
1499
1576
|
for (const field of displayFields) {
|
|
1500
|
-
if (/^(id|uuid|key|name|title|
|
|
1577
|
+
if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
|
|
1501
1578
|
}
|
|
1502
1579
|
return Array.from(out);
|
|
1503
1580
|
}
|
|
@@ -1508,17 +1585,23 @@ var ScreenAnalyzer = class {
|
|
|
1508
1585
|
CallExpression: (nodePath) => {
|
|
1509
1586
|
const node = nodePath.node;
|
|
1510
1587
|
if (!BabelTypes.isMemberExpression(node.callee)) return;
|
|
1511
|
-
if (!BabelTypes.isIdentifier(node.callee.property) || node.callee.property.name !== "filter")
|
|
1512
|
-
|
|
1588
|
+
if (!BabelTypes.isIdentifier(node.callee.property) || node.callee.property.name !== "filter")
|
|
1589
|
+
return;
|
|
1590
|
+
if (!BabelTypes.isIdentifier(node.callee.object) || node.callee.object.name !== dataSource)
|
|
1591
|
+
return;
|
|
1513
1592
|
const fn = node.arguments[0];
|
|
1514
|
-
if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn))
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1593
|
+
if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn))
|
|
1594
|
+
return;
|
|
1595
|
+
traverse4(
|
|
1596
|
+
fn.body,
|
|
1597
|
+
{
|
|
1598
|
+
noScope: true,
|
|
1599
|
+
Identifier: (innerPath) => {
|
|
1600
|
+
const name = innerPath.node.name;
|
|
1601
|
+
if (/query|search|filter/i.test(name)) queryBinding = queryBinding ?? name;
|
|
1602
|
+
}
|
|
1520
1603
|
}
|
|
1521
|
-
|
|
1604
|
+
);
|
|
1522
1605
|
}
|
|
1523
1606
|
});
|
|
1524
1607
|
return queryBinding;
|
|
@@ -1546,7 +1629,7 @@ var ScreenAnalyzer = class {
|
|
|
1546
1629
|
* E.g., /src/screens/ItemListScreen.tsx -> ItemListScreen
|
|
1547
1630
|
*/
|
|
1548
1631
|
extractScreenName(filePath) {
|
|
1549
|
-
const basename2 =
|
|
1632
|
+
const basename2 = path8__default.basename(filePath);
|
|
1550
1633
|
return basename2.replace(/\.(tsx?|jsx?)$/, "");
|
|
1551
1634
|
}
|
|
1552
1635
|
};
|
|
@@ -1601,7 +1684,7 @@ var NavigationAnalyzer = class {
|
|
|
1601
1684
|
cwd: this.config.rootDir,
|
|
1602
1685
|
ignore: excludePatterns
|
|
1603
1686
|
});
|
|
1604
|
-
return files.map((file) =>
|
|
1687
|
+
return files.map((file) => path8__default.join(this.config.rootDir, file));
|
|
1605
1688
|
}
|
|
1606
1689
|
/** Parse navigator definitions from a file */
|
|
1607
1690
|
parseNavigators(content, filePath) {
|
|
@@ -1662,11 +1745,7 @@ var NavigationAnalyzer = class {
|
|
|
1662
1745
|
if (BabelTypes.isJSXAttribute(initialRouteAttr) && BabelTypes.isStringLiteral(initialRouteAttr.value)) {
|
|
1663
1746
|
navigator.initialRouteName = initialRouteAttr.value.value;
|
|
1664
1747
|
}
|
|
1665
|
-
const screens = this.extractScreensFromNavigator(
|
|
1666
|
-
node,
|
|
1667
|
-
objectName,
|
|
1668
|
-
navigator.type
|
|
1669
|
-
);
|
|
1748
|
+
const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
|
|
1670
1749
|
screensByNavigator.set(objectName, screens);
|
|
1671
1750
|
}
|
|
1672
1751
|
}
|
|
@@ -1683,27 +1762,112 @@ var NavigationAnalyzer = class {
|
|
|
1683
1762
|
}
|
|
1684
1763
|
return navigators;
|
|
1685
1764
|
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Is this JSX element `<navigatorVarName.MEMBER …>`?
|
|
1767
|
+
*/
|
|
1768
|
+
// Deliberately NOT a type predicate (`node is t.JSXElement`): a false
|
|
1769
|
+
// result would then narrow `node` to "not a JSXElement", and the very
|
|
1770
|
+
// next check — the same node against a different member — would see
|
|
1771
|
+
// `never`. It is a question about the member name, not about the node
|
|
1772
|
+
// kind.
|
|
1773
|
+
isNavigatorMember(node, navigatorVarName, member) {
|
|
1774
|
+
if (!BabelTypes.isJSXElement(node)) return false;
|
|
1775
|
+
const name = node.openingElement.name;
|
|
1776
|
+
return BabelTypes.isJSXMemberExpression(name) && BabelTypes.isJSXIdentifier(name.object) && name.object.name === navigatorVarName && BabelTypes.isJSXIdentifier(name.property) && name.property.name === member;
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Flatten a navigator's JSX children into the `<X.Screen>` elements they
|
|
1780
|
+
* contain, unwrapping every container a real app puts in between.
|
|
1781
|
+
*
|
|
1782
|
+
* The old version compared `t.isJSXElement(child)` against the direct
|
|
1783
|
+
* children only. A ternary is a `JSXExpressionContainer`, so an
|
|
1784
|
+
* auth-gated root — the modal shape of a commercial app, and the shape
|
|
1785
|
+
* of this repo's own `apps/example-app` — contributed ZERO screens to
|
|
1786
|
+
* the graph while `appilots sync` reported success (#396).
|
|
1787
|
+
*
|
|
1788
|
+
* Both branches of a conditional are collected on purpose. The graph is
|
|
1789
|
+
* a design-time map of what routes EXIST, not a prediction of which one
|
|
1790
|
+
* a given session will render; the agent needs the destination name to
|
|
1791
|
+
* be there whether or not the user happens to be logged in right now.
|
|
1792
|
+
*/
|
|
1793
|
+
collectScreenElements(children, navigatorVarName, depth = 0) {
|
|
1794
|
+
if (depth > 12) return [];
|
|
1795
|
+
const found = [];
|
|
1796
|
+
const visit = (node) => {
|
|
1797
|
+
if (!node) return;
|
|
1798
|
+
if (this.isNavigatorMember(node, navigatorVarName, "Screen")) {
|
|
1799
|
+
found.push(node);
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
if (this.isNavigatorMember(node, navigatorVarName, "Group")) {
|
|
1803
|
+
const group = node;
|
|
1804
|
+
found.push(...this.collectScreenElements(group.children, navigatorVarName, depth + 1));
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (BabelTypes.isJSXElement(node)) {
|
|
1808
|
+
const name = node.openingElement.name;
|
|
1809
|
+
const isForeignNavigator = BabelTypes.isJSXMemberExpression(name) && BabelTypes.isJSXIdentifier(name.property) && name.property.name === "Navigator";
|
|
1810
|
+
if (isForeignNavigator) return;
|
|
1811
|
+
found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
if (BabelTypes.isJSXFragment(node)) {
|
|
1815
|
+
found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
if (BabelTypes.isJSXExpressionContainer(node)) {
|
|
1819
|
+
visit(node.expression);
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
if (BabelTypes.isConditionalExpression(node)) {
|
|
1823
|
+
visit(node.consequent);
|
|
1824
|
+
visit(node.alternate);
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
if (BabelTypes.isLogicalExpression(node)) {
|
|
1828
|
+
visit(node.left);
|
|
1829
|
+
visit(node.right);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
if (BabelTypes.isCallExpression(node)) {
|
|
1833
|
+
for (const arg of node.arguments) {
|
|
1834
|
+
if (BabelTypes.isArrowFunctionExpression(arg) || BabelTypes.isFunctionExpression(arg)) {
|
|
1835
|
+
if (BabelTypes.isBlockStatement(arg.body)) {
|
|
1836
|
+
for (const stmt of arg.body.body) {
|
|
1837
|
+
if (BabelTypes.isReturnStatement(stmt)) visit(stmt.argument);
|
|
1838
|
+
}
|
|
1839
|
+
} else {
|
|
1840
|
+
visit(arg.body);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
if (BabelTypes.isArrayExpression(node)) {
|
|
1847
|
+
for (const el of node.elements) visit(el);
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
if (BabelTypes.isTSAsExpression(node) || BabelTypes.isTSNonNullExpression(node)) {
|
|
1851
|
+
visit(node.expression);
|
|
1852
|
+
}
|
|
1853
|
+
};
|
|
1854
|
+
for (const child of children) visit(child);
|
|
1855
|
+
return found;
|
|
1856
|
+
}
|
|
1686
1857
|
/** Extract screens from a navigator JSX element */
|
|
1687
1858
|
extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
|
|
1688
1859
|
const screens = [];
|
|
1689
1860
|
if (!navigatorElement.children) return screens;
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
name: screenName,
|
|
1701
|
-
navigatorName: navigatorVarName,
|
|
1702
|
-
navigatorType
|
|
1703
|
-
});
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1861
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1862
|
+
for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
|
|
1863
|
+
const screenName = this.extractAttributeValue(element.openingElement.attributes, "name");
|
|
1864
|
+
if (!screenName || seen.has(screenName)) continue;
|
|
1865
|
+
seen.add(screenName);
|
|
1866
|
+
screens.push({
|
|
1867
|
+
name: screenName,
|
|
1868
|
+
navigatorName: navigatorVarName,
|
|
1869
|
+
navigatorType
|
|
1870
|
+
});
|
|
1707
1871
|
}
|
|
1708
1872
|
return screens;
|
|
1709
1873
|
}
|
|
@@ -1762,9 +1926,7 @@ var NavigationAnalyzer = class {
|
|
|
1762
1926
|
if (member.type === "TSPropertySignature" && member.key) {
|
|
1763
1927
|
const keyName = member.key.type === "Identifier" ? member.key.name : null;
|
|
1764
1928
|
if (keyName && member.typeAnnotation) {
|
|
1765
|
-
const params = this.extractParamsFromType(
|
|
1766
|
-
member.typeAnnotation.typeAnnotation
|
|
1767
|
-
);
|
|
1929
|
+
const params = this.extractParamsFromType(member.typeAnnotation.typeAnnotation);
|
|
1768
1930
|
entries.set(keyName, params);
|
|
1769
1931
|
}
|
|
1770
1932
|
}
|
|
@@ -1804,7 +1966,7 @@ var NavigationAnalyzer = class {
|
|
|
1804
1966
|
if (type.type === "TSUndefinedKeyword") return "undefined";
|
|
1805
1967
|
if (type.type === "TSNullKeyword") return "null";
|
|
1806
1968
|
if (type.type === "TSUnionType") {
|
|
1807
|
-
return type.types.map((
|
|
1969
|
+
return type.types.map((t12) => this.typeToString(t12)).join(" | ");
|
|
1808
1970
|
}
|
|
1809
1971
|
if (type.type === "TSTypeLiteral") {
|
|
1810
1972
|
return "object";
|
|
@@ -1825,7 +1987,7 @@ var NavigationAnalyzer = class {
|
|
|
1825
1987
|
/** Attach parsed type params to navigator screens */
|
|
1826
1988
|
attachParamsToNavigators(navigators, types) {
|
|
1827
1989
|
for (const navigator of navigators) {
|
|
1828
|
-
const matchingType = types.find((
|
|
1990
|
+
const matchingType = types.find((t12) => t12.type === navigator.type);
|
|
1829
1991
|
if (matchingType) {
|
|
1830
1992
|
for (const screen of navigator.screens) {
|
|
1831
1993
|
const screenParams = matchingType.paramEntries.get(screen.name);
|
|
@@ -1843,6 +2005,11 @@ var NavigationAnalyzer = class {
|
|
|
1843
2005
|
let initialScreen = "";
|
|
1844
2006
|
for (const navigator of parsedNavigators) {
|
|
1845
2007
|
const screenNames = navigator.screens.map((s) => s.name);
|
|
2008
|
+
if (screenNames.length === 0) {
|
|
2009
|
+
console.warn(
|
|
2010
|
+
`[MCPGenerator] navigator "${navigator.name}" (${navigator.type}) declared no reachable screens. Its routes will be missing from the app map, so the agent will have no destination name for them. If the screens are there in the source, this is a parser gap \u2014 please report it with the file.`
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
1846
2013
|
navigators.push({
|
|
1847
2014
|
name: navigator.name,
|
|
1848
2015
|
type: navigator.type,
|
|
@@ -1910,8 +2077,8 @@ var ComponentAnalyzer = class {
|
|
|
1910
2077
|
});
|
|
1911
2078
|
const components = [];
|
|
1912
2079
|
traverse4(ast, {
|
|
1913
|
-
JSXElement: (
|
|
1914
|
-
const component = this.extractComponentFromJSXElement(
|
|
2080
|
+
JSXElement: (path9) => {
|
|
2081
|
+
const component = this.extractComponentFromJSXElement(path9.node);
|
|
1915
2082
|
if (component) {
|
|
1916
2083
|
components.push(component);
|
|
1917
2084
|
}
|
|
@@ -2038,13 +2205,13 @@ var FormAnalyzer = class {
|
|
|
2038
2205
|
this.inputElements = [];
|
|
2039
2206
|
this.submitButtons = [];
|
|
2040
2207
|
traverse4(ast, {
|
|
2041
|
-
CallExpression: (
|
|
2042
|
-
this.extractStateVariables(
|
|
2208
|
+
CallExpression: (path9) => {
|
|
2209
|
+
this.extractStateVariables(path9.node);
|
|
2043
2210
|
}
|
|
2044
2211
|
});
|
|
2045
2212
|
traverse4(ast, {
|
|
2046
|
-
JSXElement: (
|
|
2047
|
-
this.extractFormElements(
|
|
2213
|
+
JSXElement: (path9) => {
|
|
2214
|
+
this.extractFormElements(path9.node);
|
|
2048
2215
|
}
|
|
2049
2216
|
});
|
|
2050
2217
|
const validationRules = this.extractValidationRules(ast);
|
|
@@ -2144,8 +2311,8 @@ var FormAnalyzer = class {
|
|
|
2144
2311
|
extractValidationRules(ast) {
|
|
2145
2312
|
const rules = {};
|
|
2146
2313
|
traverse4(ast, {
|
|
2147
|
-
IfStatement: (
|
|
2148
|
-
const test =
|
|
2314
|
+
IfStatement: (path9) => {
|
|
2315
|
+
const test = path9.node.test;
|
|
2149
2316
|
const rule = this.extractRuleFromCondition(test);
|
|
2150
2317
|
if (rule) {
|
|
2151
2318
|
const { field, description } = rule;
|
|
@@ -2199,7 +2366,7 @@ var FormAnalyzer = class {
|
|
|
2199
2366
|
}
|
|
2200
2367
|
buildForms(filePath, validationRules) {
|
|
2201
2368
|
if (this.inputElements.length === 0) return [];
|
|
2202
|
-
const fileName =
|
|
2369
|
+
const fileName = path8.basename(filePath, path8.extname(filePath));
|
|
2203
2370
|
const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
|
|
2204
2371
|
const fields = this.inputElements.map((input) => {
|
|
2205
2372
|
const fieldType = this.inferFieldType(input);
|
|
@@ -2277,7 +2444,7 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2277
2444
|
`[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
|
|
2278
2445
|
);
|
|
2279
2446
|
const enrichmentPromises = screenFiles.map(async (file) => {
|
|
2280
|
-
const filePath =
|
|
2447
|
+
const filePath = path8__default.resolve(config.rootDir, file);
|
|
2281
2448
|
try {
|
|
2282
2449
|
const [components, forms] = await Promise.all([
|
|
2283
2450
|
componentAnalyzer.analyzeFile(filePath),
|
|
@@ -2320,91 +2487,1579 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2320
2487
|
});
|
|
2321
2488
|
}
|
|
2322
2489
|
}
|
|
2323
|
-
return {
|
|
2324
|
-
...screen,
|
|
2325
|
-
components: [...screen.components, ...newComponents],
|
|
2326
|
-
forms: mergedForms
|
|
2327
|
-
};
|
|
2490
|
+
return {
|
|
2491
|
+
...screen,
|
|
2492
|
+
components: [...screen.components, ...newComponents],
|
|
2493
|
+
forms: mergedForms
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
return screen;
|
|
2497
|
+
});
|
|
2498
|
+
return {
|
|
2499
|
+
screens: enrichedScreens,
|
|
2500
|
+
navigation,
|
|
2501
|
+
analyzedFiles: screenFiles.length,
|
|
2502
|
+
...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
mergeForm(target, source) {
|
|
2506
|
+
for (const field of source.fields) {
|
|
2507
|
+
const existingField = this.findEquivalentField(target.fields, field);
|
|
2508
|
+
if (existingField) {
|
|
2509
|
+
this.mergeField(existingField, field);
|
|
2510
|
+
} else {
|
|
2511
|
+
target.fields.push(field);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
target.submitAction = target.submitAction ?? this.namedSubmitAction(source.submitAction);
|
|
2515
|
+
target.validationRules = source.validationRules ? { ...source.validationRules, ...target.validationRules ?? {} } : target.validationRules;
|
|
2516
|
+
}
|
|
2517
|
+
findEquivalentField(fields, incoming) {
|
|
2518
|
+
return fields.find((field) => {
|
|
2519
|
+
if (field.name && incoming.name && field.name === incoming.name) return true;
|
|
2520
|
+
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
|
|
2521
|
+
if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
|
|
2522
|
+
if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
|
|
2523
|
+
if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
|
|
2524
|
+
return true;
|
|
2525
|
+
}
|
|
2526
|
+
return false;
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
mergeField(target, source) {
|
|
2530
|
+
if (this.isWeakInferredFieldName(target.name) && !this.isWeakInferredFieldName(source.name)) {
|
|
2531
|
+
target.name = source.name;
|
|
2532
|
+
}
|
|
2533
|
+
target.label = target.label ?? source.label;
|
|
2534
|
+
target.placeholder = target.placeholder ?? source.placeholder;
|
|
2535
|
+
target.options = target.options ?? source.options;
|
|
2536
|
+
target.locator = target.locator ?? source.locator;
|
|
2537
|
+
target.sourceComponent = target.sourceComponent ?? source.sourceComponent;
|
|
2538
|
+
target.valueBinding = target.valueBinding ?? source.valueBinding;
|
|
2539
|
+
target.errorBinding = target.errorBinding ?? source.errorBinding;
|
|
2540
|
+
target.required = target.required || source.required;
|
|
2541
|
+
if (target.type === "text" && source.type !== "text") {
|
|
2542
|
+
target.type = source.type;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
namedSubmitAction(submitAction) {
|
|
2546
|
+
return submitAction && submitAction !== "anonymous" ? submitAction : void 0;
|
|
2547
|
+
}
|
|
2548
|
+
isWeakInferredFieldName(name) {
|
|
2549
|
+
return /^(text|value|input|query|search|selected|checked)$/i.test(name);
|
|
2550
|
+
}
|
|
2551
|
+
findFormWithSharedFields(forms, incoming) {
|
|
2552
|
+
if (incoming.fields.length === 0) return void 0;
|
|
2553
|
+
let best;
|
|
2554
|
+
for (const form of forms) {
|
|
2555
|
+
const overlap = incoming.fields.filter((field) => this.findEquivalentField(form.fields, field)).length;
|
|
2556
|
+
if (overlap > 0 && (!best || overlap > best.overlap)) {
|
|
2557
|
+
best = { form, overlap };
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
return best?.form;
|
|
2561
|
+
}
|
|
2562
|
+
};
|
|
2563
|
+
|
|
2564
|
+
// src/analyzers/GenericPlatformAnalyzer.ts
|
|
2565
|
+
var GenericPlatformAnalyzer = class {
|
|
2566
|
+
constructor(platform) {
|
|
2567
|
+
this.platform = platform;
|
|
2568
|
+
}
|
|
2569
|
+
platform;
|
|
2570
|
+
async analyze(_config, _options) {
|
|
2571
|
+
return {
|
|
2572
|
+
screens: [],
|
|
2573
|
+
navigation: { screens: {}, initialScreen: "", navigators: [] },
|
|
2574
|
+
analyzedFiles: 0
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
|
|
2579
|
+
// src/ast/jsx/web/classify.ts
|
|
2580
|
+
var VIEW_TAGS = /* @__PURE__ */ new Set([
|
|
2581
|
+
"div",
|
|
2582
|
+
"span",
|
|
2583
|
+
"section",
|
|
2584
|
+
"main",
|
|
2585
|
+
"article",
|
|
2586
|
+
"aside",
|
|
2587
|
+
"header",
|
|
2588
|
+
"footer",
|
|
2589
|
+
"nav",
|
|
2590
|
+
"fieldset",
|
|
2591
|
+
"form",
|
|
2592
|
+
"p"
|
|
2593
|
+
]);
|
|
2594
|
+
var LIST_TAGS = /* @__PURE__ */ new Set(["ul", "ol", "dl", "table", "tbody"]);
|
|
2595
|
+
var BUTTON_TAGS = /* @__PURE__ */ new Set(["button"]);
|
|
2596
|
+
var INPUT_TAGS = /* @__PURE__ */ new Set(["input", "textarea"]);
|
|
2597
|
+
var MODAL_TAGS = /* @__PURE__ */ new Set(["dialog"]);
|
|
2598
|
+
var BUTTON_COMPONENTS2 = /* @__PURE__ */ new Set(["Button", "IconButton", "Link", "NavLink"]);
|
|
2599
|
+
var INPUT_COMPONENTS2 = /* @__PURE__ */ new Set(["Input", "TextField", "TextArea", "Textarea"]);
|
|
2600
|
+
var LIST_COMPONENTS2 = /* @__PURE__ */ new Set(["List", "Table", "DataGrid", "DataTable"]);
|
|
2601
|
+
var MODAL_COMPONENTS2 = /* @__PURE__ */ new Set(["Modal", "Dialog", "Drawer", "Popover", "BottomSheet"]);
|
|
2602
|
+
var ARIA_ROLE_TO_SEMANTIC = {
|
|
2603
|
+
button: "button",
|
|
2604
|
+
link: "button",
|
|
2605
|
+
tab: "button",
|
|
2606
|
+
menuitem: "button",
|
|
2607
|
+
textbox: "input",
|
|
2608
|
+
searchbox: "input",
|
|
2609
|
+
spinbutton: "input",
|
|
2610
|
+
listbox: "select",
|
|
2611
|
+
combobox: "select",
|
|
2612
|
+
radiogroup: "select",
|
|
2613
|
+
radio: "select",
|
|
2614
|
+
option: "select",
|
|
2615
|
+
checkbox: "toggle",
|
|
2616
|
+
switch: "toggle",
|
|
2617
|
+
dialog: "modal",
|
|
2618
|
+
alertdialog: "modal",
|
|
2619
|
+
list: "list",
|
|
2620
|
+
table: "list",
|
|
2621
|
+
grid: "list",
|
|
2622
|
+
form: "view"
|
|
2623
|
+
};
|
|
2624
|
+
var INPUT_TYPE_TO_SEMANTIC = {
|
|
2625
|
+
checkbox: "toggle",
|
|
2626
|
+
radio: "select",
|
|
2627
|
+
date: "date",
|
|
2628
|
+
"datetime-local": "date",
|
|
2629
|
+
month: "date",
|
|
2630
|
+
week: "date",
|
|
2631
|
+
time: "date",
|
|
2632
|
+
submit: "button",
|
|
2633
|
+
button: "button",
|
|
2634
|
+
reset: "button",
|
|
2635
|
+
image: "button",
|
|
2636
|
+
hidden: "custom",
|
|
2637
|
+
range: "input",
|
|
2638
|
+
file: "input",
|
|
2639
|
+
color: "input"
|
|
2640
|
+
};
|
|
2641
|
+
function classifyWebJsxComponent(name, element) {
|
|
2642
|
+
if (element) {
|
|
2643
|
+
const role = getStringAttr(element, "role");
|
|
2644
|
+
if (role && ARIA_ROLE_TO_SEMANTIC[role]) return ARIA_ROLE_TO_SEMANTIC[role];
|
|
2645
|
+
}
|
|
2646
|
+
if (/^[a-z]/.test(name)) {
|
|
2647
|
+
if (name === "input") {
|
|
2648
|
+
const type = element ? getStringAttr(element, "type") : void 0;
|
|
2649
|
+
if (type && INPUT_TYPE_TO_SEMANTIC[type]) return INPUT_TYPE_TO_SEMANTIC[type];
|
|
2650
|
+
return "input";
|
|
2651
|
+
}
|
|
2652
|
+
if (INPUT_TAGS.has(name)) return "input";
|
|
2653
|
+
if (name === "select") return "select";
|
|
2654
|
+
if (BUTTON_TAGS.has(name)) return "button";
|
|
2655
|
+
if (name === "a") {
|
|
2656
|
+
if (element && (hasJsxAttribute(element, "href") || hasJsxAttribute(element, "onClick"))) {
|
|
2657
|
+
return "button";
|
|
2658
|
+
}
|
|
2659
|
+
return "view";
|
|
2660
|
+
}
|
|
2661
|
+
if (LIST_TAGS.has(name)) return "list";
|
|
2662
|
+
if (MODAL_TAGS.has(name)) return "modal";
|
|
2663
|
+
if (VIEW_TAGS.has(name)) return "view";
|
|
2664
|
+
return "custom";
|
|
2665
|
+
}
|
|
2666
|
+
if (LIST_COMPONENTS2.has(name)) return "list";
|
|
2667
|
+
if (MODAL_COMPONENTS2.has(name)) return "modal";
|
|
2668
|
+
if (/date/i.test(name)) return "date";
|
|
2669
|
+
if (/(select|picker|dropdown|radio)/i.test(name)) return "select";
|
|
2670
|
+
if (/(checkbox|switch|toggle)/i.test(name)) return "toggle";
|
|
2671
|
+
if (INPUT_COMPONENTS2.has(name)) return "input";
|
|
2672
|
+
if (BUTTON_COMPONENTS2.has(name)) return "button";
|
|
2673
|
+
if (element) {
|
|
2674
|
+
const hasOptions = hasJsxAttribute(element, "options");
|
|
2675
|
+
const hasValue = hasJsxAttribute(element, "value");
|
|
2676
|
+
const hasChecked = hasJsxAttribute(element, "checked") || hasJsxAttribute(element, "selected");
|
|
2677
|
+
const hasOnChange = hasJsxAttribute(element, "onChange") || hasJsxAttribute(element, "onValueChange");
|
|
2678
|
+
const hasOnClick = hasJsxAttribute(element, "onClick");
|
|
2679
|
+
if (hasOptions && (hasValue || hasOnChange)) return "select";
|
|
2680
|
+
if (hasChecked && hasOnChange) return "toggle";
|
|
2681
|
+
if (hasOnChange && (hasJsxAttribute(element, "label") || hasJsxAttribute(element, "placeholder"))) {
|
|
2682
|
+
return "input";
|
|
2683
|
+
}
|
|
2684
|
+
if (hasOnClick) return "button";
|
|
2685
|
+
if (getStringAttr(element, "open") || getExpressionIdentifierAttr(element, "open") || getExpressionIdentifierAttr(element, "isOpen")) {
|
|
2686
|
+
return "modal";
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
return "custom";
|
|
2690
|
+
}
|
|
2691
|
+
function getJsxElementName(openingElement) {
|
|
2692
|
+
return getJsxName(openingElement.name);
|
|
2693
|
+
}
|
|
2694
|
+
function getJsxName(name) {
|
|
2695
|
+
if (BabelTypes.isJSXIdentifier(name)) return name.name;
|
|
2696
|
+
if (BabelTypes.isJSXNamespacedName(name)) return `${name.namespace.name}:${name.name.name}`;
|
|
2697
|
+
if (BabelTypes.isJSXMemberExpression(name)) {
|
|
2698
|
+
const objectName = getJsxName(name.object);
|
|
2699
|
+
const propertyName = BabelTypes.isJSXIdentifier(name.property) ? name.property.name : null;
|
|
2700
|
+
return objectName && propertyName ? `${objectName}.${propertyName}` : null;
|
|
2701
|
+
}
|
|
2702
|
+
return null;
|
|
2703
|
+
}
|
|
2704
|
+
var ROUTERISH_OBJECTS = /^(router|history|navigation)$/;
|
|
2705
|
+
function staticRoutePath(node) {
|
|
2706
|
+
if (!node) return void 0;
|
|
2707
|
+
if (BabelTypes.isStringLiteral(node)) return node.value;
|
|
2708
|
+
if (BabelTypes.isTemplateLiteral(node)) {
|
|
2709
|
+
let path9 = "";
|
|
2710
|
+
node.quasis.forEach((quasi, index) => {
|
|
2711
|
+
path9 += quasi.value.cooked ?? quasi.value.raw;
|
|
2712
|
+
const expr = node.expressions[index];
|
|
2713
|
+
if (expr) path9 += `:${paramNameOf(expr)}`;
|
|
2714
|
+
});
|
|
2715
|
+
return path9;
|
|
2716
|
+
}
|
|
2717
|
+
return void 0;
|
|
2718
|
+
}
|
|
2719
|
+
function paramNameOf(expr) {
|
|
2720
|
+
if (BabelTypes.isIdentifier(expr)) return expr.name;
|
|
2721
|
+
if (BabelTypes.isMemberExpression(expr) && BabelTypes.isIdentifier(expr.property)) return expr.property.name;
|
|
2722
|
+
return "param";
|
|
2723
|
+
}
|
|
2724
|
+
function extractWebNavigationCalls(ast) {
|
|
2725
|
+
const calls = [];
|
|
2726
|
+
const inspect = (node) => {
|
|
2727
|
+
if (BabelTypes.isCallExpression(node)) {
|
|
2728
|
+
if (BabelTypes.isIdentifier(node.callee) && node.callee.name === "navigate") {
|
|
2729
|
+
const first = node.arguments[0];
|
|
2730
|
+
const targetPath = staticRoutePath(first);
|
|
2731
|
+
if (targetPath !== void 0) {
|
|
2732
|
+
calls.push({
|
|
2733
|
+
method: hasReplaceOption(node.arguments[1]) ? "replace" : "navigate",
|
|
2734
|
+
targetPath
|
|
2735
|
+
});
|
|
2736
|
+
} else if (BabelTypes.isNumericLiteral(first) || BabelTypes.isUnaryExpression(first) && first.operator === "-") {
|
|
2737
|
+
calls.push({ method: "goBack" });
|
|
2738
|
+
}
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
if (BabelTypes.isMemberExpression(node.callee) && BabelTypes.isIdentifier(node.callee.object) && ROUTERISH_OBJECTS.test(node.callee.object.name) && BabelTypes.isIdentifier(node.callee.property)) {
|
|
2742
|
+
const method = node.callee.property.name;
|
|
2743
|
+
const targetPath = staticRoutePath(node.arguments[0]);
|
|
2744
|
+
if ((method === "push" || method === "navigate") && targetPath !== void 0) {
|
|
2745
|
+
calls.push({ method: "navigate", targetPath });
|
|
2746
|
+
} else if (method === "replace" && targetPath !== void 0) {
|
|
2747
|
+
calls.push({ method: "replace", targetPath });
|
|
2748
|
+
} else if (method === "back" || method === "goBack") {
|
|
2749
|
+
calls.push({ method: "goBack" });
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
if (BabelTypes.isAssignmentExpression(node) && BabelTypes.isMemberExpression(node.left) && BabelTypes.isIdentifier(node.left.property) && node.left.property.name === "href" && isLocationExpression(node.left.object) && BabelTypes.isStringLiteral(node.right) && node.right.value.startsWith("/")) {
|
|
2755
|
+
calls.push({ method: "navigate", targetPath: node.right.value });
|
|
2756
|
+
}
|
|
2757
|
+
};
|
|
2758
|
+
traverse4(ast, {
|
|
2759
|
+
noScope: !BabelTypes.isFile(ast),
|
|
2760
|
+
enter: (nodePath) => inspect(nodePath.node)
|
|
2761
|
+
});
|
|
2762
|
+
return calls;
|
|
2763
|
+
}
|
|
2764
|
+
function hasReplaceOption(arg) {
|
|
2765
|
+
if (!arg || !BabelTypes.isObjectExpression(arg)) return false;
|
|
2766
|
+
return arg.properties.some(
|
|
2767
|
+
(prop) => BabelTypes.isObjectProperty(prop) && BabelTypes.isIdentifier(prop.key) && prop.key.name === "replace" && BabelTypes.isBooleanLiteral(prop.value) && prop.value.value === true
|
|
2768
|
+
);
|
|
2769
|
+
}
|
|
2770
|
+
function isLocationExpression(node) {
|
|
2771
|
+
if (BabelTypes.isIdentifier(node)) return node.name === "location";
|
|
2772
|
+
return BabelTypes.isMemberExpression(node) && BabelTypes.isIdentifier(node.object) && node.object.name === "window" && BabelTypes.isIdentifier(node.property) && node.property.name === "location";
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
// src/analyzers/web/WebScreenAnalyzer.ts
|
|
2776
|
+
var DEFAULT_WEB_SCREEN_PATTERNS = [
|
|
2777
|
+
"**/pages/**/*.{ts,tsx,js,jsx}",
|
|
2778
|
+
"**/routes/**/*.{ts,tsx,js,jsx}",
|
|
2779
|
+
"**/views/**/*.{ts,tsx,js,jsx}",
|
|
2780
|
+
"**/app/**/*.{ts,tsx,js,jsx}",
|
|
2781
|
+
"**/*Page.{ts,tsx,js,jsx}",
|
|
2782
|
+
"**/*Screen.{ts,tsx,js,jsx}",
|
|
2783
|
+
"**/*View.{ts,tsx,js,jsx}"
|
|
2784
|
+
];
|
|
2785
|
+
var DESTRUCTIVE_VERB2 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
|
|
2786
|
+
var LISTISH_TAGS = /* @__PURE__ */ new Set(["ul", "ol", "dl", "table", "tbody"]);
|
|
2787
|
+
var WebScreenAnalyzer = class {
|
|
2788
|
+
config;
|
|
2789
|
+
verbose = process.env.VERBOSE === "true";
|
|
2790
|
+
screenPatterns;
|
|
2791
|
+
constructor(config, options) {
|
|
2792
|
+
this.config = config;
|
|
2793
|
+
this.screenPatterns = options?.screenPatterns ?? DEFAULT_WEB_SCREEN_PATTERNS;
|
|
2794
|
+
}
|
|
2795
|
+
async analyze() {
|
|
2796
|
+
const {
|
|
2797
|
+
include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
|
|
2798
|
+
exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
2799
|
+
} = this.config;
|
|
2800
|
+
const files = (await glob(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
|
|
2801
|
+
(file) => !file.endsWith(".d.ts")
|
|
2802
|
+
);
|
|
2803
|
+
const patternMatches = await glob(this.screenPatterns, {
|
|
2804
|
+
cwd: this.config.rootDir,
|
|
2805
|
+
ignore: exclude
|
|
2806
|
+
});
|
|
2807
|
+
const patternSet = new Set(patternMatches.map((f) => path8__default.resolve(this.config.rootDir, f)));
|
|
2808
|
+
const candidates = [];
|
|
2809
|
+
for (const file of files) {
|
|
2810
|
+
const filePath = path8__default.resolve(this.config.rootDir, file);
|
|
2811
|
+
try {
|
|
2812
|
+
const candidate = await this.analyzeFile(filePath);
|
|
2813
|
+
if (!candidate) continue;
|
|
2814
|
+
candidate.matchesScreenPattern = patternSet.has(filePath);
|
|
2815
|
+
candidates.push(candidate);
|
|
2816
|
+
if (this.verbose) {
|
|
2817
|
+
console.log(`[WebScreenAnalyzer] \u2713 Analyzed: ${candidate.descriptor.name} (${file})`);
|
|
2818
|
+
}
|
|
2819
|
+
} catch (error) {
|
|
2820
|
+
if (this.verbose) {
|
|
2821
|
+
console.warn(
|
|
2822
|
+
`[WebScreenAnalyzer] Failed to parse ${file}:`,
|
|
2823
|
+
error instanceof Error ? error.message : error
|
|
2824
|
+
);
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
return { candidates, analyzedFiles: files.length };
|
|
2829
|
+
}
|
|
2830
|
+
async analyzeFile(filePath) {
|
|
2831
|
+
const source = await fs.readFile(filePath, "utf-8");
|
|
2832
|
+
const ast = parseSource(source, this.config.parserPlugins);
|
|
2833
|
+
const registerScreenMeta = this.extractRegisterScreenMetadata(ast);
|
|
2834
|
+
const componentName = this.extractComponentName(ast);
|
|
2835
|
+
const labelsByHtmlFor = this.collectHtmlForLabels(ast);
|
|
2836
|
+
const handlerBehaviors = this.collectWebHandlerBehaviors(ast);
|
|
2837
|
+
const forms = this.mergeForms(
|
|
2838
|
+
registerScreenMeta?.forms ?? [],
|
|
2839
|
+
this.extractForms(ast, labelsByHtmlFor)
|
|
2840
|
+
);
|
|
2841
|
+
const actions = this.extractActions(ast, registerScreenMeta, handlerBehaviors);
|
|
2842
|
+
const components = this.extractComponents(ast);
|
|
2843
|
+
const collections = this.extractCollections(ast);
|
|
2844
|
+
const navigationTargets = this.extractNavigationTargets(ast);
|
|
2845
|
+
const permissionsFromJsDoc = extractPermissionsFromJsDoc(source);
|
|
2846
|
+
const destructiveTags = extractDestructiveJsDocTargets(source);
|
|
2847
|
+
if (destructiveTags) {
|
|
2848
|
+
for (const action of actions) {
|
|
2849
|
+
if (destructiveTags === "*" || destructiveTags.has(action.id)) {
|
|
2850
|
+
action.destructive = true;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
const name = registerScreenMeta?.name || componentName || path8__default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
|
|
2855
|
+
const descriptor = {
|
|
2856
|
+
name,
|
|
2857
|
+
filePath,
|
|
2858
|
+
title: registerScreenMeta?.title,
|
|
2859
|
+
description: registerScreenMeta?.description,
|
|
2860
|
+
components,
|
|
2861
|
+
forms,
|
|
2862
|
+
actions,
|
|
2863
|
+
navigationTargets,
|
|
2864
|
+
...collections.length > 0 ? { collections } : {},
|
|
2865
|
+
...registerScreenMeta?.suggestedPrompts && registerScreenMeta.suggestedPrompts.length > 0 ? { suggestedPrompts: registerScreenMeta.suggestedPrompts } : {},
|
|
2866
|
+
...permissionsFromJsDoc ? {
|
|
2867
|
+
permissions: permissionsFromJsDoc,
|
|
2868
|
+
...permissionsFromJsDoc.isPii ? { isPii: true } : {}
|
|
2869
|
+
} : {}
|
|
2870
|
+
};
|
|
2871
|
+
return {
|
|
2872
|
+
descriptor,
|
|
2873
|
+
hasRegisterScreen: registerScreenMeta !== null || this.detectRegisterScreenCall(ast),
|
|
2874
|
+
matchesScreenPattern: false
|
|
2875
|
+
};
|
|
2876
|
+
}
|
|
2877
|
+
// ── registerScreen ────────────────────────────────────────────────
|
|
2878
|
+
detectRegisterScreenCall(ast) {
|
|
2879
|
+
let found = false;
|
|
2880
|
+
traverse4(ast, {
|
|
2881
|
+
CallExpression: (nodePath) => {
|
|
2882
|
+
if (found) return;
|
|
2883
|
+
if (isRegisterScreenCallee(nodePath.node.callee)) {
|
|
2884
|
+
found = true;
|
|
2885
|
+
nodePath.stop();
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
});
|
|
2889
|
+
return found;
|
|
2890
|
+
}
|
|
2891
|
+
extractRegisterScreenMetadata(ast) {
|
|
2892
|
+
let plain = null;
|
|
2893
|
+
traverse4(ast, {
|
|
2894
|
+
CallExpression: (nodePath) => {
|
|
2895
|
+
if (!isRegisterScreenCallee(nodePath.node.callee)) return;
|
|
2896
|
+
const arg = nodePath.node.arguments[0];
|
|
2897
|
+
if (BabelTypes.isObjectExpression(arg)) {
|
|
2898
|
+
plain = literalToPlain(arg);
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
});
|
|
2902
|
+
if (!plain) return null;
|
|
2903
|
+
const meta = plain;
|
|
2904
|
+
const result = {
|
|
2905
|
+
name: typeof meta.name === "string" ? meta.name : "",
|
|
2906
|
+
actions: [],
|
|
2907
|
+
forms: [],
|
|
2908
|
+
navigationTargets: [],
|
|
2909
|
+
components: []
|
|
2910
|
+
};
|
|
2911
|
+
if (typeof meta.title === "string") result.title = meta.title;
|
|
2912
|
+
if (typeof meta.description === "string") result.description = meta.description;
|
|
2913
|
+
if (Array.isArray(meta.suggestedPrompts)) {
|
|
2914
|
+
const prompts = meta.suggestedPrompts.filter((p) => typeof p === "string").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
2915
|
+
if (prompts.length > 0) result.suggestedPrompts = prompts;
|
|
2916
|
+
}
|
|
2917
|
+
if (Array.isArray(meta.actions)) {
|
|
2918
|
+
result.actions = meta.actions.filter((a) => typeof a === "object" && a !== null).filter((a) => typeof a.id === "string" && a.id.length > 0).map((a) => ({ type: "custom", ...a }));
|
|
2919
|
+
}
|
|
2920
|
+
if (Array.isArray(meta.fields)) {
|
|
2921
|
+
const fields = meta.fields.filter((f) => typeof f === "object" && f !== null).filter((f) => typeof f.id === "string" && f.id.length > 0).map(
|
|
2922
|
+
(f) => ({
|
|
2923
|
+
name: f.id,
|
|
2924
|
+
type: typeof f.type === "string" ? f.type : "text",
|
|
2925
|
+
required: f.required === true,
|
|
2926
|
+
...typeof f.label === "string" ? { label: f.label } : {},
|
|
2927
|
+
...typeof f.placeholder === "string" ? { placeholder: f.placeholder } : {},
|
|
2928
|
+
...f.defaultValue !== void 0 ? { defaultValue: f.defaultValue } : {},
|
|
2929
|
+
...Array.isArray(f.options) ? { options: f.options } : {}
|
|
2930
|
+
})
|
|
2931
|
+
);
|
|
2932
|
+
if (fields.length > 0) result.forms = [{ id: "default", fields }];
|
|
2933
|
+
}
|
|
2934
|
+
return result;
|
|
2935
|
+
}
|
|
2936
|
+
// ── Component name / structure ────────────────────────────────────
|
|
2937
|
+
/** Default-exported component name, else the first exported capitalized function. */
|
|
2938
|
+
extractComponentName(ast) {
|
|
2939
|
+
let defaultName = "";
|
|
2940
|
+
let firstExported = "";
|
|
2941
|
+
traverse4(ast, {
|
|
2942
|
+
ExportDefaultDeclaration: (nodePath) => {
|
|
2943
|
+
const declaration = nodePath.node.declaration;
|
|
2944
|
+
if (BabelTypes.isFunctionDeclaration(declaration) && declaration.id?.name) {
|
|
2945
|
+
defaultName = declaration.id.name;
|
|
2946
|
+
} else if (BabelTypes.isIdentifier(declaration)) {
|
|
2947
|
+
defaultName = declaration.name;
|
|
2948
|
+
}
|
|
2949
|
+
},
|
|
2950
|
+
ExportNamedDeclaration: (nodePath) => {
|
|
2951
|
+
if (firstExported) return;
|
|
2952
|
+
const declaration = nodePath.node.declaration;
|
|
2953
|
+
if (BabelTypes.isFunctionDeclaration(declaration) && declaration.id && /^[A-Z]/.test(declaration.id.name)) {
|
|
2954
|
+
firstExported = declaration.id.name;
|
|
2955
|
+
} else if (BabelTypes.isVariableDeclaration(declaration)) {
|
|
2956
|
+
for (const declarator of declaration.declarations) {
|
|
2957
|
+
if (BabelTypes.isIdentifier(declarator.id) && /^[A-Z]/.test(declarator.id.name) && (BabelTypes.isArrowFunctionExpression(declarator.init) || BabelTypes.isFunctionExpression(declarator.init))) {
|
|
2958
|
+
firstExported = declarator.id.name;
|
|
2959
|
+
break;
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
});
|
|
2965
|
+
return defaultName || firstExported;
|
|
2966
|
+
}
|
|
2967
|
+
extractComponents(ast) {
|
|
2968
|
+
const components = [];
|
|
2969
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2970
|
+
traverse4(ast, {
|
|
2971
|
+
JSXOpeningElement: (nodePath) => {
|
|
2972
|
+
const element = nodePath.node;
|
|
2973
|
+
const name = getJsxElementName(element);
|
|
2974
|
+
if (!name || seen.has(name)) return;
|
|
2975
|
+
seen.add(name);
|
|
2976
|
+
const role = classifyWebJsxComponent(name, element);
|
|
2977
|
+
const type = role === "select" || role === "toggle" || role === "date" ? "input" : role === "input" || role === "button" || role === "list" || role === "modal" || role === "view" ? role : "custom";
|
|
2978
|
+
const component = { name, type };
|
|
2979
|
+
const testId = getStringAttr(element, "data-testid") ?? getStringAttr(element, "testID");
|
|
2980
|
+
const ariaLabel = getStringAttr(element, "aria-label");
|
|
2981
|
+
if (testId) component.testID = testId;
|
|
2982
|
+
if (ariaLabel) component.accessibilityLabel = ariaLabel;
|
|
2983
|
+
components.push(component);
|
|
2984
|
+
}
|
|
2985
|
+
});
|
|
2986
|
+
return components;
|
|
2987
|
+
}
|
|
2988
|
+
// ── <label htmlFor> association ───────────────────────────────────
|
|
2989
|
+
collectHtmlForLabels(ast) {
|
|
2990
|
+
const labels = /* @__PURE__ */ new Map();
|
|
2991
|
+
traverse4(ast, {
|
|
2992
|
+
JSXElement: (nodePath) => {
|
|
2993
|
+
const element = nodePath.node;
|
|
2994
|
+
if (getJsxElementName(element.openingElement) !== "label") return;
|
|
2995
|
+
const htmlFor = getStringAttr(element.openingElement, "htmlFor");
|
|
2996
|
+
if (!htmlFor) return;
|
|
2997
|
+
const text = jsxTextContent(element);
|
|
2998
|
+
if (text) labels.set(htmlFor, text);
|
|
2999
|
+
}
|
|
3000
|
+
});
|
|
3001
|
+
return labels;
|
|
3002
|
+
}
|
|
3003
|
+
// ── Forms ─────────────────────────────────────────────────────────
|
|
3004
|
+
extractForms(ast, labelsByHtmlFor) {
|
|
3005
|
+
const formBuckets = /* @__PURE__ */ new Map();
|
|
3006
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
3007
|
+
let formCount = 0;
|
|
3008
|
+
const uniqueId = (preferred) => {
|
|
3009
|
+
if (!usedIds.has(preferred)) {
|
|
3010
|
+
usedIds.add(preferred);
|
|
3011
|
+
return preferred;
|
|
3012
|
+
}
|
|
3013
|
+
let suffix = 2;
|
|
3014
|
+
while (usedIds.has(`${preferred}-${suffix}`)) suffix += 1;
|
|
3015
|
+
const id = `${preferred}-${suffix}`;
|
|
3016
|
+
usedIds.add(id);
|
|
3017
|
+
return id;
|
|
3018
|
+
};
|
|
3019
|
+
const bucketFor = (formElement) => {
|
|
3020
|
+
let bucket = formBuckets.get(formElement);
|
|
3021
|
+
if (bucket) return bucket;
|
|
3022
|
+
formCount += 1;
|
|
3023
|
+
let preferred = "default";
|
|
3024
|
+
let submitAction;
|
|
3025
|
+
if (formElement) {
|
|
3026
|
+
const opening = formElement.openingElement;
|
|
3027
|
+
preferred = getStringAttr(opening, "id") ?? getStringAttr(opening, "name") ?? getStringAttr(opening, "data-testid") ?? (formCount === 1 ? "default" : `form-${formCount}`);
|
|
3028
|
+
submitAction = this.handlerNameFromAttr(opening, "onSubmit");
|
|
3029
|
+
}
|
|
3030
|
+
bucket = { id: uniqueId(preferred), fields: /* @__PURE__ */ new Map(), submitAction };
|
|
3031
|
+
formBuckets.set(formElement, bucket);
|
|
3032
|
+
return bucket;
|
|
3033
|
+
};
|
|
3034
|
+
traverse4(ast, {
|
|
3035
|
+
JSXElement: (nodePath) => {
|
|
3036
|
+
const element = nodePath.node;
|
|
3037
|
+
const name = getJsxElementName(element.openingElement);
|
|
3038
|
+
if (!name) return;
|
|
3039
|
+
const role = classifyWebJsxComponent(name, element.openingElement);
|
|
3040
|
+
if (!["input", "select", "toggle", "date"].includes(role)) return;
|
|
3041
|
+
if (name === "option") return;
|
|
3042
|
+
const field = this.extractField(element, role, labelsByHtmlFor);
|
|
3043
|
+
if (!field.name) return;
|
|
3044
|
+
const formParent = nodePath.findParent(
|
|
3045
|
+
(p) => p.isJSXElement() && (getJsxElementName(p.node.openingElement) === "form" || getStringAttr(p.node.openingElement, "role") === "form")
|
|
3046
|
+
);
|
|
3047
|
+
const bucket = bucketFor(formParent ? formParent.node : null);
|
|
3048
|
+
if (!bucket.fields.has(field.name)) bucket.fields.set(field.name, field);
|
|
3049
|
+
}
|
|
3050
|
+
});
|
|
3051
|
+
traverse4(ast, {
|
|
3052
|
+
JSXElement: (nodePath) => {
|
|
3053
|
+
const element = nodePath.node;
|
|
3054
|
+
const name = getJsxElementName(element.openingElement);
|
|
3055
|
+
if (!name) return;
|
|
3056
|
+
if (!this.isSubmitElement(name, element.openingElement)) return;
|
|
3057
|
+
const formParent = nodePath.findParent(
|
|
3058
|
+
(p) => p.isJSXElement() && getJsxElementName(p.node.openingElement) === "form"
|
|
3059
|
+
);
|
|
3060
|
+
if (!formParent) return;
|
|
3061
|
+
const bucket = formBuckets.get(formParent.node);
|
|
3062
|
+
if (!bucket) return;
|
|
3063
|
+
const handler = this.handlerNameFromAttr(element.openingElement, "onClick");
|
|
3064
|
+
if (!bucket.submitAction && handler) bucket.submitAction = handler;
|
|
3065
|
+
}
|
|
3066
|
+
});
|
|
3067
|
+
return Array.from(formBuckets.values()).filter((bucket) => bucket.fields.size > 0).map((bucket) => ({
|
|
3068
|
+
id: bucket.id,
|
|
3069
|
+
fields: Array.from(bucket.fields.values()),
|
|
3070
|
+
...bucket.submitAction && bucket.submitAction !== "anonymous" ? { submitAction: bucket.submitAction } : {}
|
|
3071
|
+
}));
|
|
3072
|
+
}
|
|
3073
|
+
isSubmitElement(name, opening) {
|
|
3074
|
+
const type = getStringAttr(opening, "type");
|
|
3075
|
+
if (name === "button") return type === "submit" || type === void 0;
|
|
3076
|
+
if (name === "input") return type === "submit";
|
|
3077
|
+
return false;
|
|
3078
|
+
}
|
|
3079
|
+
extractField(element, role, labelsByHtmlFor) {
|
|
3080
|
+
const opening = element.openingElement;
|
|
3081
|
+
const componentName = getJsxElementName(opening) ?? void 0;
|
|
3082
|
+
const field = {
|
|
3083
|
+
name: "",
|
|
3084
|
+
type: "text",
|
|
3085
|
+
required: false,
|
|
3086
|
+
sourceComponent: componentName
|
|
3087
|
+
};
|
|
3088
|
+
const appilotsId = getStringAttr(opening, "appilotsId");
|
|
3089
|
+
const dataTestId = getStringAttr(opening, "data-testid");
|
|
3090
|
+
const domId = getStringAttr(opening, "id");
|
|
3091
|
+
const nameAttr = getStringAttr(opening, "name");
|
|
3092
|
+
const ariaLabel = getStringAttr(opening, "aria-label");
|
|
3093
|
+
const placeholder = getStringAttr(opening, "placeholder");
|
|
3094
|
+
if (placeholder) field.placeholder = placeholder;
|
|
3095
|
+
if (ariaLabel) field.label = ariaLabel;
|
|
3096
|
+
if (domId && labelsByHtmlFor.has(domId)) field.label = labelsByHtmlFor.get(domId);
|
|
3097
|
+
if (appilotsId) {
|
|
3098
|
+
field.name = appilotsId;
|
|
3099
|
+
field.locator = mergeLocator(field.locator, {
|
|
3100
|
+
id: appilotsId,
|
|
3101
|
+
appilotsId,
|
|
3102
|
+
source: "appilotsId"
|
|
3103
|
+
});
|
|
3104
|
+
}
|
|
3105
|
+
if (dataTestId) {
|
|
3106
|
+
if (!field.name) field.name = dataTestId.replace(/^(input-|field-|txt-)/, "");
|
|
3107
|
+
field.locator = mergeLocator(field.locator, {
|
|
3108
|
+
...field.locator?.id ? {} : { id: dataTestId },
|
|
3109
|
+
testID: dataTestId,
|
|
3110
|
+
source: field.locator?.source ?? "data-testid"
|
|
3111
|
+
});
|
|
3112
|
+
}
|
|
3113
|
+
if (nameAttr && !field.name) field.name = nameAttr;
|
|
3114
|
+
if (domId) {
|
|
3115
|
+
if (!field.name) field.name = domId;
|
|
3116
|
+
field.locator = mergeLocator(field.locator, {
|
|
3117
|
+
...field.locator?.id ? {} : { id: domId },
|
|
3118
|
+
source: field.locator?.source ?? "id"
|
|
3119
|
+
});
|
|
3120
|
+
}
|
|
3121
|
+
if (ariaLabel) {
|
|
3122
|
+
field.locator = mergeLocator(field.locator, {
|
|
3123
|
+
...field.locator?.id ? {} : { id: slugify(ariaLabel) },
|
|
3124
|
+
accessibilityLabel: ariaLabel,
|
|
3125
|
+
source: field.locator?.source ?? "aria-label"
|
|
3126
|
+
});
|
|
3127
|
+
}
|
|
3128
|
+
for (const attr of opening.attributes) {
|
|
3129
|
+
if (!BabelTypes.isJSXAttribute(attr) || !BabelTypes.isJSXIdentifier(attr.name)) continue;
|
|
3130
|
+
const attrName = attr.name.name;
|
|
3131
|
+
if (attrName === "required") {
|
|
3132
|
+
if (attr.value === null) field.required = true;
|
|
3133
|
+
else if (BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isBooleanLiteral(attr.value.expression)) {
|
|
3134
|
+
field.required = attr.value.expression.value;
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
if ((attrName === "value" || attrName === "checked") && attr.value && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
|
|
3138
|
+
field.valueBinding = attr.value.expression.name;
|
|
3139
|
+
if (!field.name) field.name = attr.value.expression.name;
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
if (getStringAttr(opening, "aria-required") === "true") field.required = true;
|
|
3143
|
+
if (role === "select") field.type = "select";
|
|
3144
|
+
else if (role === "toggle") field.type = "toggle";
|
|
3145
|
+
else if (role === "date") field.type = "date";
|
|
3146
|
+
else {
|
|
3147
|
+
field.type = this.inferInputType(opening, field);
|
|
3148
|
+
}
|
|
3149
|
+
if (componentName === "select") {
|
|
3150
|
+
const options = this.extractSelectOptions(element);
|
|
3151
|
+
if (options.length > 0) field.options = options;
|
|
3152
|
+
}
|
|
3153
|
+
if (!field.name || isWeakInferredFieldName(field.name)) {
|
|
3154
|
+
const labelish = field.label ?? field.placeholder;
|
|
3155
|
+
if (labelish) field.name = slugify(labelish);
|
|
3156
|
+
}
|
|
3157
|
+
if (!field.locator && field.name) {
|
|
3158
|
+
field.locator = { id: field.name, label: field.label, source: "inferred" };
|
|
3159
|
+
} else if (field.locator && !field.locator.id && field.name) {
|
|
3160
|
+
field.locator = mergeLocator(field.locator, {
|
|
3161
|
+
id: field.name,
|
|
3162
|
+
label: field.label,
|
|
3163
|
+
source: field.locator.source ?? "inferred"
|
|
3164
|
+
});
|
|
3165
|
+
}
|
|
3166
|
+
return field;
|
|
3167
|
+
}
|
|
3168
|
+
inferInputType(opening, field) {
|
|
3169
|
+
const type = getStringAttr(opening, "type");
|
|
3170
|
+
if (type === "email") return "email";
|
|
3171
|
+
if (type === "tel") return "phone";
|
|
3172
|
+
if (type === "number") return "number";
|
|
3173
|
+
if (type === "date" || type === "datetime-local" || type === "month" || type === "week")
|
|
3174
|
+
return "date";
|
|
3175
|
+
const inputMode = getStringAttr(opening, "inputMode") ?? getStringAttr(opening, "inputmode");
|
|
3176
|
+
if (inputMode === "email") return "email";
|
|
3177
|
+
if (inputMode === "tel") return "phone";
|
|
3178
|
+
if (inputMode === "numeric" || inputMode === "decimal") return "number";
|
|
3179
|
+
const combined = `${field.name} ${field.label ?? ""} ${field.placeholder ?? ""}`.toLowerCase();
|
|
3180
|
+
if (combined.includes("email")) return "email";
|
|
3181
|
+
if (combined.includes("phone") || combined.includes("tel")) return "phone";
|
|
3182
|
+
return "text";
|
|
3183
|
+
}
|
|
3184
|
+
extractSelectOptions(selectElement) {
|
|
3185
|
+
const options = [];
|
|
3186
|
+
for (const child of selectElement.children) {
|
|
3187
|
+
if (!BabelTypes.isJSXElement(child)) continue;
|
|
3188
|
+
if (getJsxElementName(child.openingElement) !== "option") continue;
|
|
3189
|
+
const value = getStringAttr(child.openingElement, "value");
|
|
3190
|
+
const label = jsxTextContent(child) || value || "";
|
|
3191
|
+
if (label && value) options.push({ label, value });
|
|
3192
|
+
}
|
|
3193
|
+
return options;
|
|
3194
|
+
}
|
|
3195
|
+
mergeForms(primary, secondary) {
|
|
3196
|
+
const out = primary.map((form) => ({ ...form, fields: [...form.fields] }));
|
|
3197
|
+
for (const form of secondary) {
|
|
3198
|
+
const existing = out.find((candidate) => candidate.id === form.id);
|
|
3199
|
+
if (!existing) {
|
|
3200
|
+
out.push({ ...form, fields: [...form.fields] });
|
|
3201
|
+
continue;
|
|
3202
|
+
}
|
|
3203
|
+
for (const field of form.fields) {
|
|
3204
|
+
const existingField = existing.fields.find((candidate) => candidate.name === field.name);
|
|
3205
|
+
if (!existingField) {
|
|
3206
|
+
existing.fields.push(field);
|
|
3207
|
+
continue;
|
|
3208
|
+
}
|
|
3209
|
+
existingField.label = existingField.label ?? field.label;
|
|
3210
|
+
existingField.placeholder = existingField.placeholder ?? field.placeholder;
|
|
3211
|
+
existingField.options = existingField.options ?? field.options;
|
|
3212
|
+
existingField.locator = existingField.locator ?? field.locator;
|
|
3213
|
+
existingField.sourceComponent = existingField.sourceComponent ?? field.sourceComponent;
|
|
3214
|
+
existingField.valueBinding = existingField.valueBinding ?? field.valueBinding;
|
|
3215
|
+
existingField.required = existingField.required || field.required;
|
|
3216
|
+
if (existingField.type === "text" && field.type !== "text") existingField.type = field.type;
|
|
3217
|
+
}
|
|
3218
|
+
existing.submitAction = existing.submitAction ?? form.submitAction;
|
|
3219
|
+
}
|
|
3220
|
+
return out;
|
|
3221
|
+
}
|
|
3222
|
+
// ── Actions ───────────────────────────────────────────────────────
|
|
3223
|
+
extractActions(ast, registerScreenMeta, handlerBehaviors) {
|
|
3224
|
+
const actions = [...registerScreenMeta?.actions ?? []];
|
|
3225
|
+
const actionIds = new Set(actions.map((a) => a.id));
|
|
3226
|
+
const actionLabels = new Map(
|
|
3227
|
+
actions.filter((a) => a.label).map((a) => [normalizeLabel(a.label), a])
|
|
3228
|
+
);
|
|
3229
|
+
traverse4(ast, {
|
|
3230
|
+
JSXElement: (nodePath) => {
|
|
3231
|
+
const element = nodePath.node;
|
|
3232
|
+
const name = getJsxElementName(element.openingElement);
|
|
3233
|
+
if (!name) return;
|
|
3234
|
+
if (classifyWebJsxComponent(name, element.openingElement) !== "button") return;
|
|
3235
|
+
const action = this.extractActionFromElement(element, nodePath);
|
|
3236
|
+
if (!action) return;
|
|
3237
|
+
const existingByLabel = action.label ? actionLabels.get(normalizeLabel(action.label)) : void 0;
|
|
3238
|
+
if (existingByLabel) {
|
|
3239
|
+
this.mergeActionMetadata(existingByLabel, action);
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
if (action.id && !actionIds.has(action.id)) {
|
|
3243
|
+
actions.push(action);
|
|
3244
|
+
actionIds.add(action.id);
|
|
3245
|
+
if (action.label) actionLabels.set(normalizeLabel(action.label), action);
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
});
|
|
3249
|
+
this.enrichActionsFromHandlers(actions, handlerBehaviors);
|
|
3250
|
+
return actions;
|
|
3251
|
+
}
|
|
3252
|
+
extractActionFromElement(element, nodePath) {
|
|
3253
|
+
const opening = element.openingElement;
|
|
3254
|
+
const componentName = getJsxElementName(opening) ?? void 0;
|
|
3255
|
+
const action = { id: "", type: "custom", sourceComponent: componentName };
|
|
3256
|
+
const ariaLabel = getStringAttr(opening, "aria-label");
|
|
3257
|
+
const label = ariaLabel ?? jsxTextContent(element) ?? getStringAttr(opening, "value") ?? getStringAttr(opening, "title");
|
|
3258
|
+
if (label) action.label = label;
|
|
3259
|
+
if (ariaLabel) {
|
|
3260
|
+
action.locator = mergeLocator(action.locator, {
|
|
3261
|
+
accessibilityLabel: ariaLabel,
|
|
3262
|
+
source: "aria-label"
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
const appilotsId = getStringAttr(opening, "appilotsId");
|
|
3266
|
+
const dataTestId = getStringAttr(opening, "data-testid");
|
|
3267
|
+
const domId = getStringAttr(opening, "id");
|
|
3268
|
+
if (appilotsId) {
|
|
3269
|
+
action.id = appilotsId;
|
|
3270
|
+
action.locator = mergeLocator(action.locator, {
|
|
3271
|
+
id: appilotsId,
|
|
3272
|
+
appilotsId,
|
|
3273
|
+
source: "appilotsId"
|
|
3274
|
+
});
|
|
3275
|
+
} else if (dataTestId) {
|
|
3276
|
+
action.id = dataTestId;
|
|
3277
|
+
action.locator = mergeLocator(action.locator, {
|
|
3278
|
+
id: dataTestId,
|
|
3279
|
+
testID: dataTestId,
|
|
3280
|
+
source: "data-testid"
|
|
3281
|
+
});
|
|
3282
|
+
} else if (domId) {
|
|
3283
|
+
action.id = domId;
|
|
3284
|
+
action.locator = mergeLocator(action.locator, { id: domId, source: "id" });
|
|
3285
|
+
} else if (action.label) {
|
|
3286
|
+
action.id = slugify(action.label);
|
|
3287
|
+
}
|
|
3288
|
+
if (!action.id) return null;
|
|
3289
|
+
const to = routePathAttr(opening, "to") ?? routePathAttr(opening, "href");
|
|
3290
|
+
if (to && to.startsWith("/")) {
|
|
3291
|
+
action.type = "navigation";
|
|
3292
|
+
action.targetScreen = to;
|
|
3293
|
+
} else if (to && !to.startsWith("/") && !hasJsxAttribute(opening, "onClick")) {
|
|
3294
|
+
return null;
|
|
3295
|
+
}
|
|
3296
|
+
let handlerName;
|
|
3297
|
+
const onClickAttr = opening.attributes.find(
|
|
3298
|
+
(attr) => BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name) && attr.name.name === "onClick"
|
|
3299
|
+
);
|
|
3300
|
+
if (onClickAttr?.value && BabelTypes.isJSXExpressionContainer(onClickAttr.value)) {
|
|
3301
|
+
const expr = onClickAttr.value.expression;
|
|
3302
|
+
if (BabelTypes.isIdentifier(expr)) {
|
|
3303
|
+
handlerName = expr.name;
|
|
3304
|
+
action.handler = expr.name;
|
|
3305
|
+
} else if (!BabelTypes.isJSXEmptyExpression(expr)) {
|
|
3306
|
+
const inlineNavCalls = extractWebNavigationCalls(expr);
|
|
3307
|
+
const inlineNav = inlineNavCalls.find((call) => call.targetPath);
|
|
3308
|
+
if (inlineNav?.targetPath) {
|
|
3309
|
+
action.type = "navigation";
|
|
3310
|
+
action.targetScreen = inlineNav.targetPath;
|
|
3311
|
+
} else if (inlineNavCalls.some((call) => call.method === "goBack")) {
|
|
3312
|
+
action.type = "navigation";
|
|
3313
|
+
action.successSignal = {
|
|
3314
|
+
type: "goBack",
|
|
3315
|
+
description: "Action returns to the previous page"
|
|
3316
|
+
};
|
|
3317
|
+
action.appilotsInferred = {
|
|
3318
|
+
...action.appilotsInferred ?? {},
|
|
3319
|
+
expectedOutcome: "navigation"
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
const inlineHandler = firstCalledFunctionName(expr);
|
|
3323
|
+
if (inlineHandler) {
|
|
3324
|
+
handlerName = inlineHandler;
|
|
3325
|
+
action.handler = inlineHandler;
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
if (handlerName) {
|
|
3330
|
+
const lower = handlerName.toLowerCase();
|
|
3331
|
+
if (lower.includes("submit")) action.type = "submit";
|
|
3332
|
+
else if (lower.includes("navigate") && action.type === "custom") action.type = "navigation";
|
|
3333
|
+
}
|
|
3334
|
+
if (componentName && this.isSubmitElement(componentName, opening)) {
|
|
3335
|
+
const formParent = nodePath.findParent(
|
|
3336
|
+
(p) => p.isJSXElement() && getJsxElementName(p.node.openingElement) === "form"
|
|
3337
|
+
);
|
|
3338
|
+
if (formParent) {
|
|
3339
|
+
action.type = "submit";
|
|
3340
|
+
if (!action.handler) {
|
|
3341
|
+
const formHandler = this.handlerNameFromAttr(formParent.node.openingElement, "onSubmit");
|
|
3342
|
+
if (formHandler && formHandler !== "anonymous") {
|
|
3343
|
+
action.handler = formHandler;
|
|
3344
|
+
handlerName = formHandler;
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
if (hasJsxAttribute(opening, "destructive") || hasJsxAttribute(opening, "aria-destructive")) {
|
|
3350
|
+
const value = getStringAttr(opening, "destructive") ?? getStringAttr(opening, "aria-destructive");
|
|
3351
|
+
action.destructive = value !== "false";
|
|
3352
|
+
} else if (handlerName && DESTRUCTIVE_VERB2.test(handlerName) || DESTRUCTIVE_VERB2.test(action.id)) {
|
|
3353
|
+
action.destructive = true;
|
|
3354
|
+
}
|
|
3355
|
+
if (!action.locator && action.id) {
|
|
3356
|
+
action.locator = {
|
|
3357
|
+
id: action.id,
|
|
3358
|
+
label: action.label,
|
|
3359
|
+
source: action.label ? "label" : "inferred"
|
|
3360
|
+
};
|
|
3361
|
+
}
|
|
3362
|
+
return action;
|
|
3363
|
+
}
|
|
3364
|
+
mergeActionMetadata(target, source) {
|
|
3365
|
+
target.handler = target.handler ?? source.handler;
|
|
3366
|
+
target.targetScreen = target.targetScreen ?? source.targetScreen;
|
|
3367
|
+
target.description = target.description ?? source.description;
|
|
3368
|
+
target.locator = target.locator ?? source.locator;
|
|
3369
|
+
target.nativeConfirmationExpected = target.nativeConfirmationExpected || source.nativeConfirmationExpected || void 0;
|
|
3370
|
+
target.requiresConfirmation = target.requiresConfirmation || source.requiresConfirmation || void 0;
|
|
3371
|
+
target.destructive = target.destructive || source.destructive || void 0;
|
|
3372
|
+
target.effect = target.effect ?? source.effect;
|
|
3373
|
+
target.riskLevel = target.riskLevel ?? source.riskLevel;
|
|
3374
|
+
target.appilotsInferred = target.appilotsInferred ?? source.appilotsInferred;
|
|
3375
|
+
}
|
|
3376
|
+
enrichActionsFromHandlers(actions, behaviors) {
|
|
3377
|
+
for (const action of actions) {
|
|
3378
|
+
const behavior = action.handler ? behaviors.get(action.handler) : void 0;
|
|
3379
|
+
if (!behavior) continue;
|
|
3380
|
+
action.appilotsInferred = {
|
|
3381
|
+
...action.appilotsInferred ?? {},
|
|
3382
|
+
...behavior.base.appilotsInferred
|
|
3383
|
+
};
|
|
3384
|
+
if (behavior.nativeConfirmationExpected || behavior.base.nativeConfirmationExpected) {
|
|
3385
|
+
action.nativeConfirmationExpected = true;
|
|
3386
|
+
}
|
|
3387
|
+
if (behavior.targetPath && !action.targetScreen) {
|
|
3388
|
+
action.targetScreen = behavior.targetPath;
|
|
3389
|
+
if (action.type === "custom") action.type = "navigation";
|
|
3390
|
+
action.appilotsInferred = {
|
|
3391
|
+
...action.appilotsInferred ?? {},
|
|
3392
|
+
expectedOutcome: "navigation"
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
if (behavior.base.successSignal && !action.successSignal) {
|
|
3396
|
+
action.successSignal = behavior.base.successSignal;
|
|
3397
|
+
}
|
|
3398
|
+
if (behavior.base.failureSignal && !action.failureSignal) {
|
|
3399
|
+
action.failureSignal = behavior.base.failureSignal;
|
|
3400
|
+
}
|
|
3401
|
+
if (behavior.base.opensModal && !action.opensModal) {
|
|
3402
|
+
action.opensModal = behavior.base.opensModal;
|
|
3403
|
+
}
|
|
3404
|
+
if (behavior.base.destructive || action.destructive === true || action.requiresConfirmation === true || action.effect === "destructive" || action.riskLevel === "high") {
|
|
3405
|
+
action.destructive = true;
|
|
3406
|
+
action.effect = action.effect ?? "destructive";
|
|
3407
|
+
action.riskLevel = action.riskLevel ?? "high";
|
|
3408
|
+
action.requiresConfirmation = action.requiresConfirmation ?? true;
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
/**
|
|
3413
|
+
* Handler behavior via the shared, platform-neutral analyzer
|
|
3414
|
+
* (async/await, `.then`, state setters, toasts, destructive verbs)
|
|
3415
|
+
* plus the web-only signals: React Router navigation targets and
|
|
3416
|
+
* `window.confirm(...)` as the native confirmation dialog.
|
|
3417
|
+
*/
|
|
3418
|
+
collectWebHandlerBehaviors(ast) {
|
|
3419
|
+
const handlers = collectFunctions(ast);
|
|
3420
|
+
const out = /* @__PURE__ */ new Map();
|
|
3421
|
+
for (const [name, fn] of handlers) {
|
|
3422
|
+
const base = analyzeFunctionBehavior(name, fn, handlers);
|
|
3423
|
+
const navCalls = fn.body ? extractWebNavigationCalls(fn.body) : [];
|
|
3424
|
+
const firstNav = navCalls.find((call) => call.targetPath);
|
|
3425
|
+
const goesBack = navCalls.some((call) => call.method === "goBack");
|
|
3426
|
+
out.set(name, {
|
|
3427
|
+
base: {
|
|
3428
|
+
...base,
|
|
3429
|
+
...goesBack && !base.successSignal ? {
|
|
3430
|
+
successSignal: {
|
|
3431
|
+
type: "goBack",
|
|
3432
|
+
description: "Action returns to the previous page"
|
|
3433
|
+
}
|
|
3434
|
+
} : {}
|
|
3435
|
+
},
|
|
3436
|
+
targetPath: firstNav?.targetPath,
|
|
3437
|
+
nativeConfirmationExpected: fn.body ? containsWindowConfirm(fn.body) : false
|
|
3438
|
+
});
|
|
3439
|
+
}
|
|
3440
|
+
return out;
|
|
3441
|
+
}
|
|
3442
|
+
handlerNameFromAttr(opening, attrName) {
|
|
3443
|
+
const attr = opening.attributes.find(
|
|
3444
|
+
(candidate) => BabelTypes.isJSXAttribute(candidate) && BabelTypes.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
|
|
3445
|
+
);
|
|
3446
|
+
if (!attr?.value || !BabelTypes.isJSXExpressionContainer(attr.value)) return void 0;
|
|
3447
|
+
const expr = attr.value.expression;
|
|
3448
|
+
if (BabelTypes.isIdentifier(expr)) return expr.name;
|
|
3449
|
+
if (BabelTypes.isArrowFunctionExpression(expr) || BabelTypes.isFunctionExpression(expr)) {
|
|
3450
|
+
return firstCalledFunctionName(expr) ?? "anonymous";
|
|
3451
|
+
}
|
|
3452
|
+
return void 0;
|
|
3453
|
+
}
|
|
3454
|
+
// ── Navigation targets (route paths — resolved by the orchestrator) ─
|
|
3455
|
+
extractNavigationTargets(ast) {
|
|
3456
|
+
const targets = /* @__PURE__ */ new Set();
|
|
3457
|
+
for (const call of extractWebNavigationCalls(ast)) {
|
|
3458
|
+
if (call.targetPath && call.targetPath.startsWith("/")) targets.add(call.targetPath);
|
|
3459
|
+
}
|
|
3460
|
+
traverse4(ast, {
|
|
3461
|
+
JSXOpeningElement: (nodePath) => {
|
|
3462
|
+
const element = nodePath.node;
|
|
3463
|
+
const name = getJsxElementName(element);
|
|
3464
|
+
if (name === "Link" || name === "NavLink" || name === "Navigate") {
|
|
3465
|
+
const to = routePathAttr(element, "to");
|
|
3466
|
+
if (to && to.startsWith("/")) targets.add(to);
|
|
3467
|
+
}
|
|
3468
|
+
if (name === "a") {
|
|
3469
|
+
const href = routePathAttr(element, "href");
|
|
3470
|
+
if (href && href.startsWith("/")) targets.add(href);
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
});
|
|
3474
|
+
return Array.from(targets).sort();
|
|
3475
|
+
}
|
|
3476
|
+
// ── Collections ───────────────────────────────────────────────────
|
|
3477
|
+
extractCollections(ast) {
|
|
3478
|
+
const collections = [];
|
|
3479
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3480
|
+
traverse4(ast, {
|
|
3481
|
+
JSXExpressionContainer: (nodePath) => {
|
|
3482
|
+
const expr = nodePath.node.expression;
|
|
3483
|
+
if (!BabelTypes.isCallExpression(expr) || !BabelTypes.isMemberExpression(expr.callee) || !BabelTypes.isIdentifier(expr.callee.object) || !BabelTypes.isIdentifier(expr.callee.property) || expr.callee.property.name !== "map") {
|
|
3484
|
+
return;
|
|
3485
|
+
}
|
|
3486
|
+
const callback = expr.arguments[0];
|
|
3487
|
+
if (!BabelTypes.isArrowFunctionExpression(callback) && !BabelTypes.isFunctionExpression(callback)) return;
|
|
3488
|
+
const enclosing = nodePath.findParent(
|
|
3489
|
+
(p) => p.isJSXElement()
|
|
3490
|
+
);
|
|
3491
|
+
const enclosingName = enclosing ? getJsxElementName(enclosing.node.openingElement) : null;
|
|
3492
|
+
const enclosingRole = enclosing && enclosingName ? classifyWebJsxComponent(enclosingName, enclosing.node.openingElement) : null;
|
|
3493
|
+
const returnsListItem = callbackReturnsTag(callback, /* @__PURE__ */ new Set(["li", "tr"]));
|
|
3494
|
+
if (enclosingName === "select") return;
|
|
3495
|
+
const isListContext = enclosingName !== null && LISTISH_TAGS.has(enclosingName) || enclosingRole === "list" || returnsListItem;
|
|
3496
|
+
if (!isListContext) return;
|
|
3497
|
+
const dataSource = expr.callee.object.name;
|
|
3498
|
+
if (seen.has(dataSource)) return;
|
|
3499
|
+
seen.add(dataSource);
|
|
3500
|
+
const itemNames = collectionItemNames(callback);
|
|
3501
|
+
const displayFields = collectionDisplayFields(callback, itemNames);
|
|
3502
|
+
const keyField = collectionKeyField(callback, itemNames);
|
|
3503
|
+
const rowAction = collectionRowAction(callback);
|
|
3504
|
+
const itemType = inferItemType(dataSource, displayFields);
|
|
3505
|
+
const identityFields = inferIdentityFields(keyField, displayFields);
|
|
3506
|
+
collections.push({
|
|
3507
|
+
id: dataSource,
|
|
3508
|
+
component: enclosingName ?? "list",
|
|
3509
|
+
...itemType ? { itemType } : {},
|
|
3510
|
+
dataSource,
|
|
3511
|
+
...keyField ? { keyField } : {},
|
|
3512
|
+
...displayFields.length > 0 ? { displayFields } : {},
|
|
3513
|
+
...rowAction ? { rowAction, rowActions: [rowAction] } : {},
|
|
3514
|
+
...identityFields.length > 0 ? { identityFields } : {}
|
|
3515
|
+
});
|
|
3516
|
+
}
|
|
3517
|
+
});
|
|
3518
|
+
return collections;
|
|
3519
|
+
}
|
|
3520
|
+
};
|
|
3521
|
+
function isRegisterScreenCallee(callee) {
|
|
3522
|
+
return BabelTypes.isIdentifier(callee) && callee.name === "registerScreen" || BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.property) && callee.property.name === "registerScreen";
|
|
3523
|
+
}
|
|
3524
|
+
function literalToPlain(node) {
|
|
3525
|
+
if (BabelTypes.isStringLiteral(node) || BabelTypes.isNumericLiteral(node) || BabelTypes.isBooleanLiteral(node)) {
|
|
3526
|
+
return node.value;
|
|
3527
|
+
}
|
|
3528
|
+
if (BabelTypes.isNullLiteral(node)) return null;
|
|
3529
|
+
if (BabelTypes.isArrayExpression(node)) {
|
|
3530
|
+
return node.elements.filter((el) => el !== null && BabelTypes.isExpression(el)).map((el) => literalToPlain(el)).filter((value) => value !== void 0);
|
|
3531
|
+
}
|
|
3532
|
+
if (BabelTypes.isObjectExpression(node)) {
|
|
3533
|
+
const out = {};
|
|
3534
|
+
for (const prop of node.properties) {
|
|
3535
|
+
if (!BabelTypes.isObjectProperty(prop)) continue;
|
|
3536
|
+
const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : BabelTypes.isStringLiteral(prop.key) ? prop.key.value : void 0;
|
|
3537
|
+
if (!key || !BabelTypes.isExpression(prop.value)) continue;
|
|
3538
|
+
const value = literalToPlain(prop.value);
|
|
3539
|
+
if (value !== void 0) out[key] = value;
|
|
3540
|
+
}
|
|
3541
|
+
return out;
|
|
3542
|
+
}
|
|
3543
|
+
return void 0;
|
|
3544
|
+
}
|
|
3545
|
+
function jsxTextContent(element) {
|
|
3546
|
+
const parts = [];
|
|
3547
|
+
const walk = (children) => {
|
|
3548
|
+
for (const child of children) {
|
|
3549
|
+
if (BabelTypes.isJSXText(child)) {
|
|
3550
|
+
const trimmed = child.value.replace(/\s+/g, " ").trim();
|
|
3551
|
+
if (trimmed) parts.push(trimmed);
|
|
3552
|
+
} else if (BabelTypes.isJSXExpressionContainer(child) && BabelTypes.isStringLiteral(child.expression)) {
|
|
3553
|
+
parts.push(child.expression.value);
|
|
3554
|
+
} else if (BabelTypes.isJSXElement(child)) {
|
|
3555
|
+
walk(child.children);
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
};
|
|
3559
|
+
walk(element.children);
|
|
3560
|
+
const text = parts.join(" ").trim();
|
|
3561
|
+
return text.length > 0 ? text : void 0;
|
|
3562
|
+
}
|
|
3563
|
+
function firstCalledFunctionName(node) {
|
|
3564
|
+
if (BabelTypes.isIdentifier(node)) return node.name;
|
|
3565
|
+
if (BabelTypes.isArrowFunctionExpression(node) || BabelTypes.isFunctionExpression(node)) {
|
|
3566
|
+
return firstCalledFunctionName(node.body);
|
|
3567
|
+
}
|
|
3568
|
+
if (BabelTypes.isBlockStatement(node)) {
|
|
3569
|
+
for (const statement of node.body) {
|
|
3570
|
+
const handler = firstCalledFunctionName(statement);
|
|
3571
|
+
if (handler) return handler;
|
|
3572
|
+
}
|
|
3573
|
+
return void 0;
|
|
3574
|
+
}
|
|
3575
|
+
if (BabelTypes.isExpressionStatement(node)) return firstCalledFunctionName(node.expression);
|
|
3576
|
+
if (BabelTypes.isReturnStatement(node)) {
|
|
3577
|
+
return node.argument ? firstCalledFunctionName(node.argument) : void 0;
|
|
3578
|
+
}
|
|
3579
|
+
if (BabelTypes.isAwaitExpression(node) || BabelTypes.isUnaryExpression(node)) {
|
|
3580
|
+
return firstCalledFunctionName(node.argument);
|
|
3581
|
+
}
|
|
3582
|
+
if (BabelTypes.isCallExpression(node)) {
|
|
3583
|
+
if (BabelTypes.isIdentifier(node.callee) && !/^(navigate|confirm|alert)$/.test(node.callee.name)) {
|
|
3584
|
+
return node.callee.name;
|
|
3585
|
+
}
|
|
3586
|
+
return void 0;
|
|
3587
|
+
}
|
|
3588
|
+
return void 0;
|
|
3589
|
+
}
|
|
3590
|
+
function containsWindowConfirm(body) {
|
|
3591
|
+
let found = false;
|
|
3592
|
+
traverse4(
|
|
3593
|
+
body,
|
|
3594
|
+
{
|
|
3595
|
+
noScope: true,
|
|
3596
|
+
CallExpression: (nodePath) => {
|
|
3597
|
+
const callee = nodePath.node.callee;
|
|
3598
|
+
if (BabelTypes.isIdentifier(callee) && callee.name === "confirm") found = true;
|
|
3599
|
+
if (BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.object) && callee.object.name === "window" && BabelTypes.isIdentifier(callee.property) && callee.property.name === "confirm") {
|
|
3600
|
+
found = true;
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
);
|
|
3605
|
+
return found;
|
|
3606
|
+
}
|
|
3607
|
+
function mergeLocator(current, next) {
|
|
3608
|
+
return { ...current ?? {}, ...next };
|
|
3609
|
+
}
|
|
3610
|
+
function routePathAttr(opening, attrName) {
|
|
3611
|
+
const literal = getStringAttr(opening, attrName);
|
|
3612
|
+
if (literal) return literal;
|
|
3613
|
+
const attr = opening.attributes.find(
|
|
3614
|
+
(candidate) => BabelTypes.isJSXAttribute(candidate) && BabelTypes.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
|
|
3615
|
+
);
|
|
3616
|
+
if (!attr?.value || !BabelTypes.isJSXExpressionContainer(attr.value)) return void 0;
|
|
3617
|
+
return staticRoutePath(attr.value.expression);
|
|
3618
|
+
}
|
|
3619
|
+
function slugify(label) {
|
|
3620
|
+
return label.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3621
|
+
}
|
|
3622
|
+
function normalizeLabel(label) {
|
|
3623
|
+
return label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3624
|
+
}
|
|
3625
|
+
function isWeakInferredFieldName(name) {
|
|
3626
|
+
return /^(text|value|input|query|search|selected|checked)$/i.test(name);
|
|
3627
|
+
}
|
|
3628
|
+
function collectionItemNames(callback) {
|
|
3629
|
+
const names = /* @__PURE__ */ new Set(["item"]);
|
|
3630
|
+
const firstParam = callback.params[0];
|
|
3631
|
+
if (BabelTypes.isIdentifier(firstParam)) names.add(firstParam.name);
|
|
3632
|
+
if (BabelTypes.isObjectPattern(firstParam)) {
|
|
3633
|
+
for (const prop of firstParam.properties) {
|
|
3634
|
+
if (BabelTypes.isObjectProperty(prop) && BabelTypes.isIdentifier(prop.key) && BabelTypes.isIdentifier(prop.value)) {
|
|
3635
|
+
names.add(prop.value.name);
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
return names;
|
|
3640
|
+
}
|
|
3641
|
+
function collectionDisplayFields(callback, itemNames) {
|
|
3642
|
+
const fields = /* @__PURE__ */ new Set();
|
|
3643
|
+
if (!callback.body) return [];
|
|
3644
|
+
traverse4(
|
|
3645
|
+
callback.body,
|
|
3646
|
+
{
|
|
3647
|
+
noScope: true,
|
|
3648
|
+
MemberExpression: (nodePath) => {
|
|
3649
|
+
const node = nodePath.node;
|
|
3650
|
+
if (BabelTypes.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes.isIdentifier(node.property)) {
|
|
3651
|
+
fields.add(node.property.name);
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
);
|
|
3656
|
+
return Array.from(fields).sort();
|
|
3657
|
+
}
|
|
3658
|
+
function collectionKeyField(callback, itemNames) {
|
|
3659
|
+
let keyField;
|
|
3660
|
+
if (!callback.body) return void 0;
|
|
3661
|
+
traverse4(
|
|
3662
|
+
callback.body,
|
|
3663
|
+
{
|
|
3664
|
+
noScope: true,
|
|
3665
|
+
JSXAttribute: (nodePath) => {
|
|
3666
|
+
const attr = nodePath.node;
|
|
3667
|
+
if (!BabelTypes.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
|
|
3668
|
+
if (!attr.value || !BabelTypes.isJSXExpressionContainer(attr.value)) return;
|
|
3669
|
+
const expr = attr.value.expression;
|
|
3670
|
+
if (BabelTypes.isMemberExpression(expr) && BabelTypes.isIdentifier(expr.object) && itemNames.has(expr.object.name) && BabelTypes.isIdentifier(expr.property)) {
|
|
3671
|
+
keyField = keyField ?? expr.property.name;
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
);
|
|
3676
|
+
return keyField;
|
|
3677
|
+
}
|
|
3678
|
+
function collectionRowAction(callback) {
|
|
3679
|
+
if (!callback.body) return void 0;
|
|
3680
|
+
const navCall = extractWebNavigationCalls(callback.body).find((call) => call.targetPath);
|
|
3681
|
+
if (!navCall?.targetPath) return void 0;
|
|
3682
|
+
return {
|
|
3683
|
+
type: "navigation",
|
|
3684
|
+
// Route path — the orchestrator resolves it to a screen name.
|
|
3685
|
+
targetScreen: navCall.targetPath,
|
|
3686
|
+
description: `Clicking a row opens ${navCall.targetPath}`
|
|
3687
|
+
};
|
|
3688
|
+
}
|
|
3689
|
+
function callbackReturnsTag(callback, tags) {
|
|
3690
|
+
let found = false;
|
|
3691
|
+
const inspect = (node) => {
|
|
3692
|
+
if (!node || found) return;
|
|
3693
|
+
if (BabelTypes.isJSXElement(node)) {
|
|
3694
|
+
const name = getJsxElementName(node.openingElement);
|
|
3695
|
+
if (name && tags.has(name)) found = true;
|
|
3696
|
+
return;
|
|
3697
|
+
}
|
|
3698
|
+
if (BabelTypes.isBlockStatement(node)) {
|
|
3699
|
+
for (const statement of node.body) {
|
|
3700
|
+
if (BabelTypes.isReturnStatement(statement)) inspect(statement.argument);
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
if (BabelTypes.isParenthesizedExpression(node)) inspect(node.expression);
|
|
3704
|
+
if (BabelTypes.isConditionalExpression(node)) {
|
|
3705
|
+
inspect(node.consequent);
|
|
3706
|
+
inspect(node.alternate);
|
|
3707
|
+
}
|
|
3708
|
+
};
|
|
3709
|
+
inspect(callback.body);
|
|
3710
|
+
return found;
|
|
3711
|
+
}
|
|
3712
|
+
function inferItemType(dataSource, displayFields) {
|
|
3713
|
+
const singular = dataSource.replace(/^render/i, "").replace(/(List|Items|Data|Rows|Sections)$/i, "").replace(/s$/i, "");
|
|
3714
|
+
const candidate = singular.charAt(0).toUpperCase() + singular.slice(1);
|
|
3715
|
+
if (candidate.length > 1) return candidate;
|
|
3716
|
+
if (displayFields.length > 0) return "Item";
|
|
3717
|
+
return void 0;
|
|
3718
|
+
}
|
|
3719
|
+
function inferIdentityFields(keyField, displayFields) {
|
|
3720
|
+
const out = /* @__PURE__ */ new Set();
|
|
3721
|
+
if (keyField) out.add(keyField);
|
|
3722
|
+
for (const field of displayFields) {
|
|
3723
|
+
if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
|
|
3724
|
+
}
|
|
3725
|
+
return Array.from(out);
|
|
3726
|
+
}
|
|
3727
|
+
var WEB_NAVIGATOR_TYPE = "route";
|
|
3728
|
+
var WebNavigationAnalyzer = class {
|
|
3729
|
+
config;
|
|
3730
|
+
navigationInclude;
|
|
3731
|
+
navigationExclude;
|
|
3732
|
+
constructor(config, options) {
|
|
3733
|
+
this.config = config;
|
|
3734
|
+
this.navigationInclude = options?.navigationInclude ?? [];
|
|
3735
|
+
this.navigationExclude = options?.navigationExclude ?? [];
|
|
3736
|
+
}
|
|
3737
|
+
async analyze() {
|
|
3738
|
+
const files = await this.findRouteFiles();
|
|
3739
|
+
const routes = [];
|
|
3740
|
+
for (const filePath of files) {
|
|
3741
|
+
try {
|
|
3742
|
+
const content = await promises.readFile(filePath, "utf-8");
|
|
3743
|
+
if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(content)) {
|
|
3744
|
+
continue;
|
|
3745
|
+
}
|
|
3746
|
+
const ast = parseSource(content, this.config.parserPlugins);
|
|
3747
|
+
routes.push(...this.extractJsxRoutes(ast));
|
|
3748
|
+
routes.push(...this.extractObjectRoutes(ast));
|
|
3749
|
+
} catch (error) {
|
|
3750
|
+
console.warn(`[WebNavigationAnalyzer] Failed to parse ${filePath}:`, error);
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
const deduped = this.dedupeRoutes(routes);
|
|
3754
|
+
return { graph: this.buildGraph(deduped), routes: deduped };
|
|
3755
|
+
}
|
|
3756
|
+
/** Files likely to contain route configuration. */
|
|
3757
|
+
async findRouteFiles() {
|
|
3758
|
+
const patterns = [
|
|
3759
|
+
"**/*{router,routes,Router,Routes}*.{ts,tsx,js,jsx}",
|
|
3760
|
+
"**/App.{ts,tsx,js,jsx}",
|
|
3761
|
+
"**/app.{ts,tsx,js,jsx}",
|
|
3762
|
+
"**/main.{ts,tsx,js,jsx}",
|
|
3763
|
+
"**/index.{ts,tsx,js,jsx}",
|
|
3764
|
+
...this.navigationInclude
|
|
3765
|
+
];
|
|
3766
|
+
const ignore = [
|
|
3767
|
+
"**/node_modules/**",
|
|
3768
|
+
"**/dist/**",
|
|
3769
|
+
"**/build/**",
|
|
3770
|
+
...this.config.exclude || [],
|
|
3771
|
+
...this.navigationExclude
|
|
3772
|
+
];
|
|
3773
|
+
const files = await glob(patterns, { cwd: this.config.rootDir, ignore });
|
|
3774
|
+
return files.map((file) => path8__default.join(this.config.rootDir, file));
|
|
3775
|
+
}
|
|
3776
|
+
// ── JSX <Route> style ────────────────────────────────────────────
|
|
3777
|
+
extractJsxRoutes(ast) {
|
|
3778
|
+
const routes = [];
|
|
3779
|
+
const visitRoute = (element, parentPath) => {
|
|
3780
|
+
const opening = element.openingElement;
|
|
3781
|
+
const name = getJsxElementName(opening);
|
|
3782
|
+
if (name !== "Route") {
|
|
3783
|
+
for (const child of element.children) {
|
|
3784
|
+
if (BabelTypes.isJSXElement(child)) visitRoute(child, parentPath);
|
|
3785
|
+
}
|
|
3786
|
+
return;
|
|
3787
|
+
}
|
|
3788
|
+
const segment = getStringAttr(opening, "path");
|
|
3789
|
+
const isIndex = hasJsxAttribute(opening, "index") && !segment;
|
|
3790
|
+
const fullPath = this.joinPaths(parentPath, segment, isIndex);
|
|
3791
|
+
const componentName = this.componentNameFromElementAttr(opening) ?? void 0;
|
|
3792
|
+
const isLeaf = !element.children.some(
|
|
3793
|
+
(child) => BabelTypes.isJSXElement(child) && getJsxElementName(child.openingElement) === "Route"
|
|
3794
|
+
);
|
|
3795
|
+
if ((segment || isIndex) && (componentName || isLeaf)) {
|
|
3796
|
+
routes.push(this.buildRoute(fullPath, componentName, isIndex, !isLeaf));
|
|
3797
|
+
}
|
|
3798
|
+
for (const child of element.children) {
|
|
3799
|
+
if (BabelTypes.isJSXElement(child)) visitRoute(child, fullPath);
|
|
3800
|
+
}
|
|
3801
|
+
};
|
|
3802
|
+
traverse4(ast, {
|
|
3803
|
+
JSXElement: (nodePath) => {
|
|
3804
|
+
const name = getJsxElementName(nodePath.node.openingElement);
|
|
3805
|
+
if (name !== "Routes" && name !== "Route") return;
|
|
3806
|
+
if (nodePath.findParent((p) => {
|
|
3807
|
+
if (!p.isJSXElement()) return false;
|
|
3808
|
+
const parentName = getJsxElementName(p.node.openingElement);
|
|
3809
|
+
return parentName === "Routes" || parentName === "Route";
|
|
3810
|
+
})) {
|
|
3811
|
+
return;
|
|
3812
|
+
}
|
|
3813
|
+
visitRoute(nodePath.node, "");
|
|
2328
3814
|
}
|
|
2329
|
-
return screen;
|
|
2330
3815
|
});
|
|
2331
|
-
return
|
|
2332
|
-
screens: enrichedScreens,
|
|
2333
|
-
navigation,
|
|
2334
|
-
analyzedFiles: screenFiles.length,
|
|
2335
|
-
...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
|
|
2336
|
-
};
|
|
3816
|
+
return routes;
|
|
2337
3817
|
}
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
if (
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
3818
|
+
/** `element={<VehicleList/>}` or `Component={VehicleList}`. */
|
|
3819
|
+
componentNameFromElementAttr(opening) {
|
|
3820
|
+
for (const attr of opening.attributes) {
|
|
3821
|
+
if (!BabelTypes.isJSXAttribute(attr) || !BabelTypes.isJSXIdentifier(attr.name)) continue;
|
|
3822
|
+
if (attr.name.name === "element" && BabelTypes.isJSXExpressionContainer(attr.value)) {
|
|
3823
|
+
const expr = attr.value.expression;
|
|
3824
|
+
if (BabelTypes.isJSXElement(expr)) return getJsxElementName(expr.openingElement);
|
|
3825
|
+
}
|
|
3826
|
+
if (attr.name.name === "Component" && BabelTypes.isJSXExpressionContainer(attr.value)) {
|
|
3827
|
+
if (BabelTypes.isIdentifier(attr.value.expression)) return attr.value.expression.name;
|
|
2345
3828
|
}
|
|
2346
3829
|
}
|
|
2347
|
-
|
|
2348
|
-
target.validationRules = source.validationRules ? { ...source.validationRules, ...target.validationRules ?? {} } : target.validationRules;
|
|
3830
|
+
return null;
|
|
2349
3831
|
}
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
3832
|
+
// ── createBrowserRouter([...]) / useRoutes([...]) style ──────────
|
|
3833
|
+
extractObjectRoutes(ast) {
|
|
3834
|
+
const routes = [];
|
|
3835
|
+
const ROUTER_FACTORIES = /* @__PURE__ */ new Set([
|
|
3836
|
+
"createBrowserRouter",
|
|
3837
|
+
"createHashRouter",
|
|
3838
|
+
"createMemoryRouter",
|
|
3839
|
+
"useRoutes"
|
|
3840
|
+
]);
|
|
3841
|
+
traverse4(ast, {
|
|
3842
|
+
CallExpression: (nodePath) => {
|
|
3843
|
+
const callee = nodePath.node.callee;
|
|
3844
|
+
if (!BabelTypes.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
|
|
3845
|
+
const first = nodePath.node.arguments[0];
|
|
3846
|
+
if (!BabelTypes.isArrayExpression(first)) return;
|
|
3847
|
+
this.visitRouteObjects(first, "", routes);
|
|
2358
3848
|
}
|
|
2359
|
-
return false;
|
|
2360
3849
|
});
|
|
3850
|
+
return routes;
|
|
2361
3851
|
}
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
3852
|
+
visitRouteObjects(arr, parentPath, out) {
|
|
3853
|
+
for (const element of arr.elements) {
|
|
3854
|
+
if (!BabelTypes.isObjectExpression(element)) continue;
|
|
3855
|
+
let segment;
|
|
3856
|
+
let isIndex = false;
|
|
3857
|
+
let componentName;
|
|
3858
|
+
let children;
|
|
3859
|
+
for (const prop of element.properties) {
|
|
3860
|
+
if (!BabelTypes.isObjectProperty(prop) || !BabelTypes.isIdentifier(prop.key)) continue;
|
|
3861
|
+
const key = prop.key.name;
|
|
3862
|
+
if (key === "path" && BabelTypes.isStringLiteral(prop.value)) segment = prop.value.value;
|
|
3863
|
+
if (key === "index" && BabelTypes.isBooleanLiteral(prop.value)) isIndex = prop.value.value;
|
|
3864
|
+
if (key === "element" && BabelTypes.isJSXElement(prop.value)) {
|
|
3865
|
+
componentName = getJsxElementName(prop.value.openingElement) ?? void 0;
|
|
3866
|
+
}
|
|
3867
|
+
if (key === "Component" && BabelTypes.isIdentifier(prop.value)) componentName = prop.value.name;
|
|
3868
|
+
if (key === "children" && BabelTypes.isArrayExpression(prop.value)) children = prop.value;
|
|
3869
|
+
}
|
|
3870
|
+
const fullPath = this.joinPaths(parentPath, segment, isIndex);
|
|
3871
|
+
if ((segment !== void 0 || isIndex) && (componentName || !children)) {
|
|
3872
|
+
out.push(this.buildRoute(fullPath, componentName, isIndex, Boolean(children)));
|
|
3873
|
+
}
|
|
3874
|
+
if (children) this.visitRouteObjects(children, fullPath, out);
|
|
2376
3875
|
}
|
|
2377
3876
|
}
|
|
2378
|
-
|
|
2379
|
-
|
|
3877
|
+
// ── Shared route building ────────────────────────────────────────
|
|
3878
|
+
joinPaths(parent, segment, isIndex) {
|
|
3879
|
+
if (isIndex || segment === void 0) return parent || "/";
|
|
3880
|
+
if (segment.startsWith("/")) return this.normalizePath(segment);
|
|
3881
|
+
return this.normalizePath(`${parent === "/" ? "" : parent}/${segment}`);
|
|
2380
3882
|
}
|
|
2381
|
-
|
|
2382
|
-
|
|
3883
|
+
normalizePath(p) {
|
|
3884
|
+
const cleaned = `/${p}`.replace(/\/+/g, "/");
|
|
3885
|
+
return cleaned.length > 1 ? cleaned.replace(/\/$/, "") : cleaned;
|
|
2383
3886
|
}
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
3887
|
+
buildRoute(fullPath, componentName, isIndex, isLayout) {
|
|
3888
|
+
const params = this.paramsFromPath(fullPath);
|
|
3889
|
+
return {
|
|
3890
|
+
path: fullPath,
|
|
3891
|
+
screenName: componentName ?? screenNameFromPath(fullPath),
|
|
3892
|
+
...params.length > 0 ? { params } : {},
|
|
3893
|
+
...isIndex ? { index: true } : {},
|
|
3894
|
+
...isLayout ? { layout: true } : {}
|
|
3895
|
+
};
|
|
3896
|
+
}
|
|
3897
|
+
paramsFromPath(routePath) {
|
|
3898
|
+
const params = [];
|
|
3899
|
+
for (const segment of routePath.split("/")) {
|
|
3900
|
+
if (!segment.startsWith(":")) continue;
|
|
3901
|
+
const optional = segment.endsWith("?");
|
|
3902
|
+
const name = segment.slice(1, optional ? -1 : void 0);
|
|
3903
|
+
if (name) params.push({ name, type: "string", required: !optional });
|
|
3904
|
+
}
|
|
3905
|
+
return params;
|
|
3906
|
+
}
|
|
3907
|
+
/**
|
|
3908
|
+
* One entry per path. When several declarations resolve to the same
|
|
3909
|
+
* path, keep the one that best describes what the user lands on: a
|
|
3910
|
+
* page beats a layout wrapper (an `index` child and its parent layout
|
|
3911
|
+
* share a path), and a resolved component name beats a name derived
|
|
3912
|
+
* from the path.
|
|
3913
|
+
*/
|
|
3914
|
+
dedupeRoutes(routes) {
|
|
3915
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
3916
|
+
for (const route of routes) {
|
|
3917
|
+
const existing = byPath.get(route.path);
|
|
3918
|
+
if (!existing || routeScore(route) > routeScore(existing)) {
|
|
3919
|
+
byPath.set(route.path, route);
|
|
2391
3920
|
}
|
|
2392
3921
|
}
|
|
2393
|
-
return
|
|
3922
|
+
return Array.from(byPath.values());
|
|
3923
|
+
}
|
|
3924
|
+
buildGraph(routes) {
|
|
3925
|
+
const screens = {};
|
|
3926
|
+
const navigatorName = "router";
|
|
3927
|
+
const screenNames = routes.map((r) => r.screenName);
|
|
3928
|
+
for (const route of routes) {
|
|
3929
|
+
const others = screenNames.filter((name) => name !== route.screenName);
|
|
3930
|
+
screens[route.screenName] = {
|
|
3931
|
+
screenName: route.screenName,
|
|
3932
|
+
// Open-union value — web routes, not a RN stack/tab/drawer.
|
|
3933
|
+
navigatorType: WEB_NAVIGATOR_TYPE,
|
|
3934
|
+
parentNavigator: navigatorName,
|
|
3935
|
+
// Any route is one URL away from any other — both directions,
|
|
3936
|
+
// like the RN analyzer models tab navigators.
|
|
3937
|
+
reachableFrom: others,
|
|
3938
|
+
reachableTo: others,
|
|
3939
|
+
...route.params ? { params: route.params } : {}
|
|
3940
|
+
};
|
|
3941
|
+
}
|
|
3942
|
+
const initialRoute = routes.find((r) => r.path === "/") ?? routes.find((r) => r.index) ?? routes[0];
|
|
3943
|
+
return {
|
|
3944
|
+
screens,
|
|
3945
|
+
initialScreen: initialRoute?.screenName ?? "",
|
|
3946
|
+
navigators: routes.length > 0 ? [{
|
|
3947
|
+
name: navigatorName,
|
|
3948
|
+
type: WEB_NAVIGATOR_TYPE,
|
|
3949
|
+
screens: screenNames
|
|
3950
|
+
}] : []
|
|
3951
|
+
};
|
|
2394
3952
|
}
|
|
2395
3953
|
};
|
|
3954
|
+
function screenNameFromPath(routePath) {
|
|
3955
|
+
if (routePath === "/" || routePath === "") return "Home";
|
|
3956
|
+
return routePath.split("/").filter(Boolean).map((segment) => segment.replace(/^:/, "").replace(/\?$/, "")).map(
|
|
3957
|
+
(segment) => segment.split(/[-_.]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("")
|
|
3958
|
+
).join("");
|
|
3959
|
+
}
|
|
3960
|
+
function routeScore(route) {
|
|
3961
|
+
const isPage = route.layout ? 0 : 2;
|
|
3962
|
+
const hasRealName = route.screenName === screenNameFromPath(route.path) ? 0 : 1;
|
|
3963
|
+
return isPage + hasRealName;
|
|
3964
|
+
}
|
|
3965
|
+
function resolvePathToScreen(routes, target) {
|
|
3966
|
+
const normalized = `/${target}`.replace(/\/+/g, "/").replace(/\?.*$/, "").replace(/#.*$/, "");
|
|
3967
|
+
const cleaned = normalized.length > 1 ? normalized.replace(/\/$/, "") : normalized;
|
|
3968
|
+
const exact = routes.find((r) => r.path === cleaned);
|
|
3969
|
+
if (exact) return exact.screenName;
|
|
3970
|
+
const targetSegments = cleaned.split("/").filter(Boolean);
|
|
3971
|
+
for (const route of routes) {
|
|
3972
|
+
const routeSegments = route.path.split("/").filter(Boolean);
|
|
3973
|
+
if (routeSegments.length !== targetSegments.length) continue;
|
|
3974
|
+
const matches = routeSegments.every(
|
|
3975
|
+
(seg, i) => seg.startsWith(":") || seg === "*" || seg === targetSegments[i]
|
|
3976
|
+
);
|
|
3977
|
+
if (matches) return route.screenName;
|
|
3978
|
+
}
|
|
3979
|
+
return void 0;
|
|
3980
|
+
}
|
|
2396
3981
|
|
|
2397
|
-
// src/analyzers/
|
|
2398
|
-
var
|
|
2399
|
-
|
|
2400
|
-
|
|
3982
|
+
// src/analyzers/web/ReactWebPlatformAnalyzer.ts
|
|
3983
|
+
var ReactWebPlatformAnalyzer = class {
|
|
3984
|
+
platform = "web";
|
|
3985
|
+
async analyze(config, options) {
|
|
3986
|
+
const screenAnalyzer = new WebScreenAnalyzer(config, {
|
|
3987
|
+
screenPatterns: options.screenPatterns
|
|
3988
|
+
});
|
|
3989
|
+
const navigationAnalyzer = new WebNavigationAnalyzer(config, {
|
|
3990
|
+
navigationInclude: options.navigationInclude,
|
|
3991
|
+
navigationExclude: options.navigationExclude
|
|
3992
|
+
});
|
|
3993
|
+
console.log("[ReactWebPlatformAnalyzer] Running analyzers...");
|
|
3994
|
+
const [screenAnalysis, navigationResult] = await Promise.all([
|
|
3995
|
+
screenAnalyzer.analyze(),
|
|
3996
|
+
navigationAnalyzer.analyze()
|
|
3997
|
+
]);
|
|
3998
|
+
const routeScreenNames = new Set(navigationResult.routes.map((route) => route.screenName));
|
|
3999
|
+
const strictScreens = options.strictScreens ?? true;
|
|
4000
|
+
let screensFilteredOut = 0;
|
|
4001
|
+
const included = [];
|
|
4002
|
+
for (const candidate of screenAnalysis.candidates) {
|
|
4003
|
+
if (!strictScreens || this.isScreen(candidate, routeScreenNames)) {
|
|
4004
|
+
included.push(candidate);
|
|
4005
|
+
} else {
|
|
4006
|
+
screensFilteredOut++;
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
const screens = included.map(
|
|
4010
|
+
(candidate) => this.resolveRoutePaths(candidate.descriptor, navigationResult.routes)
|
|
4011
|
+
);
|
|
4012
|
+
console.log(
|
|
4013
|
+
`[ReactWebPlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens, ${navigationResult.routes.length} routes`
|
|
4014
|
+
);
|
|
4015
|
+
return {
|
|
4016
|
+
screens,
|
|
4017
|
+
navigation: navigationResult.graph,
|
|
4018
|
+
analyzedFiles: screenAnalysis.analyzedFiles,
|
|
4019
|
+
...screensFilteredOut > 0 ? { screensFilteredOut } : {}
|
|
4020
|
+
};
|
|
2401
4021
|
}
|
|
2402
|
-
|
|
2403
|
-
|
|
4022
|
+
isScreen(candidate, routeScreenNames) {
|
|
4023
|
+
return candidate.hasRegisterScreen || candidate.matchesScreenPattern || routeScreenNames.has(candidate.descriptor.name);
|
|
4024
|
+
}
|
|
4025
|
+
/** Replace route-path references with screen names where the route table resolves them. */
|
|
4026
|
+
resolveRoutePaths(screen, routes) {
|
|
4027
|
+
const resolve2 = (target) => {
|
|
4028
|
+
if (!target || !target.startsWith("/")) return target;
|
|
4029
|
+
return resolvePathToScreen(routes, target) ?? target;
|
|
4030
|
+
};
|
|
4031
|
+
const navigationTargets = Array.from(
|
|
4032
|
+
new Set(
|
|
4033
|
+
screen.navigationTargets.map((target) => resolve2(target)).filter((target) => target !== screen.name)
|
|
4034
|
+
)
|
|
4035
|
+
).sort();
|
|
4036
|
+
const actions = screen.actions.map((action) => {
|
|
4037
|
+
const resolved = resolve2(action.targetScreen);
|
|
4038
|
+
return resolved === action.targetScreen ? action : { ...action, targetScreen: resolved };
|
|
4039
|
+
});
|
|
4040
|
+
const collections = screen.collections?.map((collection) => {
|
|
4041
|
+
const resolveRow = (row) => {
|
|
4042
|
+
if (!row?.targetScreen) return row;
|
|
4043
|
+
const resolved = resolve2(row.targetScreen);
|
|
4044
|
+
if (resolved === row.targetScreen) return row;
|
|
4045
|
+
return {
|
|
4046
|
+
...row,
|
|
4047
|
+
targetScreen: resolved,
|
|
4048
|
+
...row.description ? { description: `Clicking a row opens ${resolved}` } : {}
|
|
4049
|
+
};
|
|
4050
|
+
};
|
|
4051
|
+
const rowAction = resolveRow(collection.rowAction);
|
|
4052
|
+
return {
|
|
4053
|
+
...collection,
|
|
4054
|
+
...rowAction ? { rowAction } : {},
|
|
4055
|
+
...collection.rowActions ? { rowActions: collection.rowActions.map((row) => resolveRow(row)) } : {}
|
|
4056
|
+
};
|
|
4057
|
+
});
|
|
2404
4058
|
return {
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
4059
|
+
...screen,
|
|
4060
|
+
navigationTargets,
|
|
4061
|
+
actions,
|
|
4062
|
+
...collections ? { collections } : {}
|
|
2408
4063
|
};
|
|
2409
4064
|
}
|
|
2410
4065
|
};
|
|
@@ -2614,8 +4269,8 @@ var ZodParsedType = util.arrayToEnum([
|
|
|
2614
4269
|
"set"
|
|
2615
4270
|
]);
|
|
2616
4271
|
var getParsedType = (data) => {
|
|
2617
|
-
const
|
|
2618
|
-
switch (
|
|
4272
|
+
const t12 = typeof data;
|
|
4273
|
+
switch (t12) {
|
|
2619
4274
|
case "undefined":
|
|
2620
4275
|
return ZodParsedType.undefined;
|
|
2621
4276
|
case "string":
|
|
@@ -2887,8 +4542,8 @@ function getErrorMap() {
|
|
|
2887
4542
|
|
|
2888
4543
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
2889
4544
|
var makeIssue = (params) => {
|
|
2890
|
-
const { data, path:
|
|
2891
|
-
const fullPath = [...
|
|
4545
|
+
const { data, path: path9, errorMaps, issueData } = params;
|
|
4546
|
+
const fullPath = [...path9, ...issueData.path || []];
|
|
2892
4547
|
const fullIssue = {
|
|
2893
4548
|
...issueData,
|
|
2894
4549
|
path: fullPath
|
|
@@ -3004,11 +4659,11 @@ var errorUtil;
|
|
|
3004
4659
|
|
|
3005
4660
|
// ../../node_modules/zod/v3/types.js
|
|
3006
4661
|
var ParseInputLazyPath = class {
|
|
3007
|
-
constructor(parent, value,
|
|
4662
|
+
constructor(parent, value, path9, key) {
|
|
3008
4663
|
this._cachedPath = [];
|
|
3009
4664
|
this.parent = parent;
|
|
3010
4665
|
this.data = value;
|
|
3011
|
-
this._path =
|
|
4666
|
+
this._path = path9;
|
|
3012
4667
|
this._key = key;
|
|
3013
4668
|
}
|
|
3014
4669
|
get path() {
|
|
@@ -6449,7 +8104,7 @@ var coerce = {
|
|
|
6449
8104
|
};
|
|
6450
8105
|
var NEVER = INVALID;
|
|
6451
8106
|
|
|
6452
|
-
// ../shared/dist/chunk-
|
|
8107
|
+
// ../shared/dist/chunk-PKS3VDNB.mjs
|
|
6453
8108
|
var locatorSourceSchema = external_exports.enum([
|
|
6454
8109
|
"appilotsId",
|
|
6455
8110
|
"testID",
|
|
@@ -6475,7 +8130,16 @@ var locatorDescriptorSchema = external_exports.object({
|
|
|
6475
8130
|
source: locatorSourceSchema.optional()
|
|
6476
8131
|
}).passthrough();
|
|
6477
8132
|
var signalDescriptorSchema = external_exports.object({
|
|
6478
|
-
type: external_exports.enum([
|
|
8133
|
+
type: external_exports.enum([
|
|
8134
|
+
"navigation",
|
|
8135
|
+
"goBack",
|
|
8136
|
+
"toast",
|
|
8137
|
+
"modal",
|
|
8138
|
+
"inline-error",
|
|
8139
|
+
"data-arrival",
|
|
8140
|
+
"loading",
|
|
8141
|
+
"none"
|
|
8142
|
+
]).or(external_exports.string().min(1).max(60)),
|
|
6479
8143
|
target: external_exports.string().optional(),
|
|
6480
8144
|
description: external_exports.string().optional()
|
|
6481
8145
|
}).passthrough();
|
|
@@ -6489,7 +8153,20 @@ var waitPolicyDescriptorSchema = external_exports.object({
|
|
|
6489
8153
|
signals: external_exports.array(signalDescriptorSchema).optional(),
|
|
6490
8154
|
maxMs: external_exports.number().finite().optional()
|
|
6491
8155
|
}).passthrough();
|
|
6492
|
-
var targetDescriptorRoleSchema = external_exports.enum([
|
|
8156
|
+
var targetDescriptorRoleSchema = external_exports.enum([
|
|
8157
|
+
"button",
|
|
8158
|
+
"submit",
|
|
8159
|
+
"input",
|
|
8160
|
+
"toggle",
|
|
8161
|
+
"select",
|
|
8162
|
+
"date",
|
|
8163
|
+
"list",
|
|
8164
|
+
"row",
|
|
8165
|
+
"menuItem",
|
|
8166
|
+
"modal",
|
|
8167
|
+
"custom"
|
|
8168
|
+
]).or(external_exports.string().min(1).max(60));
|
|
8169
|
+
var targetOriginSchema = external_exports.enum(["analyzer", "manifest"]).or(external_exports.string().min(1).max(40));
|
|
6493
8170
|
var targetDescriptorSchema = external_exports.object({
|
|
6494
8171
|
id: external_exports.string(),
|
|
6495
8172
|
role: targetDescriptorRoleSchema,
|
|
@@ -6503,10 +8180,20 @@ var targetDescriptorSchema = external_exports.object({
|
|
|
6503
8180
|
requiresConfirmation: external_exports.boolean().optional(),
|
|
6504
8181
|
opensModal: external_exports.string().optional(),
|
|
6505
8182
|
opensBottomSheet: external_exports.string().optional(),
|
|
6506
|
-
sourceComponent: external_exports.string().optional()
|
|
8183
|
+
sourceComponent: external_exports.string().optional(),
|
|
8184
|
+
origin: targetOriginSchema.optional()
|
|
6507
8185
|
}).passthrough();
|
|
6508
8186
|
var flowStepDescriptorSchema = external_exports.object({
|
|
6509
|
-
type: external_exports.enum([
|
|
8187
|
+
type: external_exports.enum([
|
|
8188
|
+
"navigate",
|
|
8189
|
+
"fill",
|
|
8190
|
+
"press",
|
|
8191
|
+
"select",
|
|
8192
|
+
"toggle",
|
|
8193
|
+
"wait",
|
|
8194
|
+
"confirm",
|
|
8195
|
+
"choose-list-item"
|
|
8196
|
+
]).or(external_exports.string().min(1).max(60)),
|
|
6510
8197
|
target: external_exports.string().optional(),
|
|
6511
8198
|
label: external_exports.string().optional(),
|
|
6512
8199
|
description: external_exports.string().optional(),
|
|
@@ -6634,7 +8321,19 @@ var navigationNodeSchema = external_exports.object({
|
|
|
6634
8321
|
parentNavigator: external_exports.string().optional(),
|
|
6635
8322
|
reachableFrom: external_exports.array(external_exports.string()).default([]),
|
|
6636
8323
|
reachableTo: external_exports.array(external_exports.string()).default([]),
|
|
6637
|
-
params: external_exports.array(paramDescriptorSchema).optional()
|
|
8324
|
+
params: external_exports.array(paramDescriptorSchema).optional(),
|
|
8325
|
+
/**
|
|
8326
|
+
* Web clients only (documented convention, additive — previously
|
|
8327
|
+
* round-tripped via `.passthrough()`): the screen's URL route
|
|
8328
|
+
* template, e.g. `/users/:id`. Param segments use `:name` (the
|
|
8329
|
+
* relay also accepts `[name]` / `{name}`). When present and the
|
|
8330
|
+
* request's `context.platform` is `'web'`, the relay resolves
|
|
8331
|
+
* `navigate` targets against these templates and injects the
|
|
8332
|
+
* concrete URL segments as the navigate payload's `path` — the web
|
|
8333
|
+
* equivalent of RN's nested-navigation path injection. See
|
|
8334
|
+
* docs/agent-contract.md ("Web clients" section).
|
|
8335
|
+
*/
|
|
8336
|
+
path: external_exports.string().max(500).optional()
|
|
6638
8337
|
}).passthrough();
|
|
6639
8338
|
var navigatorDescriptorSchema = external_exports.object({
|
|
6640
8339
|
name: external_exports.string(),
|
|
@@ -6681,6 +8380,50 @@ var loginSchema = external_exports.object({
|
|
|
6681
8380
|
loginSchema.extend({
|
|
6682
8381
|
name: external_exports.string().min(2, "Name must be at least 2 characters").max(100)
|
|
6683
8382
|
});
|
|
8383
|
+
external_exports.object({
|
|
8384
|
+
name: external_exports.string().min(2, "Name must be at least 2 characters").max(100).optional(),
|
|
8385
|
+
avatarUrl: external_exports.string().url("Invalid URL").max(2048).nullable().optional()
|
|
8386
|
+
}).refine((v) => v.name !== void 0 || v.avatarUrl !== void 0, {
|
|
8387
|
+
message: "At least one field must be provided"
|
|
8388
|
+
});
|
|
8389
|
+
external_exports.object({
|
|
8390
|
+
currentPassword: external_exports.string().min(1, "Current password is required"),
|
|
8391
|
+
newPassword: external_exports.string().min(8, "Password must be at least 8 characters")
|
|
8392
|
+
});
|
|
8393
|
+
var totpCodeSchema = external_exports.string().transform((v) => v.replace(/\s/g, "")).pipe(external_exports.string().regex(/^\d{6}$/, "Code must be 6 digits"));
|
|
8394
|
+
var recoveryCodeSchema = external_exports.string().transform((v) => v.toUpperCase().replace(/[^A-Z0-9]/g, "")).pipe(external_exports.string().regex(/^[23456789BCDFGHJKMNPQRSTVWXYZ]{10}$/, "Invalid recovery code"));
|
|
8395
|
+
external_exports.object({
|
|
8396
|
+
code: totpCodeSchema
|
|
8397
|
+
});
|
|
8398
|
+
external_exports.object({
|
|
8399
|
+
challengeToken: external_exports.string().min(1, "Challenge token is required"),
|
|
8400
|
+
code: totpCodeSchema.optional(),
|
|
8401
|
+
recoveryCode: recoveryCodeSchema.optional()
|
|
8402
|
+
}).refine((v) => Boolean(v.code) !== Boolean(v.recoveryCode), {
|
|
8403
|
+
message: "Provide either a TOTP code or a recovery code",
|
|
8404
|
+
path: ["code"]
|
|
8405
|
+
});
|
|
8406
|
+
external_exports.object({
|
|
8407
|
+
password: external_exports.string().min(1, "Password is required"),
|
|
8408
|
+
code: totpCodeSchema.optional(),
|
|
8409
|
+
recoveryCode: recoveryCodeSchema.optional()
|
|
8410
|
+
}).refine((v) => Boolean(v.code) !== Boolean(v.recoveryCode), {
|
|
8411
|
+
message: "Provide either a TOTP code or a recovery code",
|
|
8412
|
+
path: ["code"]
|
|
8413
|
+
});
|
|
8414
|
+
external_exports.object({
|
|
8415
|
+
password: external_exports.string().min(1, "Password is required"),
|
|
8416
|
+
code: totpCodeSchema
|
|
8417
|
+
});
|
|
8418
|
+
external_exports.object({
|
|
8419
|
+
password: external_exports.string().min(1, "Password is required"),
|
|
8420
|
+
code: totpCodeSchema.optional(),
|
|
8421
|
+
recoveryCode: recoveryCodeSchema.optional(),
|
|
8422
|
+
confirm: external_exports.literal("DELETE MY ACCOUNT")
|
|
8423
|
+
}).refine((v) => !(v.code && v.recoveryCode), {
|
|
8424
|
+
message: "Provide either a TOTP code or a recovery code, not both",
|
|
8425
|
+
path: ["code"]
|
|
8426
|
+
});
|
|
6684
8427
|
var createProjectSchema = external_exports.object({
|
|
6685
8428
|
name: external_exports.string().min(1).max(100),
|
|
6686
8429
|
description: external_exports.string().max(500).optional(),
|
|
@@ -6716,6 +8459,7 @@ external_exports.object({
|
|
|
6716
8459
|
content: external_exports.record(external_exports.unknown())
|
|
6717
8460
|
});
|
|
6718
8461
|
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
|
|
8462
|
+
var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
|
|
6719
8463
|
external_exports.object({
|
|
6720
8464
|
name: external_exports.string().min(1).max(100),
|
|
6721
8465
|
// Required for `sdk` (default), forbidden/ignored for `operator` —
|
|
@@ -6723,6 +8467,7 @@ external_exports.object({
|
|
|
6723
8467
|
// missing scope still validates as the pre-existing SDK shape.
|
|
6724
8468
|
projectId: external_exports.string().min(1).optional(),
|
|
6725
8469
|
scope: apiKeyScopeSchema.default("sdk"),
|
|
8470
|
+
environment: apiKeyEnvironmentSchema.optional(),
|
|
6726
8471
|
expiresAt: external_exports.string().datetime().optional()
|
|
6727
8472
|
}).refine((v) => v.scope !== "sdk" || !!v.projectId, {
|
|
6728
8473
|
message: 'projectId is required for scope "sdk"',
|
|
@@ -6730,13 +8475,23 @@ external_exports.object({
|
|
|
6730
8475
|
});
|
|
6731
8476
|
var boundedString = (max) => external_exports.string().max(max);
|
|
6732
8477
|
var clientPlatformSchema = external_exports.enum(["react-native", "web", "android", "ios"]).or(external_exports.string().min(1).max(40));
|
|
8478
|
+
var identityProvenanceSchema = external_exports.enum(["declared", "derived", "positional"]).or(external_exports.string().min(1).max(40));
|
|
6733
8479
|
var snapshotInputSchema = external_exports.object({
|
|
6734
8480
|
id: boundedString(160).optional(),
|
|
8481
|
+
provenance: identityProvenanceSchema.optional(),
|
|
8482
|
+
/**
|
|
8483
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
8484
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
8485
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
8486
|
+
* before the field existed), which must never be read as `false`.
|
|
8487
|
+
*/
|
|
8488
|
+
onScreen: external_exports.boolean().optional(),
|
|
6735
8489
|
label: boundedString(300).optional(),
|
|
6736
8490
|
value: boundedString(4096).optional(),
|
|
6737
8491
|
placeholder: boundedString(300).optional(),
|
|
6738
8492
|
editable: external_exports.boolean().optional(),
|
|
6739
8493
|
secure: external_exports.boolean().optional(),
|
|
8494
|
+
submitsOnReturn: external_exports.boolean().optional(),
|
|
6740
8495
|
type: boundedString(40).optional(),
|
|
6741
8496
|
required: external_exports.boolean().optional(),
|
|
6742
8497
|
invalid: external_exports.boolean().optional(),
|
|
@@ -6744,6 +8499,14 @@ var snapshotInputSchema = external_exports.object({
|
|
|
6744
8499
|
}).passthrough();
|
|
6745
8500
|
var snapshotButtonSchema = external_exports.object({
|
|
6746
8501
|
id: boundedString(160).optional(),
|
|
8502
|
+
provenance: identityProvenanceSchema.optional(),
|
|
8503
|
+
/**
|
|
8504
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
8505
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
8506
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
8507
|
+
* before the field existed), which must never be read as `false`.
|
|
8508
|
+
*/
|
|
8509
|
+
onScreen: external_exports.boolean().optional(),
|
|
6747
8510
|
label: boundedString(300).optional(),
|
|
6748
8511
|
disabled: external_exports.boolean().optional(),
|
|
6749
8512
|
inModal: external_exports.boolean().optional(),
|
|
@@ -6753,12 +8516,28 @@ var snapshotButtonSchema = external_exports.object({
|
|
|
6753
8516
|
}).passthrough();
|
|
6754
8517
|
var snapshotToggleSchema = external_exports.object({
|
|
6755
8518
|
id: boundedString(160).optional(),
|
|
8519
|
+
provenance: identityProvenanceSchema.optional(),
|
|
8520
|
+
/**
|
|
8521
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
8522
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
8523
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
8524
|
+
* before the field existed), which must never be read as `false`.
|
|
8525
|
+
*/
|
|
8526
|
+
onScreen: external_exports.boolean().optional(),
|
|
6756
8527
|
label: boundedString(300).optional(),
|
|
6757
8528
|
value: external_exports.boolean().optional(),
|
|
6758
8529
|
inModal: external_exports.boolean().optional()
|
|
6759
8530
|
}).passthrough();
|
|
6760
8531
|
var snapshotSliderSchema = external_exports.object({
|
|
6761
8532
|
id: boundedString(160).optional(),
|
|
8533
|
+
provenance: identityProvenanceSchema.optional(),
|
|
8534
|
+
/**
|
|
8535
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
8536
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
8537
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
8538
|
+
* before the field existed), which must never be read as `false`.
|
|
8539
|
+
*/
|
|
8540
|
+
onScreen: external_exports.boolean().optional(),
|
|
6762
8541
|
label: boundedString(300).optional(),
|
|
6763
8542
|
value: external_exports.number().finite().optional(),
|
|
6764
8543
|
min: external_exports.number().finite().optional(),
|
|
@@ -6799,6 +8578,26 @@ var snapshotListSchema = external_exports.object({
|
|
|
6799
8578
|
// should chase, and the relay only reads a known subset.
|
|
6800
8579
|
items: external_exports.array(external_exports.record(external_exports.unknown())).max(500).optional()
|
|
6801
8580
|
}).passthrough();
|
|
8581
|
+
var snapshotNativeDialogSchema = external_exports.object({
|
|
8582
|
+
id: boundedString(160),
|
|
8583
|
+
title: boundedString(300).optional(),
|
|
8584
|
+
message: boundedString(2e3).optional(),
|
|
8585
|
+
buttons: external_exports.array(
|
|
8586
|
+
external_exports.object({
|
|
8587
|
+
label: boundedString(160),
|
|
8588
|
+
style: external_exports.enum(["default", "cancel", "destructive"]).optional()
|
|
8589
|
+
}).passthrough()
|
|
8590
|
+
).max(10)
|
|
8591
|
+
}).passthrough();
|
|
8592
|
+
var snapshotScrollableSchema = external_exports.object({
|
|
8593
|
+
id: boundedString(160),
|
|
8594
|
+
containerType: boundedString(60).optional(),
|
|
8595
|
+
label: boundedString(300).optional(),
|
|
8596
|
+
scrollOffsetY: external_exports.number().finite().optional(),
|
|
8597
|
+
canScrollUp: external_exports.boolean().optional(),
|
|
8598
|
+
canScrollDown: external_exports.boolean().optional(),
|
|
8599
|
+
horizontal: external_exports.boolean().optional()
|
|
8600
|
+
}).passthrough();
|
|
6802
8601
|
var snapshotChoiceGroupSchema = external_exports.object({
|
|
6803
8602
|
index: external_exports.number().int().optional(),
|
|
6804
8603
|
id: boundedString(160).optional(),
|
|
@@ -6815,6 +8614,14 @@ var snapshotElementSchema = external_exports.object({
|
|
|
6815
8614
|
disabled: external_exports.boolean().optional(),
|
|
6816
8615
|
selected: external_exports.boolean().optional(),
|
|
6817
8616
|
source: boundedString(40).optional(),
|
|
8617
|
+
provenance: identityProvenanceSchema.optional(),
|
|
8618
|
+
/**
|
|
8619
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
8620
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
8621
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
8622
|
+
* before the field existed), which must never be read as `false`.
|
|
8623
|
+
*/
|
|
8624
|
+
onScreen: external_exports.boolean().optional(),
|
|
6818
8625
|
targetId: boundedString(160).optional(),
|
|
6819
8626
|
listContext: external_exports.record(external_exports.unknown()).optional(),
|
|
6820
8627
|
inModal: external_exports.boolean().optional()
|
|
@@ -6831,7 +8638,34 @@ var agentSnapshotSchema = external_exports.object({
|
|
|
6831
8638
|
modalOpen: external_exports.boolean().optional(),
|
|
6832
8639
|
lists: external_exports.array(snapshotListSchema).max(100).optional(),
|
|
6833
8640
|
choiceGroups: external_exports.array(snapshotChoiceGroupSchema).max(100).optional(),
|
|
6834
|
-
|
|
8641
|
+
scrollables: external_exports.array(snapshotScrollableSchema).max(20).optional(),
|
|
8642
|
+
nativeDialog: snapshotNativeDialogSchema.optional(),
|
|
8643
|
+
elements: external_exports.array(snapshotElementSchema).max(1e3).optional(),
|
|
8644
|
+
/**
|
|
8645
|
+
* The client clamped this observation to the caps above and lost
|
|
8646
|
+
* data doing it (`clampSnapshotToWireLimits` in
|
|
8647
|
+
* `@appilots/client-core`). Read by the prompt serializer so the
|
|
8648
|
+
* model is told the view is partial — a screen truncated at 500
|
|
8649
|
+
* texts must not be read as a screen that only has 500 things on
|
|
8650
|
+
* it. Optional: older SDKs never clamp and never send it.
|
|
8651
|
+
*/
|
|
8652
|
+
truncated: external_exports.boolean().optional(),
|
|
8653
|
+
/**
|
|
8654
|
+
* How many of this screen's controls the client could name, split by
|
|
8655
|
+
* `identityProvenance`. Diagnostic only — the relay never grounds an
|
|
8656
|
+
* action on it — but it is the number that says whether an app needs
|
|
8657
|
+
* annotating, and comparing it between a debug and a release build is
|
|
8658
|
+
* the first direct measurement of what minification costs the agent.
|
|
8659
|
+
*/
|
|
8660
|
+
stats: external_exports.object({
|
|
8661
|
+
visitedFibers: external_exports.number().int().nonnegative().optional(),
|
|
8662
|
+
skippedHidden: external_exports.number().int().nonnegative().optional(),
|
|
8663
|
+
identity: external_exports.object({
|
|
8664
|
+
declared: external_exports.number().int().nonnegative().optional(),
|
|
8665
|
+
derived: external_exports.number().int().nonnegative().optional(),
|
|
8666
|
+
positional: external_exports.number().int().nonnegative().optional()
|
|
8667
|
+
}).passthrough().optional()
|
|
8668
|
+
}).passthrough().optional()
|
|
6835
8669
|
}).passthrough();
|
|
6836
8670
|
var agentContextSchema = external_exports.object({
|
|
6837
8671
|
/**
|
|
@@ -6881,6 +8715,19 @@ var agentContextSchema = external_exports.object({
|
|
|
6881
8715
|
).max(200).optional(),
|
|
6882
8716
|
loadingPending: external_exports.boolean().optional()
|
|
6883
8717
|
}).passthrough();
|
|
8718
|
+
var introspectionFailureReasonSchema = external_exports.enum([
|
|
8719
|
+
"sentinel-missing-internals",
|
|
8720
|
+
"never-mounted",
|
|
8721
|
+
"render-crashed",
|
|
8722
|
+
"names-mangled",
|
|
8723
|
+
"walk-recognized-nothing"
|
|
8724
|
+
]);
|
|
8725
|
+
var introspectionDiagnosticsSchema = external_exports.object({
|
|
8726
|
+
captured: external_exports.boolean(),
|
|
8727
|
+
failureReason: introspectionFailureReasonSchema.nullish(),
|
|
8728
|
+
reactVersion: boundedString(32).nullish(),
|
|
8729
|
+
failureCount: external_exports.number().int().min(0).max(1e6).optional()
|
|
8730
|
+
});
|
|
6884
8731
|
external_exports.object({
|
|
6885
8732
|
content: external_exports.string().min(1).max(4096),
|
|
6886
8733
|
sessionId: external_exports.string().optional(),
|
|
@@ -6890,7 +8737,20 @@ external_exports.object({
|
|
|
6890
8737
|
* the active MCP doc and emits `mcp_version_mismatch` telemetry when
|
|
6891
8738
|
* they differ. Optional — older SDKs simply skip the check.
|
|
6892
8739
|
*/
|
|
6893
|
-
mcpVersion: external_exports.string().max(64).optional()
|
|
8740
|
+
mcpVersion: external_exports.string().max(64).optional(),
|
|
8741
|
+
/**
|
|
8742
|
+
* SDK-side introspection health. Only sent when it FAILED — a client
|
|
8743
|
+
* that reached the React tree omits the field entirely, so the happy
|
|
8744
|
+
* path costs nothing on the wire.
|
|
8745
|
+
*
|
|
8746
|
+
* Without this, an app whose introspection is dead sends a snapshot
|
|
8747
|
+
* that is empty but perfectly well-formed, indistinguishable from a
|
|
8748
|
+
* genuinely empty screen. The server emits
|
|
8749
|
+
* `introspection_unavailable` so the failure is visible to us instead
|
|
8750
|
+
* of waiting for a bug report. Optional — older SDKs simply never
|
|
8751
|
+
* send it.
|
|
8752
|
+
*/
|
|
8753
|
+
introspection: introspectionDiagnosticsSchema.optional()
|
|
6894
8754
|
});
|
|
6895
8755
|
var agentActionTypeSchema = external_exports.enum([
|
|
6896
8756
|
"navigate",
|
|
@@ -6901,11 +8761,22 @@ var agentActionTypeSchema = external_exports.enum([
|
|
|
6901
8761
|
"custom"
|
|
6902
8762
|
]);
|
|
6903
8763
|
var navigationPayloadSchema = external_exports.object({
|
|
6904
|
-
|
|
8764
|
+
// Optional ONLY so `goBack` can exist (#398): it returns to whatever
|
|
8765
|
+
// is beneath on the stack and has no destination to name. Every other
|
|
8766
|
+
// action is still required to carry one — enforced by the refine
|
|
8767
|
+
// below rather than by the field, so the error names the real problem
|
|
8768
|
+
// ("navigate needs a screenName") instead of a missing key.
|
|
8769
|
+
screenName: external_exports.string().min(1).optional(),
|
|
6905
8770
|
params: external_exports.record(external_exports.unknown()).optional(),
|
|
6906
8771
|
navigationAction: external_exports.enum(["push", "navigate", "replace", "goBack", "reset"]).default("navigate").optional(),
|
|
6907
8772
|
path: external_exports.array(external_exports.string().min(1)).optional()
|
|
6908
|
-
}).passthrough()
|
|
8773
|
+
}).passthrough().refine(
|
|
8774
|
+
(payload) => payload.navigationAction === "goBack" || typeof payload.screenName === "string" && payload.screenName.length > 0,
|
|
8775
|
+
{
|
|
8776
|
+
message: 'screenName is required for every navigationAction except "goBack"',
|
|
8777
|
+
path: ["screenName"]
|
|
8778
|
+
}
|
|
8779
|
+
);
|
|
6909
8780
|
var formFillFieldSchema = external_exports.object({
|
|
6910
8781
|
fieldId: external_exports.string().min(1),
|
|
6911
8782
|
fieldType: external_exports.enum(["text", "select", "toggle", "date", "number", "custom"]).default("text").optional(),
|
|
@@ -7239,6 +9110,8 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
7239
9110
|
inputs: external_exports.array(
|
|
7240
9111
|
external_exports.object({
|
|
7241
9112
|
id: external_exports.string().max(120),
|
|
9113
|
+
provenance: identityProvenanceSchema.optional(),
|
|
9114
|
+
onScreen: external_exports.boolean().optional(),
|
|
7242
9115
|
label: external_exports.string().max(200).optional(),
|
|
7243
9116
|
value: external_exports.string().max(2e3).optional(),
|
|
7244
9117
|
disabled: external_exports.boolean().optional(),
|
|
@@ -7252,6 +9125,8 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
7252
9125
|
buttons: external_exports.array(
|
|
7253
9126
|
external_exports.object({
|
|
7254
9127
|
id: external_exports.string().max(120),
|
|
9128
|
+
provenance: identityProvenanceSchema.optional(),
|
|
9129
|
+
onScreen: external_exports.boolean().optional(),
|
|
7255
9130
|
label: external_exports.string().max(200).optional(),
|
|
7256
9131
|
disabled: external_exports.boolean().optional(),
|
|
7257
9132
|
/** Risk metadata (e.g. "high") the SDK forwards for danger UI. */
|
|
@@ -7273,6 +9148,8 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
7273
9148
|
sliders: external_exports.array(
|
|
7274
9149
|
external_exports.object({
|
|
7275
9150
|
id: external_exports.string().max(120),
|
|
9151
|
+
provenance: identityProvenanceSchema.optional(),
|
|
9152
|
+
onScreen: external_exports.boolean().optional(),
|
|
7276
9153
|
label: external_exports.string().max(200).optional(),
|
|
7277
9154
|
value: external_exports.number().finite().optional(),
|
|
7278
9155
|
min: external_exports.number().finite().optional(),
|
|
@@ -7302,6 +9179,44 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
7302
9179
|
).max(200).optional()
|
|
7303
9180
|
}).passthrough()
|
|
7304
9181
|
).max(20).optional(),
|
|
9182
|
+
/**
|
|
9183
|
+
* Toggles exactly as the SDK snapshot reports them (#390).
|
|
9184
|
+
*
|
|
9185
|
+
* Their absence is why `generality-component-control-smarthome`
|
|
9186
|
+
* came back `errored` with a 422 instead of measuring anything:
|
|
9187
|
+
* this schema is `.strict()`, so a scenario that declares a switch
|
|
9188
|
+
* is rejected before the agent is ever asked. Which means the
|
|
9189
|
+
* corpus could not express the single control behind failure 02 of
|
|
9190
|
+
* the functional audit — "disable an agent; the switch never
|
|
9191
|
+
* moves". The one defect the eval most needed to catch was the one
|
|
9192
|
+
* shape it could not describe.
|
|
9193
|
+
*/
|
|
9194
|
+
toggles: external_exports.array(
|
|
9195
|
+
external_exports.object({
|
|
9196
|
+
id: external_exports.string().max(120),
|
|
9197
|
+
provenance: identityProvenanceSchema.optional(),
|
|
9198
|
+
onScreen: external_exports.boolean().optional(),
|
|
9199
|
+
label: external_exports.string().max(200).optional(),
|
|
9200
|
+
value: external_exports.boolean().optional(),
|
|
9201
|
+
disabled: external_exports.boolean().optional()
|
|
9202
|
+
})
|
|
9203
|
+
).max(100).optional(),
|
|
9204
|
+
/**
|
|
9205
|
+
* The interaction graph — the `el:*` ids the prompt tells the model
|
|
9206
|
+
* to prefer over ordinal handles. Passthrough per entry for the
|
|
9207
|
+
* same reason `lists` is: the SDK adds fields faster than this
|
|
9208
|
+
* schema should chase them.
|
|
9209
|
+
*/
|
|
9210
|
+
elements: external_exports.array(
|
|
9211
|
+
external_exports.object({
|
|
9212
|
+
id: external_exports.string().max(160),
|
|
9213
|
+
role: external_exports.string().max(40).optional(),
|
|
9214
|
+
label: external_exports.string().max(300).optional()
|
|
9215
|
+
}).passthrough()
|
|
9216
|
+
).max(500).optional(),
|
|
9217
|
+
/** Screen-level state the relay renders and branches on. */
|
|
9218
|
+
loading: external_exports.boolean().optional(),
|
|
9219
|
+
modalOpen: external_exports.boolean().optional(),
|
|
7305
9220
|
extra: external_exports.record(external_exports.unknown()).optional()
|
|
7306
9221
|
}).strict();
|
|
7307
9222
|
external_exports.object({
|
|
@@ -7342,8 +9257,36 @@ external_exports.object({
|
|
|
7342
9257
|
* heuristics, forces tool choice when the last user message carries a
|
|
7343
9258
|
* failure marker). Only meaningful alongside `history`.
|
|
7344
9259
|
*/
|
|
7345
|
-
isRecoveryHop: external_exports.boolean().optional()
|
|
9260
|
+
isRecoveryHop: external_exports.boolean().optional(),
|
|
9261
|
+
/**
|
|
9262
|
+
* Run this call against the application structure supplied here
|
|
9263
|
+
* instead of the project's active MCP document.
|
|
9264
|
+
*
|
|
9265
|
+
* Sandbox-only, and the relay honours it only on a `dryRun` call —
|
|
9266
|
+
* see `resolveMcpOverride` in apps/api. It exists because the eval
|
|
9267
|
+
* corpus could otherwise only ever describe ONE app: every scenario
|
|
9268
|
+
* resolves to the same seeded project, so every navigate destination,
|
|
9269
|
+
* every screen title and every bit of domain priming in the prompt
|
|
9270
|
+
* came from the same vehicle-management demo. A suite that can only
|
|
9271
|
+
* pose questions about one app cannot measure whether the agent works
|
|
9272
|
+
* on another one.
|
|
9273
|
+
*
|
|
9274
|
+
* Deliberately loose (`z.record`): the MCP document's shape is owned
|
|
9275
|
+
* by the generator, and pinning it here would turn every additive
|
|
9276
|
+
* generator change into a 422 in the sandbox.
|
|
9277
|
+
*/
|
|
9278
|
+
applicationStructure: external_exports.record(external_exports.unknown()).optional()
|
|
7346
9279
|
}).strict();
|
|
9280
|
+
var relayDiagnosticsSchema = external_exports.object({
|
|
9281
|
+
unverifiedTargets: external_exports.array(external_exports.string()),
|
|
9282
|
+
guardsFired: external_exports.array(external_exports.object({ guard: external_exports.string(), detail: external_exports.string().optional() })),
|
|
9283
|
+
repairMode: external_exports.enum(["repair", "strict", "disabled"]),
|
|
9284
|
+
promptVersion: external_exports.string(),
|
|
9285
|
+
// Defaulted for producers that predate the field: before it existed
|
|
9286
|
+
// every response had reached the model, so `true` is the correct
|
|
9287
|
+
// backfill.
|
|
9288
|
+
modelInvoked: external_exports.boolean().default(true)
|
|
9289
|
+
});
|
|
7347
9290
|
var sandboxTraceHopSchema = external_exports.object({
|
|
7348
9291
|
hop: external_exports.number().int().nonnegative(),
|
|
7349
9292
|
inputPreview: external_exports.string(),
|
|
@@ -7382,7 +9325,16 @@ external_exports.object({
|
|
|
7382
9325
|
/** Hop-by-hop trace. Always at least one entry. */
|
|
7383
9326
|
trace: external_exports.array(sandboxTraceHopSchema),
|
|
7384
9327
|
/** True when the run was a no-op (e.g. project has no MCP yet). */
|
|
7385
|
-
warning: external_exports.string().optional()
|
|
9328
|
+
warning: external_exports.string().optional(),
|
|
9329
|
+
/**
|
|
9330
|
+
* SPEC-046 — guard / hallucination telemetry for the run.
|
|
9331
|
+
*
|
|
9332
|
+
* ADDITIVE AND OPTIONAL, permanently: SDK and dashboard consumers
|
|
9333
|
+
* predate it, and older API deployments will not send it. Consumers
|
|
9334
|
+
* must treat an absent block as "no information", never as "no guards
|
|
9335
|
+
* fired".
|
|
9336
|
+
*/
|
|
9337
|
+
diagnostics: relayDiagnosticsSchema.optional()
|
|
7386
9338
|
});
|
|
7387
9339
|
external_exports.object({
|
|
7388
9340
|
name: external_exports.string().min(1).max(120),
|
|
@@ -7586,6 +9538,30 @@ external_exports.object({
|
|
|
7586
9538
|
name: external_exports.string().min(1).max(200).optional(),
|
|
7587
9539
|
identifiers: endUserIdentifiersSchema.optional()
|
|
7588
9540
|
});
|
|
9541
|
+
var MAX_EXTERNAL_USER_ID_LENGTH = 128;
|
|
9542
|
+
function normalizeExternalUserId(value) {
|
|
9543
|
+
let raw;
|
|
9544
|
+
if (typeof value === "string") raw = value;
|
|
9545
|
+
else if (typeof value === "number" && Number.isFinite(value)) raw = String(value);
|
|
9546
|
+
else return void 0;
|
|
9547
|
+
const trimmed = raw.trim();
|
|
9548
|
+
if (!trimmed) return void 0;
|
|
9549
|
+
if (trimmed.length > MAX_EXTERNAL_USER_ID_LENGTH) return void 0;
|
|
9550
|
+
return trimmed;
|
|
9551
|
+
}
|
|
9552
|
+
var MAX_DEVICE_INFO_BYTES = 4096;
|
|
9553
|
+
var sessionDeviceInfoSchema = external_exports.object({
|
|
9554
|
+
platform: external_exports.string().max(64).optional(),
|
|
9555
|
+
osVersion: external_exports.string().max(64).optional(),
|
|
9556
|
+
appVersion: external_exports.string().max(64).optional(),
|
|
9557
|
+
sdkVersion: external_exports.string().max(64).optional()
|
|
9558
|
+
}).passthrough().refine((info) => JSON.stringify(info).length <= MAX_DEVICE_INFO_BYTES, {
|
|
9559
|
+
message: `device_info exceeds ${MAX_DEVICE_INFO_BYTES} bytes`
|
|
9560
|
+
});
|
|
9561
|
+
external_exports.object({
|
|
9562
|
+
userId: external_exports.preprocess(normalizeExternalUserId, external_exports.string().optional()),
|
|
9563
|
+
deviceInfo: sessionDeviceInfoSchema.optional().catch(void 0)
|
|
9564
|
+
});
|
|
7589
9565
|
external_exports.object({
|
|
7590
9566
|
projectId: external_exports.string().min(1).max(64),
|
|
7591
9567
|
externalId: external_exports.string().min(1).max(128),
|
|
@@ -7593,6 +9569,27 @@ external_exports.object({
|
|
|
7593
9569
|
identifiers: endUserIdentifiersSchema.optional()
|
|
7594
9570
|
});
|
|
7595
9571
|
|
|
9572
|
+
// src/utils/stringify.ts
|
|
9573
|
+
function stringifyUnknown(value) {
|
|
9574
|
+
if (value instanceof Error) {
|
|
9575
|
+
const message = value.message || value.name;
|
|
9576
|
+
const cause = value.cause;
|
|
9577
|
+
if (cause instanceof Error && cause.message && cause.message !== message) {
|
|
9578
|
+
return `${message} (${cause.message})`;
|
|
9579
|
+
}
|
|
9580
|
+
return message;
|
|
9581
|
+
}
|
|
9582
|
+
if (typeof value === "string") return value;
|
|
9583
|
+
if (value === null || value === void 0) return String(value);
|
|
9584
|
+
if (typeof value !== "object") return String(value);
|
|
9585
|
+
try {
|
|
9586
|
+
const json = JSON.stringify(value);
|
|
9587
|
+
if (json && json !== "{}") return json;
|
|
9588
|
+
} catch {
|
|
9589
|
+
}
|
|
9590
|
+
return String(value);
|
|
9591
|
+
}
|
|
9592
|
+
|
|
7596
9593
|
// src/manifest/loadManifest.ts
|
|
7597
9594
|
var DEFAULT_MANIFEST_FILENAME = "appilots.manifest.json";
|
|
7598
9595
|
var manifestSchema = external_exports.object({
|
|
@@ -7601,7 +9598,7 @@ var manifestSchema = external_exports.object({
|
|
|
7601
9598
|
navigation: navigationGraphSchema.partial().optional()
|
|
7602
9599
|
}).passthrough();
|
|
7603
9600
|
async function loadManifest(rootDir, manifestPath) {
|
|
7604
|
-
const resolvedPath =
|
|
9601
|
+
const resolvedPath = path8__default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
|
|
7605
9602
|
let raw;
|
|
7606
9603
|
try {
|
|
7607
9604
|
raw = await readFile(resolvedPath, "utf-8");
|
|
@@ -7609,14 +9606,16 @@ async function loadManifest(rootDir, manifestPath) {
|
|
|
7609
9606
|
if (error?.code === "ENOENT") {
|
|
7610
9607
|
return { manifest: null, resolvedPath };
|
|
7611
9608
|
}
|
|
7612
|
-
throw new Error(
|
|
9609
|
+
throw new Error(
|
|
9610
|
+
`Failed to read Appilots manifest at ${resolvedPath}: ${stringifyUnknown(error)}`
|
|
9611
|
+
);
|
|
7613
9612
|
}
|
|
7614
9613
|
let parsedJson;
|
|
7615
9614
|
try {
|
|
7616
9615
|
parsedJson = JSON.parse(raw);
|
|
7617
9616
|
} catch (error) {
|
|
7618
9617
|
throw new Error(
|
|
7619
|
-
`Appilots manifest at ${resolvedPath} is not valid JSON: ${
|
|
9618
|
+
`Appilots manifest at ${resolvedPath} is not valid JSON: ${stringifyUnknown(error)}`
|
|
7620
9619
|
);
|
|
7621
9620
|
}
|
|
7622
9621
|
const result = manifestSchema.safeParse(parsedJson);
|
|
@@ -7630,11 +9629,47 @@ Each entry in "screens" must match the ScreenDescriptor shape (see docs/platform
|
|
|
7630
9629
|
}
|
|
7631
9630
|
return { manifest: result.data, resolvedPath };
|
|
7632
9631
|
}
|
|
9632
|
+
function mergeScreen(base, declared) {
|
|
9633
|
+
const fields = { ...base };
|
|
9634
|
+
for (const [key, value] of Object.entries(declared)) {
|
|
9635
|
+
if (value === void 0) continue;
|
|
9636
|
+
if (Array.isArray(value) && value.length === 0) continue;
|
|
9637
|
+
fields[key] = value;
|
|
9638
|
+
}
|
|
9639
|
+
const merged = fields;
|
|
9640
|
+
merged.targets = mergeById(base.targets, markManifestOrigin(declared.targets));
|
|
9641
|
+
merged.actions = mergeById(base.actions, declared.actions) ?? [];
|
|
9642
|
+
if (base.agentHints || declared.agentHints) {
|
|
9643
|
+
merged.agentHints = { ...base.agentHints, ...declared.agentHints };
|
|
9644
|
+
}
|
|
9645
|
+
return merged;
|
|
9646
|
+
}
|
|
9647
|
+
function mergeById(base, overlay) {
|
|
9648
|
+
if (!overlay || overlay.length === 0) return base;
|
|
9649
|
+
if (!base || base.length === 0) return overlay;
|
|
9650
|
+
const byId = /* @__PURE__ */ new Map();
|
|
9651
|
+
for (const entry of base) byId.set(entry.id, entry);
|
|
9652
|
+
for (const entry of overlay) {
|
|
9653
|
+
const existing = byId.get(entry.id);
|
|
9654
|
+
byId.set(entry.id, existing ? { ...existing, ...entry } : entry);
|
|
9655
|
+
}
|
|
9656
|
+
return Array.from(byId.values());
|
|
9657
|
+
}
|
|
9658
|
+
function markManifestOrigin(targets) {
|
|
9659
|
+
if (!targets || targets.length === 0) return targets;
|
|
9660
|
+
return targets.map((target) => ({ origin: "manifest", ...target }));
|
|
9661
|
+
}
|
|
7633
9662
|
function mergeManifestScreens(analyzerScreens, manifestScreens) {
|
|
7634
9663
|
if (!manifestScreens || manifestScreens.length === 0) return analyzerScreens;
|
|
7635
9664
|
const byName = /* @__PURE__ */ new Map();
|
|
7636
9665
|
for (const screen of analyzerScreens) byName.set(screen.name, screen);
|
|
7637
|
-
for (const
|
|
9666
|
+
for (const declared of manifestScreens) {
|
|
9667
|
+
const base = byName.get(declared.name);
|
|
9668
|
+
byName.set(
|
|
9669
|
+
declared.name,
|
|
9670
|
+
base ? mergeScreen(base, declared) : { ...declared, targets: markManifestOrigin(declared.targets) }
|
|
9671
|
+
);
|
|
9672
|
+
}
|
|
7638
9673
|
return Array.from(byName.values());
|
|
7639
9674
|
}
|
|
7640
9675
|
function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
|
|
@@ -7886,12 +9921,15 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
7886
9921
|
/**
|
|
7887
9922
|
* Select the `PlatformAnalyzer` implementation for a `.appilotsrc`
|
|
7888
9923
|
* `platform` value. `'react-native'` (or unset — the existing default)
|
|
7889
|
-
* gets the
|
|
7890
|
-
*
|
|
9924
|
+
* gets the RN Babel/JSX pipeline; `'web'` gets the React web (DOM +
|
|
9925
|
+
* React Router) pipeline; anything else gets the no-op generic
|
|
9926
|
+
* analyzer, relying entirely on a declared manifest. The manifest
|
|
9927
|
+
* still merges on top of every analyzer's output either way.
|
|
7891
9928
|
*/
|
|
7892
9929
|
static createPlatformAnalyzer(platform) {
|
|
7893
9930
|
const resolved = platform ?? "react-native";
|
|
7894
9931
|
if (resolved === "react-native") return new ReactNativePlatformAnalyzer();
|
|
9932
|
+
if (resolved === "web") return new ReactWebPlatformAnalyzer();
|
|
7895
9933
|
return new GenericPlatformAnalyzer(resolved);
|
|
7896
9934
|
}
|
|
7897
9935
|
/** Generate MCP documents from the project */
|
|
@@ -7921,7 +9959,12 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
7921
9959
|
}
|
|
7922
9960
|
const mergedScreens = mergeManifestScreens(analyzed.screens, manifest?.screens);
|
|
7923
9961
|
const navigation = mergeManifestNavigation(analyzed.navigation, manifest?.navigation);
|
|
7924
|
-
const agentReadyScreens = mergedScreens.map(
|
|
9962
|
+
const agentReadyScreens = mergedScreens.map(
|
|
9963
|
+
(screen) => enrichScreenForAgent({
|
|
9964
|
+
...screen,
|
|
9965
|
+
filePath: screen.filePath ? path8__default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
|
|
9966
|
+
})
|
|
9967
|
+
);
|
|
7925
9968
|
const projectInfo = await this.getProjectInfo();
|
|
7926
9969
|
const projectName = projectInfo.name;
|
|
7927
9970
|
console.log(`[MCPGenerator] Project name: ${projectName}`);
|
|
@@ -7944,13 +9987,10 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
7944
9987
|
};
|
|
7945
9988
|
const serialized = JSON.stringify(document, null, 2);
|
|
7946
9989
|
const checksum = this.calculateChecksum(serialized);
|
|
7947
|
-
const filePath =
|
|
7948
|
-
outputDir,
|
|
7949
|
-
`mcp-document.${this.options.format}`
|
|
7950
|
-
);
|
|
9990
|
+
const filePath = path8__default.resolve(outputDir, `mcp-document.${this.options.format}`);
|
|
7951
9991
|
await writeFile(filePath, serialized, "utf-8");
|
|
7952
9992
|
console.log(`[MCPGenerator] Document written to: ${filePath}`);
|
|
7953
|
-
const checksumFilePath =
|
|
9993
|
+
const checksumFilePath = path8__default.resolve(outputDir, ".appilots-checksum");
|
|
7954
9994
|
await writeFile(checksumFilePath, checksum, "utf-8");
|
|
7955
9995
|
console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
|
|
7956
9996
|
console.log("[MCPGenerator] Generation complete!");
|
|
@@ -7970,7 +10010,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
7970
10010
|
* after calling `generate()`.
|
|
7971
10011
|
*/
|
|
7972
10012
|
static async readPreviousChecksum(outputDir) {
|
|
7973
|
-
const checksumFilePath =
|
|
10013
|
+
const checksumFilePath = path8__default.resolve(outputDir, ".appilots-checksum");
|
|
7974
10014
|
try {
|
|
7975
10015
|
const content = await readFile(checksumFilePath, "utf-8");
|
|
7976
10016
|
return content.trim() || null;
|
|
@@ -7989,7 +10029,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
7989
10029
|
*/
|
|
7990
10030
|
async getProjectInfo() {
|
|
7991
10031
|
try {
|
|
7992
|
-
const packageJsonPath =
|
|
10032
|
+
const packageJsonPath = path8__default.resolve(this.analyzerConfig.rootDir, "package.json");
|
|
7993
10033
|
const packageJsonContent = await readFile(packageJsonPath, "utf-8");
|
|
7994
10034
|
const packageJson = JSON.parse(packageJsonContent);
|
|
7995
10035
|
return {
|
|
@@ -8002,6 +10042,70 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
8002
10042
|
}
|
|
8003
10043
|
}
|
|
8004
10044
|
};
|
|
10045
|
+
var DEFAULT_SERVER_URL = "http://localhost:4000";
|
|
10046
|
+
var KNOWN_CONFIG_KEYS = [
|
|
10047
|
+
"apiKey",
|
|
10048
|
+
"projectId",
|
|
10049
|
+
"serverUrl",
|
|
10050
|
+
"outputDir",
|
|
10051
|
+
"include",
|
|
10052
|
+
"exclude",
|
|
10053
|
+
"autoActivate",
|
|
10054
|
+
"strictScreens",
|
|
10055
|
+
"screenPatterns",
|
|
10056
|
+
"navigationInclude",
|
|
10057
|
+
"navigationExclude",
|
|
10058
|
+
"platform",
|
|
10059
|
+
"manifestPath",
|
|
10060
|
+
"eval"
|
|
10061
|
+
];
|
|
10062
|
+
var KEY_ALIASES = {
|
|
10063
|
+
apiUrl: "serverUrl",
|
|
10064
|
+
// `@appilots/sdk`'s own name for this. One `.appilotsrc` feeds both
|
|
10065
|
+
// packages, so an integrator who wrote the SDK's key should not be told
|
|
10066
|
+
// it is unrecognized.
|
|
10067
|
+
apiBaseUrl: "serverUrl",
|
|
10068
|
+
apiURL: "serverUrl",
|
|
10069
|
+
baseUrl: "serverUrl",
|
|
10070
|
+
url: "serverUrl",
|
|
10071
|
+
host: "serverUrl",
|
|
10072
|
+
endpoint: "serverUrl",
|
|
10073
|
+
server: "serverUrl",
|
|
10074
|
+
key: "apiKey",
|
|
10075
|
+
token: "apiKey",
|
|
10076
|
+
project: "projectId",
|
|
10077
|
+
sourceDir: "include",
|
|
10078
|
+
srcDir: "include",
|
|
10079
|
+
source: "include",
|
|
10080
|
+
outDir: "outputDir"
|
|
10081
|
+
};
|
|
10082
|
+
function editDistance(a, b) {
|
|
10083
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
10084
|
+
for (let i = 1; i <= a.length; i++) {
|
|
10085
|
+
const current = [i];
|
|
10086
|
+
for (let j = 1; j <= b.length; j++) {
|
|
10087
|
+
const deletion = (previous[j] ?? 0) + 1;
|
|
10088
|
+
const insertion = (current[j - 1] ?? 0) + 1;
|
|
10089
|
+
const substitution = (previous[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
|
|
10090
|
+
current.push(Math.min(deletion, insertion, substitution));
|
|
10091
|
+
}
|
|
10092
|
+
previous = current;
|
|
10093
|
+
}
|
|
10094
|
+
return previous[b.length] ?? 0;
|
|
10095
|
+
}
|
|
10096
|
+
function suggestConfigKey(key) {
|
|
10097
|
+
const alias = KEY_ALIASES[key];
|
|
10098
|
+
if (alias) return alias;
|
|
10099
|
+
const lower = key.toLowerCase();
|
|
10100
|
+
let best;
|
|
10101
|
+
for (const known of KNOWN_CONFIG_KEYS) {
|
|
10102
|
+
const distance = editDistance(lower, known.toLowerCase());
|
|
10103
|
+
if (distance <= 2 && (!best || distance < best.distance)) {
|
|
10104
|
+
best = { key: known, distance };
|
|
10105
|
+
}
|
|
10106
|
+
}
|
|
10107
|
+
return best?.key;
|
|
10108
|
+
}
|
|
8005
10109
|
function getEnvOverrides(env = process.env) {
|
|
8006
10110
|
const clean = (value) => {
|
|
8007
10111
|
const trimmed = value?.trim();
|
|
@@ -8013,7 +10117,7 @@ function getEnvOverrides(env = process.env) {
|
|
|
8013
10117
|
serverUrl: clean(env.APPILOTS_SERVER_URL)
|
|
8014
10118
|
};
|
|
8015
10119
|
}
|
|
8016
|
-
function loadConfig() {
|
|
10120
|
+
function loadConfig(onWarn) {
|
|
8017
10121
|
const configPath = getConfigPath();
|
|
8018
10122
|
const env = getEnvOverrides();
|
|
8019
10123
|
let fileConfig = {};
|
|
@@ -8021,13 +10125,15 @@ function loadConfig() {
|
|
|
8021
10125
|
try {
|
|
8022
10126
|
fileConfig = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
8023
10127
|
} catch (error) {
|
|
8024
|
-
throw new Error(
|
|
10128
|
+
throw new Error(
|
|
10129
|
+
`Failed to parse .appilotsrc at ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
10130
|
+
);
|
|
8025
10131
|
}
|
|
8026
10132
|
} else if (!env.apiKey) {
|
|
8027
10133
|
return null;
|
|
8028
10134
|
}
|
|
8029
10135
|
const merged = {
|
|
8030
|
-
serverUrl:
|
|
10136
|
+
serverUrl: DEFAULT_SERVER_URL,
|
|
8031
10137
|
outputDir: ".appilots",
|
|
8032
10138
|
autoActivate: true,
|
|
8033
10139
|
strictScreens: true,
|
|
@@ -8037,6 +10143,12 @@ function loadConfig() {
|
|
|
8037
10143
|
...env.serverUrl ? { serverUrl: env.serverUrl } : {}
|
|
8038
10144
|
};
|
|
8039
10145
|
const validation = validateConfig(merged);
|
|
10146
|
+
if (onWarn) {
|
|
10147
|
+
const source = configPath ?? "environment variables";
|
|
10148
|
+
for (const warning of validation.warnings) {
|
|
10149
|
+
onWarn(`${source}: ${warning}`);
|
|
10150
|
+
}
|
|
10151
|
+
}
|
|
8040
10152
|
if (!validation.valid) {
|
|
8041
10153
|
const source = configPath ?? "environment variables";
|
|
8042
10154
|
throw new Error(
|
|
@@ -8052,7 +10164,7 @@ function saveConfig(dir, config) {
|
|
|
8052
10164
|
const mergedConfig = {
|
|
8053
10165
|
apiKey: config.apiKey || existingConfig?.apiKey || "",
|
|
8054
10166
|
projectId: config.projectId || existingConfig?.projectId || "",
|
|
8055
|
-
serverUrl: config.serverUrl || existingConfig?.serverUrl ||
|
|
10167
|
+
serverUrl: config.serverUrl || existingConfig?.serverUrl || DEFAULT_SERVER_URL,
|
|
8056
10168
|
outputDir: config.outputDir || existingConfig?.outputDir || ".appilots",
|
|
8057
10169
|
autoActivate: config.autoActivate !== void 0 ? config.autoActivate : existingConfig?.autoActivate ?? true,
|
|
8058
10170
|
include: config.include || existingConfig?.include,
|
|
@@ -8068,7 +10180,9 @@ function saveConfig(dir, config) {
|
|
|
8068
10180
|
try {
|
|
8069
10181
|
writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
|
|
8070
10182
|
} catch (error) {
|
|
8071
|
-
throw new Error(
|
|
10183
|
+
throw new Error(
|
|
10184
|
+
`Failed to write .appilotsrc to ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
10185
|
+
);
|
|
8072
10186
|
}
|
|
8073
10187
|
}
|
|
8074
10188
|
function getConfigPath() {
|
|
@@ -8095,14 +10209,25 @@ function getConfigPath() {
|
|
|
8095
10209
|
}
|
|
8096
10210
|
function validateConfig(config) {
|
|
8097
10211
|
const errors = [];
|
|
10212
|
+
const warnings = [];
|
|
8098
10213
|
if (!config || typeof config !== "object") {
|
|
8099
10214
|
return {
|
|
8100
10215
|
valid: false,
|
|
8101
|
-
errors: ["Configuration must be an object"]
|
|
10216
|
+
errors: ["Configuration must be an object"],
|
|
10217
|
+
warnings
|
|
8102
10218
|
};
|
|
8103
10219
|
}
|
|
10220
|
+
for (const key of Object.keys(config)) {
|
|
10221
|
+
if (KNOWN_CONFIG_KEYS.includes(key)) continue;
|
|
10222
|
+
const suggestion = suggestConfigKey(key);
|
|
10223
|
+
warnings.push(
|
|
10224
|
+
`unknown key "${key}" \u2014 ignored` + (suggestion ? ` (did you mean "${suggestion}"?)` : "")
|
|
10225
|
+
);
|
|
10226
|
+
}
|
|
8104
10227
|
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
8105
|
-
errors.push(
|
|
10228
|
+
errors.push(
|
|
10229
|
+
"apiKey is required and must be a string (set it in .appilotsrc or via APPILOTS_API_KEY)"
|
|
10230
|
+
);
|
|
8106
10231
|
} else if (!config.apiKey.startsWith("ak_")) {
|
|
8107
10232
|
errors.push('apiKey must start with "ak_"');
|
|
8108
10233
|
}
|
|
@@ -8164,7 +10289,8 @@ function validateConfig(config) {
|
|
|
8164
10289
|
}
|
|
8165
10290
|
return {
|
|
8166
10291
|
valid: errors.length === 0,
|
|
8167
|
-
errors
|
|
10292
|
+
errors,
|
|
10293
|
+
warnings
|
|
8168
10294
|
};
|
|
8169
10295
|
}
|
|
8170
10296
|
|
|
@@ -8210,19 +10336,76 @@ function formatMetadataWarnings(warnings) {
|
|
|
8210
10336
|
);
|
|
8211
10337
|
}
|
|
8212
10338
|
|
|
10339
|
+
// ../shared/dist/chunk-UEMUBXFH.mjs
|
|
10340
|
+
var DEFAULT_API_BASE_URL = "https://api.appilots.com";
|
|
10341
|
+
var APPILOTS_API_PATH_PREFIX = "/api/v1";
|
|
10342
|
+
function normalizeApiBaseUrl(configured) {
|
|
10343
|
+
const raw = (configured ?? DEFAULT_API_BASE_URL).trim().replace(/\/+$/, "");
|
|
10344
|
+
if (!raw) return `${DEFAULT_API_BASE_URL}${APPILOTS_API_PATH_PREFIX}`;
|
|
10345
|
+
if (new RegExp(`${APPILOTS_API_PATH_PREFIX}(/|$)`).test(raw)) return raw;
|
|
10346
|
+
return `${raw}${APPILOTS_API_PATH_PREFIX}`;
|
|
10347
|
+
}
|
|
10348
|
+
|
|
8213
10349
|
// src/services/api-client.ts
|
|
8214
|
-
var
|
|
10350
|
+
var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
8215
10351
|
var DEFAULT_MAX_RETRIES = 2;
|
|
8216
10352
|
var RETRY_BASE_DELAY_MS = 500;
|
|
10353
|
+
function describeNetworkError(url, err) {
|
|
10354
|
+
let origin = url;
|
|
10355
|
+
try {
|
|
10356
|
+
origin = new URL(url).origin;
|
|
10357
|
+
} catch {
|
|
10358
|
+
}
|
|
10359
|
+
const cause = err?.cause;
|
|
10360
|
+
const code = typeof cause?.code === "string" ? cause.code : void 0;
|
|
10361
|
+
const detail = code ?? stringifyUnknown(err);
|
|
10362
|
+
return new Error(`Cannot reach Appilots server at ${origin} (${detail})`);
|
|
10363
|
+
}
|
|
10364
|
+
function errorCodeOf(payload) {
|
|
10365
|
+
const error = payload?.error;
|
|
10366
|
+
if (error && typeof error === "object") {
|
|
10367
|
+
const { code } = error;
|
|
10368
|
+
if (typeof code === "string" && code.trim()) return code;
|
|
10369
|
+
}
|
|
10370
|
+
return void 0;
|
|
10371
|
+
}
|
|
10372
|
+
function describeApiError(payload, fallback) {
|
|
10373
|
+
const error = payload?.error;
|
|
10374
|
+
if (typeof error === "string" && error.trim()) return error;
|
|
10375
|
+
if (error && typeof error === "object") {
|
|
10376
|
+
const { code, message: message2 } = error;
|
|
10377
|
+
const text = typeof message2 === "string" && message2.trim() ? message2 : void 0;
|
|
10378
|
+
const tag = typeof code === "string" && code.trim() ? code : void 0;
|
|
10379
|
+
if (text && tag) return `${text} (${tag})`;
|
|
10380
|
+
if (text) return text;
|
|
10381
|
+
if (tag) return tag;
|
|
10382
|
+
}
|
|
10383
|
+
const message = payload?.message;
|
|
10384
|
+
if (typeof message === "string" && message.trim()) return message;
|
|
10385
|
+
return fallback;
|
|
10386
|
+
}
|
|
8217
10387
|
var AppilotsAPIClient = class {
|
|
8218
|
-
|
|
10388
|
+
/**
|
|
10389
|
+
* `serverUrl` with the API's mount prefix resolved — every path below
|
|
10390
|
+
* is relative to THIS, not to the configured origin.
|
|
10391
|
+
*
|
|
10392
|
+
* It used to be the raw `serverUrl` with `/api/v1` hardcoded into each
|
|
10393
|
+
* path, which made the field mean something different here than in the
|
|
10394
|
+
* SDK. Both read the same `.appilotsrc`: the SDK completes the prefix
|
|
10395
|
+
* when it is missing, so `https://api.appilots.com/api/v1` is correct
|
|
10396
|
+
* there — and here that same value produced
|
|
10397
|
+
* `/api/v1/api/v1/cli/sync`, a 404 whose body reads `Route not found`.
|
|
10398
|
+
* Sharing `normalizeApiBaseUrl` is what makes one file mean one thing:
|
|
10399
|
+
* with or without the prefix now works in both.
|
|
10400
|
+
*/
|
|
10401
|
+
baseUrl;
|
|
8219
10402
|
apiKey;
|
|
8220
10403
|
timeoutMs;
|
|
8221
10404
|
maxRetries;
|
|
8222
10405
|
constructor(config) {
|
|
8223
|
-
this.
|
|
10406
|
+
this.baseUrl = normalizeApiBaseUrl(config.serverUrl);
|
|
8224
10407
|
this.apiKey = config.apiKey;
|
|
8225
|
-
this.timeoutMs = config.timeoutMs ??
|
|
10408
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
8226
10409
|
this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
8227
10410
|
}
|
|
8228
10411
|
/**
|
|
@@ -8246,7 +10429,7 @@ var AppilotsAPIClient = class {
|
|
|
8246
10429
|
}
|
|
8247
10430
|
return response;
|
|
8248
10431
|
} catch (err) {
|
|
8249
|
-
lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : err;
|
|
10432
|
+
lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : describeNetworkError(url, err);
|
|
8250
10433
|
} finally {
|
|
8251
10434
|
clearTimeout(timer);
|
|
8252
10435
|
}
|
|
@@ -8265,7 +10448,7 @@ var AppilotsAPIClient = class {
|
|
|
8265
10448
|
*/
|
|
8266
10449
|
async sync(content, version, appVersion) {
|
|
8267
10450
|
try {
|
|
8268
|
-
const response = await this.request(`${this.
|
|
10451
|
+
const response = await this.request(`${this.baseUrl}/cli/sync`, {
|
|
8269
10452
|
method: "POST",
|
|
8270
10453
|
headers: {
|
|
8271
10454
|
"Content-Type": "application/json",
|
|
@@ -8279,7 +10462,9 @@ var AppilotsAPIClient = class {
|
|
|
8279
10462
|
return {
|
|
8280
10463
|
success: false,
|
|
8281
10464
|
unchanged: false,
|
|
8282
|
-
error: errorData
|
|
10465
|
+
error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`),
|
|
10466
|
+
errorCode: errorCodeOf(errorData),
|
|
10467
|
+
errorStatus: response.status
|
|
8283
10468
|
};
|
|
8284
10469
|
}
|
|
8285
10470
|
const json = await response.json();
|
|
@@ -8292,7 +10477,8 @@ var AppilotsAPIClient = class {
|
|
|
8292
10477
|
return {
|
|
8293
10478
|
success: false,
|
|
8294
10479
|
unchanged: false,
|
|
8295
|
-
error: error instanceof Error ? error.message : "Failed to sync with Appilots API"
|
|
10480
|
+
error: error instanceof Error ? error.message : "Failed to sync with Appilots API",
|
|
10481
|
+
errorStatus: 0
|
|
8296
10482
|
};
|
|
8297
10483
|
}
|
|
8298
10484
|
}
|
|
@@ -8303,7 +10489,7 @@ var AppilotsAPIClient = class {
|
|
|
8303
10489
|
*/
|
|
8304
10490
|
async status() {
|
|
8305
10491
|
try {
|
|
8306
|
-
const response = await this.request(`${this.
|
|
10492
|
+
const response = await this.request(`${this.baseUrl}/cli/status`, {
|
|
8307
10493
|
method: "GET",
|
|
8308
10494
|
headers: {
|
|
8309
10495
|
Authorization: `Bearer ${this.apiKey}`
|
|
@@ -8312,7 +10498,7 @@ var AppilotsAPIClient = class {
|
|
|
8312
10498
|
if (!response.ok) {
|
|
8313
10499
|
const errorData = await response.json().catch(() => ({}));
|
|
8314
10500
|
return {
|
|
8315
|
-
error: errorData
|
|
10501
|
+
error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`)
|
|
8316
10502
|
};
|
|
8317
10503
|
}
|
|
8318
10504
|
const json = await response.json();
|
|
@@ -8334,7 +10520,7 @@ var AppilotsAPIClient = class {
|
|
|
8334
10520
|
*/
|
|
8335
10521
|
async evalRun(scenarios) {
|
|
8336
10522
|
try {
|
|
8337
|
-
const response = await this.request(`${this.
|
|
10523
|
+
const response = await this.request(`${this.baseUrl}/cli/eval/run`, {
|
|
8338
10524
|
method: "POST",
|
|
8339
10525
|
headers: {
|
|
8340
10526
|
"Content-Type": "application/json",
|
|
@@ -8365,7 +10551,7 @@ var AppilotsAPIClient = class {
|
|
|
8365
10551
|
*/
|
|
8366
10552
|
async health() {
|
|
8367
10553
|
try {
|
|
8368
|
-
const response = await this.request(`${this.
|
|
10554
|
+
const response = await this.request(`${this.baseUrl}/health`, {
|
|
8369
10555
|
method: "GET"
|
|
8370
10556
|
});
|
|
8371
10557
|
return response.ok;
|
|
@@ -8375,6 +10561,9 @@ var AppilotsAPIClient = class {
|
|
|
8375
10561
|
}
|
|
8376
10562
|
};
|
|
8377
10563
|
|
|
8378
|
-
|
|
10564
|
+
// src/version.ts
|
|
10565
|
+
var CLI_VERSION = "0.10.0";
|
|
10566
|
+
|
|
10567
|
+
export { AppilotsAPIClient, CLI_VERSION, ComponentAnalyzer, DEFAULT_MANIFEST_FILENAME, DEFAULT_WEB_SCREEN_PATTERNS, FormAnalyzer, GenericPlatformAnalyzer, MCPGenerator, NavigationAnalyzer, ReactNativePlatformAnalyzer, ReactWebPlatformAnalyzer, ScreenAnalyzer, WebNavigationAnalyzer, WebScreenAnalyzer, formatMetadataWarnings, getConfigPath, getEnvOverrides, lintActionMetadata, loadConfig, loadManifest, mergeManifestNavigation, mergeManifestScreens, resolvePathToScreen, saveConfig, screenNameFromPath, validateConfig };
|
|
8379
10568
|
//# sourceMappingURL=index.mjs.map
|
|
8380
10569
|
//# sourceMappingURL=index.mjs.map
|