@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.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var fs = require('fs/promises');
|
|
4
|
-
var
|
|
4
|
+
var path8 = require('path');
|
|
5
5
|
var traverse4 = require('@babel/traverse');
|
|
6
6
|
var BabelTypes = require('@babel/types');
|
|
7
7
|
var glob = require('fast-glob');
|
|
@@ -30,7 +30,7 @@ function _interopNamespace(e) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
33
|
-
var
|
|
33
|
+
var path8__namespace = /*#__PURE__*/_interopNamespace(path8);
|
|
34
34
|
var traverse4__default = /*#__PURE__*/_interopDefault(traverse4);
|
|
35
35
|
var BabelTypes__namespace = /*#__PURE__*/_interopNamespace(BabelTypes);
|
|
36
36
|
var glob__default = /*#__PURE__*/_interopDefault(glob);
|
|
@@ -83,6 +83,45 @@ function hasJsxAttribute(element, name) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// src/ast/jsx/classify.ts
|
|
86
|
+
var NON_INTERACTION_HANDLERS = /* @__PURE__ */ new Set([
|
|
87
|
+
"onLayout",
|
|
88
|
+
"onScroll",
|
|
89
|
+
"onScrollBeginDrag",
|
|
90
|
+
"onScrollEndDrag",
|
|
91
|
+
"onMomentumScrollBegin",
|
|
92
|
+
"onMomentumScrollEnd",
|
|
93
|
+
"onContentSizeChange",
|
|
94
|
+
"onEndReached",
|
|
95
|
+
"onViewableItemsChanged",
|
|
96
|
+
"onLoad",
|
|
97
|
+
"onLoadStart",
|
|
98
|
+
"onLoadEnd",
|
|
99
|
+
"onError",
|
|
100
|
+
"onProgress",
|
|
101
|
+
"onFocus",
|
|
102
|
+
"onBlur"
|
|
103
|
+
]);
|
|
104
|
+
var VALUE_HANDLERS = /* @__PURE__ */ new Set([
|
|
105
|
+
"onChange",
|
|
106
|
+
"onChangeText",
|
|
107
|
+
"onValueChange",
|
|
108
|
+
"onSelect",
|
|
109
|
+
"onSelectionChange",
|
|
110
|
+
"onSubmitEditing"
|
|
111
|
+
]);
|
|
112
|
+
function pressHandlerProp(element) {
|
|
113
|
+
let fallback;
|
|
114
|
+
for (const attr of element.attributes) {
|
|
115
|
+
if (attr.type !== "JSXAttribute" || attr.name.type !== "JSXIdentifier") continue;
|
|
116
|
+
const name = attr.name.name;
|
|
117
|
+
if (!/^on[A-Z]/.test(name)) continue;
|
|
118
|
+
if (attr.value?.type !== "JSXExpressionContainer") continue;
|
|
119
|
+
if (name === "onPress") return name;
|
|
120
|
+
if (NON_INTERACTION_HANDLERS.has(name) || VALUE_HANDLERS.has(name)) continue;
|
|
121
|
+
fallback ??= name;
|
|
122
|
+
}
|
|
123
|
+
return fallback;
|
|
124
|
+
}
|
|
86
125
|
var VIEW_COMPONENTS = /* @__PURE__ */ new Set(["View", "ScrollView", "SafeAreaView", "KeyboardAvoidingView"]);
|
|
87
126
|
var INPUT_COMPONENTS = /* @__PURE__ */ new Set(["TextInput", "Input", "HocInput"]);
|
|
88
127
|
var BUTTON_COMPONENTS = /* @__PURE__ */ new Set(["TouchableOpacity", "Pressable", "Button"]);
|
|
@@ -97,17 +136,16 @@ function classifyJsxComponent(name, element) {
|
|
|
97
136
|
if (INPUT_COMPONENTS.has(name)) return "input";
|
|
98
137
|
if (BUTTON_COMPONENTS.has(name)) return "button";
|
|
99
138
|
if (element) {
|
|
100
|
-
const hasOptions = hasJsxAttribute(element, "options");
|
|
139
|
+
const hasOptions = hasJsxAttribute(element, "options") || hasJsxAttribute(element, "segments") || hasJsxAttribute(element, "choices") || hasJsxAttribute(element, "tabs");
|
|
101
140
|
const hasValue = hasJsxAttribute(element, "value");
|
|
102
141
|
const hasChecked = hasJsxAttribute(element, "checked") || hasJsxAttribute(element, "selected");
|
|
103
142
|
const hasOnChange = hasJsxAttribute(element, "onChange") || hasJsxAttribute(element, "onValueChange") || hasJsxAttribute(element, "onChangeText");
|
|
104
|
-
const hasOnPress = hasJsxAttribute(element, "onPress");
|
|
105
143
|
if (hasOptions && (hasValue || hasOnChange)) return "select";
|
|
106
144
|
if (hasChecked && hasOnChange) return "toggle";
|
|
107
145
|
if (hasOnChange && (hasJsxAttribute(element, "label") || hasJsxAttribute(element, "placeholder"))) {
|
|
108
146
|
return "input";
|
|
109
147
|
}
|
|
110
|
-
if (
|
|
148
|
+
if (pressHandlerProp(element)) return "button";
|
|
111
149
|
if (getStringAttr(element, "visible") || getExpressionIdentifierAttr(element, "visible")) {
|
|
112
150
|
return "modal";
|
|
113
151
|
}
|
|
@@ -419,12 +457,12 @@ var ScreenAnalyzer = class {
|
|
|
419
457
|
cwd: this.config.rootDir,
|
|
420
458
|
ignore: exclude
|
|
421
459
|
});
|
|
422
|
-
screenPatternFiles = new Set(matched.map((f) =>
|
|
460
|
+
screenPatternFiles = new Set(matched.map((f) => path8__namespace.default.resolve(this.config.rootDir, f)));
|
|
423
461
|
}
|
|
424
462
|
const screens = [];
|
|
425
463
|
this.screensFilteredOut = 0;
|
|
426
464
|
for (const file of files) {
|
|
427
|
-
const filePath =
|
|
465
|
+
const filePath = path8__namespace.default.resolve(this.config.rootDir, file);
|
|
428
466
|
try {
|
|
429
467
|
const descriptor = await this.analyzeFile(filePath);
|
|
430
468
|
if (!descriptor) continue;
|
|
@@ -445,12 +483,17 @@ var ScreenAnalyzer = class {
|
|
|
445
483
|
}
|
|
446
484
|
} catch (error) {
|
|
447
485
|
if (this.verbose) {
|
|
448
|
-
console.warn(
|
|
486
|
+
console.warn(
|
|
487
|
+
`[ScreenAnalyzer] Failed to parse ${file}:`,
|
|
488
|
+
error instanceof Error ? error.message : error
|
|
489
|
+
);
|
|
449
490
|
}
|
|
450
491
|
}
|
|
451
492
|
}
|
|
452
493
|
if (this.verbose && this.strictScreens) {
|
|
453
|
-
console.log(
|
|
494
|
+
console.log(
|
|
495
|
+
`[ScreenAnalyzer] Strict mode: ${screens.length} included, ${this.screensFilteredOut} filtered out`
|
|
496
|
+
);
|
|
454
497
|
}
|
|
455
498
|
return screens;
|
|
456
499
|
}
|
|
@@ -738,7 +781,11 @@ var ScreenAnalyzer = class {
|
|
|
738
781
|
for (const form of secondary) {
|
|
739
782
|
const existing = out.find((candidate) => candidate.id === form.id) ?? this.findFormWithSharedFields(out, form);
|
|
740
783
|
if (!existing) {
|
|
741
|
-
out.push({
|
|
784
|
+
out.push({
|
|
785
|
+
...form,
|
|
786
|
+
fields: [...form.fields],
|
|
787
|
+
submitAction: this.namedSubmitAction(form.submitAction)
|
|
788
|
+
});
|
|
742
789
|
continue;
|
|
743
790
|
}
|
|
744
791
|
for (const field of form.fields) {
|
|
@@ -757,9 +804,12 @@ var ScreenAnalyzer = class {
|
|
|
757
804
|
findEquivalentField(fields, incoming) {
|
|
758
805
|
return fields.find((field) => {
|
|
759
806
|
if (field.name && incoming.name && field.name === incoming.name) return true;
|
|
760
|
-
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
761
|
-
|
|
762
|
-
if (field.
|
|
807
|
+
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
808
|
+
return true;
|
|
809
|
+
if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
|
|
810
|
+
return true;
|
|
811
|
+
if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
|
|
812
|
+
return true;
|
|
763
813
|
if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
|
|
764
814
|
return true;
|
|
765
815
|
}
|
|
@@ -919,9 +969,11 @@ var ScreenAnalyzer = class {
|
|
|
919
969
|
source: field.locator?.source ?? "accessibilityLabel"
|
|
920
970
|
});
|
|
921
971
|
} else if (attrName === "keyboardType") {
|
|
922
|
-
if (attr.value.value === "email-address" || attr.value.value === "email")
|
|
972
|
+
if (attr.value.value === "email-address" || attr.value.value === "email")
|
|
973
|
+
field.type = "email";
|
|
923
974
|
if (attr.value.value === "phone-pad") field.type = "phone";
|
|
924
|
-
if (attr.value.value === "numeric" || attr.value.value === "number-pad")
|
|
975
|
+
if (attr.value.value === "numeric" || attr.value.value === "number-pad")
|
|
976
|
+
field.type = "number";
|
|
925
977
|
}
|
|
926
978
|
} else if (BabelTypes__namespace.isJSXExpressionContainer(attr.value)) {
|
|
927
979
|
if (attrName === "value" && BabelTypes__namespace.isIdentifier(attr.value.expression)) {
|
|
@@ -1087,8 +1139,10 @@ var ScreenAnalyzer = class {
|
|
|
1087
1139
|
};
|
|
1088
1140
|
if (handler.nativeConfirmationExpected) action.nativeConfirmationExpected = true;
|
|
1089
1141
|
if (handler.targetScreen && !action.targetScreen) action.targetScreen = handler.targetScreen;
|
|
1090
|
-
if (handler.successSignal && !action.successSignal)
|
|
1091
|
-
|
|
1142
|
+
if (handler.successSignal && !action.successSignal)
|
|
1143
|
+
action.successSignal = handler.successSignal;
|
|
1144
|
+
if (handler.failureSignal && !action.failureSignal)
|
|
1145
|
+
action.failureSignal = handler.failureSignal;
|
|
1092
1146
|
if (handler.opensModal && !action.opensModal) action.opensModal = handler.opensModal;
|
|
1093
1147
|
if (handler.destructive || action.destructive === true || action.requiresConfirmation === true || action.effect === "destructive" || action.riskLevel === "high") {
|
|
1094
1148
|
action.destructive = true;
|
|
@@ -1106,6 +1160,7 @@ var ScreenAnalyzer = class {
|
|
|
1106
1160
|
if (!BabelTypes__namespace.isJSXIdentifier(element.name)) return;
|
|
1107
1161
|
const componentName = element.name.name;
|
|
1108
1162
|
if (classifyJsxComponent(componentName, element) !== "button") return;
|
|
1163
|
+
const pressProp = pressHandlerProp(element);
|
|
1109
1164
|
let label;
|
|
1110
1165
|
let handler;
|
|
1111
1166
|
for (const attr of element.attributes) {
|
|
@@ -1115,7 +1170,7 @@ var ScreenAnalyzer = class {
|
|
|
1115
1170
|
if ((attrName === "title" || attrName === "accessibilityLabel" || attrName === "label") && BabelTypes__namespace.isStringLiteral(attr.value)) {
|
|
1116
1171
|
label = attr.value.value;
|
|
1117
1172
|
}
|
|
1118
|
-
if (attrName ===
|
|
1173
|
+
if (attrName === pressProp && BabelTypes__namespace.isJSXExpressionContainer(attr.value) && BabelTypes__namespace.isIdentifier(attr.value.expression)) {
|
|
1119
1174
|
handler = attr.value.expression.name;
|
|
1120
1175
|
}
|
|
1121
1176
|
}
|
|
@@ -1181,10 +1236,13 @@ var ScreenAnalyzer = class {
|
|
|
1181
1236
|
}
|
|
1182
1237
|
};
|
|
1183
1238
|
if (fn.body) {
|
|
1184
|
-
traverse4__default.default(
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1239
|
+
traverse4__default.default(
|
|
1240
|
+
fn.body,
|
|
1241
|
+
{
|
|
1242
|
+
noScope: true,
|
|
1243
|
+
enter: (nodePath) => inspectNode(nodePath.node)
|
|
1244
|
+
}
|
|
1245
|
+
);
|
|
1188
1246
|
}
|
|
1189
1247
|
const expectedOutcome = targetScreen ? "navigation" : hasStateSetter ? "inline-feedback" : hasAwait || hasThen ? "mixed" : "none";
|
|
1190
1248
|
return {
|
|
@@ -1213,7 +1271,7 @@ var ScreenAnalyzer = class {
|
|
|
1213
1271
|
* heuristic must hit OR the JSDoc tag must be present.
|
|
1214
1272
|
*/
|
|
1215
1273
|
isElementDestructive(element, handlerName, actionId) {
|
|
1216
|
-
const
|
|
1274
|
+
const DESTRUCTIVE_VERB3 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
|
|
1217
1275
|
for (const attr of element.attributes) {
|
|
1218
1276
|
if (!BabelTypes__namespace.isJSXAttribute(attr)) continue;
|
|
1219
1277
|
const attrName = BabelTypes__namespace.isJSXIdentifier(attr.name) ? attr.name.name : null;
|
|
@@ -1228,8 +1286,8 @@ var ScreenAnalyzer = class {
|
|
|
1228
1286
|
}
|
|
1229
1287
|
}
|
|
1230
1288
|
}
|
|
1231
|
-
if (handlerName &&
|
|
1232
|
-
if (actionId &&
|
|
1289
|
+
if (handlerName && DESTRUCTIVE_VERB3.test(handlerName)) return true;
|
|
1290
|
+
if (actionId && DESTRUCTIVE_VERB3.test(actionId)) return true;
|
|
1233
1291
|
return false;
|
|
1234
1292
|
}
|
|
1235
1293
|
/**
|
|
@@ -1242,6 +1300,7 @@ var ScreenAnalyzer = class {
|
|
|
1242
1300
|
};
|
|
1243
1301
|
const componentName = BabelTypes__namespace.isJSXIdentifier(element.name) ? element.name.name : void 0;
|
|
1244
1302
|
action.sourceComponent = componentName;
|
|
1303
|
+
const pressProp = pressHandlerProp(element) ?? "onPress";
|
|
1245
1304
|
let handlerName;
|
|
1246
1305
|
for (const attr of element.attributes) {
|
|
1247
1306
|
if (!BabelTypes__namespace.isJSXAttribute(attr)) continue;
|
|
@@ -1276,7 +1335,7 @@ var ScreenAnalyzer = class {
|
|
|
1276
1335
|
}
|
|
1277
1336
|
}
|
|
1278
1337
|
} else if (BabelTypes__namespace.isJSXExpressionContainer(attr.value)) {
|
|
1279
|
-
if (attrName ===
|
|
1338
|
+
if (attrName === pressProp && BabelTypes__namespace.isIdentifier(attr.value.expression)) {
|
|
1280
1339
|
handlerName = attr.value.expression.name.toLowerCase();
|
|
1281
1340
|
action.handler = attr.value.expression.name;
|
|
1282
1341
|
if (handlerName.includes("submit")) {
|
|
@@ -1284,7 +1343,7 @@ var ScreenAnalyzer = class {
|
|
|
1284
1343
|
} else if (handlerName.includes("navigate")) {
|
|
1285
1344
|
action.type = "navigation";
|
|
1286
1345
|
}
|
|
1287
|
-
} else if (attrName ===
|
|
1346
|
+
} else if (attrName === pressProp) {
|
|
1288
1347
|
const inline = this.extractInlineOnPressMetadata(attr.value.expression);
|
|
1289
1348
|
if (inline.handler) {
|
|
1290
1349
|
handlerName = inline.handler.toLowerCase();
|
|
@@ -1308,7 +1367,11 @@ var ScreenAnalyzer = class {
|
|
|
1308
1367
|
}
|
|
1309
1368
|
}
|
|
1310
1369
|
if (!action.locator && action.id) {
|
|
1311
|
-
action.locator = {
|
|
1370
|
+
action.locator = {
|
|
1371
|
+
id: action.id,
|
|
1372
|
+
label: action.label,
|
|
1373
|
+
source: action.label ? "label" : "inferred"
|
|
1374
|
+
};
|
|
1312
1375
|
}
|
|
1313
1376
|
if (this.isElementDestructive(element, handlerName, action.id)) {
|
|
1314
1377
|
action.destructive = true;
|
|
@@ -1450,24 +1513,27 @@ var ScreenAnalyzer = class {
|
|
|
1450
1513
|
}
|
|
1451
1514
|
extractRowAction(fn) {
|
|
1452
1515
|
let action;
|
|
1453
|
-
traverse4__default.default(
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1516
|
+
traverse4__default.default(
|
|
1517
|
+
fn.body,
|
|
1518
|
+
{
|
|
1519
|
+
noScope: true,
|
|
1520
|
+
CallExpression: (nodePath) => {
|
|
1521
|
+
const node = nodePath.node;
|
|
1522
|
+
if (!BabelTypes__namespace.isMemberExpression(node.callee) || !BabelTypes__namespace.isIdentifier(node.callee.object) || node.callee.object.name !== "navigation" || !BabelTypes__namespace.isIdentifier(node.callee.property) || !["navigate", "push", "replace"].includes(node.callee.property.name)) {
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
const firstArg = node.arguments[0];
|
|
1526
|
+
if (!BabelTypes__namespace.isStringLiteral(firstArg)) return;
|
|
1527
|
+
const params = this.extractNavigationParams(node.arguments[1]);
|
|
1528
|
+
action = {
|
|
1529
|
+
type: "navigation",
|
|
1530
|
+
targetScreen: firstArg.value,
|
|
1531
|
+
...Object.keys(params).length > 0 ? { params } : {},
|
|
1532
|
+
description: `Pressing a row opens ${firstArg.value}`
|
|
1533
|
+
};
|
|
1459
1534
|
}
|
|
1460
|
-
const firstArg = node.arguments[0];
|
|
1461
|
-
if (!BabelTypes__namespace.isStringLiteral(firstArg)) return;
|
|
1462
|
-
const params = this.extractNavigationParams(node.arguments[1]);
|
|
1463
|
-
action = {
|
|
1464
|
-
type: "navigation",
|
|
1465
|
-
targetScreen: firstArg.value,
|
|
1466
|
-
...Object.keys(params).length > 0 ? { params } : {},
|
|
1467
|
-
description: `Pressing a row opens ${firstArg.value}`
|
|
1468
|
-
};
|
|
1469
1535
|
}
|
|
1470
|
-
|
|
1536
|
+
);
|
|
1471
1537
|
return action;
|
|
1472
1538
|
}
|
|
1473
1539
|
extractNavigationParams(arg) {
|
|
@@ -1500,15 +1566,18 @@ var ScreenAnalyzer = class {
|
|
|
1500
1566
|
} else if (BabelTypes__namespace.isIdentifier(firstParam)) {
|
|
1501
1567
|
itemNames.add(firstParam.name);
|
|
1502
1568
|
}
|
|
1503
|
-
traverse4__default.default(
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1569
|
+
traverse4__default.default(
|
|
1570
|
+
fn.body,
|
|
1571
|
+
{
|
|
1572
|
+
noScope: true,
|
|
1573
|
+
MemberExpression: (nodePath) => {
|
|
1574
|
+
const node = nodePath.node;
|
|
1575
|
+
if (BabelTypes__namespace.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes__namespace.isIdentifier(node.property)) {
|
|
1576
|
+
fields.add(node.property.name);
|
|
1577
|
+
}
|
|
1509
1578
|
}
|
|
1510
1579
|
}
|
|
1511
|
-
|
|
1580
|
+
);
|
|
1512
1581
|
return Array.from(fields).sort();
|
|
1513
1582
|
}
|
|
1514
1583
|
inferItemType(dataSource, renderItem, displayFields) {
|
|
@@ -1520,11 +1589,19 @@ var ScreenAnalyzer = class {
|
|
|
1520
1589
|
if (displayFields.length > 0) return "Item";
|
|
1521
1590
|
return void 0;
|
|
1522
1591
|
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Identity comes from how a field is NAMED, not from what the app
|
|
1594
|
+
* sells: `id`/`uuid`/`key`/`slug` are conventions any codebase uses,
|
|
1595
|
+
* `name`/`title`/`email` are how any row introduces itself. `plate`
|
|
1596
|
+
* used to sit in this list and read like one of them — but it is the
|
|
1597
|
+
* example app's schema, and no other tenant ever got its equivalent
|
|
1598
|
+
* (`mrn`, `trackingNumber`, `sku`) added here.
|
|
1599
|
+
*/
|
|
1523
1600
|
inferIdentityFields(keyField, displayFields) {
|
|
1524
1601
|
const out = /* @__PURE__ */ new Set();
|
|
1525
1602
|
if (keyField) out.add(keyField);
|
|
1526
1603
|
for (const field of displayFields) {
|
|
1527
|
-
if (/^(id|uuid|key|name|title|
|
|
1604
|
+
if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
|
|
1528
1605
|
}
|
|
1529
1606
|
return Array.from(out);
|
|
1530
1607
|
}
|
|
@@ -1535,17 +1612,23 @@ var ScreenAnalyzer = class {
|
|
|
1535
1612
|
CallExpression: (nodePath) => {
|
|
1536
1613
|
const node = nodePath.node;
|
|
1537
1614
|
if (!BabelTypes__namespace.isMemberExpression(node.callee)) return;
|
|
1538
|
-
if (!BabelTypes__namespace.isIdentifier(node.callee.property) || node.callee.property.name !== "filter")
|
|
1539
|
-
|
|
1615
|
+
if (!BabelTypes__namespace.isIdentifier(node.callee.property) || node.callee.property.name !== "filter")
|
|
1616
|
+
return;
|
|
1617
|
+
if (!BabelTypes__namespace.isIdentifier(node.callee.object) || node.callee.object.name !== dataSource)
|
|
1618
|
+
return;
|
|
1540
1619
|
const fn = node.arguments[0];
|
|
1541
|
-
if (!BabelTypes__namespace.isArrowFunctionExpression(fn) && !BabelTypes__namespace.isFunctionExpression(fn))
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1620
|
+
if (!BabelTypes__namespace.isArrowFunctionExpression(fn) && !BabelTypes__namespace.isFunctionExpression(fn))
|
|
1621
|
+
return;
|
|
1622
|
+
traverse4__default.default(
|
|
1623
|
+
fn.body,
|
|
1624
|
+
{
|
|
1625
|
+
noScope: true,
|
|
1626
|
+
Identifier: (innerPath) => {
|
|
1627
|
+
const name = innerPath.node.name;
|
|
1628
|
+
if (/query|search|filter/i.test(name)) queryBinding = queryBinding ?? name;
|
|
1629
|
+
}
|
|
1547
1630
|
}
|
|
1548
|
-
|
|
1631
|
+
);
|
|
1549
1632
|
}
|
|
1550
1633
|
});
|
|
1551
1634
|
return queryBinding;
|
|
@@ -1573,7 +1656,7 @@ var ScreenAnalyzer = class {
|
|
|
1573
1656
|
* E.g., /src/screens/ItemListScreen.tsx -> ItemListScreen
|
|
1574
1657
|
*/
|
|
1575
1658
|
extractScreenName(filePath) {
|
|
1576
|
-
const basename2 =
|
|
1659
|
+
const basename2 = path8__namespace.default.basename(filePath);
|
|
1577
1660
|
return basename2.replace(/\.(tsx?|jsx?)$/, "");
|
|
1578
1661
|
}
|
|
1579
1662
|
};
|
|
@@ -1628,7 +1711,7 @@ var NavigationAnalyzer = class {
|
|
|
1628
1711
|
cwd: this.config.rootDir,
|
|
1629
1712
|
ignore: excludePatterns
|
|
1630
1713
|
});
|
|
1631
|
-
return files.map((file) =>
|
|
1714
|
+
return files.map((file) => path8__namespace.default.join(this.config.rootDir, file));
|
|
1632
1715
|
}
|
|
1633
1716
|
/** Parse navigator definitions from a file */
|
|
1634
1717
|
parseNavigators(content, filePath) {
|
|
@@ -1689,11 +1772,7 @@ var NavigationAnalyzer = class {
|
|
|
1689
1772
|
if (BabelTypes__namespace.isJSXAttribute(initialRouteAttr) && BabelTypes__namespace.isStringLiteral(initialRouteAttr.value)) {
|
|
1690
1773
|
navigator.initialRouteName = initialRouteAttr.value.value;
|
|
1691
1774
|
}
|
|
1692
|
-
const screens = this.extractScreensFromNavigator(
|
|
1693
|
-
node,
|
|
1694
|
-
objectName,
|
|
1695
|
-
navigator.type
|
|
1696
|
-
);
|
|
1775
|
+
const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
|
|
1697
1776
|
screensByNavigator.set(objectName, screens);
|
|
1698
1777
|
}
|
|
1699
1778
|
}
|
|
@@ -1710,27 +1789,112 @@ var NavigationAnalyzer = class {
|
|
|
1710
1789
|
}
|
|
1711
1790
|
return navigators;
|
|
1712
1791
|
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Is this JSX element `<navigatorVarName.MEMBER …>`?
|
|
1794
|
+
*/
|
|
1795
|
+
// Deliberately NOT a type predicate (`node is t.JSXElement`): a false
|
|
1796
|
+
// result would then narrow `node` to "not a JSXElement", and the very
|
|
1797
|
+
// next check — the same node against a different member — would see
|
|
1798
|
+
// `never`. It is a question about the member name, not about the node
|
|
1799
|
+
// kind.
|
|
1800
|
+
isNavigatorMember(node, navigatorVarName, member) {
|
|
1801
|
+
if (!BabelTypes__namespace.isJSXElement(node)) return false;
|
|
1802
|
+
const name = node.openingElement.name;
|
|
1803
|
+
return BabelTypes__namespace.isJSXMemberExpression(name) && BabelTypes__namespace.isJSXIdentifier(name.object) && name.object.name === navigatorVarName && BabelTypes__namespace.isJSXIdentifier(name.property) && name.property.name === member;
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Flatten a navigator's JSX children into the `<X.Screen>` elements they
|
|
1807
|
+
* contain, unwrapping every container a real app puts in between.
|
|
1808
|
+
*
|
|
1809
|
+
* The old version compared `t.isJSXElement(child)` against the direct
|
|
1810
|
+
* children only. A ternary is a `JSXExpressionContainer`, so an
|
|
1811
|
+
* auth-gated root — the modal shape of a commercial app, and the shape
|
|
1812
|
+
* of this repo's own `apps/example-app` — contributed ZERO screens to
|
|
1813
|
+
* the graph while `appilots sync` reported success (#396).
|
|
1814
|
+
*
|
|
1815
|
+
* Both branches of a conditional are collected on purpose. The graph is
|
|
1816
|
+
* a design-time map of what routes EXIST, not a prediction of which one
|
|
1817
|
+
* a given session will render; the agent needs the destination name to
|
|
1818
|
+
* be there whether or not the user happens to be logged in right now.
|
|
1819
|
+
*/
|
|
1820
|
+
collectScreenElements(children, navigatorVarName, depth = 0) {
|
|
1821
|
+
if (depth > 12) return [];
|
|
1822
|
+
const found = [];
|
|
1823
|
+
const visit = (node) => {
|
|
1824
|
+
if (!node) return;
|
|
1825
|
+
if (this.isNavigatorMember(node, navigatorVarName, "Screen")) {
|
|
1826
|
+
found.push(node);
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
if (this.isNavigatorMember(node, navigatorVarName, "Group")) {
|
|
1830
|
+
const group = node;
|
|
1831
|
+
found.push(...this.collectScreenElements(group.children, navigatorVarName, depth + 1));
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
if (BabelTypes__namespace.isJSXElement(node)) {
|
|
1835
|
+
const name = node.openingElement.name;
|
|
1836
|
+
const isForeignNavigator = BabelTypes__namespace.isJSXMemberExpression(name) && BabelTypes__namespace.isJSXIdentifier(name.property) && name.property.name === "Navigator";
|
|
1837
|
+
if (isForeignNavigator) return;
|
|
1838
|
+
found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
if (BabelTypes__namespace.isJSXFragment(node)) {
|
|
1842
|
+
found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
if (BabelTypes__namespace.isJSXExpressionContainer(node)) {
|
|
1846
|
+
visit(node.expression);
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
if (BabelTypes__namespace.isConditionalExpression(node)) {
|
|
1850
|
+
visit(node.consequent);
|
|
1851
|
+
visit(node.alternate);
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
if (BabelTypes__namespace.isLogicalExpression(node)) {
|
|
1855
|
+
visit(node.left);
|
|
1856
|
+
visit(node.right);
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
if (BabelTypes__namespace.isCallExpression(node)) {
|
|
1860
|
+
for (const arg of node.arguments) {
|
|
1861
|
+
if (BabelTypes__namespace.isArrowFunctionExpression(arg) || BabelTypes__namespace.isFunctionExpression(arg)) {
|
|
1862
|
+
if (BabelTypes__namespace.isBlockStatement(arg.body)) {
|
|
1863
|
+
for (const stmt of arg.body.body) {
|
|
1864
|
+
if (BabelTypes__namespace.isReturnStatement(stmt)) visit(stmt.argument);
|
|
1865
|
+
}
|
|
1866
|
+
} else {
|
|
1867
|
+
visit(arg.body);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
if (BabelTypes__namespace.isArrayExpression(node)) {
|
|
1874
|
+
for (const el of node.elements) visit(el);
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
if (BabelTypes__namespace.isTSAsExpression(node) || BabelTypes__namespace.isTSNonNullExpression(node)) {
|
|
1878
|
+
visit(node.expression);
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
for (const child of children) visit(child);
|
|
1882
|
+
return found;
|
|
1883
|
+
}
|
|
1713
1884
|
/** Extract screens from a navigator JSX element */
|
|
1714
1885
|
extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
|
|
1715
1886
|
const screens = [];
|
|
1716
1887
|
if (!navigatorElement.children) return screens;
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
name: screenName,
|
|
1728
|
-
navigatorName: navigatorVarName,
|
|
1729
|
-
navigatorType
|
|
1730
|
-
});
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1888
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1889
|
+
for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
|
|
1890
|
+
const screenName = this.extractAttributeValue(element.openingElement.attributes, "name");
|
|
1891
|
+
if (!screenName || seen.has(screenName)) continue;
|
|
1892
|
+
seen.add(screenName);
|
|
1893
|
+
screens.push({
|
|
1894
|
+
name: screenName,
|
|
1895
|
+
navigatorName: navigatorVarName,
|
|
1896
|
+
navigatorType
|
|
1897
|
+
});
|
|
1734
1898
|
}
|
|
1735
1899
|
return screens;
|
|
1736
1900
|
}
|
|
@@ -1789,9 +1953,7 @@ var NavigationAnalyzer = class {
|
|
|
1789
1953
|
if (member.type === "TSPropertySignature" && member.key) {
|
|
1790
1954
|
const keyName = member.key.type === "Identifier" ? member.key.name : null;
|
|
1791
1955
|
if (keyName && member.typeAnnotation) {
|
|
1792
|
-
const params = this.extractParamsFromType(
|
|
1793
|
-
member.typeAnnotation.typeAnnotation
|
|
1794
|
-
);
|
|
1956
|
+
const params = this.extractParamsFromType(member.typeAnnotation.typeAnnotation);
|
|
1795
1957
|
entries.set(keyName, params);
|
|
1796
1958
|
}
|
|
1797
1959
|
}
|
|
@@ -1831,7 +1993,7 @@ var NavigationAnalyzer = class {
|
|
|
1831
1993
|
if (type.type === "TSUndefinedKeyword") return "undefined";
|
|
1832
1994
|
if (type.type === "TSNullKeyword") return "null";
|
|
1833
1995
|
if (type.type === "TSUnionType") {
|
|
1834
|
-
return type.types.map((
|
|
1996
|
+
return type.types.map((t12) => this.typeToString(t12)).join(" | ");
|
|
1835
1997
|
}
|
|
1836
1998
|
if (type.type === "TSTypeLiteral") {
|
|
1837
1999
|
return "object";
|
|
@@ -1852,7 +2014,7 @@ var NavigationAnalyzer = class {
|
|
|
1852
2014
|
/** Attach parsed type params to navigator screens */
|
|
1853
2015
|
attachParamsToNavigators(navigators, types) {
|
|
1854
2016
|
for (const navigator of navigators) {
|
|
1855
|
-
const matchingType = types.find((
|
|
2017
|
+
const matchingType = types.find((t12) => t12.type === navigator.type);
|
|
1856
2018
|
if (matchingType) {
|
|
1857
2019
|
for (const screen of navigator.screens) {
|
|
1858
2020
|
const screenParams = matchingType.paramEntries.get(screen.name);
|
|
@@ -1870,6 +2032,11 @@ var NavigationAnalyzer = class {
|
|
|
1870
2032
|
let initialScreen = "";
|
|
1871
2033
|
for (const navigator of parsedNavigators) {
|
|
1872
2034
|
const screenNames = navigator.screens.map((s) => s.name);
|
|
2035
|
+
if (screenNames.length === 0) {
|
|
2036
|
+
console.warn(
|
|
2037
|
+
`[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.`
|
|
2038
|
+
);
|
|
2039
|
+
}
|
|
1873
2040
|
navigators.push({
|
|
1874
2041
|
name: navigator.name,
|
|
1875
2042
|
type: navigator.type,
|
|
@@ -1937,8 +2104,8 @@ var ComponentAnalyzer = class {
|
|
|
1937
2104
|
});
|
|
1938
2105
|
const components = [];
|
|
1939
2106
|
traverse4__default.default(ast, {
|
|
1940
|
-
JSXElement: (
|
|
1941
|
-
const component = this.extractComponentFromJSXElement(
|
|
2107
|
+
JSXElement: (path9) => {
|
|
2108
|
+
const component = this.extractComponentFromJSXElement(path9.node);
|
|
1942
2109
|
if (component) {
|
|
1943
2110
|
components.push(component);
|
|
1944
2111
|
}
|
|
@@ -2065,13 +2232,13 @@ var FormAnalyzer = class {
|
|
|
2065
2232
|
this.inputElements = [];
|
|
2066
2233
|
this.submitButtons = [];
|
|
2067
2234
|
traverse4__default.default(ast, {
|
|
2068
|
-
CallExpression: (
|
|
2069
|
-
this.extractStateVariables(
|
|
2235
|
+
CallExpression: (path9) => {
|
|
2236
|
+
this.extractStateVariables(path9.node);
|
|
2070
2237
|
}
|
|
2071
2238
|
});
|
|
2072
2239
|
traverse4__default.default(ast, {
|
|
2073
|
-
JSXElement: (
|
|
2074
|
-
this.extractFormElements(
|
|
2240
|
+
JSXElement: (path9) => {
|
|
2241
|
+
this.extractFormElements(path9.node);
|
|
2075
2242
|
}
|
|
2076
2243
|
});
|
|
2077
2244
|
const validationRules = this.extractValidationRules(ast);
|
|
@@ -2171,8 +2338,8 @@ var FormAnalyzer = class {
|
|
|
2171
2338
|
extractValidationRules(ast) {
|
|
2172
2339
|
const rules = {};
|
|
2173
2340
|
traverse4__default.default(ast, {
|
|
2174
|
-
IfStatement: (
|
|
2175
|
-
const test =
|
|
2341
|
+
IfStatement: (path9) => {
|
|
2342
|
+
const test = path9.node.test;
|
|
2176
2343
|
const rule = this.extractRuleFromCondition(test);
|
|
2177
2344
|
if (rule) {
|
|
2178
2345
|
const { field, description } = rule;
|
|
@@ -2226,7 +2393,7 @@ var FormAnalyzer = class {
|
|
|
2226
2393
|
}
|
|
2227
2394
|
buildForms(filePath, validationRules) {
|
|
2228
2395
|
if (this.inputElements.length === 0) return [];
|
|
2229
|
-
const fileName =
|
|
2396
|
+
const fileName = path8__namespace.basename(filePath, path8__namespace.extname(filePath));
|
|
2230
2397
|
const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
|
|
2231
2398
|
const fields = this.inputElements.map((input) => {
|
|
2232
2399
|
const fieldType = this.inferFieldType(input);
|
|
@@ -2304,7 +2471,7 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2304
2471
|
`[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
|
|
2305
2472
|
);
|
|
2306
2473
|
const enrichmentPromises = screenFiles.map(async (file) => {
|
|
2307
|
-
const filePath =
|
|
2474
|
+
const filePath = path8__namespace.default.resolve(config.rootDir, file);
|
|
2308
2475
|
try {
|
|
2309
2476
|
const [components, forms] = await Promise.all([
|
|
2310
2477
|
componentAnalyzer.analyzeFile(filePath),
|
|
@@ -2347,91 +2514,1579 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2347
2514
|
});
|
|
2348
2515
|
}
|
|
2349
2516
|
}
|
|
2350
|
-
return {
|
|
2351
|
-
...screen,
|
|
2352
|
-
components: [...screen.components, ...newComponents],
|
|
2353
|
-
forms: mergedForms
|
|
2354
|
-
};
|
|
2517
|
+
return {
|
|
2518
|
+
...screen,
|
|
2519
|
+
components: [...screen.components, ...newComponents],
|
|
2520
|
+
forms: mergedForms
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
return screen;
|
|
2524
|
+
});
|
|
2525
|
+
return {
|
|
2526
|
+
screens: enrichedScreens,
|
|
2527
|
+
navigation,
|
|
2528
|
+
analyzedFiles: screenFiles.length,
|
|
2529
|
+
...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
mergeForm(target, source) {
|
|
2533
|
+
for (const field of source.fields) {
|
|
2534
|
+
const existingField = this.findEquivalentField(target.fields, field);
|
|
2535
|
+
if (existingField) {
|
|
2536
|
+
this.mergeField(existingField, field);
|
|
2537
|
+
} else {
|
|
2538
|
+
target.fields.push(field);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
target.submitAction = target.submitAction ?? this.namedSubmitAction(source.submitAction);
|
|
2542
|
+
target.validationRules = source.validationRules ? { ...source.validationRules, ...target.validationRules ?? {} } : target.validationRules;
|
|
2543
|
+
}
|
|
2544
|
+
findEquivalentField(fields, incoming) {
|
|
2545
|
+
return fields.find((field) => {
|
|
2546
|
+
if (field.name && incoming.name && field.name === incoming.name) return true;
|
|
2547
|
+
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
|
|
2548
|
+
if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
|
|
2549
|
+
if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
|
|
2550
|
+
if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
|
|
2551
|
+
return true;
|
|
2552
|
+
}
|
|
2553
|
+
return false;
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
mergeField(target, source) {
|
|
2557
|
+
if (this.isWeakInferredFieldName(target.name) && !this.isWeakInferredFieldName(source.name)) {
|
|
2558
|
+
target.name = source.name;
|
|
2559
|
+
}
|
|
2560
|
+
target.label = target.label ?? source.label;
|
|
2561
|
+
target.placeholder = target.placeholder ?? source.placeholder;
|
|
2562
|
+
target.options = target.options ?? source.options;
|
|
2563
|
+
target.locator = target.locator ?? source.locator;
|
|
2564
|
+
target.sourceComponent = target.sourceComponent ?? source.sourceComponent;
|
|
2565
|
+
target.valueBinding = target.valueBinding ?? source.valueBinding;
|
|
2566
|
+
target.errorBinding = target.errorBinding ?? source.errorBinding;
|
|
2567
|
+
target.required = target.required || source.required;
|
|
2568
|
+
if (target.type === "text" && source.type !== "text") {
|
|
2569
|
+
target.type = source.type;
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
namedSubmitAction(submitAction) {
|
|
2573
|
+
return submitAction && submitAction !== "anonymous" ? submitAction : void 0;
|
|
2574
|
+
}
|
|
2575
|
+
isWeakInferredFieldName(name) {
|
|
2576
|
+
return /^(text|value|input|query|search|selected|checked)$/i.test(name);
|
|
2577
|
+
}
|
|
2578
|
+
findFormWithSharedFields(forms, incoming) {
|
|
2579
|
+
if (incoming.fields.length === 0) return void 0;
|
|
2580
|
+
let best;
|
|
2581
|
+
for (const form of forms) {
|
|
2582
|
+
const overlap = incoming.fields.filter((field) => this.findEquivalentField(form.fields, field)).length;
|
|
2583
|
+
if (overlap > 0 && (!best || overlap > best.overlap)) {
|
|
2584
|
+
best = { form, overlap };
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
return best?.form;
|
|
2588
|
+
}
|
|
2589
|
+
};
|
|
2590
|
+
|
|
2591
|
+
// src/analyzers/GenericPlatformAnalyzer.ts
|
|
2592
|
+
var GenericPlatformAnalyzer = class {
|
|
2593
|
+
constructor(platform) {
|
|
2594
|
+
this.platform = platform;
|
|
2595
|
+
}
|
|
2596
|
+
platform;
|
|
2597
|
+
async analyze(_config, _options) {
|
|
2598
|
+
return {
|
|
2599
|
+
screens: [],
|
|
2600
|
+
navigation: { screens: {}, initialScreen: "", navigators: [] },
|
|
2601
|
+
analyzedFiles: 0
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
};
|
|
2605
|
+
|
|
2606
|
+
// src/ast/jsx/web/classify.ts
|
|
2607
|
+
var VIEW_TAGS = /* @__PURE__ */ new Set([
|
|
2608
|
+
"div",
|
|
2609
|
+
"span",
|
|
2610
|
+
"section",
|
|
2611
|
+
"main",
|
|
2612
|
+
"article",
|
|
2613
|
+
"aside",
|
|
2614
|
+
"header",
|
|
2615
|
+
"footer",
|
|
2616
|
+
"nav",
|
|
2617
|
+
"fieldset",
|
|
2618
|
+
"form",
|
|
2619
|
+
"p"
|
|
2620
|
+
]);
|
|
2621
|
+
var LIST_TAGS = /* @__PURE__ */ new Set(["ul", "ol", "dl", "table", "tbody"]);
|
|
2622
|
+
var BUTTON_TAGS = /* @__PURE__ */ new Set(["button"]);
|
|
2623
|
+
var INPUT_TAGS = /* @__PURE__ */ new Set(["input", "textarea"]);
|
|
2624
|
+
var MODAL_TAGS = /* @__PURE__ */ new Set(["dialog"]);
|
|
2625
|
+
var BUTTON_COMPONENTS2 = /* @__PURE__ */ new Set(["Button", "IconButton", "Link", "NavLink"]);
|
|
2626
|
+
var INPUT_COMPONENTS2 = /* @__PURE__ */ new Set(["Input", "TextField", "TextArea", "Textarea"]);
|
|
2627
|
+
var LIST_COMPONENTS2 = /* @__PURE__ */ new Set(["List", "Table", "DataGrid", "DataTable"]);
|
|
2628
|
+
var MODAL_COMPONENTS2 = /* @__PURE__ */ new Set(["Modal", "Dialog", "Drawer", "Popover", "BottomSheet"]);
|
|
2629
|
+
var ARIA_ROLE_TO_SEMANTIC = {
|
|
2630
|
+
button: "button",
|
|
2631
|
+
link: "button",
|
|
2632
|
+
tab: "button",
|
|
2633
|
+
menuitem: "button",
|
|
2634
|
+
textbox: "input",
|
|
2635
|
+
searchbox: "input",
|
|
2636
|
+
spinbutton: "input",
|
|
2637
|
+
listbox: "select",
|
|
2638
|
+
combobox: "select",
|
|
2639
|
+
radiogroup: "select",
|
|
2640
|
+
radio: "select",
|
|
2641
|
+
option: "select",
|
|
2642
|
+
checkbox: "toggle",
|
|
2643
|
+
switch: "toggle",
|
|
2644
|
+
dialog: "modal",
|
|
2645
|
+
alertdialog: "modal",
|
|
2646
|
+
list: "list",
|
|
2647
|
+
table: "list",
|
|
2648
|
+
grid: "list",
|
|
2649
|
+
form: "view"
|
|
2650
|
+
};
|
|
2651
|
+
var INPUT_TYPE_TO_SEMANTIC = {
|
|
2652
|
+
checkbox: "toggle",
|
|
2653
|
+
radio: "select",
|
|
2654
|
+
date: "date",
|
|
2655
|
+
"datetime-local": "date",
|
|
2656
|
+
month: "date",
|
|
2657
|
+
week: "date",
|
|
2658
|
+
time: "date",
|
|
2659
|
+
submit: "button",
|
|
2660
|
+
button: "button",
|
|
2661
|
+
reset: "button",
|
|
2662
|
+
image: "button",
|
|
2663
|
+
hidden: "custom",
|
|
2664
|
+
range: "input",
|
|
2665
|
+
file: "input",
|
|
2666
|
+
color: "input"
|
|
2667
|
+
};
|
|
2668
|
+
function classifyWebJsxComponent(name, element) {
|
|
2669
|
+
if (element) {
|
|
2670
|
+
const role = getStringAttr(element, "role");
|
|
2671
|
+
if (role && ARIA_ROLE_TO_SEMANTIC[role]) return ARIA_ROLE_TO_SEMANTIC[role];
|
|
2672
|
+
}
|
|
2673
|
+
if (/^[a-z]/.test(name)) {
|
|
2674
|
+
if (name === "input") {
|
|
2675
|
+
const type = element ? getStringAttr(element, "type") : void 0;
|
|
2676
|
+
if (type && INPUT_TYPE_TO_SEMANTIC[type]) return INPUT_TYPE_TO_SEMANTIC[type];
|
|
2677
|
+
return "input";
|
|
2678
|
+
}
|
|
2679
|
+
if (INPUT_TAGS.has(name)) return "input";
|
|
2680
|
+
if (name === "select") return "select";
|
|
2681
|
+
if (BUTTON_TAGS.has(name)) return "button";
|
|
2682
|
+
if (name === "a") {
|
|
2683
|
+
if (element && (hasJsxAttribute(element, "href") || hasJsxAttribute(element, "onClick"))) {
|
|
2684
|
+
return "button";
|
|
2685
|
+
}
|
|
2686
|
+
return "view";
|
|
2687
|
+
}
|
|
2688
|
+
if (LIST_TAGS.has(name)) return "list";
|
|
2689
|
+
if (MODAL_TAGS.has(name)) return "modal";
|
|
2690
|
+
if (VIEW_TAGS.has(name)) return "view";
|
|
2691
|
+
return "custom";
|
|
2692
|
+
}
|
|
2693
|
+
if (LIST_COMPONENTS2.has(name)) return "list";
|
|
2694
|
+
if (MODAL_COMPONENTS2.has(name)) return "modal";
|
|
2695
|
+
if (/date/i.test(name)) return "date";
|
|
2696
|
+
if (/(select|picker|dropdown|radio)/i.test(name)) return "select";
|
|
2697
|
+
if (/(checkbox|switch|toggle)/i.test(name)) return "toggle";
|
|
2698
|
+
if (INPUT_COMPONENTS2.has(name)) return "input";
|
|
2699
|
+
if (BUTTON_COMPONENTS2.has(name)) return "button";
|
|
2700
|
+
if (element) {
|
|
2701
|
+
const hasOptions = hasJsxAttribute(element, "options");
|
|
2702
|
+
const hasValue = hasJsxAttribute(element, "value");
|
|
2703
|
+
const hasChecked = hasJsxAttribute(element, "checked") || hasJsxAttribute(element, "selected");
|
|
2704
|
+
const hasOnChange = hasJsxAttribute(element, "onChange") || hasJsxAttribute(element, "onValueChange");
|
|
2705
|
+
const hasOnClick = hasJsxAttribute(element, "onClick");
|
|
2706
|
+
if (hasOptions && (hasValue || hasOnChange)) return "select";
|
|
2707
|
+
if (hasChecked && hasOnChange) return "toggle";
|
|
2708
|
+
if (hasOnChange && (hasJsxAttribute(element, "label") || hasJsxAttribute(element, "placeholder"))) {
|
|
2709
|
+
return "input";
|
|
2710
|
+
}
|
|
2711
|
+
if (hasOnClick) return "button";
|
|
2712
|
+
if (getStringAttr(element, "open") || getExpressionIdentifierAttr(element, "open") || getExpressionIdentifierAttr(element, "isOpen")) {
|
|
2713
|
+
return "modal";
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
return "custom";
|
|
2717
|
+
}
|
|
2718
|
+
function getJsxElementName(openingElement) {
|
|
2719
|
+
return getJsxName(openingElement.name);
|
|
2720
|
+
}
|
|
2721
|
+
function getJsxName(name) {
|
|
2722
|
+
if (BabelTypes__namespace.isJSXIdentifier(name)) return name.name;
|
|
2723
|
+
if (BabelTypes__namespace.isJSXNamespacedName(name)) return `${name.namespace.name}:${name.name.name}`;
|
|
2724
|
+
if (BabelTypes__namespace.isJSXMemberExpression(name)) {
|
|
2725
|
+
const objectName = getJsxName(name.object);
|
|
2726
|
+
const propertyName = BabelTypes__namespace.isJSXIdentifier(name.property) ? name.property.name : null;
|
|
2727
|
+
return objectName && propertyName ? `${objectName}.${propertyName}` : null;
|
|
2728
|
+
}
|
|
2729
|
+
return null;
|
|
2730
|
+
}
|
|
2731
|
+
var ROUTERISH_OBJECTS = /^(router|history|navigation)$/;
|
|
2732
|
+
function staticRoutePath(node) {
|
|
2733
|
+
if (!node) return void 0;
|
|
2734
|
+
if (BabelTypes__namespace.isStringLiteral(node)) return node.value;
|
|
2735
|
+
if (BabelTypes__namespace.isTemplateLiteral(node)) {
|
|
2736
|
+
let path9 = "";
|
|
2737
|
+
node.quasis.forEach((quasi, index) => {
|
|
2738
|
+
path9 += quasi.value.cooked ?? quasi.value.raw;
|
|
2739
|
+
const expr = node.expressions[index];
|
|
2740
|
+
if (expr) path9 += `:${paramNameOf(expr)}`;
|
|
2741
|
+
});
|
|
2742
|
+
return path9;
|
|
2743
|
+
}
|
|
2744
|
+
return void 0;
|
|
2745
|
+
}
|
|
2746
|
+
function paramNameOf(expr) {
|
|
2747
|
+
if (BabelTypes__namespace.isIdentifier(expr)) return expr.name;
|
|
2748
|
+
if (BabelTypes__namespace.isMemberExpression(expr) && BabelTypes__namespace.isIdentifier(expr.property)) return expr.property.name;
|
|
2749
|
+
return "param";
|
|
2750
|
+
}
|
|
2751
|
+
function extractWebNavigationCalls(ast) {
|
|
2752
|
+
const calls = [];
|
|
2753
|
+
const inspect = (node) => {
|
|
2754
|
+
if (BabelTypes__namespace.isCallExpression(node)) {
|
|
2755
|
+
if (BabelTypes__namespace.isIdentifier(node.callee) && node.callee.name === "navigate") {
|
|
2756
|
+
const first = node.arguments[0];
|
|
2757
|
+
const targetPath = staticRoutePath(first);
|
|
2758
|
+
if (targetPath !== void 0) {
|
|
2759
|
+
calls.push({
|
|
2760
|
+
method: hasReplaceOption(node.arguments[1]) ? "replace" : "navigate",
|
|
2761
|
+
targetPath
|
|
2762
|
+
});
|
|
2763
|
+
} else if (BabelTypes__namespace.isNumericLiteral(first) || BabelTypes__namespace.isUnaryExpression(first) && first.operator === "-") {
|
|
2764
|
+
calls.push({ method: "goBack" });
|
|
2765
|
+
}
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
if (BabelTypes__namespace.isMemberExpression(node.callee) && BabelTypes__namespace.isIdentifier(node.callee.object) && ROUTERISH_OBJECTS.test(node.callee.object.name) && BabelTypes__namespace.isIdentifier(node.callee.property)) {
|
|
2769
|
+
const method = node.callee.property.name;
|
|
2770
|
+
const targetPath = staticRoutePath(node.arguments[0]);
|
|
2771
|
+
if ((method === "push" || method === "navigate") && targetPath !== void 0) {
|
|
2772
|
+
calls.push({ method: "navigate", targetPath });
|
|
2773
|
+
} else if (method === "replace" && targetPath !== void 0) {
|
|
2774
|
+
calls.push({ method: "replace", targetPath });
|
|
2775
|
+
} else if (method === "back" || method === "goBack") {
|
|
2776
|
+
calls.push({ method: "goBack" });
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
if (BabelTypes__namespace.isAssignmentExpression(node) && BabelTypes__namespace.isMemberExpression(node.left) && BabelTypes__namespace.isIdentifier(node.left.property) && node.left.property.name === "href" && isLocationExpression(node.left.object) && BabelTypes__namespace.isStringLiteral(node.right) && node.right.value.startsWith("/")) {
|
|
2782
|
+
calls.push({ method: "navigate", targetPath: node.right.value });
|
|
2783
|
+
}
|
|
2784
|
+
};
|
|
2785
|
+
traverse4__default.default(ast, {
|
|
2786
|
+
noScope: !BabelTypes__namespace.isFile(ast),
|
|
2787
|
+
enter: (nodePath) => inspect(nodePath.node)
|
|
2788
|
+
});
|
|
2789
|
+
return calls;
|
|
2790
|
+
}
|
|
2791
|
+
function hasReplaceOption(arg) {
|
|
2792
|
+
if (!arg || !BabelTypes__namespace.isObjectExpression(arg)) return false;
|
|
2793
|
+
return arg.properties.some(
|
|
2794
|
+
(prop) => BabelTypes__namespace.isObjectProperty(prop) && BabelTypes__namespace.isIdentifier(prop.key) && prop.key.name === "replace" && BabelTypes__namespace.isBooleanLiteral(prop.value) && prop.value.value === true
|
|
2795
|
+
);
|
|
2796
|
+
}
|
|
2797
|
+
function isLocationExpression(node) {
|
|
2798
|
+
if (BabelTypes__namespace.isIdentifier(node)) return node.name === "location";
|
|
2799
|
+
return BabelTypes__namespace.isMemberExpression(node) && BabelTypes__namespace.isIdentifier(node.object) && node.object.name === "window" && BabelTypes__namespace.isIdentifier(node.property) && node.property.name === "location";
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
// src/analyzers/web/WebScreenAnalyzer.ts
|
|
2803
|
+
var DEFAULT_WEB_SCREEN_PATTERNS = [
|
|
2804
|
+
"**/pages/**/*.{ts,tsx,js,jsx}",
|
|
2805
|
+
"**/routes/**/*.{ts,tsx,js,jsx}",
|
|
2806
|
+
"**/views/**/*.{ts,tsx,js,jsx}",
|
|
2807
|
+
"**/app/**/*.{ts,tsx,js,jsx}",
|
|
2808
|
+
"**/*Page.{ts,tsx,js,jsx}",
|
|
2809
|
+
"**/*Screen.{ts,tsx,js,jsx}",
|
|
2810
|
+
"**/*View.{ts,tsx,js,jsx}"
|
|
2811
|
+
];
|
|
2812
|
+
var DESTRUCTIVE_VERB2 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
|
|
2813
|
+
var LISTISH_TAGS = /* @__PURE__ */ new Set(["ul", "ol", "dl", "table", "tbody"]);
|
|
2814
|
+
var WebScreenAnalyzer = class {
|
|
2815
|
+
config;
|
|
2816
|
+
verbose = process.env.VERBOSE === "true";
|
|
2817
|
+
screenPatterns;
|
|
2818
|
+
constructor(config, options) {
|
|
2819
|
+
this.config = config;
|
|
2820
|
+
this.screenPatterns = options?.screenPatterns ?? DEFAULT_WEB_SCREEN_PATTERNS;
|
|
2821
|
+
}
|
|
2822
|
+
async analyze() {
|
|
2823
|
+
const {
|
|
2824
|
+
include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
|
|
2825
|
+
exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
2826
|
+
} = this.config;
|
|
2827
|
+
const files = (await glob__default.default(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
|
|
2828
|
+
(file) => !file.endsWith(".d.ts")
|
|
2829
|
+
);
|
|
2830
|
+
const patternMatches = await glob__default.default(this.screenPatterns, {
|
|
2831
|
+
cwd: this.config.rootDir,
|
|
2832
|
+
ignore: exclude
|
|
2833
|
+
});
|
|
2834
|
+
const patternSet = new Set(patternMatches.map((f) => path8__namespace.default.resolve(this.config.rootDir, f)));
|
|
2835
|
+
const candidates = [];
|
|
2836
|
+
for (const file of files) {
|
|
2837
|
+
const filePath = path8__namespace.default.resolve(this.config.rootDir, file);
|
|
2838
|
+
try {
|
|
2839
|
+
const candidate = await this.analyzeFile(filePath);
|
|
2840
|
+
if (!candidate) continue;
|
|
2841
|
+
candidate.matchesScreenPattern = patternSet.has(filePath);
|
|
2842
|
+
candidates.push(candidate);
|
|
2843
|
+
if (this.verbose) {
|
|
2844
|
+
console.log(`[WebScreenAnalyzer] \u2713 Analyzed: ${candidate.descriptor.name} (${file})`);
|
|
2845
|
+
}
|
|
2846
|
+
} catch (error) {
|
|
2847
|
+
if (this.verbose) {
|
|
2848
|
+
console.warn(
|
|
2849
|
+
`[WebScreenAnalyzer] Failed to parse ${file}:`,
|
|
2850
|
+
error instanceof Error ? error.message : error
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
return { candidates, analyzedFiles: files.length };
|
|
2856
|
+
}
|
|
2857
|
+
async analyzeFile(filePath) {
|
|
2858
|
+
const source = await fs__default.default.readFile(filePath, "utf-8");
|
|
2859
|
+
const ast = parseSource(source, this.config.parserPlugins);
|
|
2860
|
+
const registerScreenMeta = this.extractRegisterScreenMetadata(ast);
|
|
2861
|
+
const componentName = this.extractComponentName(ast);
|
|
2862
|
+
const labelsByHtmlFor = this.collectHtmlForLabels(ast);
|
|
2863
|
+
const handlerBehaviors = this.collectWebHandlerBehaviors(ast);
|
|
2864
|
+
const forms = this.mergeForms(
|
|
2865
|
+
registerScreenMeta?.forms ?? [],
|
|
2866
|
+
this.extractForms(ast, labelsByHtmlFor)
|
|
2867
|
+
);
|
|
2868
|
+
const actions = this.extractActions(ast, registerScreenMeta, handlerBehaviors);
|
|
2869
|
+
const components = this.extractComponents(ast);
|
|
2870
|
+
const collections = this.extractCollections(ast);
|
|
2871
|
+
const navigationTargets = this.extractNavigationTargets(ast);
|
|
2872
|
+
const permissionsFromJsDoc = extractPermissionsFromJsDoc(source);
|
|
2873
|
+
const destructiveTags = extractDestructiveJsDocTargets(source);
|
|
2874
|
+
if (destructiveTags) {
|
|
2875
|
+
for (const action of actions) {
|
|
2876
|
+
if (destructiveTags === "*" || destructiveTags.has(action.id)) {
|
|
2877
|
+
action.destructive = true;
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
const name = registerScreenMeta?.name || componentName || path8__namespace.default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
|
|
2882
|
+
const descriptor = {
|
|
2883
|
+
name,
|
|
2884
|
+
filePath,
|
|
2885
|
+
title: registerScreenMeta?.title,
|
|
2886
|
+
description: registerScreenMeta?.description,
|
|
2887
|
+
components,
|
|
2888
|
+
forms,
|
|
2889
|
+
actions,
|
|
2890
|
+
navigationTargets,
|
|
2891
|
+
...collections.length > 0 ? { collections } : {},
|
|
2892
|
+
...registerScreenMeta?.suggestedPrompts && registerScreenMeta.suggestedPrompts.length > 0 ? { suggestedPrompts: registerScreenMeta.suggestedPrompts } : {},
|
|
2893
|
+
...permissionsFromJsDoc ? {
|
|
2894
|
+
permissions: permissionsFromJsDoc,
|
|
2895
|
+
...permissionsFromJsDoc.isPii ? { isPii: true } : {}
|
|
2896
|
+
} : {}
|
|
2897
|
+
};
|
|
2898
|
+
return {
|
|
2899
|
+
descriptor,
|
|
2900
|
+
hasRegisterScreen: registerScreenMeta !== null || this.detectRegisterScreenCall(ast),
|
|
2901
|
+
matchesScreenPattern: false
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
// ── registerScreen ────────────────────────────────────────────────
|
|
2905
|
+
detectRegisterScreenCall(ast) {
|
|
2906
|
+
let found = false;
|
|
2907
|
+
traverse4__default.default(ast, {
|
|
2908
|
+
CallExpression: (nodePath) => {
|
|
2909
|
+
if (found) return;
|
|
2910
|
+
if (isRegisterScreenCallee(nodePath.node.callee)) {
|
|
2911
|
+
found = true;
|
|
2912
|
+
nodePath.stop();
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
});
|
|
2916
|
+
return found;
|
|
2917
|
+
}
|
|
2918
|
+
extractRegisterScreenMetadata(ast) {
|
|
2919
|
+
let plain = null;
|
|
2920
|
+
traverse4__default.default(ast, {
|
|
2921
|
+
CallExpression: (nodePath) => {
|
|
2922
|
+
if (!isRegisterScreenCallee(nodePath.node.callee)) return;
|
|
2923
|
+
const arg = nodePath.node.arguments[0];
|
|
2924
|
+
if (BabelTypes__namespace.isObjectExpression(arg)) {
|
|
2925
|
+
plain = literalToPlain(arg);
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
});
|
|
2929
|
+
if (!plain) return null;
|
|
2930
|
+
const meta = plain;
|
|
2931
|
+
const result = {
|
|
2932
|
+
name: typeof meta.name === "string" ? meta.name : "",
|
|
2933
|
+
actions: [],
|
|
2934
|
+
forms: [],
|
|
2935
|
+
navigationTargets: [],
|
|
2936
|
+
components: []
|
|
2937
|
+
};
|
|
2938
|
+
if (typeof meta.title === "string") result.title = meta.title;
|
|
2939
|
+
if (typeof meta.description === "string") result.description = meta.description;
|
|
2940
|
+
if (Array.isArray(meta.suggestedPrompts)) {
|
|
2941
|
+
const prompts = meta.suggestedPrompts.filter((p) => typeof p === "string").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
2942
|
+
if (prompts.length > 0) result.suggestedPrompts = prompts;
|
|
2943
|
+
}
|
|
2944
|
+
if (Array.isArray(meta.actions)) {
|
|
2945
|
+
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 }));
|
|
2946
|
+
}
|
|
2947
|
+
if (Array.isArray(meta.fields)) {
|
|
2948
|
+
const fields = meta.fields.filter((f) => typeof f === "object" && f !== null).filter((f) => typeof f.id === "string" && f.id.length > 0).map(
|
|
2949
|
+
(f) => ({
|
|
2950
|
+
name: f.id,
|
|
2951
|
+
type: typeof f.type === "string" ? f.type : "text",
|
|
2952
|
+
required: f.required === true,
|
|
2953
|
+
...typeof f.label === "string" ? { label: f.label } : {},
|
|
2954
|
+
...typeof f.placeholder === "string" ? { placeholder: f.placeholder } : {},
|
|
2955
|
+
...f.defaultValue !== void 0 ? { defaultValue: f.defaultValue } : {},
|
|
2956
|
+
...Array.isArray(f.options) ? { options: f.options } : {}
|
|
2957
|
+
})
|
|
2958
|
+
);
|
|
2959
|
+
if (fields.length > 0) result.forms = [{ id: "default", fields }];
|
|
2960
|
+
}
|
|
2961
|
+
return result;
|
|
2962
|
+
}
|
|
2963
|
+
// ── Component name / structure ────────────────────────────────────
|
|
2964
|
+
/** Default-exported component name, else the first exported capitalized function. */
|
|
2965
|
+
extractComponentName(ast) {
|
|
2966
|
+
let defaultName = "";
|
|
2967
|
+
let firstExported = "";
|
|
2968
|
+
traverse4__default.default(ast, {
|
|
2969
|
+
ExportDefaultDeclaration: (nodePath) => {
|
|
2970
|
+
const declaration = nodePath.node.declaration;
|
|
2971
|
+
if (BabelTypes__namespace.isFunctionDeclaration(declaration) && declaration.id?.name) {
|
|
2972
|
+
defaultName = declaration.id.name;
|
|
2973
|
+
} else if (BabelTypes__namespace.isIdentifier(declaration)) {
|
|
2974
|
+
defaultName = declaration.name;
|
|
2975
|
+
}
|
|
2976
|
+
},
|
|
2977
|
+
ExportNamedDeclaration: (nodePath) => {
|
|
2978
|
+
if (firstExported) return;
|
|
2979
|
+
const declaration = nodePath.node.declaration;
|
|
2980
|
+
if (BabelTypes__namespace.isFunctionDeclaration(declaration) && declaration.id && /^[A-Z]/.test(declaration.id.name)) {
|
|
2981
|
+
firstExported = declaration.id.name;
|
|
2982
|
+
} else if (BabelTypes__namespace.isVariableDeclaration(declaration)) {
|
|
2983
|
+
for (const declarator of declaration.declarations) {
|
|
2984
|
+
if (BabelTypes__namespace.isIdentifier(declarator.id) && /^[A-Z]/.test(declarator.id.name) && (BabelTypes__namespace.isArrowFunctionExpression(declarator.init) || BabelTypes__namespace.isFunctionExpression(declarator.init))) {
|
|
2985
|
+
firstExported = declarator.id.name;
|
|
2986
|
+
break;
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
});
|
|
2992
|
+
return defaultName || firstExported;
|
|
2993
|
+
}
|
|
2994
|
+
extractComponents(ast) {
|
|
2995
|
+
const components = [];
|
|
2996
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2997
|
+
traverse4__default.default(ast, {
|
|
2998
|
+
JSXOpeningElement: (nodePath) => {
|
|
2999
|
+
const element = nodePath.node;
|
|
3000
|
+
const name = getJsxElementName(element);
|
|
3001
|
+
if (!name || seen.has(name)) return;
|
|
3002
|
+
seen.add(name);
|
|
3003
|
+
const role = classifyWebJsxComponent(name, element);
|
|
3004
|
+
const type = role === "select" || role === "toggle" || role === "date" ? "input" : role === "input" || role === "button" || role === "list" || role === "modal" || role === "view" ? role : "custom";
|
|
3005
|
+
const component = { name, type };
|
|
3006
|
+
const testId = getStringAttr(element, "data-testid") ?? getStringAttr(element, "testID");
|
|
3007
|
+
const ariaLabel = getStringAttr(element, "aria-label");
|
|
3008
|
+
if (testId) component.testID = testId;
|
|
3009
|
+
if (ariaLabel) component.accessibilityLabel = ariaLabel;
|
|
3010
|
+
components.push(component);
|
|
3011
|
+
}
|
|
3012
|
+
});
|
|
3013
|
+
return components;
|
|
3014
|
+
}
|
|
3015
|
+
// ── <label htmlFor> association ───────────────────────────────────
|
|
3016
|
+
collectHtmlForLabels(ast) {
|
|
3017
|
+
const labels = /* @__PURE__ */ new Map();
|
|
3018
|
+
traverse4__default.default(ast, {
|
|
3019
|
+
JSXElement: (nodePath) => {
|
|
3020
|
+
const element = nodePath.node;
|
|
3021
|
+
if (getJsxElementName(element.openingElement) !== "label") return;
|
|
3022
|
+
const htmlFor = getStringAttr(element.openingElement, "htmlFor");
|
|
3023
|
+
if (!htmlFor) return;
|
|
3024
|
+
const text = jsxTextContent(element);
|
|
3025
|
+
if (text) labels.set(htmlFor, text);
|
|
3026
|
+
}
|
|
3027
|
+
});
|
|
3028
|
+
return labels;
|
|
3029
|
+
}
|
|
3030
|
+
// ── Forms ─────────────────────────────────────────────────────────
|
|
3031
|
+
extractForms(ast, labelsByHtmlFor) {
|
|
3032
|
+
const formBuckets = /* @__PURE__ */ new Map();
|
|
3033
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
3034
|
+
let formCount = 0;
|
|
3035
|
+
const uniqueId = (preferred) => {
|
|
3036
|
+
if (!usedIds.has(preferred)) {
|
|
3037
|
+
usedIds.add(preferred);
|
|
3038
|
+
return preferred;
|
|
3039
|
+
}
|
|
3040
|
+
let suffix = 2;
|
|
3041
|
+
while (usedIds.has(`${preferred}-${suffix}`)) suffix += 1;
|
|
3042
|
+
const id = `${preferred}-${suffix}`;
|
|
3043
|
+
usedIds.add(id);
|
|
3044
|
+
return id;
|
|
3045
|
+
};
|
|
3046
|
+
const bucketFor = (formElement) => {
|
|
3047
|
+
let bucket = formBuckets.get(formElement);
|
|
3048
|
+
if (bucket) return bucket;
|
|
3049
|
+
formCount += 1;
|
|
3050
|
+
let preferred = "default";
|
|
3051
|
+
let submitAction;
|
|
3052
|
+
if (formElement) {
|
|
3053
|
+
const opening = formElement.openingElement;
|
|
3054
|
+
preferred = getStringAttr(opening, "id") ?? getStringAttr(opening, "name") ?? getStringAttr(opening, "data-testid") ?? (formCount === 1 ? "default" : `form-${formCount}`);
|
|
3055
|
+
submitAction = this.handlerNameFromAttr(opening, "onSubmit");
|
|
3056
|
+
}
|
|
3057
|
+
bucket = { id: uniqueId(preferred), fields: /* @__PURE__ */ new Map(), submitAction };
|
|
3058
|
+
formBuckets.set(formElement, bucket);
|
|
3059
|
+
return bucket;
|
|
3060
|
+
};
|
|
3061
|
+
traverse4__default.default(ast, {
|
|
3062
|
+
JSXElement: (nodePath) => {
|
|
3063
|
+
const element = nodePath.node;
|
|
3064
|
+
const name = getJsxElementName(element.openingElement);
|
|
3065
|
+
if (!name) return;
|
|
3066
|
+
const role = classifyWebJsxComponent(name, element.openingElement);
|
|
3067
|
+
if (!["input", "select", "toggle", "date"].includes(role)) return;
|
|
3068
|
+
if (name === "option") return;
|
|
3069
|
+
const field = this.extractField(element, role, labelsByHtmlFor);
|
|
3070
|
+
if (!field.name) return;
|
|
3071
|
+
const formParent = nodePath.findParent(
|
|
3072
|
+
(p) => p.isJSXElement() && (getJsxElementName(p.node.openingElement) === "form" || getStringAttr(p.node.openingElement, "role") === "form")
|
|
3073
|
+
);
|
|
3074
|
+
const bucket = bucketFor(formParent ? formParent.node : null);
|
|
3075
|
+
if (!bucket.fields.has(field.name)) bucket.fields.set(field.name, field);
|
|
3076
|
+
}
|
|
3077
|
+
});
|
|
3078
|
+
traverse4__default.default(ast, {
|
|
3079
|
+
JSXElement: (nodePath) => {
|
|
3080
|
+
const element = nodePath.node;
|
|
3081
|
+
const name = getJsxElementName(element.openingElement);
|
|
3082
|
+
if (!name) return;
|
|
3083
|
+
if (!this.isSubmitElement(name, element.openingElement)) return;
|
|
3084
|
+
const formParent = nodePath.findParent(
|
|
3085
|
+
(p) => p.isJSXElement() && getJsxElementName(p.node.openingElement) === "form"
|
|
3086
|
+
);
|
|
3087
|
+
if (!formParent) return;
|
|
3088
|
+
const bucket = formBuckets.get(formParent.node);
|
|
3089
|
+
if (!bucket) return;
|
|
3090
|
+
const handler = this.handlerNameFromAttr(element.openingElement, "onClick");
|
|
3091
|
+
if (!bucket.submitAction && handler) bucket.submitAction = handler;
|
|
3092
|
+
}
|
|
3093
|
+
});
|
|
3094
|
+
return Array.from(formBuckets.values()).filter((bucket) => bucket.fields.size > 0).map((bucket) => ({
|
|
3095
|
+
id: bucket.id,
|
|
3096
|
+
fields: Array.from(bucket.fields.values()),
|
|
3097
|
+
...bucket.submitAction && bucket.submitAction !== "anonymous" ? { submitAction: bucket.submitAction } : {}
|
|
3098
|
+
}));
|
|
3099
|
+
}
|
|
3100
|
+
isSubmitElement(name, opening) {
|
|
3101
|
+
const type = getStringAttr(opening, "type");
|
|
3102
|
+
if (name === "button") return type === "submit" || type === void 0;
|
|
3103
|
+
if (name === "input") return type === "submit";
|
|
3104
|
+
return false;
|
|
3105
|
+
}
|
|
3106
|
+
extractField(element, role, labelsByHtmlFor) {
|
|
3107
|
+
const opening = element.openingElement;
|
|
3108
|
+
const componentName = getJsxElementName(opening) ?? void 0;
|
|
3109
|
+
const field = {
|
|
3110
|
+
name: "",
|
|
3111
|
+
type: "text",
|
|
3112
|
+
required: false,
|
|
3113
|
+
sourceComponent: componentName
|
|
3114
|
+
};
|
|
3115
|
+
const appilotsId = getStringAttr(opening, "appilotsId");
|
|
3116
|
+
const dataTestId = getStringAttr(opening, "data-testid");
|
|
3117
|
+
const domId = getStringAttr(opening, "id");
|
|
3118
|
+
const nameAttr = getStringAttr(opening, "name");
|
|
3119
|
+
const ariaLabel = getStringAttr(opening, "aria-label");
|
|
3120
|
+
const placeholder = getStringAttr(opening, "placeholder");
|
|
3121
|
+
if (placeholder) field.placeholder = placeholder;
|
|
3122
|
+
if (ariaLabel) field.label = ariaLabel;
|
|
3123
|
+
if (domId && labelsByHtmlFor.has(domId)) field.label = labelsByHtmlFor.get(domId);
|
|
3124
|
+
if (appilotsId) {
|
|
3125
|
+
field.name = appilotsId;
|
|
3126
|
+
field.locator = mergeLocator(field.locator, {
|
|
3127
|
+
id: appilotsId,
|
|
3128
|
+
appilotsId,
|
|
3129
|
+
source: "appilotsId"
|
|
3130
|
+
});
|
|
3131
|
+
}
|
|
3132
|
+
if (dataTestId) {
|
|
3133
|
+
if (!field.name) field.name = dataTestId.replace(/^(input-|field-|txt-)/, "");
|
|
3134
|
+
field.locator = mergeLocator(field.locator, {
|
|
3135
|
+
...field.locator?.id ? {} : { id: dataTestId },
|
|
3136
|
+
testID: dataTestId,
|
|
3137
|
+
source: field.locator?.source ?? "data-testid"
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
3140
|
+
if (nameAttr && !field.name) field.name = nameAttr;
|
|
3141
|
+
if (domId) {
|
|
3142
|
+
if (!field.name) field.name = domId;
|
|
3143
|
+
field.locator = mergeLocator(field.locator, {
|
|
3144
|
+
...field.locator?.id ? {} : { id: domId },
|
|
3145
|
+
source: field.locator?.source ?? "id"
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
if (ariaLabel) {
|
|
3149
|
+
field.locator = mergeLocator(field.locator, {
|
|
3150
|
+
...field.locator?.id ? {} : { id: slugify(ariaLabel) },
|
|
3151
|
+
accessibilityLabel: ariaLabel,
|
|
3152
|
+
source: field.locator?.source ?? "aria-label"
|
|
3153
|
+
});
|
|
3154
|
+
}
|
|
3155
|
+
for (const attr of opening.attributes) {
|
|
3156
|
+
if (!BabelTypes__namespace.isJSXAttribute(attr) || !BabelTypes__namespace.isJSXIdentifier(attr.name)) continue;
|
|
3157
|
+
const attrName = attr.name.name;
|
|
3158
|
+
if (attrName === "required") {
|
|
3159
|
+
if (attr.value === null) field.required = true;
|
|
3160
|
+
else if (BabelTypes__namespace.isJSXExpressionContainer(attr.value) && BabelTypes__namespace.isBooleanLiteral(attr.value.expression)) {
|
|
3161
|
+
field.required = attr.value.expression.value;
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
if ((attrName === "value" || attrName === "checked") && attr.value && BabelTypes__namespace.isJSXExpressionContainer(attr.value) && BabelTypes__namespace.isIdentifier(attr.value.expression)) {
|
|
3165
|
+
field.valueBinding = attr.value.expression.name;
|
|
3166
|
+
if (!field.name) field.name = attr.value.expression.name;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
if (getStringAttr(opening, "aria-required") === "true") field.required = true;
|
|
3170
|
+
if (role === "select") field.type = "select";
|
|
3171
|
+
else if (role === "toggle") field.type = "toggle";
|
|
3172
|
+
else if (role === "date") field.type = "date";
|
|
3173
|
+
else {
|
|
3174
|
+
field.type = this.inferInputType(opening, field);
|
|
3175
|
+
}
|
|
3176
|
+
if (componentName === "select") {
|
|
3177
|
+
const options = this.extractSelectOptions(element);
|
|
3178
|
+
if (options.length > 0) field.options = options;
|
|
3179
|
+
}
|
|
3180
|
+
if (!field.name || isWeakInferredFieldName(field.name)) {
|
|
3181
|
+
const labelish = field.label ?? field.placeholder;
|
|
3182
|
+
if (labelish) field.name = slugify(labelish);
|
|
3183
|
+
}
|
|
3184
|
+
if (!field.locator && field.name) {
|
|
3185
|
+
field.locator = { id: field.name, label: field.label, source: "inferred" };
|
|
3186
|
+
} else if (field.locator && !field.locator.id && field.name) {
|
|
3187
|
+
field.locator = mergeLocator(field.locator, {
|
|
3188
|
+
id: field.name,
|
|
3189
|
+
label: field.label,
|
|
3190
|
+
source: field.locator.source ?? "inferred"
|
|
3191
|
+
});
|
|
3192
|
+
}
|
|
3193
|
+
return field;
|
|
3194
|
+
}
|
|
3195
|
+
inferInputType(opening, field) {
|
|
3196
|
+
const type = getStringAttr(opening, "type");
|
|
3197
|
+
if (type === "email") return "email";
|
|
3198
|
+
if (type === "tel") return "phone";
|
|
3199
|
+
if (type === "number") return "number";
|
|
3200
|
+
if (type === "date" || type === "datetime-local" || type === "month" || type === "week")
|
|
3201
|
+
return "date";
|
|
3202
|
+
const inputMode = getStringAttr(opening, "inputMode") ?? getStringAttr(opening, "inputmode");
|
|
3203
|
+
if (inputMode === "email") return "email";
|
|
3204
|
+
if (inputMode === "tel") return "phone";
|
|
3205
|
+
if (inputMode === "numeric" || inputMode === "decimal") return "number";
|
|
3206
|
+
const combined = `${field.name} ${field.label ?? ""} ${field.placeholder ?? ""}`.toLowerCase();
|
|
3207
|
+
if (combined.includes("email")) return "email";
|
|
3208
|
+
if (combined.includes("phone") || combined.includes("tel")) return "phone";
|
|
3209
|
+
return "text";
|
|
3210
|
+
}
|
|
3211
|
+
extractSelectOptions(selectElement) {
|
|
3212
|
+
const options = [];
|
|
3213
|
+
for (const child of selectElement.children) {
|
|
3214
|
+
if (!BabelTypes__namespace.isJSXElement(child)) continue;
|
|
3215
|
+
if (getJsxElementName(child.openingElement) !== "option") continue;
|
|
3216
|
+
const value = getStringAttr(child.openingElement, "value");
|
|
3217
|
+
const label = jsxTextContent(child) || value || "";
|
|
3218
|
+
if (label && value) options.push({ label, value });
|
|
3219
|
+
}
|
|
3220
|
+
return options;
|
|
3221
|
+
}
|
|
3222
|
+
mergeForms(primary, secondary) {
|
|
3223
|
+
const out = primary.map((form) => ({ ...form, fields: [...form.fields] }));
|
|
3224
|
+
for (const form of secondary) {
|
|
3225
|
+
const existing = out.find((candidate) => candidate.id === form.id);
|
|
3226
|
+
if (!existing) {
|
|
3227
|
+
out.push({ ...form, fields: [...form.fields] });
|
|
3228
|
+
continue;
|
|
3229
|
+
}
|
|
3230
|
+
for (const field of form.fields) {
|
|
3231
|
+
const existingField = existing.fields.find((candidate) => candidate.name === field.name);
|
|
3232
|
+
if (!existingField) {
|
|
3233
|
+
existing.fields.push(field);
|
|
3234
|
+
continue;
|
|
3235
|
+
}
|
|
3236
|
+
existingField.label = existingField.label ?? field.label;
|
|
3237
|
+
existingField.placeholder = existingField.placeholder ?? field.placeholder;
|
|
3238
|
+
existingField.options = existingField.options ?? field.options;
|
|
3239
|
+
existingField.locator = existingField.locator ?? field.locator;
|
|
3240
|
+
existingField.sourceComponent = existingField.sourceComponent ?? field.sourceComponent;
|
|
3241
|
+
existingField.valueBinding = existingField.valueBinding ?? field.valueBinding;
|
|
3242
|
+
existingField.required = existingField.required || field.required;
|
|
3243
|
+
if (existingField.type === "text" && field.type !== "text") existingField.type = field.type;
|
|
3244
|
+
}
|
|
3245
|
+
existing.submitAction = existing.submitAction ?? form.submitAction;
|
|
3246
|
+
}
|
|
3247
|
+
return out;
|
|
3248
|
+
}
|
|
3249
|
+
// ── Actions ───────────────────────────────────────────────────────
|
|
3250
|
+
extractActions(ast, registerScreenMeta, handlerBehaviors) {
|
|
3251
|
+
const actions = [...registerScreenMeta?.actions ?? []];
|
|
3252
|
+
const actionIds = new Set(actions.map((a) => a.id));
|
|
3253
|
+
const actionLabels = new Map(
|
|
3254
|
+
actions.filter((a) => a.label).map((a) => [normalizeLabel(a.label), a])
|
|
3255
|
+
);
|
|
3256
|
+
traverse4__default.default(ast, {
|
|
3257
|
+
JSXElement: (nodePath) => {
|
|
3258
|
+
const element = nodePath.node;
|
|
3259
|
+
const name = getJsxElementName(element.openingElement);
|
|
3260
|
+
if (!name) return;
|
|
3261
|
+
if (classifyWebJsxComponent(name, element.openingElement) !== "button") return;
|
|
3262
|
+
const action = this.extractActionFromElement(element, nodePath);
|
|
3263
|
+
if (!action) return;
|
|
3264
|
+
const existingByLabel = action.label ? actionLabels.get(normalizeLabel(action.label)) : void 0;
|
|
3265
|
+
if (existingByLabel) {
|
|
3266
|
+
this.mergeActionMetadata(existingByLabel, action);
|
|
3267
|
+
return;
|
|
3268
|
+
}
|
|
3269
|
+
if (action.id && !actionIds.has(action.id)) {
|
|
3270
|
+
actions.push(action);
|
|
3271
|
+
actionIds.add(action.id);
|
|
3272
|
+
if (action.label) actionLabels.set(normalizeLabel(action.label), action);
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
});
|
|
3276
|
+
this.enrichActionsFromHandlers(actions, handlerBehaviors);
|
|
3277
|
+
return actions;
|
|
3278
|
+
}
|
|
3279
|
+
extractActionFromElement(element, nodePath) {
|
|
3280
|
+
const opening = element.openingElement;
|
|
3281
|
+
const componentName = getJsxElementName(opening) ?? void 0;
|
|
3282
|
+
const action = { id: "", type: "custom", sourceComponent: componentName };
|
|
3283
|
+
const ariaLabel = getStringAttr(opening, "aria-label");
|
|
3284
|
+
const label = ariaLabel ?? jsxTextContent(element) ?? getStringAttr(opening, "value") ?? getStringAttr(opening, "title");
|
|
3285
|
+
if (label) action.label = label;
|
|
3286
|
+
if (ariaLabel) {
|
|
3287
|
+
action.locator = mergeLocator(action.locator, {
|
|
3288
|
+
accessibilityLabel: ariaLabel,
|
|
3289
|
+
source: "aria-label"
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
const appilotsId = getStringAttr(opening, "appilotsId");
|
|
3293
|
+
const dataTestId = getStringAttr(opening, "data-testid");
|
|
3294
|
+
const domId = getStringAttr(opening, "id");
|
|
3295
|
+
if (appilotsId) {
|
|
3296
|
+
action.id = appilotsId;
|
|
3297
|
+
action.locator = mergeLocator(action.locator, {
|
|
3298
|
+
id: appilotsId,
|
|
3299
|
+
appilotsId,
|
|
3300
|
+
source: "appilotsId"
|
|
3301
|
+
});
|
|
3302
|
+
} else if (dataTestId) {
|
|
3303
|
+
action.id = dataTestId;
|
|
3304
|
+
action.locator = mergeLocator(action.locator, {
|
|
3305
|
+
id: dataTestId,
|
|
3306
|
+
testID: dataTestId,
|
|
3307
|
+
source: "data-testid"
|
|
3308
|
+
});
|
|
3309
|
+
} else if (domId) {
|
|
3310
|
+
action.id = domId;
|
|
3311
|
+
action.locator = mergeLocator(action.locator, { id: domId, source: "id" });
|
|
3312
|
+
} else if (action.label) {
|
|
3313
|
+
action.id = slugify(action.label);
|
|
3314
|
+
}
|
|
3315
|
+
if (!action.id) return null;
|
|
3316
|
+
const to = routePathAttr(opening, "to") ?? routePathAttr(opening, "href");
|
|
3317
|
+
if (to && to.startsWith("/")) {
|
|
3318
|
+
action.type = "navigation";
|
|
3319
|
+
action.targetScreen = to;
|
|
3320
|
+
} else if (to && !to.startsWith("/") && !hasJsxAttribute(opening, "onClick")) {
|
|
3321
|
+
return null;
|
|
3322
|
+
}
|
|
3323
|
+
let handlerName;
|
|
3324
|
+
const onClickAttr = opening.attributes.find(
|
|
3325
|
+
(attr) => BabelTypes__namespace.isJSXAttribute(attr) && BabelTypes__namespace.isJSXIdentifier(attr.name) && attr.name.name === "onClick"
|
|
3326
|
+
);
|
|
3327
|
+
if (onClickAttr?.value && BabelTypes__namespace.isJSXExpressionContainer(onClickAttr.value)) {
|
|
3328
|
+
const expr = onClickAttr.value.expression;
|
|
3329
|
+
if (BabelTypes__namespace.isIdentifier(expr)) {
|
|
3330
|
+
handlerName = expr.name;
|
|
3331
|
+
action.handler = expr.name;
|
|
3332
|
+
} else if (!BabelTypes__namespace.isJSXEmptyExpression(expr)) {
|
|
3333
|
+
const inlineNavCalls = extractWebNavigationCalls(expr);
|
|
3334
|
+
const inlineNav = inlineNavCalls.find((call) => call.targetPath);
|
|
3335
|
+
if (inlineNav?.targetPath) {
|
|
3336
|
+
action.type = "navigation";
|
|
3337
|
+
action.targetScreen = inlineNav.targetPath;
|
|
3338
|
+
} else if (inlineNavCalls.some((call) => call.method === "goBack")) {
|
|
3339
|
+
action.type = "navigation";
|
|
3340
|
+
action.successSignal = {
|
|
3341
|
+
type: "goBack",
|
|
3342
|
+
description: "Action returns to the previous page"
|
|
3343
|
+
};
|
|
3344
|
+
action.appilotsInferred = {
|
|
3345
|
+
...action.appilotsInferred ?? {},
|
|
3346
|
+
expectedOutcome: "navigation"
|
|
3347
|
+
};
|
|
3348
|
+
}
|
|
3349
|
+
const inlineHandler = firstCalledFunctionName(expr);
|
|
3350
|
+
if (inlineHandler) {
|
|
3351
|
+
handlerName = inlineHandler;
|
|
3352
|
+
action.handler = inlineHandler;
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
if (handlerName) {
|
|
3357
|
+
const lower = handlerName.toLowerCase();
|
|
3358
|
+
if (lower.includes("submit")) action.type = "submit";
|
|
3359
|
+
else if (lower.includes("navigate") && action.type === "custom") action.type = "navigation";
|
|
3360
|
+
}
|
|
3361
|
+
if (componentName && this.isSubmitElement(componentName, opening)) {
|
|
3362
|
+
const formParent = nodePath.findParent(
|
|
3363
|
+
(p) => p.isJSXElement() && getJsxElementName(p.node.openingElement) === "form"
|
|
3364
|
+
);
|
|
3365
|
+
if (formParent) {
|
|
3366
|
+
action.type = "submit";
|
|
3367
|
+
if (!action.handler) {
|
|
3368
|
+
const formHandler = this.handlerNameFromAttr(formParent.node.openingElement, "onSubmit");
|
|
3369
|
+
if (formHandler && formHandler !== "anonymous") {
|
|
3370
|
+
action.handler = formHandler;
|
|
3371
|
+
handlerName = formHandler;
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
if (hasJsxAttribute(opening, "destructive") || hasJsxAttribute(opening, "aria-destructive")) {
|
|
3377
|
+
const value = getStringAttr(opening, "destructive") ?? getStringAttr(opening, "aria-destructive");
|
|
3378
|
+
action.destructive = value !== "false";
|
|
3379
|
+
} else if (handlerName && DESTRUCTIVE_VERB2.test(handlerName) || DESTRUCTIVE_VERB2.test(action.id)) {
|
|
3380
|
+
action.destructive = true;
|
|
3381
|
+
}
|
|
3382
|
+
if (!action.locator && action.id) {
|
|
3383
|
+
action.locator = {
|
|
3384
|
+
id: action.id,
|
|
3385
|
+
label: action.label,
|
|
3386
|
+
source: action.label ? "label" : "inferred"
|
|
3387
|
+
};
|
|
3388
|
+
}
|
|
3389
|
+
return action;
|
|
3390
|
+
}
|
|
3391
|
+
mergeActionMetadata(target, source) {
|
|
3392
|
+
target.handler = target.handler ?? source.handler;
|
|
3393
|
+
target.targetScreen = target.targetScreen ?? source.targetScreen;
|
|
3394
|
+
target.description = target.description ?? source.description;
|
|
3395
|
+
target.locator = target.locator ?? source.locator;
|
|
3396
|
+
target.nativeConfirmationExpected = target.nativeConfirmationExpected || source.nativeConfirmationExpected || void 0;
|
|
3397
|
+
target.requiresConfirmation = target.requiresConfirmation || source.requiresConfirmation || void 0;
|
|
3398
|
+
target.destructive = target.destructive || source.destructive || void 0;
|
|
3399
|
+
target.effect = target.effect ?? source.effect;
|
|
3400
|
+
target.riskLevel = target.riskLevel ?? source.riskLevel;
|
|
3401
|
+
target.appilotsInferred = target.appilotsInferred ?? source.appilotsInferred;
|
|
3402
|
+
}
|
|
3403
|
+
enrichActionsFromHandlers(actions, behaviors) {
|
|
3404
|
+
for (const action of actions) {
|
|
3405
|
+
const behavior = action.handler ? behaviors.get(action.handler) : void 0;
|
|
3406
|
+
if (!behavior) continue;
|
|
3407
|
+
action.appilotsInferred = {
|
|
3408
|
+
...action.appilotsInferred ?? {},
|
|
3409
|
+
...behavior.base.appilotsInferred
|
|
3410
|
+
};
|
|
3411
|
+
if (behavior.nativeConfirmationExpected || behavior.base.nativeConfirmationExpected) {
|
|
3412
|
+
action.nativeConfirmationExpected = true;
|
|
3413
|
+
}
|
|
3414
|
+
if (behavior.targetPath && !action.targetScreen) {
|
|
3415
|
+
action.targetScreen = behavior.targetPath;
|
|
3416
|
+
if (action.type === "custom") action.type = "navigation";
|
|
3417
|
+
action.appilotsInferred = {
|
|
3418
|
+
...action.appilotsInferred ?? {},
|
|
3419
|
+
expectedOutcome: "navigation"
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
if (behavior.base.successSignal && !action.successSignal) {
|
|
3423
|
+
action.successSignal = behavior.base.successSignal;
|
|
3424
|
+
}
|
|
3425
|
+
if (behavior.base.failureSignal && !action.failureSignal) {
|
|
3426
|
+
action.failureSignal = behavior.base.failureSignal;
|
|
3427
|
+
}
|
|
3428
|
+
if (behavior.base.opensModal && !action.opensModal) {
|
|
3429
|
+
action.opensModal = behavior.base.opensModal;
|
|
3430
|
+
}
|
|
3431
|
+
if (behavior.base.destructive || action.destructive === true || action.requiresConfirmation === true || action.effect === "destructive" || action.riskLevel === "high") {
|
|
3432
|
+
action.destructive = true;
|
|
3433
|
+
action.effect = action.effect ?? "destructive";
|
|
3434
|
+
action.riskLevel = action.riskLevel ?? "high";
|
|
3435
|
+
action.requiresConfirmation = action.requiresConfirmation ?? true;
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
/**
|
|
3440
|
+
* Handler behavior via the shared, platform-neutral analyzer
|
|
3441
|
+
* (async/await, `.then`, state setters, toasts, destructive verbs)
|
|
3442
|
+
* plus the web-only signals: React Router navigation targets and
|
|
3443
|
+
* `window.confirm(...)` as the native confirmation dialog.
|
|
3444
|
+
*/
|
|
3445
|
+
collectWebHandlerBehaviors(ast) {
|
|
3446
|
+
const handlers = collectFunctions(ast);
|
|
3447
|
+
const out = /* @__PURE__ */ new Map();
|
|
3448
|
+
for (const [name, fn] of handlers) {
|
|
3449
|
+
const base = analyzeFunctionBehavior(name, fn, handlers);
|
|
3450
|
+
const navCalls = fn.body ? extractWebNavigationCalls(fn.body) : [];
|
|
3451
|
+
const firstNav = navCalls.find((call) => call.targetPath);
|
|
3452
|
+
const goesBack = navCalls.some((call) => call.method === "goBack");
|
|
3453
|
+
out.set(name, {
|
|
3454
|
+
base: {
|
|
3455
|
+
...base,
|
|
3456
|
+
...goesBack && !base.successSignal ? {
|
|
3457
|
+
successSignal: {
|
|
3458
|
+
type: "goBack",
|
|
3459
|
+
description: "Action returns to the previous page"
|
|
3460
|
+
}
|
|
3461
|
+
} : {}
|
|
3462
|
+
},
|
|
3463
|
+
targetPath: firstNav?.targetPath,
|
|
3464
|
+
nativeConfirmationExpected: fn.body ? containsWindowConfirm(fn.body) : false
|
|
3465
|
+
});
|
|
3466
|
+
}
|
|
3467
|
+
return out;
|
|
3468
|
+
}
|
|
3469
|
+
handlerNameFromAttr(opening, attrName) {
|
|
3470
|
+
const attr = opening.attributes.find(
|
|
3471
|
+
(candidate) => BabelTypes__namespace.isJSXAttribute(candidate) && BabelTypes__namespace.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
|
|
3472
|
+
);
|
|
3473
|
+
if (!attr?.value || !BabelTypes__namespace.isJSXExpressionContainer(attr.value)) return void 0;
|
|
3474
|
+
const expr = attr.value.expression;
|
|
3475
|
+
if (BabelTypes__namespace.isIdentifier(expr)) return expr.name;
|
|
3476
|
+
if (BabelTypes__namespace.isArrowFunctionExpression(expr) || BabelTypes__namespace.isFunctionExpression(expr)) {
|
|
3477
|
+
return firstCalledFunctionName(expr) ?? "anonymous";
|
|
3478
|
+
}
|
|
3479
|
+
return void 0;
|
|
3480
|
+
}
|
|
3481
|
+
// ── Navigation targets (route paths — resolved by the orchestrator) ─
|
|
3482
|
+
extractNavigationTargets(ast) {
|
|
3483
|
+
const targets = /* @__PURE__ */ new Set();
|
|
3484
|
+
for (const call of extractWebNavigationCalls(ast)) {
|
|
3485
|
+
if (call.targetPath && call.targetPath.startsWith("/")) targets.add(call.targetPath);
|
|
3486
|
+
}
|
|
3487
|
+
traverse4__default.default(ast, {
|
|
3488
|
+
JSXOpeningElement: (nodePath) => {
|
|
3489
|
+
const element = nodePath.node;
|
|
3490
|
+
const name = getJsxElementName(element);
|
|
3491
|
+
if (name === "Link" || name === "NavLink" || name === "Navigate") {
|
|
3492
|
+
const to = routePathAttr(element, "to");
|
|
3493
|
+
if (to && to.startsWith("/")) targets.add(to);
|
|
3494
|
+
}
|
|
3495
|
+
if (name === "a") {
|
|
3496
|
+
const href = routePathAttr(element, "href");
|
|
3497
|
+
if (href && href.startsWith("/")) targets.add(href);
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
});
|
|
3501
|
+
return Array.from(targets).sort();
|
|
3502
|
+
}
|
|
3503
|
+
// ── Collections ───────────────────────────────────────────────────
|
|
3504
|
+
extractCollections(ast) {
|
|
3505
|
+
const collections = [];
|
|
3506
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3507
|
+
traverse4__default.default(ast, {
|
|
3508
|
+
JSXExpressionContainer: (nodePath) => {
|
|
3509
|
+
const expr = nodePath.node.expression;
|
|
3510
|
+
if (!BabelTypes__namespace.isCallExpression(expr) || !BabelTypes__namespace.isMemberExpression(expr.callee) || !BabelTypes__namespace.isIdentifier(expr.callee.object) || !BabelTypes__namespace.isIdentifier(expr.callee.property) || expr.callee.property.name !== "map") {
|
|
3511
|
+
return;
|
|
3512
|
+
}
|
|
3513
|
+
const callback = expr.arguments[0];
|
|
3514
|
+
if (!BabelTypes__namespace.isArrowFunctionExpression(callback) && !BabelTypes__namespace.isFunctionExpression(callback)) return;
|
|
3515
|
+
const enclosing = nodePath.findParent(
|
|
3516
|
+
(p) => p.isJSXElement()
|
|
3517
|
+
);
|
|
3518
|
+
const enclosingName = enclosing ? getJsxElementName(enclosing.node.openingElement) : null;
|
|
3519
|
+
const enclosingRole = enclosing && enclosingName ? classifyWebJsxComponent(enclosingName, enclosing.node.openingElement) : null;
|
|
3520
|
+
const returnsListItem = callbackReturnsTag(callback, /* @__PURE__ */ new Set(["li", "tr"]));
|
|
3521
|
+
if (enclosingName === "select") return;
|
|
3522
|
+
const isListContext = enclosingName !== null && LISTISH_TAGS.has(enclosingName) || enclosingRole === "list" || returnsListItem;
|
|
3523
|
+
if (!isListContext) return;
|
|
3524
|
+
const dataSource = expr.callee.object.name;
|
|
3525
|
+
if (seen.has(dataSource)) return;
|
|
3526
|
+
seen.add(dataSource);
|
|
3527
|
+
const itemNames = collectionItemNames(callback);
|
|
3528
|
+
const displayFields = collectionDisplayFields(callback, itemNames);
|
|
3529
|
+
const keyField = collectionKeyField(callback, itemNames);
|
|
3530
|
+
const rowAction = collectionRowAction(callback);
|
|
3531
|
+
const itemType = inferItemType(dataSource, displayFields);
|
|
3532
|
+
const identityFields = inferIdentityFields(keyField, displayFields);
|
|
3533
|
+
collections.push({
|
|
3534
|
+
id: dataSource,
|
|
3535
|
+
component: enclosingName ?? "list",
|
|
3536
|
+
...itemType ? { itemType } : {},
|
|
3537
|
+
dataSource,
|
|
3538
|
+
...keyField ? { keyField } : {},
|
|
3539
|
+
...displayFields.length > 0 ? { displayFields } : {},
|
|
3540
|
+
...rowAction ? { rowAction, rowActions: [rowAction] } : {},
|
|
3541
|
+
...identityFields.length > 0 ? { identityFields } : {}
|
|
3542
|
+
});
|
|
3543
|
+
}
|
|
3544
|
+
});
|
|
3545
|
+
return collections;
|
|
3546
|
+
}
|
|
3547
|
+
};
|
|
3548
|
+
function isRegisterScreenCallee(callee) {
|
|
3549
|
+
return BabelTypes__namespace.isIdentifier(callee) && callee.name === "registerScreen" || BabelTypes__namespace.isMemberExpression(callee) && BabelTypes__namespace.isIdentifier(callee.property) && callee.property.name === "registerScreen";
|
|
3550
|
+
}
|
|
3551
|
+
function literalToPlain(node) {
|
|
3552
|
+
if (BabelTypes__namespace.isStringLiteral(node) || BabelTypes__namespace.isNumericLiteral(node) || BabelTypes__namespace.isBooleanLiteral(node)) {
|
|
3553
|
+
return node.value;
|
|
3554
|
+
}
|
|
3555
|
+
if (BabelTypes__namespace.isNullLiteral(node)) return null;
|
|
3556
|
+
if (BabelTypes__namespace.isArrayExpression(node)) {
|
|
3557
|
+
return node.elements.filter((el) => el !== null && BabelTypes__namespace.isExpression(el)).map((el) => literalToPlain(el)).filter((value) => value !== void 0);
|
|
3558
|
+
}
|
|
3559
|
+
if (BabelTypes__namespace.isObjectExpression(node)) {
|
|
3560
|
+
const out = {};
|
|
3561
|
+
for (const prop of node.properties) {
|
|
3562
|
+
if (!BabelTypes__namespace.isObjectProperty(prop)) continue;
|
|
3563
|
+
const key = BabelTypes__namespace.isIdentifier(prop.key) ? prop.key.name : BabelTypes__namespace.isStringLiteral(prop.key) ? prop.key.value : void 0;
|
|
3564
|
+
if (!key || !BabelTypes__namespace.isExpression(prop.value)) continue;
|
|
3565
|
+
const value = literalToPlain(prop.value);
|
|
3566
|
+
if (value !== void 0) out[key] = value;
|
|
3567
|
+
}
|
|
3568
|
+
return out;
|
|
3569
|
+
}
|
|
3570
|
+
return void 0;
|
|
3571
|
+
}
|
|
3572
|
+
function jsxTextContent(element) {
|
|
3573
|
+
const parts = [];
|
|
3574
|
+
const walk = (children) => {
|
|
3575
|
+
for (const child of children) {
|
|
3576
|
+
if (BabelTypes__namespace.isJSXText(child)) {
|
|
3577
|
+
const trimmed = child.value.replace(/\s+/g, " ").trim();
|
|
3578
|
+
if (trimmed) parts.push(trimmed);
|
|
3579
|
+
} else if (BabelTypes__namespace.isJSXExpressionContainer(child) && BabelTypes__namespace.isStringLiteral(child.expression)) {
|
|
3580
|
+
parts.push(child.expression.value);
|
|
3581
|
+
} else if (BabelTypes__namespace.isJSXElement(child)) {
|
|
3582
|
+
walk(child.children);
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
};
|
|
3586
|
+
walk(element.children);
|
|
3587
|
+
const text = parts.join(" ").trim();
|
|
3588
|
+
return text.length > 0 ? text : void 0;
|
|
3589
|
+
}
|
|
3590
|
+
function firstCalledFunctionName(node) {
|
|
3591
|
+
if (BabelTypes__namespace.isIdentifier(node)) return node.name;
|
|
3592
|
+
if (BabelTypes__namespace.isArrowFunctionExpression(node) || BabelTypes__namespace.isFunctionExpression(node)) {
|
|
3593
|
+
return firstCalledFunctionName(node.body);
|
|
3594
|
+
}
|
|
3595
|
+
if (BabelTypes__namespace.isBlockStatement(node)) {
|
|
3596
|
+
for (const statement of node.body) {
|
|
3597
|
+
const handler = firstCalledFunctionName(statement);
|
|
3598
|
+
if (handler) return handler;
|
|
3599
|
+
}
|
|
3600
|
+
return void 0;
|
|
3601
|
+
}
|
|
3602
|
+
if (BabelTypes__namespace.isExpressionStatement(node)) return firstCalledFunctionName(node.expression);
|
|
3603
|
+
if (BabelTypes__namespace.isReturnStatement(node)) {
|
|
3604
|
+
return node.argument ? firstCalledFunctionName(node.argument) : void 0;
|
|
3605
|
+
}
|
|
3606
|
+
if (BabelTypes__namespace.isAwaitExpression(node) || BabelTypes__namespace.isUnaryExpression(node)) {
|
|
3607
|
+
return firstCalledFunctionName(node.argument);
|
|
3608
|
+
}
|
|
3609
|
+
if (BabelTypes__namespace.isCallExpression(node)) {
|
|
3610
|
+
if (BabelTypes__namespace.isIdentifier(node.callee) && !/^(navigate|confirm|alert)$/.test(node.callee.name)) {
|
|
3611
|
+
return node.callee.name;
|
|
3612
|
+
}
|
|
3613
|
+
return void 0;
|
|
3614
|
+
}
|
|
3615
|
+
return void 0;
|
|
3616
|
+
}
|
|
3617
|
+
function containsWindowConfirm(body) {
|
|
3618
|
+
let found = false;
|
|
3619
|
+
traverse4__default.default(
|
|
3620
|
+
body,
|
|
3621
|
+
{
|
|
3622
|
+
noScope: true,
|
|
3623
|
+
CallExpression: (nodePath) => {
|
|
3624
|
+
const callee = nodePath.node.callee;
|
|
3625
|
+
if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "confirm") found = true;
|
|
3626
|
+
if (BabelTypes__namespace.isMemberExpression(callee) && BabelTypes__namespace.isIdentifier(callee.object) && callee.object.name === "window" && BabelTypes__namespace.isIdentifier(callee.property) && callee.property.name === "confirm") {
|
|
3627
|
+
found = true;
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
);
|
|
3632
|
+
return found;
|
|
3633
|
+
}
|
|
3634
|
+
function mergeLocator(current, next) {
|
|
3635
|
+
return { ...current ?? {}, ...next };
|
|
3636
|
+
}
|
|
3637
|
+
function routePathAttr(opening, attrName) {
|
|
3638
|
+
const literal = getStringAttr(opening, attrName);
|
|
3639
|
+
if (literal) return literal;
|
|
3640
|
+
const attr = opening.attributes.find(
|
|
3641
|
+
(candidate) => BabelTypes__namespace.isJSXAttribute(candidate) && BabelTypes__namespace.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
|
|
3642
|
+
);
|
|
3643
|
+
if (!attr?.value || !BabelTypes__namespace.isJSXExpressionContainer(attr.value)) return void 0;
|
|
3644
|
+
return staticRoutePath(attr.value.expression);
|
|
3645
|
+
}
|
|
3646
|
+
function slugify(label) {
|
|
3647
|
+
return label.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3648
|
+
}
|
|
3649
|
+
function normalizeLabel(label) {
|
|
3650
|
+
return label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3651
|
+
}
|
|
3652
|
+
function isWeakInferredFieldName(name) {
|
|
3653
|
+
return /^(text|value|input|query|search|selected|checked)$/i.test(name);
|
|
3654
|
+
}
|
|
3655
|
+
function collectionItemNames(callback) {
|
|
3656
|
+
const names = /* @__PURE__ */ new Set(["item"]);
|
|
3657
|
+
const firstParam = callback.params[0];
|
|
3658
|
+
if (BabelTypes__namespace.isIdentifier(firstParam)) names.add(firstParam.name);
|
|
3659
|
+
if (BabelTypes__namespace.isObjectPattern(firstParam)) {
|
|
3660
|
+
for (const prop of firstParam.properties) {
|
|
3661
|
+
if (BabelTypes__namespace.isObjectProperty(prop) && BabelTypes__namespace.isIdentifier(prop.key) && BabelTypes__namespace.isIdentifier(prop.value)) {
|
|
3662
|
+
names.add(prop.value.name);
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
return names;
|
|
3667
|
+
}
|
|
3668
|
+
function collectionDisplayFields(callback, itemNames) {
|
|
3669
|
+
const fields = /* @__PURE__ */ new Set();
|
|
3670
|
+
if (!callback.body) return [];
|
|
3671
|
+
traverse4__default.default(
|
|
3672
|
+
callback.body,
|
|
3673
|
+
{
|
|
3674
|
+
noScope: true,
|
|
3675
|
+
MemberExpression: (nodePath) => {
|
|
3676
|
+
const node = nodePath.node;
|
|
3677
|
+
if (BabelTypes__namespace.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes__namespace.isIdentifier(node.property)) {
|
|
3678
|
+
fields.add(node.property.name);
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
);
|
|
3683
|
+
return Array.from(fields).sort();
|
|
3684
|
+
}
|
|
3685
|
+
function collectionKeyField(callback, itemNames) {
|
|
3686
|
+
let keyField;
|
|
3687
|
+
if (!callback.body) return void 0;
|
|
3688
|
+
traverse4__default.default(
|
|
3689
|
+
callback.body,
|
|
3690
|
+
{
|
|
3691
|
+
noScope: true,
|
|
3692
|
+
JSXAttribute: (nodePath) => {
|
|
3693
|
+
const attr = nodePath.node;
|
|
3694
|
+
if (!BabelTypes__namespace.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
|
|
3695
|
+
if (!attr.value || !BabelTypes__namespace.isJSXExpressionContainer(attr.value)) return;
|
|
3696
|
+
const expr = attr.value.expression;
|
|
3697
|
+
if (BabelTypes__namespace.isMemberExpression(expr) && BabelTypes__namespace.isIdentifier(expr.object) && itemNames.has(expr.object.name) && BabelTypes__namespace.isIdentifier(expr.property)) {
|
|
3698
|
+
keyField = keyField ?? expr.property.name;
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
);
|
|
3703
|
+
return keyField;
|
|
3704
|
+
}
|
|
3705
|
+
function collectionRowAction(callback) {
|
|
3706
|
+
if (!callback.body) return void 0;
|
|
3707
|
+
const navCall = extractWebNavigationCalls(callback.body).find((call) => call.targetPath);
|
|
3708
|
+
if (!navCall?.targetPath) return void 0;
|
|
3709
|
+
return {
|
|
3710
|
+
type: "navigation",
|
|
3711
|
+
// Route path — the orchestrator resolves it to a screen name.
|
|
3712
|
+
targetScreen: navCall.targetPath,
|
|
3713
|
+
description: `Clicking a row opens ${navCall.targetPath}`
|
|
3714
|
+
};
|
|
3715
|
+
}
|
|
3716
|
+
function callbackReturnsTag(callback, tags) {
|
|
3717
|
+
let found = false;
|
|
3718
|
+
const inspect = (node) => {
|
|
3719
|
+
if (!node || found) return;
|
|
3720
|
+
if (BabelTypes__namespace.isJSXElement(node)) {
|
|
3721
|
+
const name = getJsxElementName(node.openingElement);
|
|
3722
|
+
if (name && tags.has(name)) found = true;
|
|
3723
|
+
return;
|
|
3724
|
+
}
|
|
3725
|
+
if (BabelTypes__namespace.isBlockStatement(node)) {
|
|
3726
|
+
for (const statement of node.body) {
|
|
3727
|
+
if (BabelTypes__namespace.isReturnStatement(statement)) inspect(statement.argument);
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
if (BabelTypes__namespace.isParenthesizedExpression(node)) inspect(node.expression);
|
|
3731
|
+
if (BabelTypes__namespace.isConditionalExpression(node)) {
|
|
3732
|
+
inspect(node.consequent);
|
|
3733
|
+
inspect(node.alternate);
|
|
3734
|
+
}
|
|
3735
|
+
};
|
|
3736
|
+
inspect(callback.body);
|
|
3737
|
+
return found;
|
|
3738
|
+
}
|
|
3739
|
+
function inferItemType(dataSource, displayFields) {
|
|
3740
|
+
const singular = dataSource.replace(/^render/i, "").replace(/(List|Items|Data|Rows|Sections)$/i, "").replace(/s$/i, "");
|
|
3741
|
+
const candidate = singular.charAt(0).toUpperCase() + singular.slice(1);
|
|
3742
|
+
if (candidate.length > 1) return candidate;
|
|
3743
|
+
if (displayFields.length > 0) return "Item";
|
|
3744
|
+
return void 0;
|
|
3745
|
+
}
|
|
3746
|
+
function inferIdentityFields(keyField, displayFields) {
|
|
3747
|
+
const out = /* @__PURE__ */ new Set();
|
|
3748
|
+
if (keyField) out.add(keyField);
|
|
3749
|
+
for (const field of displayFields) {
|
|
3750
|
+
if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
|
|
3751
|
+
}
|
|
3752
|
+
return Array.from(out);
|
|
3753
|
+
}
|
|
3754
|
+
var WEB_NAVIGATOR_TYPE = "route";
|
|
3755
|
+
var WebNavigationAnalyzer = class {
|
|
3756
|
+
config;
|
|
3757
|
+
navigationInclude;
|
|
3758
|
+
navigationExclude;
|
|
3759
|
+
constructor(config, options) {
|
|
3760
|
+
this.config = config;
|
|
3761
|
+
this.navigationInclude = options?.navigationInclude ?? [];
|
|
3762
|
+
this.navigationExclude = options?.navigationExclude ?? [];
|
|
3763
|
+
}
|
|
3764
|
+
async analyze() {
|
|
3765
|
+
const files = await this.findRouteFiles();
|
|
3766
|
+
const routes = [];
|
|
3767
|
+
for (const filePath of files) {
|
|
3768
|
+
try {
|
|
3769
|
+
const content = await fs$1.promises.readFile(filePath, "utf-8");
|
|
3770
|
+
if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(content)) {
|
|
3771
|
+
continue;
|
|
3772
|
+
}
|
|
3773
|
+
const ast = parseSource(content, this.config.parserPlugins);
|
|
3774
|
+
routes.push(...this.extractJsxRoutes(ast));
|
|
3775
|
+
routes.push(...this.extractObjectRoutes(ast));
|
|
3776
|
+
} catch (error) {
|
|
3777
|
+
console.warn(`[WebNavigationAnalyzer] Failed to parse ${filePath}:`, error);
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
const deduped = this.dedupeRoutes(routes);
|
|
3781
|
+
return { graph: this.buildGraph(deduped), routes: deduped };
|
|
3782
|
+
}
|
|
3783
|
+
/** Files likely to contain route configuration. */
|
|
3784
|
+
async findRouteFiles() {
|
|
3785
|
+
const patterns = [
|
|
3786
|
+
"**/*{router,routes,Router,Routes}*.{ts,tsx,js,jsx}",
|
|
3787
|
+
"**/App.{ts,tsx,js,jsx}",
|
|
3788
|
+
"**/app.{ts,tsx,js,jsx}",
|
|
3789
|
+
"**/main.{ts,tsx,js,jsx}",
|
|
3790
|
+
"**/index.{ts,tsx,js,jsx}",
|
|
3791
|
+
...this.navigationInclude
|
|
3792
|
+
];
|
|
3793
|
+
const ignore = [
|
|
3794
|
+
"**/node_modules/**",
|
|
3795
|
+
"**/dist/**",
|
|
3796
|
+
"**/build/**",
|
|
3797
|
+
...this.config.exclude || [],
|
|
3798
|
+
...this.navigationExclude
|
|
3799
|
+
];
|
|
3800
|
+
const files = await glob__default.default(patterns, { cwd: this.config.rootDir, ignore });
|
|
3801
|
+
return files.map((file) => path8__namespace.default.join(this.config.rootDir, file));
|
|
3802
|
+
}
|
|
3803
|
+
// ── JSX <Route> style ────────────────────────────────────────────
|
|
3804
|
+
extractJsxRoutes(ast) {
|
|
3805
|
+
const routes = [];
|
|
3806
|
+
const visitRoute = (element, parentPath) => {
|
|
3807
|
+
const opening = element.openingElement;
|
|
3808
|
+
const name = getJsxElementName(opening);
|
|
3809
|
+
if (name !== "Route") {
|
|
3810
|
+
for (const child of element.children) {
|
|
3811
|
+
if (BabelTypes__namespace.isJSXElement(child)) visitRoute(child, parentPath);
|
|
3812
|
+
}
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
3815
|
+
const segment = getStringAttr(opening, "path");
|
|
3816
|
+
const isIndex = hasJsxAttribute(opening, "index") && !segment;
|
|
3817
|
+
const fullPath = this.joinPaths(parentPath, segment, isIndex);
|
|
3818
|
+
const componentName = this.componentNameFromElementAttr(opening) ?? void 0;
|
|
3819
|
+
const isLeaf = !element.children.some(
|
|
3820
|
+
(child) => BabelTypes__namespace.isJSXElement(child) && getJsxElementName(child.openingElement) === "Route"
|
|
3821
|
+
);
|
|
3822
|
+
if ((segment || isIndex) && (componentName || isLeaf)) {
|
|
3823
|
+
routes.push(this.buildRoute(fullPath, componentName, isIndex, !isLeaf));
|
|
3824
|
+
}
|
|
3825
|
+
for (const child of element.children) {
|
|
3826
|
+
if (BabelTypes__namespace.isJSXElement(child)) visitRoute(child, fullPath);
|
|
3827
|
+
}
|
|
3828
|
+
};
|
|
3829
|
+
traverse4__default.default(ast, {
|
|
3830
|
+
JSXElement: (nodePath) => {
|
|
3831
|
+
const name = getJsxElementName(nodePath.node.openingElement);
|
|
3832
|
+
if (name !== "Routes" && name !== "Route") return;
|
|
3833
|
+
if (nodePath.findParent((p) => {
|
|
3834
|
+
if (!p.isJSXElement()) return false;
|
|
3835
|
+
const parentName = getJsxElementName(p.node.openingElement);
|
|
3836
|
+
return parentName === "Routes" || parentName === "Route";
|
|
3837
|
+
})) {
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
visitRoute(nodePath.node, "");
|
|
2355
3841
|
}
|
|
2356
|
-
return screen;
|
|
2357
3842
|
});
|
|
2358
|
-
return
|
|
2359
|
-
screens: enrichedScreens,
|
|
2360
|
-
navigation,
|
|
2361
|
-
analyzedFiles: screenFiles.length,
|
|
2362
|
-
...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
|
|
2363
|
-
};
|
|
3843
|
+
return routes;
|
|
2364
3844
|
}
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
if (
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
3845
|
+
/** `element={<VehicleList/>}` or `Component={VehicleList}`. */
|
|
3846
|
+
componentNameFromElementAttr(opening) {
|
|
3847
|
+
for (const attr of opening.attributes) {
|
|
3848
|
+
if (!BabelTypes__namespace.isJSXAttribute(attr) || !BabelTypes__namespace.isJSXIdentifier(attr.name)) continue;
|
|
3849
|
+
if (attr.name.name === "element" && BabelTypes__namespace.isJSXExpressionContainer(attr.value)) {
|
|
3850
|
+
const expr = attr.value.expression;
|
|
3851
|
+
if (BabelTypes__namespace.isJSXElement(expr)) return getJsxElementName(expr.openingElement);
|
|
3852
|
+
}
|
|
3853
|
+
if (attr.name.name === "Component" && BabelTypes__namespace.isJSXExpressionContainer(attr.value)) {
|
|
3854
|
+
if (BabelTypes__namespace.isIdentifier(attr.value.expression)) return attr.value.expression.name;
|
|
2372
3855
|
}
|
|
2373
3856
|
}
|
|
2374
|
-
|
|
2375
|
-
target.validationRules = source.validationRules ? { ...source.validationRules, ...target.validationRules ?? {} } : target.validationRules;
|
|
3857
|
+
return null;
|
|
2376
3858
|
}
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
3859
|
+
// ── createBrowserRouter([...]) / useRoutes([...]) style ──────────
|
|
3860
|
+
extractObjectRoutes(ast) {
|
|
3861
|
+
const routes = [];
|
|
3862
|
+
const ROUTER_FACTORIES = /* @__PURE__ */ new Set([
|
|
3863
|
+
"createBrowserRouter",
|
|
3864
|
+
"createHashRouter",
|
|
3865
|
+
"createMemoryRouter",
|
|
3866
|
+
"useRoutes"
|
|
3867
|
+
]);
|
|
3868
|
+
traverse4__default.default(ast, {
|
|
3869
|
+
CallExpression: (nodePath) => {
|
|
3870
|
+
const callee = nodePath.node.callee;
|
|
3871
|
+
if (!BabelTypes__namespace.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
|
|
3872
|
+
const first = nodePath.node.arguments[0];
|
|
3873
|
+
if (!BabelTypes__namespace.isArrayExpression(first)) return;
|
|
3874
|
+
this.visitRouteObjects(first, "", routes);
|
|
2385
3875
|
}
|
|
2386
|
-
return false;
|
|
2387
3876
|
});
|
|
3877
|
+
return routes;
|
|
2388
3878
|
}
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
3879
|
+
visitRouteObjects(arr, parentPath, out) {
|
|
3880
|
+
for (const element of arr.elements) {
|
|
3881
|
+
if (!BabelTypes__namespace.isObjectExpression(element)) continue;
|
|
3882
|
+
let segment;
|
|
3883
|
+
let isIndex = false;
|
|
3884
|
+
let componentName;
|
|
3885
|
+
let children;
|
|
3886
|
+
for (const prop of element.properties) {
|
|
3887
|
+
if (!BabelTypes__namespace.isObjectProperty(prop) || !BabelTypes__namespace.isIdentifier(prop.key)) continue;
|
|
3888
|
+
const key = prop.key.name;
|
|
3889
|
+
if (key === "path" && BabelTypes__namespace.isStringLiteral(prop.value)) segment = prop.value.value;
|
|
3890
|
+
if (key === "index" && BabelTypes__namespace.isBooleanLiteral(prop.value)) isIndex = prop.value.value;
|
|
3891
|
+
if (key === "element" && BabelTypes__namespace.isJSXElement(prop.value)) {
|
|
3892
|
+
componentName = getJsxElementName(prop.value.openingElement) ?? void 0;
|
|
3893
|
+
}
|
|
3894
|
+
if (key === "Component" && BabelTypes__namespace.isIdentifier(prop.value)) componentName = prop.value.name;
|
|
3895
|
+
if (key === "children" && BabelTypes__namespace.isArrayExpression(prop.value)) children = prop.value;
|
|
3896
|
+
}
|
|
3897
|
+
const fullPath = this.joinPaths(parentPath, segment, isIndex);
|
|
3898
|
+
if ((segment !== void 0 || isIndex) && (componentName || !children)) {
|
|
3899
|
+
out.push(this.buildRoute(fullPath, componentName, isIndex, Boolean(children)));
|
|
3900
|
+
}
|
|
3901
|
+
if (children) this.visitRouteObjects(children, fullPath, out);
|
|
2403
3902
|
}
|
|
2404
3903
|
}
|
|
2405
|
-
|
|
2406
|
-
|
|
3904
|
+
// ── Shared route building ────────────────────────────────────────
|
|
3905
|
+
joinPaths(parent, segment, isIndex) {
|
|
3906
|
+
if (isIndex || segment === void 0) return parent || "/";
|
|
3907
|
+
if (segment.startsWith("/")) return this.normalizePath(segment);
|
|
3908
|
+
return this.normalizePath(`${parent === "/" ? "" : parent}/${segment}`);
|
|
2407
3909
|
}
|
|
2408
|
-
|
|
2409
|
-
|
|
3910
|
+
normalizePath(p) {
|
|
3911
|
+
const cleaned = `/${p}`.replace(/\/+/g, "/");
|
|
3912
|
+
return cleaned.length > 1 ? cleaned.replace(/\/$/, "") : cleaned;
|
|
2410
3913
|
}
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
3914
|
+
buildRoute(fullPath, componentName, isIndex, isLayout) {
|
|
3915
|
+
const params = this.paramsFromPath(fullPath);
|
|
3916
|
+
return {
|
|
3917
|
+
path: fullPath,
|
|
3918
|
+
screenName: componentName ?? screenNameFromPath(fullPath),
|
|
3919
|
+
...params.length > 0 ? { params } : {},
|
|
3920
|
+
...isIndex ? { index: true } : {},
|
|
3921
|
+
...isLayout ? { layout: true } : {}
|
|
3922
|
+
};
|
|
3923
|
+
}
|
|
3924
|
+
paramsFromPath(routePath) {
|
|
3925
|
+
const params = [];
|
|
3926
|
+
for (const segment of routePath.split("/")) {
|
|
3927
|
+
if (!segment.startsWith(":")) continue;
|
|
3928
|
+
const optional = segment.endsWith("?");
|
|
3929
|
+
const name = segment.slice(1, optional ? -1 : void 0);
|
|
3930
|
+
if (name) params.push({ name, type: "string", required: !optional });
|
|
3931
|
+
}
|
|
3932
|
+
return params;
|
|
3933
|
+
}
|
|
3934
|
+
/**
|
|
3935
|
+
* One entry per path. When several declarations resolve to the same
|
|
3936
|
+
* path, keep the one that best describes what the user lands on: a
|
|
3937
|
+
* page beats a layout wrapper (an `index` child and its parent layout
|
|
3938
|
+
* share a path), and a resolved component name beats a name derived
|
|
3939
|
+
* from the path.
|
|
3940
|
+
*/
|
|
3941
|
+
dedupeRoutes(routes) {
|
|
3942
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
3943
|
+
for (const route of routes) {
|
|
3944
|
+
const existing = byPath.get(route.path);
|
|
3945
|
+
if (!existing || routeScore(route) > routeScore(existing)) {
|
|
3946
|
+
byPath.set(route.path, route);
|
|
2418
3947
|
}
|
|
2419
3948
|
}
|
|
2420
|
-
return
|
|
3949
|
+
return Array.from(byPath.values());
|
|
3950
|
+
}
|
|
3951
|
+
buildGraph(routes) {
|
|
3952
|
+
const screens = {};
|
|
3953
|
+
const navigatorName = "router";
|
|
3954
|
+
const screenNames = routes.map((r) => r.screenName);
|
|
3955
|
+
for (const route of routes) {
|
|
3956
|
+
const others = screenNames.filter((name) => name !== route.screenName);
|
|
3957
|
+
screens[route.screenName] = {
|
|
3958
|
+
screenName: route.screenName,
|
|
3959
|
+
// Open-union value — web routes, not a RN stack/tab/drawer.
|
|
3960
|
+
navigatorType: WEB_NAVIGATOR_TYPE,
|
|
3961
|
+
parentNavigator: navigatorName,
|
|
3962
|
+
// Any route is one URL away from any other — both directions,
|
|
3963
|
+
// like the RN analyzer models tab navigators.
|
|
3964
|
+
reachableFrom: others,
|
|
3965
|
+
reachableTo: others,
|
|
3966
|
+
...route.params ? { params: route.params } : {}
|
|
3967
|
+
};
|
|
3968
|
+
}
|
|
3969
|
+
const initialRoute = routes.find((r) => r.path === "/") ?? routes.find((r) => r.index) ?? routes[0];
|
|
3970
|
+
return {
|
|
3971
|
+
screens,
|
|
3972
|
+
initialScreen: initialRoute?.screenName ?? "",
|
|
3973
|
+
navigators: routes.length > 0 ? [{
|
|
3974
|
+
name: navigatorName,
|
|
3975
|
+
type: WEB_NAVIGATOR_TYPE,
|
|
3976
|
+
screens: screenNames
|
|
3977
|
+
}] : []
|
|
3978
|
+
};
|
|
2421
3979
|
}
|
|
2422
3980
|
};
|
|
3981
|
+
function screenNameFromPath(routePath) {
|
|
3982
|
+
if (routePath === "/" || routePath === "") return "Home";
|
|
3983
|
+
return routePath.split("/").filter(Boolean).map((segment) => segment.replace(/^:/, "").replace(/\?$/, "")).map(
|
|
3984
|
+
(segment) => segment.split(/[-_.]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("")
|
|
3985
|
+
).join("");
|
|
3986
|
+
}
|
|
3987
|
+
function routeScore(route) {
|
|
3988
|
+
const isPage = route.layout ? 0 : 2;
|
|
3989
|
+
const hasRealName = route.screenName === screenNameFromPath(route.path) ? 0 : 1;
|
|
3990
|
+
return isPage + hasRealName;
|
|
3991
|
+
}
|
|
3992
|
+
function resolvePathToScreen(routes, target) {
|
|
3993
|
+
const normalized = `/${target}`.replace(/\/+/g, "/").replace(/\?.*$/, "").replace(/#.*$/, "");
|
|
3994
|
+
const cleaned = normalized.length > 1 ? normalized.replace(/\/$/, "") : normalized;
|
|
3995
|
+
const exact = routes.find((r) => r.path === cleaned);
|
|
3996
|
+
if (exact) return exact.screenName;
|
|
3997
|
+
const targetSegments = cleaned.split("/").filter(Boolean);
|
|
3998
|
+
for (const route of routes) {
|
|
3999
|
+
const routeSegments = route.path.split("/").filter(Boolean);
|
|
4000
|
+
if (routeSegments.length !== targetSegments.length) continue;
|
|
4001
|
+
const matches = routeSegments.every(
|
|
4002
|
+
(seg, i) => seg.startsWith(":") || seg === "*" || seg === targetSegments[i]
|
|
4003
|
+
);
|
|
4004
|
+
if (matches) return route.screenName;
|
|
4005
|
+
}
|
|
4006
|
+
return void 0;
|
|
4007
|
+
}
|
|
2423
4008
|
|
|
2424
|
-
// src/analyzers/
|
|
2425
|
-
var
|
|
2426
|
-
|
|
2427
|
-
|
|
4009
|
+
// src/analyzers/web/ReactWebPlatformAnalyzer.ts
|
|
4010
|
+
var ReactWebPlatformAnalyzer = class {
|
|
4011
|
+
platform = "web";
|
|
4012
|
+
async analyze(config, options) {
|
|
4013
|
+
const screenAnalyzer = new WebScreenAnalyzer(config, {
|
|
4014
|
+
screenPatterns: options.screenPatterns
|
|
4015
|
+
});
|
|
4016
|
+
const navigationAnalyzer = new WebNavigationAnalyzer(config, {
|
|
4017
|
+
navigationInclude: options.navigationInclude,
|
|
4018
|
+
navigationExclude: options.navigationExclude
|
|
4019
|
+
});
|
|
4020
|
+
console.log("[ReactWebPlatformAnalyzer] Running analyzers...");
|
|
4021
|
+
const [screenAnalysis, navigationResult] = await Promise.all([
|
|
4022
|
+
screenAnalyzer.analyze(),
|
|
4023
|
+
navigationAnalyzer.analyze()
|
|
4024
|
+
]);
|
|
4025
|
+
const routeScreenNames = new Set(navigationResult.routes.map((route) => route.screenName));
|
|
4026
|
+
const strictScreens = options.strictScreens ?? true;
|
|
4027
|
+
let screensFilteredOut = 0;
|
|
4028
|
+
const included = [];
|
|
4029
|
+
for (const candidate of screenAnalysis.candidates) {
|
|
4030
|
+
if (!strictScreens || this.isScreen(candidate, routeScreenNames)) {
|
|
4031
|
+
included.push(candidate);
|
|
4032
|
+
} else {
|
|
4033
|
+
screensFilteredOut++;
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
const screens = included.map(
|
|
4037
|
+
(candidate) => this.resolveRoutePaths(candidate.descriptor, navigationResult.routes)
|
|
4038
|
+
);
|
|
4039
|
+
console.log(
|
|
4040
|
+
`[ReactWebPlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens, ${navigationResult.routes.length} routes`
|
|
4041
|
+
);
|
|
4042
|
+
return {
|
|
4043
|
+
screens,
|
|
4044
|
+
navigation: navigationResult.graph,
|
|
4045
|
+
analyzedFiles: screenAnalysis.analyzedFiles,
|
|
4046
|
+
...screensFilteredOut > 0 ? { screensFilteredOut } : {}
|
|
4047
|
+
};
|
|
2428
4048
|
}
|
|
2429
|
-
|
|
2430
|
-
|
|
4049
|
+
isScreen(candidate, routeScreenNames) {
|
|
4050
|
+
return candidate.hasRegisterScreen || candidate.matchesScreenPattern || routeScreenNames.has(candidate.descriptor.name);
|
|
4051
|
+
}
|
|
4052
|
+
/** Replace route-path references with screen names where the route table resolves them. */
|
|
4053
|
+
resolveRoutePaths(screen, routes) {
|
|
4054
|
+
const resolve2 = (target) => {
|
|
4055
|
+
if (!target || !target.startsWith("/")) return target;
|
|
4056
|
+
return resolvePathToScreen(routes, target) ?? target;
|
|
4057
|
+
};
|
|
4058
|
+
const navigationTargets = Array.from(
|
|
4059
|
+
new Set(
|
|
4060
|
+
screen.navigationTargets.map((target) => resolve2(target)).filter((target) => target !== screen.name)
|
|
4061
|
+
)
|
|
4062
|
+
).sort();
|
|
4063
|
+
const actions = screen.actions.map((action) => {
|
|
4064
|
+
const resolved = resolve2(action.targetScreen);
|
|
4065
|
+
return resolved === action.targetScreen ? action : { ...action, targetScreen: resolved };
|
|
4066
|
+
});
|
|
4067
|
+
const collections = screen.collections?.map((collection) => {
|
|
4068
|
+
const resolveRow = (row) => {
|
|
4069
|
+
if (!row?.targetScreen) return row;
|
|
4070
|
+
const resolved = resolve2(row.targetScreen);
|
|
4071
|
+
if (resolved === row.targetScreen) return row;
|
|
4072
|
+
return {
|
|
4073
|
+
...row,
|
|
4074
|
+
targetScreen: resolved,
|
|
4075
|
+
...row.description ? { description: `Clicking a row opens ${resolved}` } : {}
|
|
4076
|
+
};
|
|
4077
|
+
};
|
|
4078
|
+
const rowAction = resolveRow(collection.rowAction);
|
|
4079
|
+
return {
|
|
4080
|
+
...collection,
|
|
4081
|
+
...rowAction ? { rowAction } : {},
|
|
4082
|
+
...collection.rowActions ? { rowActions: collection.rowActions.map((row) => resolveRow(row)) } : {}
|
|
4083
|
+
};
|
|
4084
|
+
});
|
|
2431
4085
|
return {
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
4086
|
+
...screen,
|
|
4087
|
+
navigationTargets,
|
|
4088
|
+
actions,
|
|
4089
|
+
...collections ? { collections } : {}
|
|
2435
4090
|
};
|
|
2436
4091
|
}
|
|
2437
4092
|
};
|
|
@@ -2641,8 +4296,8 @@ var ZodParsedType = util.arrayToEnum([
|
|
|
2641
4296
|
"set"
|
|
2642
4297
|
]);
|
|
2643
4298
|
var getParsedType = (data) => {
|
|
2644
|
-
const
|
|
2645
|
-
switch (
|
|
4299
|
+
const t12 = typeof data;
|
|
4300
|
+
switch (t12) {
|
|
2646
4301
|
case "undefined":
|
|
2647
4302
|
return ZodParsedType.undefined;
|
|
2648
4303
|
case "string":
|
|
@@ -2914,8 +4569,8 @@ function getErrorMap() {
|
|
|
2914
4569
|
|
|
2915
4570
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
2916
4571
|
var makeIssue = (params) => {
|
|
2917
|
-
const { data, path:
|
|
2918
|
-
const fullPath = [...
|
|
4572
|
+
const { data, path: path9, errorMaps, issueData } = params;
|
|
4573
|
+
const fullPath = [...path9, ...issueData.path || []];
|
|
2919
4574
|
const fullIssue = {
|
|
2920
4575
|
...issueData,
|
|
2921
4576
|
path: fullPath
|
|
@@ -3031,11 +4686,11 @@ var errorUtil;
|
|
|
3031
4686
|
|
|
3032
4687
|
// ../../node_modules/zod/v3/types.js
|
|
3033
4688
|
var ParseInputLazyPath = class {
|
|
3034
|
-
constructor(parent, value,
|
|
4689
|
+
constructor(parent, value, path9, key) {
|
|
3035
4690
|
this._cachedPath = [];
|
|
3036
4691
|
this.parent = parent;
|
|
3037
4692
|
this.data = value;
|
|
3038
|
-
this._path =
|
|
4693
|
+
this._path = path9;
|
|
3039
4694
|
this._key = key;
|
|
3040
4695
|
}
|
|
3041
4696
|
get path() {
|
|
@@ -6476,7 +8131,7 @@ var coerce = {
|
|
|
6476
8131
|
};
|
|
6477
8132
|
var NEVER = INVALID;
|
|
6478
8133
|
|
|
6479
|
-
// ../shared/dist/chunk-
|
|
8134
|
+
// ../shared/dist/chunk-PKS3VDNB.mjs
|
|
6480
8135
|
var locatorSourceSchema = external_exports.enum([
|
|
6481
8136
|
"appilotsId",
|
|
6482
8137
|
"testID",
|
|
@@ -6502,7 +8157,16 @@ var locatorDescriptorSchema = external_exports.object({
|
|
|
6502
8157
|
source: locatorSourceSchema.optional()
|
|
6503
8158
|
}).passthrough();
|
|
6504
8159
|
var signalDescriptorSchema = external_exports.object({
|
|
6505
|
-
type: external_exports.enum([
|
|
8160
|
+
type: external_exports.enum([
|
|
8161
|
+
"navigation",
|
|
8162
|
+
"goBack",
|
|
8163
|
+
"toast",
|
|
8164
|
+
"modal",
|
|
8165
|
+
"inline-error",
|
|
8166
|
+
"data-arrival",
|
|
8167
|
+
"loading",
|
|
8168
|
+
"none"
|
|
8169
|
+
]).or(external_exports.string().min(1).max(60)),
|
|
6506
8170
|
target: external_exports.string().optional(),
|
|
6507
8171
|
description: external_exports.string().optional()
|
|
6508
8172
|
}).passthrough();
|
|
@@ -6516,7 +8180,20 @@ var waitPolicyDescriptorSchema = external_exports.object({
|
|
|
6516
8180
|
signals: external_exports.array(signalDescriptorSchema).optional(),
|
|
6517
8181
|
maxMs: external_exports.number().finite().optional()
|
|
6518
8182
|
}).passthrough();
|
|
6519
|
-
var targetDescriptorRoleSchema = external_exports.enum([
|
|
8183
|
+
var targetDescriptorRoleSchema = external_exports.enum([
|
|
8184
|
+
"button",
|
|
8185
|
+
"submit",
|
|
8186
|
+
"input",
|
|
8187
|
+
"toggle",
|
|
8188
|
+
"select",
|
|
8189
|
+
"date",
|
|
8190
|
+
"list",
|
|
8191
|
+
"row",
|
|
8192
|
+
"menuItem",
|
|
8193
|
+
"modal",
|
|
8194
|
+
"custom"
|
|
8195
|
+
]).or(external_exports.string().min(1).max(60));
|
|
8196
|
+
var targetOriginSchema = external_exports.enum(["analyzer", "manifest"]).or(external_exports.string().min(1).max(40));
|
|
6520
8197
|
var targetDescriptorSchema = external_exports.object({
|
|
6521
8198
|
id: external_exports.string(),
|
|
6522
8199
|
role: targetDescriptorRoleSchema,
|
|
@@ -6530,10 +8207,20 @@ var targetDescriptorSchema = external_exports.object({
|
|
|
6530
8207
|
requiresConfirmation: external_exports.boolean().optional(),
|
|
6531
8208
|
opensModal: external_exports.string().optional(),
|
|
6532
8209
|
opensBottomSheet: external_exports.string().optional(),
|
|
6533
|
-
sourceComponent: external_exports.string().optional()
|
|
8210
|
+
sourceComponent: external_exports.string().optional(),
|
|
8211
|
+
origin: targetOriginSchema.optional()
|
|
6534
8212
|
}).passthrough();
|
|
6535
8213
|
var flowStepDescriptorSchema = external_exports.object({
|
|
6536
|
-
type: external_exports.enum([
|
|
8214
|
+
type: external_exports.enum([
|
|
8215
|
+
"navigate",
|
|
8216
|
+
"fill",
|
|
8217
|
+
"press",
|
|
8218
|
+
"select",
|
|
8219
|
+
"toggle",
|
|
8220
|
+
"wait",
|
|
8221
|
+
"confirm",
|
|
8222
|
+
"choose-list-item"
|
|
8223
|
+
]).or(external_exports.string().min(1).max(60)),
|
|
6537
8224
|
target: external_exports.string().optional(),
|
|
6538
8225
|
label: external_exports.string().optional(),
|
|
6539
8226
|
description: external_exports.string().optional(),
|
|
@@ -6661,7 +8348,19 @@ var navigationNodeSchema = external_exports.object({
|
|
|
6661
8348
|
parentNavigator: external_exports.string().optional(),
|
|
6662
8349
|
reachableFrom: external_exports.array(external_exports.string()).default([]),
|
|
6663
8350
|
reachableTo: external_exports.array(external_exports.string()).default([]),
|
|
6664
|
-
params: external_exports.array(paramDescriptorSchema).optional()
|
|
8351
|
+
params: external_exports.array(paramDescriptorSchema).optional(),
|
|
8352
|
+
/**
|
|
8353
|
+
* Web clients only (documented convention, additive — previously
|
|
8354
|
+
* round-tripped via `.passthrough()`): the screen's URL route
|
|
8355
|
+
* template, e.g. `/users/:id`. Param segments use `:name` (the
|
|
8356
|
+
* relay also accepts `[name]` / `{name}`). When present and the
|
|
8357
|
+
* request's `context.platform` is `'web'`, the relay resolves
|
|
8358
|
+
* `navigate` targets against these templates and injects the
|
|
8359
|
+
* concrete URL segments as the navigate payload's `path` — the web
|
|
8360
|
+
* equivalent of RN's nested-navigation path injection. See
|
|
8361
|
+
* docs/agent-contract.md ("Web clients" section).
|
|
8362
|
+
*/
|
|
8363
|
+
path: external_exports.string().max(500).optional()
|
|
6665
8364
|
}).passthrough();
|
|
6666
8365
|
var navigatorDescriptorSchema = external_exports.object({
|
|
6667
8366
|
name: external_exports.string(),
|
|
@@ -6708,6 +8407,50 @@ var loginSchema = external_exports.object({
|
|
|
6708
8407
|
loginSchema.extend({
|
|
6709
8408
|
name: external_exports.string().min(2, "Name must be at least 2 characters").max(100)
|
|
6710
8409
|
});
|
|
8410
|
+
external_exports.object({
|
|
8411
|
+
name: external_exports.string().min(2, "Name must be at least 2 characters").max(100).optional(),
|
|
8412
|
+
avatarUrl: external_exports.string().url("Invalid URL").max(2048).nullable().optional()
|
|
8413
|
+
}).refine((v) => v.name !== void 0 || v.avatarUrl !== void 0, {
|
|
8414
|
+
message: "At least one field must be provided"
|
|
8415
|
+
});
|
|
8416
|
+
external_exports.object({
|
|
8417
|
+
currentPassword: external_exports.string().min(1, "Current password is required"),
|
|
8418
|
+
newPassword: external_exports.string().min(8, "Password must be at least 8 characters")
|
|
8419
|
+
});
|
|
8420
|
+
var totpCodeSchema = external_exports.string().transform((v) => v.replace(/\s/g, "")).pipe(external_exports.string().regex(/^\d{6}$/, "Code must be 6 digits"));
|
|
8421
|
+
var recoveryCodeSchema = external_exports.string().transform((v) => v.toUpperCase().replace(/[^A-Z0-9]/g, "")).pipe(external_exports.string().regex(/^[23456789BCDFGHJKMNPQRSTVWXYZ]{10}$/, "Invalid recovery code"));
|
|
8422
|
+
external_exports.object({
|
|
8423
|
+
code: totpCodeSchema
|
|
8424
|
+
});
|
|
8425
|
+
external_exports.object({
|
|
8426
|
+
challengeToken: external_exports.string().min(1, "Challenge token is required"),
|
|
8427
|
+
code: totpCodeSchema.optional(),
|
|
8428
|
+
recoveryCode: recoveryCodeSchema.optional()
|
|
8429
|
+
}).refine((v) => Boolean(v.code) !== Boolean(v.recoveryCode), {
|
|
8430
|
+
message: "Provide either a TOTP code or a recovery code",
|
|
8431
|
+
path: ["code"]
|
|
8432
|
+
});
|
|
8433
|
+
external_exports.object({
|
|
8434
|
+
password: external_exports.string().min(1, "Password is required"),
|
|
8435
|
+
code: totpCodeSchema.optional(),
|
|
8436
|
+
recoveryCode: recoveryCodeSchema.optional()
|
|
8437
|
+
}).refine((v) => Boolean(v.code) !== Boolean(v.recoveryCode), {
|
|
8438
|
+
message: "Provide either a TOTP code or a recovery code",
|
|
8439
|
+
path: ["code"]
|
|
8440
|
+
});
|
|
8441
|
+
external_exports.object({
|
|
8442
|
+
password: external_exports.string().min(1, "Password is required"),
|
|
8443
|
+
code: totpCodeSchema
|
|
8444
|
+
});
|
|
8445
|
+
external_exports.object({
|
|
8446
|
+
password: external_exports.string().min(1, "Password is required"),
|
|
8447
|
+
code: totpCodeSchema.optional(),
|
|
8448
|
+
recoveryCode: recoveryCodeSchema.optional(),
|
|
8449
|
+
confirm: external_exports.literal("DELETE MY ACCOUNT")
|
|
8450
|
+
}).refine((v) => !(v.code && v.recoveryCode), {
|
|
8451
|
+
message: "Provide either a TOTP code or a recovery code, not both",
|
|
8452
|
+
path: ["code"]
|
|
8453
|
+
});
|
|
6711
8454
|
var createProjectSchema = external_exports.object({
|
|
6712
8455
|
name: external_exports.string().min(1).max(100),
|
|
6713
8456
|
description: external_exports.string().max(500).optional(),
|
|
@@ -6743,6 +8486,7 @@ external_exports.object({
|
|
|
6743
8486
|
content: external_exports.record(external_exports.unknown())
|
|
6744
8487
|
});
|
|
6745
8488
|
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
|
|
8489
|
+
var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
|
|
6746
8490
|
external_exports.object({
|
|
6747
8491
|
name: external_exports.string().min(1).max(100),
|
|
6748
8492
|
// Required for `sdk` (default), forbidden/ignored for `operator` —
|
|
@@ -6750,6 +8494,7 @@ external_exports.object({
|
|
|
6750
8494
|
// missing scope still validates as the pre-existing SDK shape.
|
|
6751
8495
|
projectId: external_exports.string().min(1).optional(),
|
|
6752
8496
|
scope: apiKeyScopeSchema.default("sdk"),
|
|
8497
|
+
environment: apiKeyEnvironmentSchema.optional(),
|
|
6753
8498
|
expiresAt: external_exports.string().datetime().optional()
|
|
6754
8499
|
}).refine((v) => v.scope !== "sdk" || !!v.projectId, {
|
|
6755
8500
|
message: 'projectId is required for scope "sdk"',
|
|
@@ -6757,13 +8502,23 @@ external_exports.object({
|
|
|
6757
8502
|
});
|
|
6758
8503
|
var boundedString = (max) => external_exports.string().max(max);
|
|
6759
8504
|
var clientPlatformSchema = external_exports.enum(["react-native", "web", "android", "ios"]).or(external_exports.string().min(1).max(40));
|
|
8505
|
+
var identityProvenanceSchema = external_exports.enum(["declared", "derived", "positional"]).or(external_exports.string().min(1).max(40));
|
|
6760
8506
|
var snapshotInputSchema = external_exports.object({
|
|
6761
8507
|
id: boundedString(160).optional(),
|
|
8508
|
+
provenance: identityProvenanceSchema.optional(),
|
|
8509
|
+
/**
|
|
8510
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
8511
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
8512
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
8513
|
+
* before the field existed), which must never be read as `false`.
|
|
8514
|
+
*/
|
|
8515
|
+
onScreen: external_exports.boolean().optional(),
|
|
6762
8516
|
label: boundedString(300).optional(),
|
|
6763
8517
|
value: boundedString(4096).optional(),
|
|
6764
8518
|
placeholder: boundedString(300).optional(),
|
|
6765
8519
|
editable: external_exports.boolean().optional(),
|
|
6766
8520
|
secure: external_exports.boolean().optional(),
|
|
8521
|
+
submitsOnReturn: external_exports.boolean().optional(),
|
|
6767
8522
|
type: boundedString(40).optional(),
|
|
6768
8523
|
required: external_exports.boolean().optional(),
|
|
6769
8524
|
invalid: external_exports.boolean().optional(),
|
|
@@ -6771,6 +8526,14 @@ var snapshotInputSchema = external_exports.object({
|
|
|
6771
8526
|
}).passthrough();
|
|
6772
8527
|
var snapshotButtonSchema = external_exports.object({
|
|
6773
8528
|
id: boundedString(160).optional(),
|
|
8529
|
+
provenance: identityProvenanceSchema.optional(),
|
|
8530
|
+
/**
|
|
8531
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
8532
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
8533
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
8534
|
+
* before the field existed), which must never be read as `false`.
|
|
8535
|
+
*/
|
|
8536
|
+
onScreen: external_exports.boolean().optional(),
|
|
6774
8537
|
label: boundedString(300).optional(),
|
|
6775
8538
|
disabled: external_exports.boolean().optional(),
|
|
6776
8539
|
inModal: external_exports.boolean().optional(),
|
|
@@ -6780,12 +8543,28 @@ var snapshotButtonSchema = external_exports.object({
|
|
|
6780
8543
|
}).passthrough();
|
|
6781
8544
|
var snapshotToggleSchema = external_exports.object({
|
|
6782
8545
|
id: boundedString(160).optional(),
|
|
8546
|
+
provenance: identityProvenanceSchema.optional(),
|
|
8547
|
+
/**
|
|
8548
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
8549
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
8550
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
8551
|
+
* before the field existed), which must never be read as `false`.
|
|
8552
|
+
*/
|
|
8553
|
+
onScreen: external_exports.boolean().optional(),
|
|
6783
8554
|
label: boundedString(300).optional(),
|
|
6784
8555
|
value: external_exports.boolean().optional(),
|
|
6785
8556
|
inModal: external_exports.boolean().optional()
|
|
6786
8557
|
}).passthrough();
|
|
6787
8558
|
var snapshotSliderSchema = external_exports.object({
|
|
6788
8559
|
id: boundedString(160).optional(),
|
|
8560
|
+
provenance: identityProvenanceSchema.optional(),
|
|
8561
|
+
/**
|
|
8562
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
8563
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
8564
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
8565
|
+
* before the field existed), which must never be read as `false`.
|
|
8566
|
+
*/
|
|
8567
|
+
onScreen: external_exports.boolean().optional(),
|
|
6789
8568
|
label: boundedString(300).optional(),
|
|
6790
8569
|
value: external_exports.number().finite().optional(),
|
|
6791
8570
|
min: external_exports.number().finite().optional(),
|
|
@@ -6826,6 +8605,26 @@ var snapshotListSchema = external_exports.object({
|
|
|
6826
8605
|
// should chase, and the relay only reads a known subset.
|
|
6827
8606
|
items: external_exports.array(external_exports.record(external_exports.unknown())).max(500).optional()
|
|
6828
8607
|
}).passthrough();
|
|
8608
|
+
var snapshotNativeDialogSchema = external_exports.object({
|
|
8609
|
+
id: boundedString(160),
|
|
8610
|
+
title: boundedString(300).optional(),
|
|
8611
|
+
message: boundedString(2e3).optional(),
|
|
8612
|
+
buttons: external_exports.array(
|
|
8613
|
+
external_exports.object({
|
|
8614
|
+
label: boundedString(160),
|
|
8615
|
+
style: external_exports.enum(["default", "cancel", "destructive"]).optional()
|
|
8616
|
+
}).passthrough()
|
|
8617
|
+
).max(10)
|
|
8618
|
+
}).passthrough();
|
|
8619
|
+
var snapshotScrollableSchema = external_exports.object({
|
|
8620
|
+
id: boundedString(160),
|
|
8621
|
+
containerType: boundedString(60).optional(),
|
|
8622
|
+
label: boundedString(300).optional(),
|
|
8623
|
+
scrollOffsetY: external_exports.number().finite().optional(),
|
|
8624
|
+
canScrollUp: external_exports.boolean().optional(),
|
|
8625
|
+
canScrollDown: external_exports.boolean().optional(),
|
|
8626
|
+
horizontal: external_exports.boolean().optional()
|
|
8627
|
+
}).passthrough();
|
|
6829
8628
|
var snapshotChoiceGroupSchema = external_exports.object({
|
|
6830
8629
|
index: external_exports.number().int().optional(),
|
|
6831
8630
|
id: boundedString(160).optional(),
|
|
@@ -6842,6 +8641,14 @@ var snapshotElementSchema = external_exports.object({
|
|
|
6842
8641
|
disabled: external_exports.boolean().optional(),
|
|
6843
8642
|
selected: external_exports.boolean().optional(),
|
|
6844
8643
|
source: boundedString(40).optional(),
|
|
8644
|
+
provenance: identityProvenanceSchema.optional(),
|
|
8645
|
+
/**
|
|
8646
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
8647
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
8648
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
8649
|
+
* before the field existed), which must never be read as `false`.
|
|
8650
|
+
*/
|
|
8651
|
+
onScreen: external_exports.boolean().optional(),
|
|
6845
8652
|
targetId: boundedString(160).optional(),
|
|
6846
8653
|
listContext: external_exports.record(external_exports.unknown()).optional(),
|
|
6847
8654
|
inModal: external_exports.boolean().optional()
|
|
@@ -6858,7 +8665,34 @@ var agentSnapshotSchema = external_exports.object({
|
|
|
6858
8665
|
modalOpen: external_exports.boolean().optional(),
|
|
6859
8666
|
lists: external_exports.array(snapshotListSchema).max(100).optional(),
|
|
6860
8667
|
choiceGroups: external_exports.array(snapshotChoiceGroupSchema).max(100).optional(),
|
|
6861
|
-
|
|
8668
|
+
scrollables: external_exports.array(snapshotScrollableSchema).max(20).optional(),
|
|
8669
|
+
nativeDialog: snapshotNativeDialogSchema.optional(),
|
|
8670
|
+
elements: external_exports.array(snapshotElementSchema).max(1e3).optional(),
|
|
8671
|
+
/**
|
|
8672
|
+
* The client clamped this observation to the caps above and lost
|
|
8673
|
+
* data doing it (`clampSnapshotToWireLimits` in
|
|
8674
|
+
* `@appilots/client-core`). Read by the prompt serializer so the
|
|
8675
|
+
* model is told the view is partial — a screen truncated at 500
|
|
8676
|
+
* texts must not be read as a screen that only has 500 things on
|
|
8677
|
+
* it. Optional: older SDKs never clamp and never send it.
|
|
8678
|
+
*/
|
|
8679
|
+
truncated: external_exports.boolean().optional(),
|
|
8680
|
+
/**
|
|
8681
|
+
* How many of this screen's controls the client could name, split by
|
|
8682
|
+
* `identityProvenance`. Diagnostic only — the relay never grounds an
|
|
8683
|
+
* action on it — but it is the number that says whether an app needs
|
|
8684
|
+
* annotating, and comparing it between a debug and a release build is
|
|
8685
|
+
* the first direct measurement of what minification costs the agent.
|
|
8686
|
+
*/
|
|
8687
|
+
stats: external_exports.object({
|
|
8688
|
+
visitedFibers: external_exports.number().int().nonnegative().optional(),
|
|
8689
|
+
skippedHidden: external_exports.number().int().nonnegative().optional(),
|
|
8690
|
+
identity: external_exports.object({
|
|
8691
|
+
declared: external_exports.number().int().nonnegative().optional(),
|
|
8692
|
+
derived: external_exports.number().int().nonnegative().optional(),
|
|
8693
|
+
positional: external_exports.number().int().nonnegative().optional()
|
|
8694
|
+
}).passthrough().optional()
|
|
8695
|
+
}).passthrough().optional()
|
|
6862
8696
|
}).passthrough();
|
|
6863
8697
|
var agentContextSchema = external_exports.object({
|
|
6864
8698
|
/**
|
|
@@ -6908,6 +8742,19 @@ var agentContextSchema = external_exports.object({
|
|
|
6908
8742
|
).max(200).optional(),
|
|
6909
8743
|
loadingPending: external_exports.boolean().optional()
|
|
6910
8744
|
}).passthrough();
|
|
8745
|
+
var introspectionFailureReasonSchema = external_exports.enum([
|
|
8746
|
+
"sentinel-missing-internals",
|
|
8747
|
+
"never-mounted",
|
|
8748
|
+
"render-crashed",
|
|
8749
|
+
"names-mangled",
|
|
8750
|
+
"walk-recognized-nothing"
|
|
8751
|
+
]);
|
|
8752
|
+
var introspectionDiagnosticsSchema = external_exports.object({
|
|
8753
|
+
captured: external_exports.boolean(),
|
|
8754
|
+
failureReason: introspectionFailureReasonSchema.nullish(),
|
|
8755
|
+
reactVersion: boundedString(32).nullish(),
|
|
8756
|
+
failureCount: external_exports.number().int().min(0).max(1e6).optional()
|
|
8757
|
+
});
|
|
6911
8758
|
external_exports.object({
|
|
6912
8759
|
content: external_exports.string().min(1).max(4096),
|
|
6913
8760
|
sessionId: external_exports.string().optional(),
|
|
@@ -6917,7 +8764,20 @@ external_exports.object({
|
|
|
6917
8764
|
* the active MCP doc and emits `mcp_version_mismatch` telemetry when
|
|
6918
8765
|
* they differ. Optional — older SDKs simply skip the check.
|
|
6919
8766
|
*/
|
|
6920
|
-
mcpVersion: external_exports.string().max(64).optional()
|
|
8767
|
+
mcpVersion: external_exports.string().max(64).optional(),
|
|
8768
|
+
/**
|
|
8769
|
+
* SDK-side introspection health. Only sent when it FAILED — a client
|
|
8770
|
+
* that reached the React tree omits the field entirely, so the happy
|
|
8771
|
+
* path costs nothing on the wire.
|
|
8772
|
+
*
|
|
8773
|
+
* Without this, an app whose introspection is dead sends a snapshot
|
|
8774
|
+
* that is empty but perfectly well-formed, indistinguishable from a
|
|
8775
|
+
* genuinely empty screen. The server emits
|
|
8776
|
+
* `introspection_unavailable` so the failure is visible to us instead
|
|
8777
|
+
* of waiting for a bug report. Optional — older SDKs simply never
|
|
8778
|
+
* send it.
|
|
8779
|
+
*/
|
|
8780
|
+
introspection: introspectionDiagnosticsSchema.optional()
|
|
6921
8781
|
});
|
|
6922
8782
|
var agentActionTypeSchema = external_exports.enum([
|
|
6923
8783
|
"navigate",
|
|
@@ -6928,11 +8788,22 @@ var agentActionTypeSchema = external_exports.enum([
|
|
|
6928
8788
|
"custom"
|
|
6929
8789
|
]);
|
|
6930
8790
|
var navigationPayloadSchema = external_exports.object({
|
|
6931
|
-
|
|
8791
|
+
// Optional ONLY so `goBack` can exist (#398): it returns to whatever
|
|
8792
|
+
// is beneath on the stack and has no destination to name. Every other
|
|
8793
|
+
// action is still required to carry one — enforced by the refine
|
|
8794
|
+
// below rather than by the field, so the error names the real problem
|
|
8795
|
+
// ("navigate needs a screenName") instead of a missing key.
|
|
8796
|
+
screenName: external_exports.string().min(1).optional(),
|
|
6932
8797
|
params: external_exports.record(external_exports.unknown()).optional(),
|
|
6933
8798
|
navigationAction: external_exports.enum(["push", "navigate", "replace", "goBack", "reset"]).default("navigate").optional(),
|
|
6934
8799
|
path: external_exports.array(external_exports.string().min(1)).optional()
|
|
6935
|
-
}).passthrough()
|
|
8800
|
+
}).passthrough().refine(
|
|
8801
|
+
(payload) => payload.navigationAction === "goBack" || typeof payload.screenName === "string" && payload.screenName.length > 0,
|
|
8802
|
+
{
|
|
8803
|
+
message: 'screenName is required for every navigationAction except "goBack"',
|
|
8804
|
+
path: ["screenName"]
|
|
8805
|
+
}
|
|
8806
|
+
);
|
|
6936
8807
|
var formFillFieldSchema = external_exports.object({
|
|
6937
8808
|
fieldId: external_exports.string().min(1),
|
|
6938
8809
|
fieldType: external_exports.enum(["text", "select", "toggle", "date", "number", "custom"]).default("text").optional(),
|
|
@@ -7266,6 +9137,8 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
7266
9137
|
inputs: external_exports.array(
|
|
7267
9138
|
external_exports.object({
|
|
7268
9139
|
id: external_exports.string().max(120),
|
|
9140
|
+
provenance: identityProvenanceSchema.optional(),
|
|
9141
|
+
onScreen: external_exports.boolean().optional(),
|
|
7269
9142
|
label: external_exports.string().max(200).optional(),
|
|
7270
9143
|
value: external_exports.string().max(2e3).optional(),
|
|
7271
9144
|
disabled: external_exports.boolean().optional(),
|
|
@@ -7279,6 +9152,8 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
7279
9152
|
buttons: external_exports.array(
|
|
7280
9153
|
external_exports.object({
|
|
7281
9154
|
id: external_exports.string().max(120),
|
|
9155
|
+
provenance: identityProvenanceSchema.optional(),
|
|
9156
|
+
onScreen: external_exports.boolean().optional(),
|
|
7282
9157
|
label: external_exports.string().max(200).optional(),
|
|
7283
9158
|
disabled: external_exports.boolean().optional(),
|
|
7284
9159
|
/** Risk metadata (e.g. "high") the SDK forwards for danger UI. */
|
|
@@ -7300,6 +9175,8 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
7300
9175
|
sliders: external_exports.array(
|
|
7301
9176
|
external_exports.object({
|
|
7302
9177
|
id: external_exports.string().max(120),
|
|
9178
|
+
provenance: identityProvenanceSchema.optional(),
|
|
9179
|
+
onScreen: external_exports.boolean().optional(),
|
|
7303
9180
|
label: external_exports.string().max(200).optional(),
|
|
7304
9181
|
value: external_exports.number().finite().optional(),
|
|
7305
9182
|
min: external_exports.number().finite().optional(),
|
|
@@ -7329,6 +9206,44 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
7329
9206
|
).max(200).optional()
|
|
7330
9207
|
}).passthrough()
|
|
7331
9208
|
).max(20).optional(),
|
|
9209
|
+
/**
|
|
9210
|
+
* Toggles exactly as the SDK snapshot reports them (#390).
|
|
9211
|
+
*
|
|
9212
|
+
* Their absence is why `generality-component-control-smarthome`
|
|
9213
|
+
* came back `errored` with a 422 instead of measuring anything:
|
|
9214
|
+
* this schema is `.strict()`, so a scenario that declares a switch
|
|
9215
|
+
* is rejected before the agent is ever asked. Which means the
|
|
9216
|
+
* corpus could not express the single control behind failure 02 of
|
|
9217
|
+
* the functional audit — "disable an agent; the switch never
|
|
9218
|
+
* moves". The one defect the eval most needed to catch was the one
|
|
9219
|
+
* shape it could not describe.
|
|
9220
|
+
*/
|
|
9221
|
+
toggles: external_exports.array(
|
|
9222
|
+
external_exports.object({
|
|
9223
|
+
id: external_exports.string().max(120),
|
|
9224
|
+
provenance: identityProvenanceSchema.optional(),
|
|
9225
|
+
onScreen: external_exports.boolean().optional(),
|
|
9226
|
+
label: external_exports.string().max(200).optional(),
|
|
9227
|
+
value: external_exports.boolean().optional(),
|
|
9228
|
+
disabled: external_exports.boolean().optional()
|
|
9229
|
+
})
|
|
9230
|
+
).max(100).optional(),
|
|
9231
|
+
/**
|
|
9232
|
+
* The interaction graph — the `el:*` ids the prompt tells the model
|
|
9233
|
+
* to prefer over ordinal handles. Passthrough per entry for the
|
|
9234
|
+
* same reason `lists` is: the SDK adds fields faster than this
|
|
9235
|
+
* schema should chase them.
|
|
9236
|
+
*/
|
|
9237
|
+
elements: external_exports.array(
|
|
9238
|
+
external_exports.object({
|
|
9239
|
+
id: external_exports.string().max(160),
|
|
9240
|
+
role: external_exports.string().max(40).optional(),
|
|
9241
|
+
label: external_exports.string().max(300).optional()
|
|
9242
|
+
}).passthrough()
|
|
9243
|
+
).max(500).optional(),
|
|
9244
|
+
/** Screen-level state the relay renders and branches on. */
|
|
9245
|
+
loading: external_exports.boolean().optional(),
|
|
9246
|
+
modalOpen: external_exports.boolean().optional(),
|
|
7332
9247
|
extra: external_exports.record(external_exports.unknown()).optional()
|
|
7333
9248
|
}).strict();
|
|
7334
9249
|
external_exports.object({
|
|
@@ -7369,8 +9284,36 @@ external_exports.object({
|
|
|
7369
9284
|
* heuristics, forces tool choice when the last user message carries a
|
|
7370
9285
|
* failure marker). Only meaningful alongside `history`.
|
|
7371
9286
|
*/
|
|
7372
|
-
isRecoveryHop: external_exports.boolean().optional()
|
|
9287
|
+
isRecoveryHop: external_exports.boolean().optional(),
|
|
9288
|
+
/**
|
|
9289
|
+
* Run this call against the application structure supplied here
|
|
9290
|
+
* instead of the project's active MCP document.
|
|
9291
|
+
*
|
|
9292
|
+
* Sandbox-only, and the relay honours it only on a `dryRun` call —
|
|
9293
|
+
* see `resolveMcpOverride` in apps/api. It exists because the eval
|
|
9294
|
+
* corpus could otherwise only ever describe ONE app: every scenario
|
|
9295
|
+
* resolves to the same seeded project, so every navigate destination,
|
|
9296
|
+
* every screen title and every bit of domain priming in the prompt
|
|
9297
|
+
* came from the same vehicle-management demo. A suite that can only
|
|
9298
|
+
* pose questions about one app cannot measure whether the agent works
|
|
9299
|
+
* on another one.
|
|
9300
|
+
*
|
|
9301
|
+
* Deliberately loose (`z.record`): the MCP document's shape is owned
|
|
9302
|
+
* by the generator, and pinning it here would turn every additive
|
|
9303
|
+
* generator change into a 422 in the sandbox.
|
|
9304
|
+
*/
|
|
9305
|
+
applicationStructure: external_exports.record(external_exports.unknown()).optional()
|
|
7373
9306
|
}).strict();
|
|
9307
|
+
var relayDiagnosticsSchema = external_exports.object({
|
|
9308
|
+
unverifiedTargets: external_exports.array(external_exports.string()),
|
|
9309
|
+
guardsFired: external_exports.array(external_exports.object({ guard: external_exports.string(), detail: external_exports.string().optional() })),
|
|
9310
|
+
repairMode: external_exports.enum(["repair", "strict", "disabled"]),
|
|
9311
|
+
promptVersion: external_exports.string(),
|
|
9312
|
+
// Defaulted for producers that predate the field: before it existed
|
|
9313
|
+
// every response had reached the model, so `true` is the correct
|
|
9314
|
+
// backfill.
|
|
9315
|
+
modelInvoked: external_exports.boolean().default(true)
|
|
9316
|
+
});
|
|
7374
9317
|
var sandboxTraceHopSchema = external_exports.object({
|
|
7375
9318
|
hop: external_exports.number().int().nonnegative(),
|
|
7376
9319
|
inputPreview: external_exports.string(),
|
|
@@ -7409,7 +9352,16 @@ external_exports.object({
|
|
|
7409
9352
|
/** Hop-by-hop trace. Always at least one entry. */
|
|
7410
9353
|
trace: external_exports.array(sandboxTraceHopSchema),
|
|
7411
9354
|
/** True when the run was a no-op (e.g. project has no MCP yet). */
|
|
7412
|
-
warning: external_exports.string().optional()
|
|
9355
|
+
warning: external_exports.string().optional(),
|
|
9356
|
+
/**
|
|
9357
|
+
* SPEC-046 — guard / hallucination telemetry for the run.
|
|
9358
|
+
*
|
|
9359
|
+
* ADDITIVE AND OPTIONAL, permanently: SDK and dashboard consumers
|
|
9360
|
+
* predate it, and older API deployments will not send it. Consumers
|
|
9361
|
+
* must treat an absent block as "no information", never as "no guards
|
|
9362
|
+
* fired".
|
|
9363
|
+
*/
|
|
9364
|
+
diagnostics: relayDiagnosticsSchema.optional()
|
|
7413
9365
|
});
|
|
7414
9366
|
external_exports.object({
|
|
7415
9367
|
name: external_exports.string().min(1).max(120),
|
|
@@ -7613,6 +9565,30 @@ external_exports.object({
|
|
|
7613
9565
|
name: external_exports.string().min(1).max(200).optional(),
|
|
7614
9566
|
identifiers: endUserIdentifiersSchema.optional()
|
|
7615
9567
|
});
|
|
9568
|
+
var MAX_EXTERNAL_USER_ID_LENGTH = 128;
|
|
9569
|
+
function normalizeExternalUserId(value) {
|
|
9570
|
+
let raw;
|
|
9571
|
+
if (typeof value === "string") raw = value;
|
|
9572
|
+
else if (typeof value === "number" && Number.isFinite(value)) raw = String(value);
|
|
9573
|
+
else return void 0;
|
|
9574
|
+
const trimmed = raw.trim();
|
|
9575
|
+
if (!trimmed) return void 0;
|
|
9576
|
+
if (trimmed.length > MAX_EXTERNAL_USER_ID_LENGTH) return void 0;
|
|
9577
|
+
return trimmed;
|
|
9578
|
+
}
|
|
9579
|
+
var MAX_DEVICE_INFO_BYTES = 4096;
|
|
9580
|
+
var sessionDeviceInfoSchema = external_exports.object({
|
|
9581
|
+
platform: external_exports.string().max(64).optional(),
|
|
9582
|
+
osVersion: external_exports.string().max(64).optional(),
|
|
9583
|
+
appVersion: external_exports.string().max(64).optional(),
|
|
9584
|
+
sdkVersion: external_exports.string().max(64).optional()
|
|
9585
|
+
}).passthrough().refine((info) => JSON.stringify(info).length <= MAX_DEVICE_INFO_BYTES, {
|
|
9586
|
+
message: `device_info exceeds ${MAX_DEVICE_INFO_BYTES} bytes`
|
|
9587
|
+
});
|
|
9588
|
+
external_exports.object({
|
|
9589
|
+
userId: external_exports.preprocess(normalizeExternalUserId, external_exports.string().optional()),
|
|
9590
|
+
deviceInfo: sessionDeviceInfoSchema.optional().catch(void 0)
|
|
9591
|
+
});
|
|
7616
9592
|
external_exports.object({
|
|
7617
9593
|
projectId: external_exports.string().min(1).max(64),
|
|
7618
9594
|
externalId: external_exports.string().min(1).max(128),
|
|
@@ -7620,6 +9596,27 @@ external_exports.object({
|
|
|
7620
9596
|
identifiers: endUserIdentifiersSchema.optional()
|
|
7621
9597
|
});
|
|
7622
9598
|
|
|
9599
|
+
// src/utils/stringify.ts
|
|
9600
|
+
function stringifyUnknown(value) {
|
|
9601
|
+
if (value instanceof Error) {
|
|
9602
|
+
const message = value.message || value.name;
|
|
9603
|
+
const cause = value.cause;
|
|
9604
|
+
if (cause instanceof Error && cause.message && cause.message !== message) {
|
|
9605
|
+
return `${message} (${cause.message})`;
|
|
9606
|
+
}
|
|
9607
|
+
return message;
|
|
9608
|
+
}
|
|
9609
|
+
if (typeof value === "string") return value;
|
|
9610
|
+
if (value === null || value === void 0) return String(value);
|
|
9611
|
+
if (typeof value !== "object") return String(value);
|
|
9612
|
+
try {
|
|
9613
|
+
const json = JSON.stringify(value);
|
|
9614
|
+
if (json && json !== "{}") return json;
|
|
9615
|
+
} catch {
|
|
9616
|
+
}
|
|
9617
|
+
return String(value);
|
|
9618
|
+
}
|
|
9619
|
+
|
|
7623
9620
|
// src/manifest/loadManifest.ts
|
|
7624
9621
|
var DEFAULT_MANIFEST_FILENAME = "appilots.manifest.json";
|
|
7625
9622
|
var manifestSchema = external_exports.object({
|
|
@@ -7628,7 +9625,7 @@ var manifestSchema = external_exports.object({
|
|
|
7628
9625
|
navigation: navigationGraphSchema.partial().optional()
|
|
7629
9626
|
}).passthrough();
|
|
7630
9627
|
async function loadManifest(rootDir, manifestPath) {
|
|
7631
|
-
const resolvedPath =
|
|
9628
|
+
const resolvedPath = path8__namespace.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
|
|
7632
9629
|
let raw;
|
|
7633
9630
|
try {
|
|
7634
9631
|
raw = await fs.readFile(resolvedPath, "utf-8");
|
|
@@ -7636,14 +9633,16 @@ async function loadManifest(rootDir, manifestPath) {
|
|
|
7636
9633
|
if (error?.code === "ENOENT") {
|
|
7637
9634
|
return { manifest: null, resolvedPath };
|
|
7638
9635
|
}
|
|
7639
|
-
throw new Error(
|
|
9636
|
+
throw new Error(
|
|
9637
|
+
`Failed to read Appilots manifest at ${resolvedPath}: ${stringifyUnknown(error)}`
|
|
9638
|
+
);
|
|
7640
9639
|
}
|
|
7641
9640
|
let parsedJson;
|
|
7642
9641
|
try {
|
|
7643
9642
|
parsedJson = JSON.parse(raw);
|
|
7644
9643
|
} catch (error) {
|
|
7645
9644
|
throw new Error(
|
|
7646
|
-
`Appilots manifest at ${resolvedPath} is not valid JSON: ${
|
|
9645
|
+
`Appilots manifest at ${resolvedPath} is not valid JSON: ${stringifyUnknown(error)}`
|
|
7647
9646
|
);
|
|
7648
9647
|
}
|
|
7649
9648
|
const result = manifestSchema.safeParse(parsedJson);
|
|
@@ -7657,11 +9656,47 @@ Each entry in "screens" must match the ScreenDescriptor shape (see docs/platform
|
|
|
7657
9656
|
}
|
|
7658
9657
|
return { manifest: result.data, resolvedPath };
|
|
7659
9658
|
}
|
|
9659
|
+
function mergeScreen(base, declared) {
|
|
9660
|
+
const fields = { ...base };
|
|
9661
|
+
for (const [key, value] of Object.entries(declared)) {
|
|
9662
|
+
if (value === void 0) continue;
|
|
9663
|
+
if (Array.isArray(value) && value.length === 0) continue;
|
|
9664
|
+
fields[key] = value;
|
|
9665
|
+
}
|
|
9666
|
+
const merged = fields;
|
|
9667
|
+
merged.targets = mergeById(base.targets, markManifestOrigin(declared.targets));
|
|
9668
|
+
merged.actions = mergeById(base.actions, declared.actions) ?? [];
|
|
9669
|
+
if (base.agentHints || declared.agentHints) {
|
|
9670
|
+
merged.agentHints = { ...base.agentHints, ...declared.agentHints };
|
|
9671
|
+
}
|
|
9672
|
+
return merged;
|
|
9673
|
+
}
|
|
9674
|
+
function mergeById(base, overlay) {
|
|
9675
|
+
if (!overlay || overlay.length === 0) return base;
|
|
9676
|
+
if (!base || base.length === 0) return overlay;
|
|
9677
|
+
const byId = /* @__PURE__ */ new Map();
|
|
9678
|
+
for (const entry of base) byId.set(entry.id, entry);
|
|
9679
|
+
for (const entry of overlay) {
|
|
9680
|
+
const existing = byId.get(entry.id);
|
|
9681
|
+
byId.set(entry.id, existing ? { ...existing, ...entry } : entry);
|
|
9682
|
+
}
|
|
9683
|
+
return Array.from(byId.values());
|
|
9684
|
+
}
|
|
9685
|
+
function markManifestOrigin(targets) {
|
|
9686
|
+
if (!targets || targets.length === 0) return targets;
|
|
9687
|
+
return targets.map((target) => ({ origin: "manifest", ...target }));
|
|
9688
|
+
}
|
|
7660
9689
|
function mergeManifestScreens(analyzerScreens, manifestScreens) {
|
|
7661
9690
|
if (!manifestScreens || manifestScreens.length === 0) return analyzerScreens;
|
|
7662
9691
|
const byName = /* @__PURE__ */ new Map();
|
|
7663
9692
|
for (const screen of analyzerScreens) byName.set(screen.name, screen);
|
|
7664
|
-
for (const
|
|
9693
|
+
for (const declared of manifestScreens) {
|
|
9694
|
+
const base = byName.get(declared.name);
|
|
9695
|
+
byName.set(
|
|
9696
|
+
declared.name,
|
|
9697
|
+
base ? mergeScreen(base, declared) : { ...declared, targets: markManifestOrigin(declared.targets) }
|
|
9698
|
+
);
|
|
9699
|
+
}
|
|
7665
9700
|
return Array.from(byName.values());
|
|
7666
9701
|
}
|
|
7667
9702
|
function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
|
|
@@ -7913,12 +9948,15 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
7913
9948
|
/**
|
|
7914
9949
|
* Select the `PlatformAnalyzer` implementation for a `.appilotsrc`
|
|
7915
9950
|
* `platform` value. `'react-native'` (or unset — the existing default)
|
|
7916
|
-
* gets the
|
|
7917
|
-
*
|
|
9951
|
+
* gets the RN Babel/JSX pipeline; `'web'` gets the React web (DOM +
|
|
9952
|
+
* React Router) pipeline; anything else gets the no-op generic
|
|
9953
|
+
* analyzer, relying entirely on a declared manifest. The manifest
|
|
9954
|
+
* still merges on top of every analyzer's output either way.
|
|
7918
9955
|
*/
|
|
7919
9956
|
static createPlatformAnalyzer(platform) {
|
|
7920
9957
|
const resolved = platform ?? "react-native";
|
|
7921
9958
|
if (resolved === "react-native") return new ReactNativePlatformAnalyzer();
|
|
9959
|
+
if (resolved === "web") return new ReactWebPlatformAnalyzer();
|
|
7922
9960
|
return new GenericPlatformAnalyzer(resolved);
|
|
7923
9961
|
}
|
|
7924
9962
|
/** Generate MCP documents from the project */
|
|
@@ -7948,7 +9986,12 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
7948
9986
|
}
|
|
7949
9987
|
const mergedScreens = mergeManifestScreens(analyzed.screens, manifest?.screens);
|
|
7950
9988
|
const navigation = mergeManifestNavigation(analyzed.navigation, manifest?.navigation);
|
|
7951
|
-
const agentReadyScreens = mergedScreens.map(
|
|
9989
|
+
const agentReadyScreens = mergedScreens.map(
|
|
9990
|
+
(screen) => enrichScreenForAgent({
|
|
9991
|
+
...screen,
|
|
9992
|
+
filePath: screen.filePath ? path8__namespace.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
|
|
9993
|
+
})
|
|
9994
|
+
);
|
|
7952
9995
|
const projectInfo = await this.getProjectInfo();
|
|
7953
9996
|
const projectName = projectInfo.name;
|
|
7954
9997
|
console.log(`[MCPGenerator] Project name: ${projectName}`);
|
|
@@ -7971,13 +10014,10 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
7971
10014
|
};
|
|
7972
10015
|
const serialized = JSON.stringify(document, null, 2);
|
|
7973
10016
|
const checksum = this.calculateChecksum(serialized);
|
|
7974
|
-
const filePath =
|
|
7975
|
-
outputDir,
|
|
7976
|
-
`mcp-document.${this.options.format}`
|
|
7977
|
-
);
|
|
10017
|
+
const filePath = path8__namespace.default.resolve(outputDir, `mcp-document.${this.options.format}`);
|
|
7978
10018
|
await fs.writeFile(filePath, serialized, "utf-8");
|
|
7979
10019
|
console.log(`[MCPGenerator] Document written to: ${filePath}`);
|
|
7980
|
-
const checksumFilePath =
|
|
10020
|
+
const checksumFilePath = path8__namespace.default.resolve(outputDir, ".appilots-checksum");
|
|
7981
10021
|
await fs.writeFile(checksumFilePath, checksum, "utf-8");
|
|
7982
10022
|
console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
|
|
7983
10023
|
console.log("[MCPGenerator] Generation complete!");
|
|
@@ -7997,7 +10037,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
7997
10037
|
* after calling `generate()`.
|
|
7998
10038
|
*/
|
|
7999
10039
|
static async readPreviousChecksum(outputDir) {
|
|
8000
|
-
const checksumFilePath =
|
|
10040
|
+
const checksumFilePath = path8__namespace.default.resolve(outputDir, ".appilots-checksum");
|
|
8001
10041
|
try {
|
|
8002
10042
|
const content = await fs.readFile(checksumFilePath, "utf-8");
|
|
8003
10043
|
return content.trim() || null;
|
|
@@ -8016,7 +10056,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
8016
10056
|
*/
|
|
8017
10057
|
async getProjectInfo() {
|
|
8018
10058
|
try {
|
|
8019
|
-
const packageJsonPath =
|
|
10059
|
+
const packageJsonPath = path8__namespace.default.resolve(this.analyzerConfig.rootDir, "package.json");
|
|
8020
10060
|
const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
|
|
8021
10061
|
const packageJson = JSON.parse(packageJsonContent);
|
|
8022
10062
|
return {
|
|
@@ -8029,6 +10069,70 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
8029
10069
|
}
|
|
8030
10070
|
}
|
|
8031
10071
|
};
|
|
10072
|
+
var DEFAULT_SERVER_URL = "http://localhost:4000";
|
|
10073
|
+
var KNOWN_CONFIG_KEYS = [
|
|
10074
|
+
"apiKey",
|
|
10075
|
+
"projectId",
|
|
10076
|
+
"serverUrl",
|
|
10077
|
+
"outputDir",
|
|
10078
|
+
"include",
|
|
10079
|
+
"exclude",
|
|
10080
|
+
"autoActivate",
|
|
10081
|
+
"strictScreens",
|
|
10082
|
+
"screenPatterns",
|
|
10083
|
+
"navigationInclude",
|
|
10084
|
+
"navigationExclude",
|
|
10085
|
+
"platform",
|
|
10086
|
+
"manifestPath",
|
|
10087
|
+
"eval"
|
|
10088
|
+
];
|
|
10089
|
+
var KEY_ALIASES = {
|
|
10090
|
+
apiUrl: "serverUrl",
|
|
10091
|
+
// `@appilots/sdk`'s own name for this. One `.appilotsrc` feeds both
|
|
10092
|
+
// packages, so an integrator who wrote the SDK's key should not be told
|
|
10093
|
+
// it is unrecognized.
|
|
10094
|
+
apiBaseUrl: "serverUrl",
|
|
10095
|
+
apiURL: "serverUrl",
|
|
10096
|
+
baseUrl: "serverUrl",
|
|
10097
|
+
url: "serverUrl",
|
|
10098
|
+
host: "serverUrl",
|
|
10099
|
+
endpoint: "serverUrl",
|
|
10100
|
+
server: "serverUrl",
|
|
10101
|
+
key: "apiKey",
|
|
10102
|
+
token: "apiKey",
|
|
10103
|
+
project: "projectId",
|
|
10104
|
+
sourceDir: "include",
|
|
10105
|
+
srcDir: "include",
|
|
10106
|
+
source: "include",
|
|
10107
|
+
outDir: "outputDir"
|
|
10108
|
+
};
|
|
10109
|
+
function editDistance(a, b) {
|
|
10110
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
10111
|
+
for (let i = 1; i <= a.length; i++) {
|
|
10112
|
+
const current = [i];
|
|
10113
|
+
for (let j = 1; j <= b.length; j++) {
|
|
10114
|
+
const deletion = (previous[j] ?? 0) + 1;
|
|
10115
|
+
const insertion = (current[j - 1] ?? 0) + 1;
|
|
10116
|
+
const substitution = (previous[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
|
|
10117
|
+
current.push(Math.min(deletion, insertion, substitution));
|
|
10118
|
+
}
|
|
10119
|
+
previous = current;
|
|
10120
|
+
}
|
|
10121
|
+
return previous[b.length] ?? 0;
|
|
10122
|
+
}
|
|
10123
|
+
function suggestConfigKey(key) {
|
|
10124
|
+
const alias = KEY_ALIASES[key];
|
|
10125
|
+
if (alias) return alias;
|
|
10126
|
+
const lower = key.toLowerCase();
|
|
10127
|
+
let best;
|
|
10128
|
+
for (const known of KNOWN_CONFIG_KEYS) {
|
|
10129
|
+
const distance = editDistance(lower, known.toLowerCase());
|
|
10130
|
+
if (distance <= 2 && (!best || distance < best.distance)) {
|
|
10131
|
+
best = { key: known, distance };
|
|
10132
|
+
}
|
|
10133
|
+
}
|
|
10134
|
+
return best?.key;
|
|
10135
|
+
}
|
|
8032
10136
|
function getEnvOverrides(env = process.env) {
|
|
8033
10137
|
const clean = (value) => {
|
|
8034
10138
|
const trimmed = value?.trim();
|
|
@@ -8040,7 +10144,7 @@ function getEnvOverrides(env = process.env) {
|
|
|
8040
10144
|
serverUrl: clean(env.APPILOTS_SERVER_URL)
|
|
8041
10145
|
};
|
|
8042
10146
|
}
|
|
8043
|
-
function loadConfig() {
|
|
10147
|
+
function loadConfig(onWarn) {
|
|
8044
10148
|
const configPath = getConfigPath();
|
|
8045
10149
|
const env = getEnvOverrides();
|
|
8046
10150
|
let fileConfig = {};
|
|
@@ -8048,13 +10152,15 @@ function loadConfig() {
|
|
|
8048
10152
|
try {
|
|
8049
10153
|
fileConfig = JSON.parse(fs$1.readFileSync(configPath, "utf-8"));
|
|
8050
10154
|
} catch (error) {
|
|
8051
|
-
throw new Error(
|
|
10155
|
+
throw new Error(
|
|
10156
|
+
`Failed to parse .appilotsrc at ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
10157
|
+
);
|
|
8052
10158
|
}
|
|
8053
10159
|
} else if (!env.apiKey) {
|
|
8054
10160
|
return null;
|
|
8055
10161
|
}
|
|
8056
10162
|
const merged = {
|
|
8057
|
-
serverUrl:
|
|
10163
|
+
serverUrl: DEFAULT_SERVER_URL,
|
|
8058
10164
|
outputDir: ".appilots",
|
|
8059
10165
|
autoActivate: true,
|
|
8060
10166
|
strictScreens: true,
|
|
@@ -8064,6 +10170,12 @@ function loadConfig() {
|
|
|
8064
10170
|
...env.serverUrl ? { serverUrl: env.serverUrl } : {}
|
|
8065
10171
|
};
|
|
8066
10172
|
const validation = validateConfig(merged);
|
|
10173
|
+
if (onWarn) {
|
|
10174
|
+
const source = configPath ?? "environment variables";
|
|
10175
|
+
for (const warning of validation.warnings) {
|
|
10176
|
+
onWarn(`${source}: ${warning}`);
|
|
10177
|
+
}
|
|
10178
|
+
}
|
|
8067
10179
|
if (!validation.valid) {
|
|
8068
10180
|
const source = configPath ?? "environment variables";
|
|
8069
10181
|
throw new Error(
|
|
@@ -8074,12 +10186,12 @@ function loadConfig() {
|
|
|
8074
10186
|
return merged;
|
|
8075
10187
|
}
|
|
8076
10188
|
function saveConfig(dir, config) {
|
|
8077
|
-
const configPath =
|
|
10189
|
+
const configPath = path8.join(dir, ".appilotsrc");
|
|
8078
10190
|
const existingConfig = getConfigPath() ? loadConfig() : null;
|
|
8079
10191
|
const mergedConfig = {
|
|
8080
10192
|
apiKey: config.apiKey || existingConfig?.apiKey || "",
|
|
8081
10193
|
projectId: config.projectId || existingConfig?.projectId || "",
|
|
8082
|
-
serverUrl: config.serverUrl || existingConfig?.serverUrl ||
|
|
10194
|
+
serverUrl: config.serverUrl || existingConfig?.serverUrl || DEFAULT_SERVER_URL,
|
|
8083
10195
|
outputDir: config.outputDir || existingConfig?.outputDir || ".appilots",
|
|
8084
10196
|
autoActivate: config.autoActivate !== void 0 ? config.autoActivate : existingConfig?.autoActivate ?? true,
|
|
8085
10197
|
include: config.include || existingConfig?.include,
|
|
@@ -8095,23 +10207,25 @@ function saveConfig(dir, config) {
|
|
|
8095
10207
|
try {
|
|
8096
10208
|
fs$1.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
|
|
8097
10209
|
} catch (error) {
|
|
8098
|
-
throw new Error(
|
|
10210
|
+
throw new Error(
|
|
10211
|
+
`Failed to write .appilotsrc to ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
10212
|
+
);
|
|
8099
10213
|
}
|
|
8100
10214
|
}
|
|
8101
10215
|
function getConfigPath() {
|
|
8102
|
-
let currentDir =
|
|
8103
|
-
const root =
|
|
10216
|
+
let currentDir = path8.resolve(process.cwd());
|
|
10217
|
+
const root = path8.resolve("/");
|
|
8104
10218
|
while (currentDir !== root) {
|
|
8105
|
-
const configPath =
|
|
10219
|
+
const configPath = path8.join(currentDir, ".appilotsrc");
|
|
8106
10220
|
try {
|
|
8107
10221
|
if (fs$1.existsSync(configPath) && fs$1.statSync(configPath).isFile()) {
|
|
8108
10222
|
return configPath;
|
|
8109
10223
|
}
|
|
8110
10224
|
} catch {
|
|
8111
10225
|
}
|
|
8112
|
-
currentDir =
|
|
10226
|
+
currentDir = path8.resolve(currentDir, "..");
|
|
8113
10227
|
}
|
|
8114
|
-
const rootConfigPath =
|
|
10228
|
+
const rootConfigPath = path8.join(root, ".appilotsrc");
|
|
8115
10229
|
try {
|
|
8116
10230
|
if (fs$1.existsSync(rootConfigPath) && fs$1.statSync(rootConfigPath).isFile()) {
|
|
8117
10231
|
return rootConfigPath;
|
|
@@ -8122,14 +10236,25 @@ function getConfigPath() {
|
|
|
8122
10236
|
}
|
|
8123
10237
|
function validateConfig(config) {
|
|
8124
10238
|
const errors = [];
|
|
10239
|
+
const warnings = [];
|
|
8125
10240
|
if (!config || typeof config !== "object") {
|
|
8126
10241
|
return {
|
|
8127
10242
|
valid: false,
|
|
8128
|
-
errors: ["Configuration must be an object"]
|
|
10243
|
+
errors: ["Configuration must be an object"],
|
|
10244
|
+
warnings
|
|
8129
10245
|
};
|
|
8130
10246
|
}
|
|
10247
|
+
for (const key of Object.keys(config)) {
|
|
10248
|
+
if (KNOWN_CONFIG_KEYS.includes(key)) continue;
|
|
10249
|
+
const suggestion = suggestConfigKey(key);
|
|
10250
|
+
warnings.push(
|
|
10251
|
+
`unknown key "${key}" \u2014 ignored` + (suggestion ? ` (did you mean "${suggestion}"?)` : "")
|
|
10252
|
+
);
|
|
10253
|
+
}
|
|
8131
10254
|
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
8132
|
-
errors.push(
|
|
10255
|
+
errors.push(
|
|
10256
|
+
"apiKey is required and must be a string (set it in .appilotsrc or via APPILOTS_API_KEY)"
|
|
10257
|
+
);
|
|
8133
10258
|
} else if (!config.apiKey.startsWith("ak_")) {
|
|
8134
10259
|
errors.push('apiKey must start with "ak_"');
|
|
8135
10260
|
}
|
|
@@ -8191,7 +10316,8 @@ function validateConfig(config) {
|
|
|
8191
10316
|
}
|
|
8192
10317
|
return {
|
|
8193
10318
|
valid: errors.length === 0,
|
|
8194
|
-
errors
|
|
10319
|
+
errors,
|
|
10320
|
+
warnings
|
|
8195
10321
|
};
|
|
8196
10322
|
}
|
|
8197
10323
|
|
|
@@ -8237,19 +10363,76 @@ function formatMetadataWarnings(warnings) {
|
|
|
8237
10363
|
);
|
|
8238
10364
|
}
|
|
8239
10365
|
|
|
10366
|
+
// ../shared/dist/chunk-UEMUBXFH.mjs
|
|
10367
|
+
var DEFAULT_API_BASE_URL = "https://api.appilots.com";
|
|
10368
|
+
var APPILOTS_API_PATH_PREFIX = "/api/v1";
|
|
10369
|
+
function normalizeApiBaseUrl(configured) {
|
|
10370
|
+
const raw = (configured ?? DEFAULT_API_BASE_URL).trim().replace(/\/+$/, "");
|
|
10371
|
+
if (!raw) return `${DEFAULT_API_BASE_URL}${APPILOTS_API_PATH_PREFIX}`;
|
|
10372
|
+
if (new RegExp(`${APPILOTS_API_PATH_PREFIX}(/|$)`).test(raw)) return raw;
|
|
10373
|
+
return `${raw}${APPILOTS_API_PATH_PREFIX}`;
|
|
10374
|
+
}
|
|
10375
|
+
|
|
8240
10376
|
// src/services/api-client.ts
|
|
8241
|
-
var
|
|
10377
|
+
var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
8242
10378
|
var DEFAULT_MAX_RETRIES = 2;
|
|
8243
10379
|
var RETRY_BASE_DELAY_MS = 500;
|
|
10380
|
+
function describeNetworkError(url, err) {
|
|
10381
|
+
let origin = url;
|
|
10382
|
+
try {
|
|
10383
|
+
origin = new URL(url).origin;
|
|
10384
|
+
} catch {
|
|
10385
|
+
}
|
|
10386
|
+
const cause = err?.cause;
|
|
10387
|
+
const code = typeof cause?.code === "string" ? cause.code : void 0;
|
|
10388
|
+
const detail = code ?? stringifyUnknown(err);
|
|
10389
|
+
return new Error(`Cannot reach Appilots server at ${origin} (${detail})`);
|
|
10390
|
+
}
|
|
10391
|
+
function errorCodeOf(payload) {
|
|
10392
|
+
const error = payload?.error;
|
|
10393
|
+
if (error && typeof error === "object") {
|
|
10394
|
+
const { code } = error;
|
|
10395
|
+
if (typeof code === "string" && code.trim()) return code;
|
|
10396
|
+
}
|
|
10397
|
+
return void 0;
|
|
10398
|
+
}
|
|
10399
|
+
function describeApiError(payload, fallback) {
|
|
10400
|
+
const error = payload?.error;
|
|
10401
|
+
if (typeof error === "string" && error.trim()) return error;
|
|
10402
|
+
if (error && typeof error === "object") {
|
|
10403
|
+
const { code, message: message2 } = error;
|
|
10404
|
+
const text = typeof message2 === "string" && message2.trim() ? message2 : void 0;
|
|
10405
|
+
const tag = typeof code === "string" && code.trim() ? code : void 0;
|
|
10406
|
+
if (text && tag) return `${text} (${tag})`;
|
|
10407
|
+
if (text) return text;
|
|
10408
|
+
if (tag) return tag;
|
|
10409
|
+
}
|
|
10410
|
+
const message = payload?.message;
|
|
10411
|
+
if (typeof message === "string" && message.trim()) return message;
|
|
10412
|
+
return fallback;
|
|
10413
|
+
}
|
|
8244
10414
|
var AppilotsAPIClient = class {
|
|
8245
|
-
|
|
10415
|
+
/**
|
|
10416
|
+
* `serverUrl` with the API's mount prefix resolved — every path below
|
|
10417
|
+
* is relative to THIS, not to the configured origin.
|
|
10418
|
+
*
|
|
10419
|
+
* It used to be the raw `serverUrl` with `/api/v1` hardcoded into each
|
|
10420
|
+
* path, which made the field mean something different here than in the
|
|
10421
|
+
* SDK. Both read the same `.appilotsrc`: the SDK completes the prefix
|
|
10422
|
+
* when it is missing, so `https://api.appilots.com/api/v1` is correct
|
|
10423
|
+
* there — and here that same value produced
|
|
10424
|
+
* `/api/v1/api/v1/cli/sync`, a 404 whose body reads `Route not found`.
|
|
10425
|
+
* Sharing `normalizeApiBaseUrl` is what makes one file mean one thing:
|
|
10426
|
+
* with or without the prefix now works in both.
|
|
10427
|
+
*/
|
|
10428
|
+
baseUrl;
|
|
8246
10429
|
apiKey;
|
|
8247
10430
|
timeoutMs;
|
|
8248
10431
|
maxRetries;
|
|
8249
10432
|
constructor(config) {
|
|
8250
|
-
this.
|
|
10433
|
+
this.baseUrl = normalizeApiBaseUrl(config.serverUrl);
|
|
8251
10434
|
this.apiKey = config.apiKey;
|
|
8252
|
-
this.timeoutMs = config.timeoutMs ??
|
|
10435
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
8253
10436
|
this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
8254
10437
|
}
|
|
8255
10438
|
/**
|
|
@@ -8273,7 +10456,7 @@ var AppilotsAPIClient = class {
|
|
|
8273
10456
|
}
|
|
8274
10457
|
return response;
|
|
8275
10458
|
} catch (err) {
|
|
8276
|
-
lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : err;
|
|
10459
|
+
lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : describeNetworkError(url, err);
|
|
8277
10460
|
} finally {
|
|
8278
10461
|
clearTimeout(timer);
|
|
8279
10462
|
}
|
|
@@ -8292,7 +10475,7 @@ var AppilotsAPIClient = class {
|
|
|
8292
10475
|
*/
|
|
8293
10476
|
async sync(content, version, appVersion) {
|
|
8294
10477
|
try {
|
|
8295
|
-
const response = await this.request(`${this.
|
|
10478
|
+
const response = await this.request(`${this.baseUrl}/cli/sync`, {
|
|
8296
10479
|
method: "POST",
|
|
8297
10480
|
headers: {
|
|
8298
10481
|
"Content-Type": "application/json",
|
|
@@ -8306,7 +10489,9 @@ var AppilotsAPIClient = class {
|
|
|
8306
10489
|
return {
|
|
8307
10490
|
success: false,
|
|
8308
10491
|
unchanged: false,
|
|
8309
|
-
error: errorData
|
|
10492
|
+
error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`),
|
|
10493
|
+
errorCode: errorCodeOf(errorData),
|
|
10494
|
+
errorStatus: response.status
|
|
8310
10495
|
};
|
|
8311
10496
|
}
|
|
8312
10497
|
const json = await response.json();
|
|
@@ -8319,7 +10504,8 @@ var AppilotsAPIClient = class {
|
|
|
8319
10504
|
return {
|
|
8320
10505
|
success: false,
|
|
8321
10506
|
unchanged: false,
|
|
8322
|
-
error: error instanceof Error ? error.message : "Failed to sync with Appilots API"
|
|
10507
|
+
error: error instanceof Error ? error.message : "Failed to sync with Appilots API",
|
|
10508
|
+
errorStatus: 0
|
|
8323
10509
|
};
|
|
8324
10510
|
}
|
|
8325
10511
|
}
|
|
@@ -8330,7 +10516,7 @@ var AppilotsAPIClient = class {
|
|
|
8330
10516
|
*/
|
|
8331
10517
|
async status() {
|
|
8332
10518
|
try {
|
|
8333
|
-
const response = await this.request(`${this.
|
|
10519
|
+
const response = await this.request(`${this.baseUrl}/cli/status`, {
|
|
8334
10520
|
method: "GET",
|
|
8335
10521
|
headers: {
|
|
8336
10522
|
Authorization: `Bearer ${this.apiKey}`
|
|
@@ -8339,7 +10525,7 @@ var AppilotsAPIClient = class {
|
|
|
8339
10525
|
if (!response.ok) {
|
|
8340
10526
|
const errorData = await response.json().catch(() => ({}));
|
|
8341
10527
|
return {
|
|
8342
|
-
error: errorData
|
|
10528
|
+
error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`)
|
|
8343
10529
|
};
|
|
8344
10530
|
}
|
|
8345
10531
|
const json = await response.json();
|
|
@@ -8361,7 +10547,7 @@ var AppilotsAPIClient = class {
|
|
|
8361
10547
|
*/
|
|
8362
10548
|
async evalRun(scenarios) {
|
|
8363
10549
|
try {
|
|
8364
|
-
const response = await this.request(`${this.
|
|
10550
|
+
const response = await this.request(`${this.baseUrl}/cli/eval/run`, {
|
|
8365
10551
|
method: "POST",
|
|
8366
10552
|
headers: {
|
|
8367
10553
|
"Content-Type": "application/json",
|
|
@@ -8392,7 +10578,7 @@ var AppilotsAPIClient = class {
|
|
|
8392
10578
|
*/
|
|
8393
10579
|
async health() {
|
|
8394
10580
|
try {
|
|
8395
|
-
const response = await this.request(`${this.
|
|
10581
|
+
const response = await this.request(`${this.baseUrl}/health`, {
|
|
8396
10582
|
method: "GET"
|
|
8397
10583
|
});
|
|
8398
10584
|
return response.ok;
|
|
@@ -8402,15 +10588,23 @@ var AppilotsAPIClient = class {
|
|
|
8402
10588
|
}
|
|
8403
10589
|
};
|
|
8404
10590
|
|
|
10591
|
+
// src/version.ts
|
|
10592
|
+
var CLI_VERSION = "0.10.0";
|
|
10593
|
+
|
|
8405
10594
|
exports.AppilotsAPIClient = AppilotsAPIClient;
|
|
10595
|
+
exports.CLI_VERSION = CLI_VERSION;
|
|
8406
10596
|
exports.ComponentAnalyzer = ComponentAnalyzer;
|
|
8407
10597
|
exports.DEFAULT_MANIFEST_FILENAME = DEFAULT_MANIFEST_FILENAME;
|
|
10598
|
+
exports.DEFAULT_WEB_SCREEN_PATTERNS = DEFAULT_WEB_SCREEN_PATTERNS;
|
|
8408
10599
|
exports.FormAnalyzer = FormAnalyzer;
|
|
8409
10600
|
exports.GenericPlatformAnalyzer = GenericPlatformAnalyzer;
|
|
8410
10601
|
exports.MCPGenerator = MCPGenerator;
|
|
8411
10602
|
exports.NavigationAnalyzer = NavigationAnalyzer;
|
|
8412
10603
|
exports.ReactNativePlatformAnalyzer = ReactNativePlatformAnalyzer;
|
|
10604
|
+
exports.ReactWebPlatformAnalyzer = ReactWebPlatformAnalyzer;
|
|
8413
10605
|
exports.ScreenAnalyzer = ScreenAnalyzer;
|
|
10606
|
+
exports.WebNavigationAnalyzer = WebNavigationAnalyzer;
|
|
10607
|
+
exports.WebScreenAnalyzer = WebScreenAnalyzer;
|
|
8414
10608
|
exports.formatMetadataWarnings = formatMetadataWarnings;
|
|
8415
10609
|
exports.getConfigPath = getConfigPath;
|
|
8416
10610
|
exports.getEnvOverrides = getEnvOverrides;
|
|
@@ -8419,7 +10613,9 @@ exports.loadConfig = loadConfig;
|
|
|
8419
10613
|
exports.loadManifest = loadManifest;
|
|
8420
10614
|
exports.mergeManifestNavigation = mergeManifestNavigation;
|
|
8421
10615
|
exports.mergeManifestScreens = mergeManifestScreens;
|
|
10616
|
+
exports.resolvePathToScreen = resolvePathToScreen;
|
|
8422
10617
|
exports.saveConfig = saveConfig;
|
|
10618
|
+
exports.screenNameFromPath = screenNameFromPath;
|
|
8423
10619
|
exports.validateConfig = validateConfig;
|
|
8424
10620
|
//# sourceMappingURL=index.js.map
|
|
8425
10621
|
//# sourceMappingURL=index.js.map
|