@appilots/cli 0.3.0 → 0.4.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 +1812 -241
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +200 -12
- package/dist/index.d.ts +200 -12
- package/dist/index.js +1594 -43
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1583 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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);
|
|
@@ -419,12 +419,12 @@ var ScreenAnalyzer = class {
|
|
|
419
419
|
cwd: this.config.rootDir,
|
|
420
420
|
ignore: exclude
|
|
421
421
|
});
|
|
422
|
-
screenPatternFiles = new Set(matched.map((f) =>
|
|
422
|
+
screenPatternFiles = new Set(matched.map((f) => path8__namespace.default.resolve(this.config.rootDir, f)));
|
|
423
423
|
}
|
|
424
424
|
const screens = [];
|
|
425
425
|
this.screensFilteredOut = 0;
|
|
426
426
|
for (const file of files) {
|
|
427
|
-
const filePath =
|
|
427
|
+
const filePath = path8__namespace.default.resolve(this.config.rootDir, file);
|
|
428
428
|
try {
|
|
429
429
|
const descriptor = await this.analyzeFile(filePath);
|
|
430
430
|
if (!descriptor) continue;
|
|
@@ -1213,7 +1213,7 @@ var ScreenAnalyzer = class {
|
|
|
1213
1213
|
* heuristic must hit OR the JSDoc tag must be present.
|
|
1214
1214
|
*/
|
|
1215
1215
|
isElementDestructive(element, handlerName, actionId) {
|
|
1216
|
-
const
|
|
1216
|
+
const DESTRUCTIVE_VERB3 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
|
|
1217
1217
|
for (const attr of element.attributes) {
|
|
1218
1218
|
if (!BabelTypes__namespace.isJSXAttribute(attr)) continue;
|
|
1219
1219
|
const attrName = BabelTypes__namespace.isJSXIdentifier(attr.name) ? attr.name.name : null;
|
|
@@ -1228,8 +1228,8 @@ var ScreenAnalyzer = class {
|
|
|
1228
1228
|
}
|
|
1229
1229
|
}
|
|
1230
1230
|
}
|
|
1231
|
-
if (handlerName &&
|
|
1232
|
-
if (actionId &&
|
|
1231
|
+
if (handlerName && DESTRUCTIVE_VERB3.test(handlerName)) return true;
|
|
1232
|
+
if (actionId && DESTRUCTIVE_VERB3.test(actionId)) return true;
|
|
1233
1233
|
return false;
|
|
1234
1234
|
}
|
|
1235
1235
|
/**
|
|
@@ -1573,7 +1573,7 @@ var ScreenAnalyzer = class {
|
|
|
1573
1573
|
* E.g., /src/screens/ItemListScreen.tsx -> ItemListScreen
|
|
1574
1574
|
*/
|
|
1575
1575
|
extractScreenName(filePath) {
|
|
1576
|
-
const basename2 =
|
|
1576
|
+
const basename2 = path8__namespace.default.basename(filePath);
|
|
1577
1577
|
return basename2.replace(/\.(tsx?|jsx?)$/, "");
|
|
1578
1578
|
}
|
|
1579
1579
|
};
|
|
@@ -1628,7 +1628,7 @@ var NavigationAnalyzer = class {
|
|
|
1628
1628
|
cwd: this.config.rootDir,
|
|
1629
1629
|
ignore: excludePatterns
|
|
1630
1630
|
});
|
|
1631
|
-
return files.map((file) =>
|
|
1631
|
+
return files.map((file) => path8__namespace.default.join(this.config.rootDir, file));
|
|
1632
1632
|
}
|
|
1633
1633
|
/** Parse navigator definitions from a file */
|
|
1634
1634
|
parseNavigators(content, filePath) {
|
|
@@ -1831,7 +1831,7 @@ var NavigationAnalyzer = class {
|
|
|
1831
1831
|
if (type.type === "TSUndefinedKeyword") return "undefined";
|
|
1832
1832
|
if (type.type === "TSNullKeyword") return "null";
|
|
1833
1833
|
if (type.type === "TSUnionType") {
|
|
1834
|
-
return type.types.map((
|
|
1834
|
+
return type.types.map((t12) => this.typeToString(t12)).join(" | ");
|
|
1835
1835
|
}
|
|
1836
1836
|
if (type.type === "TSTypeLiteral") {
|
|
1837
1837
|
return "object";
|
|
@@ -1852,7 +1852,7 @@ var NavigationAnalyzer = class {
|
|
|
1852
1852
|
/** Attach parsed type params to navigator screens */
|
|
1853
1853
|
attachParamsToNavigators(navigators, types) {
|
|
1854
1854
|
for (const navigator of navigators) {
|
|
1855
|
-
const matchingType = types.find((
|
|
1855
|
+
const matchingType = types.find((t12) => t12.type === navigator.type);
|
|
1856
1856
|
if (matchingType) {
|
|
1857
1857
|
for (const screen of navigator.screens) {
|
|
1858
1858
|
const screenParams = matchingType.paramEntries.get(screen.name);
|
|
@@ -1937,8 +1937,8 @@ var ComponentAnalyzer = class {
|
|
|
1937
1937
|
});
|
|
1938
1938
|
const components = [];
|
|
1939
1939
|
traverse4__default.default(ast, {
|
|
1940
|
-
JSXElement: (
|
|
1941
|
-
const component = this.extractComponentFromJSXElement(
|
|
1940
|
+
JSXElement: (path9) => {
|
|
1941
|
+
const component = this.extractComponentFromJSXElement(path9.node);
|
|
1942
1942
|
if (component) {
|
|
1943
1943
|
components.push(component);
|
|
1944
1944
|
}
|
|
@@ -2065,13 +2065,13 @@ var FormAnalyzer = class {
|
|
|
2065
2065
|
this.inputElements = [];
|
|
2066
2066
|
this.submitButtons = [];
|
|
2067
2067
|
traverse4__default.default(ast, {
|
|
2068
|
-
CallExpression: (
|
|
2069
|
-
this.extractStateVariables(
|
|
2068
|
+
CallExpression: (path9) => {
|
|
2069
|
+
this.extractStateVariables(path9.node);
|
|
2070
2070
|
}
|
|
2071
2071
|
});
|
|
2072
2072
|
traverse4__default.default(ast, {
|
|
2073
|
-
JSXElement: (
|
|
2074
|
-
this.extractFormElements(
|
|
2073
|
+
JSXElement: (path9) => {
|
|
2074
|
+
this.extractFormElements(path9.node);
|
|
2075
2075
|
}
|
|
2076
2076
|
});
|
|
2077
2077
|
const validationRules = this.extractValidationRules(ast);
|
|
@@ -2171,8 +2171,8 @@ var FormAnalyzer = class {
|
|
|
2171
2171
|
extractValidationRules(ast) {
|
|
2172
2172
|
const rules = {};
|
|
2173
2173
|
traverse4__default.default(ast, {
|
|
2174
|
-
IfStatement: (
|
|
2175
|
-
const test =
|
|
2174
|
+
IfStatement: (path9) => {
|
|
2175
|
+
const test = path9.node.test;
|
|
2176
2176
|
const rule = this.extractRuleFromCondition(test);
|
|
2177
2177
|
if (rule) {
|
|
2178
2178
|
const { field, description } = rule;
|
|
@@ -2226,7 +2226,7 @@ var FormAnalyzer = class {
|
|
|
2226
2226
|
}
|
|
2227
2227
|
buildForms(filePath, validationRules) {
|
|
2228
2228
|
if (this.inputElements.length === 0) return [];
|
|
2229
|
-
const fileName =
|
|
2229
|
+
const fileName = path8__namespace.basename(filePath, path8__namespace.extname(filePath));
|
|
2230
2230
|
const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
|
|
2231
2231
|
const fields = this.inputElements.map((input) => {
|
|
2232
2232
|
const fieldType = this.inferFieldType(input);
|
|
@@ -2304,7 +2304,7 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2304
2304
|
`[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
|
|
2305
2305
|
);
|
|
2306
2306
|
const enrichmentPromises = screenFiles.map(async (file) => {
|
|
2307
|
-
const filePath =
|
|
2307
|
+
const filePath = path8__namespace.default.resolve(config.rootDir, file);
|
|
2308
2308
|
try {
|
|
2309
2309
|
const [components, forms] = await Promise.all([
|
|
2310
2310
|
componentAnalyzer.analyzeFile(filePath),
|
|
@@ -2436,6 +2436,1480 @@ var GenericPlatformAnalyzer = class {
|
|
|
2436
2436
|
}
|
|
2437
2437
|
};
|
|
2438
2438
|
|
|
2439
|
+
// src/ast/jsx/web/classify.ts
|
|
2440
|
+
var VIEW_TAGS = /* @__PURE__ */ new Set([
|
|
2441
|
+
"div",
|
|
2442
|
+
"span",
|
|
2443
|
+
"section",
|
|
2444
|
+
"main",
|
|
2445
|
+
"article",
|
|
2446
|
+
"aside",
|
|
2447
|
+
"header",
|
|
2448
|
+
"footer",
|
|
2449
|
+
"nav",
|
|
2450
|
+
"fieldset",
|
|
2451
|
+
"form",
|
|
2452
|
+
"p"
|
|
2453
|
+
]);
|
|
2454
|
+
var LIST_TAGS = /* @__PURE__ */ new Set(["ul", "ol", "dl", "table", "tbody"]);
|
|
2455
|
+
var BUTTON_TAGS = /* @__PURE__ */ new Set(["button"]);
|
|
2456
|
+
var INPUT_TAGS = /* @__PURE__ */ new Set(["input", "textarea"]);
|
|
2457
|
+
var MODAL_TAGS = /* @__PURE__ */ new Set(["dialog"]);
|
|
2458
|
+
var BUTTON_COMPONENTS2 = /* @__PURE__ */ new Set(["Button", "IconButton", "Link", "NavLink"]);
|
|
2459
|
+
var INPUT_COMPONENTS2 = /* @__PURE__ */ new Set(["Input", "TextField", "TextArea", "Textarea"]);
|
|
2460
|
+
var LIST_COMPONENTS2 = /* @__PURE__ */ new Set(["List", "Table", "DataGrid", "DataTable"]);
|
|
2461
|
+
var MODAL_COMPONENTS2 = /* @__PURE__ */ new Set(["Modal", "Dialog", "Drawer", "Popover", "BottomSheet"]);
|
|
2462
|
+
var ARIA_ROLE_TO_SEMANTIC = {
|
|
2463
|
+
button: "button",
|
|
2464
|
+
link: "button",
|
|
2465
|
+
tab: "button",
|
|
2466
|
+
menuitem: "button",
|
|
2467
|
+
textbox: "input",
|
|
2468
|
+
searchbox: "input",
|
|
2469
|
+
spinbutton: "input",
|
|
2470
|
+
listbox: "select",
|
|
2471
|
+
combobox: "select",
|
|
2472
|
+
radiogroup: "select",
|
|
2473
|
+
radio: "select",
|
|
2474
|
+
option: "select",
|
|
2475
|
+
checkbox: "toggle",
|
|
2476
|
+
switch: "toggle",
|
|
2477
|
+
dialog: "modal",
|
|
2478
|
+
alertdialog: "modal",
|
|
2479
|
+
list: "list",
|
|
2480
|
+
table: "list",
|
|
2481
|
+
grid: "list",
|
|
2482
|
+
form: "view"
|
|
2483
|
+
};
|
|
2484
|
+
var INPUT_TYPE_TO_SEMANTIC = {
|
|
2485
|
+
checkbox: "toggle",
|
|
2486
|
+
radio: "select",
|
|
2487
|
+
date: "date",
|
|
2488
|
+
"datetime-local": "date",
|
|
2489
|
+
month: "date",
|
|
2490
|
+
week: "date",
|
|
2491
|
+
time: "date",
|
|
2492
|
+
submit: "button",
|
|
2493
|
+
button: "button",
|
|
2494
|
+
reset: "button",
|
|
2495
|
+
image: "button",
|
|
2496
|
+
hidden: "custom",
|
|
2497
|
+
range: "input",
|
|
2498
|
+
file: "input",
|
|
2499
|
+
color: "input"
|
|
2500
|
+
};
|
|
2501
|
+
function classifyWebJsxComponent(name, element) {
|
|
2502
|
+
if (element) {
|
|
2503
|
+
const role = getStringAttr(element, "role");
|
|
2504
|
+
if (role && ARIA_ROLE_TO_SEMANTIC[role]) return ARIA_ROLE_TO_SEMANTIC[role];
|
|
2505
|
+
}
|
|
2506
|
+
if (/^[a-z]/.test(name)) {
|
|
2507
|
+
if (name === "input") {
|
|
2508
|
+
const type = element ? getStringAttr(element, "type") : void 0;
|
|
2509
|
+
if (type && INPUT_TYPE_TO_SEMANTIC[type]) return INPUT_TYPE_TO_SEMANTIC[type];
|
|
2510
|
+
return "input";
|
|
2511
|
+
}
|
|
2512
|
+
if (INPUT_TAGS.has(name)) return "input";
|
|
2513
|
+
if (name === "select") return "select";
|
|
2514
|
+
if (BUTTON_TAGS.has(name)) return "button";
|
|
2515
|
+
if (name === "a") {
|
|
2516
|
+
if (element && (hasJsxAttribute(element, "href") || hasJsxAttribute(element, "onClick"))) {
|
|
2517
|
+
return "button";
|
|
2518
|
+
}
|
|
2519
|
+
return "view";
|
|
2520
|
+
}
|
|
2521
|
+
if (LIST_TAGS.has(name)) return "list";
|
|
2522
|
+
if (MODAL_TAGS.has(name)) return "modal";
|
|
2523
|
+
if (VIEW_TAGS.has(name)) return "view";
|
|
2524
|
+
return "custom";
|
|
2525
|
+
}
|
|
2526
|
+
if (LIST_COMPONENTS2.has(name)) return "list";
|
|
2527
|
+
if (MODAL_COMPONENTS2.has(name)) return "modal";
|
|
2528
|
+
if (/date/i.test(name)) return "date";
|
|
2529
|
+
if (/(select|picker|dropdown|radio)/i.test(name)) return "select";
|
|
2530
|
+
if (/(checkbox|switch|toggle)/i.test(name)) return "toggle";
|
|
2531
|
+
if (INPUT_COMPONENTS2.has(name)) return "input";
|
|
2532
|
+
if (BUTTON_COMPONENTS2.has(name)) return "button";
|
|
2533
|
+
if (element) {
|
|
2534
|
+
const hasOptions = hasJsxAttribute(element, "options");
|
|
2535
|
+
const hasValue = hasJsxAttribute(element, "value");
|
|
2536
|
+
const hasChecked = hasJsxAttribute(element, "checked") || hasJsxAttribute(element, "selected");
|
|
2537
|
+
const hasOnChange = hasJsxAttribute(element, "onChange") || hasJsxAttribute(element, "onValueChange");
|
|
2538
|
+
const hasOnClick = hasJsxAttribute(element, "onClick");
|
|
2539
|
+
if (hasOptions && (hasValue || hasOnChange)) return "select";
|
|
2540
|
+
if (hasChecked && hasOnChange) return "toggle";
|
|
2541
|
+
if (hasOnChange && (hasJsxAttribute(element, "label") || hasJsxAttribute(element, "placeholder"))) {
|
|
2542
|
+
return "input";
|
|
2543
|
+
}
|
|
2544
|
+
if (hasOnClick) return "button";
|
|
2545
|
+
if (getStringAttr(element, "open") || getExpressionIdentifierAttr(element, "open") || getExpressionIdentifierAttr(element, "isOpen")) {
|
|
2546
|
+
return "modal";
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
return "custom";
|
|
2550
|
+
}
|
|
2551
|
+
function getJsxElementName(openingElement) {
|
|
2552
|
+
return getJsxName(openingElement.name);
|
|
2553
|
+
}
|
|
2554
|
+
function getJsxName(name) {
|
|
2555
|
+
if (BabelTypes__namespace.isJSXIdentifier(name)) return name.name;
|
|
2556
|
+
if (BabelTypes__namespace.isJSXNamespacedName(name)) return `${name.namespace.name}:${name.name.name}`;
|
|
2557
|
+
if (BabelTypes__namespace.isJSXMemberExpression(name)) {
|
|
2558
|
+
const objectName = getJsxName(name.object);
|
|
2559
|
+
const propertyName = BabelTypes__namespace.isJSXIdentifier(name.property) ? name.property.name : null;
|
|
2560
|
+
return objectName && propertyName ? `${objectName}.${propertyName}` : null;
|
|
2561
|
+
}
|
|
2562
|
+
return null;
|
|
2563
|
+
}
|
|
2564
|
+
var ROUTERISH_OBJECTS = /^(router|history|navigation)$/;
|
|
2565
|
+
function staticRoutePath(node) {
|
|
2566
|
+
if (!node) return void 0;
|
|
2567
|
+
if (BabelTypes__namespace.isStringLiteral(node)) return node.value;
|
|
2568
|
+
if (BabelTypes__namespace.isTemplateLiteral(node)) {
|
|
2569
|
+
let path9 = "";
|
|
2570
|
+
node.quasis.forEach((quasi, index) => {
|
|
2571
|
+
path9 += quasi.value.cooked ?? quasi.value.raw;
|
|
2572
|
+
const expr = node.expressions[index];
|
|
2573
|
+
if (expr) path9 += `:${paramNameOf(expr)}`;
|
|
2574
|
+
});
|
|
2575
|
+
return path9;
|
|
2576
|
+
}
|
|
2577
|
+
return void 0;
|
|
2578
|
+
}
|
|
2579
|
+
function paramNameOf(expr) {
|
|
2580
|
+
if (BabelTypes__namespace.isIdentifier(expr)) return expr.name;
|
|
2581
|
+
if (BabelTypes__namespace.isMemberExpression(expr) && BabelTypes__namespace.isIdentifier(expr.property)) return expr.property.name;
|
|
2582
|
+
return "param";
|
|
2583
|
+
}
|
|
2584
|
+
function extractWebNavigationCalls(ast) {
|
|
2585
|
+
const calls = [];
|
|
2586
|
+
const inspect = (node) => {
|
|
2587
|
+
if (BabelTypes__namespace.isCallExpression(node)) {
|
|
2588
|
+
if (BabelTypes__namespace.isIdentifier(node.callee) && node.callee.name === "navigate") {
|
|
2589
|
+
const first = node.arguments[0];
|
|
2590
|
+
const targetPath = staticRoutePath(first);
|
|
2591
|
+
if (targetPath !== void 0) {
|
|
2592
|
+
calls.push({
|
|
2593
|
+
method: hasReplaceOption(node.arguments[1]) ? "replace" : "navigate",
|
|
2594
|
+
targetPath
|
|
2595
|
+
});
|
|
2596
|
+
} else if (BabelTypes__namespace.isNumericLiteral(first) || BabelTypes__namespace.isUnaryExpression(first) && first.operator === "-") {
|
|
2597
|
+
calls.push({ method: "goBack" });
|
|
2598
|
+
}
|
|
2599
|
+
return;
|
|
2600
|
+
}
|
|
2601
|
+
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)) {
|
|
2602
|
+
const method = node.callee.property.name;
|
|
2603
|
+
const targetPath = staticRoutePath(node.arguments[0]);
|
|
2604
|
+
if ((method === "push" || method === "navigate") && targetPath !== void 0) {
|
|
2605
|
+
calls.push({ method: "navigate", targetPath });
|
|
2606
|
+
} else if (method === "replace" && targetPath !== void 0) {
|
|
2607
|
+
calls.push({ method: "replace", targetPath });
|
|
2608
|
+
} else if (method === "back" || method === "goBack") {
|
|
2609
|
+
calls.push({ method: "goBack" });
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
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("/")) {
|
|
2615
|
+
calls.push({ method: "navigate", targetPath: node.right.value });
|
|
2616
|
+
}
|
|
2617
|
+
};
|
|
2618
|
+
traverse4__default.default(ast, {
|
|
2619
|
+
noScope: !BabelTypes__namespace.isFile(ast),
|
|
2620
|
+
enter: (nodePath) => inspect(nodePath.node)
|
|
2621
|
+
});
|
|
2622
|
+
return calls;
|
|
2623
|
+
}
|
|
2624
|
+
function hasReplaceOption(arg) {
|
|
2625
|
+
if (!arg || !BabelTypes__namespace.isObjectExpression(arg)) return false;
|
|
2626
|
+
return arg.properties.some(
|
|
2627
|
+
(prop) => BabelTypes__namespace.isObjectProperty(prop) && BabelTypes__namespace.isIdentifier(prop.key) && prop.key.name === "replace" && BabelTypes__namespace.isBooleanLiteral(prop.value) && prop.value.value === true
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2630
|
+
function isLocationExpression(node) {
|
|
2631
|
+
if (BabelTypes__namespace.isIdentifier(node)) return node.name === "location";
|
|
2632
|
+
return BabelTypes__namespace.isMemberExpression(node) && BabelTypes__namespace.isIdentifier(node.object) && node.object.name === "window" && BabelTypes__namespace.isIdentifier(node.property) && node.property.name === "location";
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
// src/analyzers/web/WebScreenAnalyzer.ts
|
|
2636
|
+
var DEFAULT_WEB_SCREEN_PATTERNS = [
|
|
2637
|
+
"**/pages/**/*.{ts,tsx,js,jsx}",
|
|
2638
|
+
"**/routes/**/*.{ts,tsx,js,jsx}",
|
|
2639
|
+
"**/views/**/*.{ts,tsx,js,jsx}",
|
|
2640
|
+
"**/app/**/*.{ts,tsx,js,jsx}",
|
|
2641
|
+
"**/*Page.{ts,tsx,js,jsx}",
|
|
2642
|
+
"**/*Screen.{ts,tsx,js,jsx}",
|
|
2643
|
+
"**/*View.{ts,tsx,js,jsx}"
|
|
2644
|
+
];
|
|
2645
|
+
var DESTRUCTIVE_VERB2 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
|
|
2646
|
+
var LISTISH_TAGS = /* @__PURE__ */ new Set(["ul", "ol", "dl", "table", "tbody"]);
|
|
2647
|
+
var WebScreenAnalyzer = class {
|
|
2648
|
+
config;
|
|
2649
|
+
verbose = process.env.VERBOSE === "true";
|
|
2650
|
+
screenPatterns;
|
|
2651
|
+
constructor(config, options) {
|
|
2652
|
+
this.config = config;
|
|
2653
|
+
this.screenPatterns = options?.screenPatterns ?? DEFAULT_WEB_SCREEN_PATTERNS;
|
|
2654
|
+
}
|
|
2655
|
+
async analyze() {
|
|
2656
|
+
const {
|
|
2657
|
+
include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
|
|
2658
|
+
exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
2659
|
+
} = this.config;
|
|
2660
|
+
const files = await glob__default.default(include, { cwd: this.config.rootDir, ignore: exclude });
|
|
2661
|
+
const patternMatches = await glob__default.default(this.screenPatterns, {
|
|
2662
|
+
cwd: this.config.rootDir,
|
|
2663
|
+
ignore: exclude
|
|
2664
|
+
});
|
|
2665
|
+
const patternSet = new Set(patternMatches.map((f) => path8__namespace.default.resolve(this.config.rootDir, f)));
|
|
2666
|
+
const candidates = [];
|
|
2667
|
+
for (const file of files) {
|
|
2668
|
+
const filePath = path8__namespace.default.resolve(this.config.rootDir, file);
|
|
2669
|
+
try {
|
|
2670
|
+
const candidate = await this.analyzeFile(filePath);
|
|
2671
|
+
if (!candidate) continue;
|
|
2672
|
+
candidate.matchesScreenPattern = patternSet.has(filePath);
|
|
2673
|
+
candidates.push(candidate);
|
|
2674
|
+
if (this.verbose) {
|
|
2675
|
+
console.log(`[WebScreenAnalyzer] \u2713 Analyzed: ${candidate.descriptor.name} (${file})`);
|
|
2676
|
+
}
|
|
2677
|
+
} catch (error) {
|
|
2678
|
+
if (this.verbose) {
|
|
2679
|
+
console.warn(
|
|
2680
|
+
`[WebScreenAnalyzer] Failed to parse ${file}:`,
|
|
2681
|
+
error instanceof Error ? error.message : error
|
|
2682
|
+
);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
return { candidates, analyzedFiles: files.length };
|
|
2687
|
+
}
|
|
2688
|
+
async analyzeFile(filePath) {
|
|
2689
|
+
const source = await fs__default.default.readFile(filePath, "utf-8");
|
|
2690
|
+
const ast = parseSource(source, this.config.parserPlugins);
|
|
2691
|
+
const registerScreenMeta = this.extractRegisterScreenMetadata(ast);
|
|
2692
|
+
const componentName = this.extractComponentName(ast);
|
|
2693
|
+
const labelsByHtmlFor = this.collectHtmlForLabels(ast);
|
|
2694
|
+
const handlerBehaviors = this.collectWebHandlerBehaviors(ast);
|
|
2695
|
+
const forms = this.mergeForms(
|
|
2696
|
+
registerScreenMeta?.forms ?? [],
|
|
2697
|
+
this.extractForms(ast, labelsByHtmlFor)
|
|
2698
|
+
);
|
|
2699
|
+
const actions = this.extractActions(ast, registerScreenMeta, handlerBehaviors);
|
|
2700
|
+
const components = this.extractComponents(ast);
|
|
2701
|
+
const collections = this.extractCollections(ast);
|
|
2702
|
+
const navigationTargets = this.extractNavigationTargets(ast);
|
|
2703
|
+
const permissionsFromJsDoc = extractPermissionsFromJsDoc(source);
|
|
2704
|
+
const destructiveTags = extractDestructiveJsDocTargets(source);
|
|
2705
|
+
if (destructiveTags) {
|
|
2706
|
+
for (const action of actions) {
|
|
2707
|
+
if (destructiveTags === "*" || destructiveTags.has(action.id)) {
|
|
2708
|
+
action.destructive = true;
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
const name = registerScreenMeta?.name || componentName || path8__namespace.default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
|
|
2713
|
+
const descriptor = {
|
|
2714
|
+
name,
|
|
2715
|
+
filePath,
|
|
2716
|
+
title: registerScreenMeta?.title,
|
|
2717
|
+
description: registerScreenMeta?.description,
|
|
2718
|
+
components,
|
|
2719
|
+
forms,
|
|
2720
|
+
actions,
|
|
2721
|
+
navigationTargets,
|
|
2722
|
+
...collections.length > 0 ? { collections } : {},
|
|
2723
|
+
...registerScreenMeta?.suggestedPrompts && registerScreenMeta.suggestedPrompts.length > 0 ? { suggestedPrompts: registerScreenMeta.suggestedPrompts } : {},
|
|
2724
|
+
...permissionsFromJsDoc ? {
|
|
2725
|
+
permissions: permissionsFromJsDoc,
|
|
2726
|
+
...permissionsFromJsDoc.isPii ? { isPii: true } : {}
|
|
2727
|
+
} : {}
|
|
2728
|
+
};
|
|
2729
|
+
return {
|
|
2730
|
+
descriptor,
|
|
2731
|
+
hasRegisterScreen: registerScreenMeta !== null || this.detectRegisterScreenCall(ast),
|
|
2732
|
+
matchesScreenPattern: false
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
// ── registerScreen ────────────────────────────────────────────────
|
|
2736
|
+
detectRegisterScreenCall(ast) {
|
|
2737
|
+
let found = false;
|
|
2738
|
+
traverse4__default.default(ast, {
|
|
2739
|
+
CallExpression: (nodePath) => {
|
|
2740
|
+
if (found) return;
|
|
2741
|
+
if (isRegisterScreenCallee(nodePath.node.callee)) {
|
|
2742
|
+
found = true;
|
|
2743
|
+
nodePath.stop();
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
});
|
|
2747
|
+
return found;
|
|
2748
|
+
}
|
|
2749
|
+
extractRegisterScreenMetadata(ast) {
|
|
2750
|
+
let plain = null;
|
|
2751
|
+
traverse4__default.default(ast, {
|
|
2752
|
+
CallExpression: (nodePath) => {
|
|
2753
|
+
if (!isRegisterScreenCallee(nodePath.node.callee)) return;
|
|
2754
|
+
const arg = nodePath.node.arguments[0];
|
|
2755
|
+
if (BabelTypes__namespace.isObjectExpression(arg)) {
|
|
2756
|
+
plain = literalToPlain(arg);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
});
|
|
2760
|
+
if (!plain) return null;
|
|
2761
|
+
const meta = plain;
|
|
2762
|
+
const result = {
|
|
2763
|
+
name: typeof meta.name === "string" ? meta.name : "",
|
|
2764
|
+
actions: [],
|
|
2765
|
+
forms: [],
|
|
2766
|
+
navigationTargets: [],
|
|
2767
|
+
components: []
|
|
2768
|
+
};
|
|
2769
|
+
if (typeof meta.title === "string") result.title = meta.title;
|
|
2770
|
+
if (typeof meta.description === "string") result.description = meta.description;
|
|
2771
|
+
if (Array.isArray(meta.suggestedPrompts)) {
|
|
2772
|
+
const prompts = meta.suggestedPrompts.filter((p) => typeof p === "string").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
2773
|
+
if (prompts.length > 0) result.suggestedPrompts = prompts;
|
|
2774
|
+
}
|
|
2775
|
+
if (Array.isArray(meta.actions)) {
|
|
2776
|
+
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 }));
|
|
2777
|
+
}
|
|
2778
|
+
if (Array.isArray(meta.fields)) {
|
|
2779
|
+
const fields = meta.fields.filter((f) => typeof f === "object" && f !== null).filter((f) => typeof f.id === "string" && f.id.length > 0).map(
|
|
2780
|
+
(f) => ({
|
|
2781
|
+
name: f.id,
|
|
2782
|
+
type: typeof f.type === "string" ? f.type : "text",
|
|
2783
|
+
required: f.required === true,
|
|
2784
|
+
...typeof f.label === "string" ? { label: f.label } : {},
|
|
2785
|
+
...typeof f.placeholder === "string" ? { placeholder: f.placeholder } : {},
|
|
2786
|
+
...f.defaultValue !== void 0 ? { defaultValue: f.defaultValue } : {},
|
|
2787
|
+
...Array.isArray(f.options) ? { options: f.options } : {}
|
|
2788
|
+
})
|
|
2789
|
+
);
|
|
2790
|
+
if (fields.length > 0) result.forms = [{ id: "default", fields }];
|
|
2791
|
+
}
|
|
2792
|
+
return result;
|
|
2793
|
+
}
|
|
2794
|
+
// ── Component name / structure ────────────────────────────────────
|
|
2795
|
+
/** Default-exported component name, else the first exported capitalized function. */
|
|
2796
|
+
extractComponentName(ast) {
|
|
2797
|
+
let defaultName = "";
|
|
2798
|
+
let firstExported = "";
|
|
2799
|
+
traverse4__default.default(ast, {
|
|
2800
|
+
ExportDefaultDeclaration: (nodePath) => {
|
|
2801
|
+
const declaration = nodePath.node.declaration;
|
|
2802
|
+
if (BabelTypes__namespace.isFunctionDeclaration(declaration) && declaration.id?.name) {
|
|
2803
|
+
defaultName = declaration.id.name;
|
|
2804
|
+
} else if (BabelTypes__namespace.isIdentifier(declaration)) {
|
|
2805
|
+
defaultName = declaration.name;
|
|
2806
|
+
}
|
|
2807
|
+
},
|
|
2808
|
+
ExportNamedDeclaration: (nodePath) => {
|
|
2809
|
+
if (firstExported) return;
|
|
2810
|
+
const declaration = nodePath.node.declaration;
|
|
2811
|
+
if (BabelTypes__namespace.isFunctionDeclaration(declaration) && declaration.id && /^[A-Z]/.test(declaration.id.name)) {
|
|
2812
|
+
firstExported = declaration.id.name;
|
|
2813
|
+
} else if (BabelTypes__namespace.isVariableDeclaration(declaration)) {
|
|
2814
|
+
for (const declarator of declaration.declarations) {
|
|
2815
|
+
if (BabelTypes__namespace.isIdentifier(declarator.id) && /^[A-Z]/.test(declarator.id.name) && (BabelTypes__namespace.isArrowFunctionExpression(declarator.init) || BabelTypes__namespace.isFunctionExpression(declarator.init))) {
|
|
2816
|
+
firstExported = declarator.id.name;
|
|
2817
|
+
break;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
});
|
|
2823
|
+
return defaultName || firstExported;
|
|
2824
|
+
}
|
|
2825
|
+
extractComponents(ast) {
|
|
2826
|
+
const components = [];
|
|
2827
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2828
|
+
traverse4__default.default(ast, {
|
|
2829
|
+
JSXOpeningElement: (nodePath) => {
|
|
2830
|
+
const element = nodePath.node;
|
|
2831
|
+
const name = getJsxElementName(element);
|
|
2832
|
+
if (!name || seen.has(name)) return;
|
|
2833
|
+
seen.add(name);
|
|
2834
|
+
const role = classifyWebJsxComponent(name, element);
|
|
2835
|
+
const type = role === "select" || role === "toggle" || role === "date" ? "input" : role === "input" || role === "button" || role === "list" || role === "modal" || role === "view" ? role : "custom";
|
|
2836
|
+
const component = { name, type };
|
|
2837
|
+
const testId = getStringAttr(element, "data-testid") ?? getStringAttr(element, "testID");
|
|
2838
|
+
const ariaLabel = getStringAttr(element, "aria-label");
|
|
2839
|
+
if (testId) component.testID = testId;
|
|
2840
|
+
if (ariaLabel) component.accessibilityLabel = ariaLabel;
|
|
2841
|
+
components.push(component);
|
|
2842
|
+
}
|
|
2843
|
+
});
|
|
2844
|
+
return components;
|
|
2845
|
+
}
|
|
2846
|
+
// ── <label htmlFor> association ───────────────────────────────────
|
|
2847
|
+
collectHtmlForLabels(ast) {
|
|
2848
|
+
const labels = /* @__PURE__ */ new Map();
|
|
2849
|
+
traverse4__default.default(ast, {
|
|
2850
|
+
JSXElement: (nodePath) => {
|
|
2851
|
+
const element = nodePath.node;
|
|
2852
|
+
if (getJsxElementName(element.openingElement) !== "label") return;
|
|
2853
|
+
const htmlFor = getStringAttr(element.openingElement, "htmlFor");
|
|
2854
|
+
if (!htmlFor) return;
|
|
2855
|
+
const text = jsxTextContent(element);
|
|
2856
|
+
if (text) labels.set(htmlFor, text);
|
|
2857
|
+
}
|
|
2858
|
+
});
|
|
2859
|
+
return labels;
|
|
2860
|
+
}
|
|
2861
|
+
// ── Forms ─────────────────────────────────────────────────────────
|
|
2862
|
+
extractForms(ast, labelsByHtmlFor) {
|
|
2863
|
+
const formBuckets = /* @__PURE__ */ new Map();
|
|
2864
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
2865
|
+
let formCount = 0;
|
|
2866
|
+
const uniqueId = (preferred) => {
|
|
2867
|
+
if (!usedIds.has(preferred)) {
|
|
2868
|
+
usedIds.add(preferred);
|
|
2869
|
+
return preferred;
|
|
2870
|
+
}
|
|
2871
|
+
let suffix = 2;
|
|
2872
|
+
while (usedIds.has(`${preferred}-${suffix}`)) suffix += 1;
|
|
2873
|
+
const id = `${preferred}-${suffix}`;
|
|
2874
|
+
usedIds.add(id);
|
|
2875
|
+
return id;
|
|
2876
|
+
};
|
|
2877
|
+
const bucketFor = (formElement) => {
|
|
2878
|
+
let bucket = formBuckets.get(formElement);
|
|
2879
|
+
if (bucket) return bucket;
|
|
2880
|
+
formCount += 1;
|
|
2881
|
+
let preferred = "default";
|
|
2882
|
+
let submitAction;
|
|
2883
|
+
if (formElement) {
|
|
2884
|
+
const opening = formElement.openingElement;
|
|
2885
|
+
preferred = getStringAttr(opening, "id") ?? getStringAttr(opening, "name") ?? getStringAttr(opening, "data-testid") ?? (formCount === 1 ? "default" : `form-${formCount}`);
|
|
2886
|
+
submitAction = this.handlerNameFromAttr(opening, "onSubmit");
|
|
2887
|
+
}
|
|
2888
|
+
bucket = { id: uniqueId(preferred), fields: /* @__PURE__ */ new Map(), submitAction };
|
|
2889
|
+
formBuckets.set(formElement, bucket);
|
|
2890
|
+
return bucket;
|
|
2891
|
+
};
|
|
2892
|
+
traverse4__default.default(ast, {
|
|
2893
|
+
JSXElement: (nodePath) => {
|
|
2894
|
+
const element = nodePath.node;
|
|
2895
|
+
const name = getJsxElementName(element.openingElement);
|
|
2896
|
+
if (!name) return;
|
|
2897
|
+
const role = classifyWebJsxComponent(name, element.openingElement);
|
|
2898
|
+
if (!["input", "select", "toggle", "date"].includes(role)) return;
|
|
2899
|
+
if (name === "option") return;
|
|
2900
|
+
const field = this.extractField(element, role, labelsByHtmlFor);
|
|
2901
|
+
if (!field.name) return;
|
|
2902
|
+
const formParent = nodePath.findParent(
|
|
2903
|
+
(p) => p.isJSXElement() && (getJsxElementName(p.node.openingElement) === "form" || getStringAttr(p.node.openingElement, "role") === "form")
|
|
2904
|
+
);
|
|
2905
|
+
const bucket = bucketFor(formParent ? formParent.node : null);
|
|
2906
|
+
if (!bucket.fields.has(field.name)) bucket.fields.set(field.name, field);
|
|
2907
|
+
}
|
|
2908
|
+
});
|
|
2909
|
+
traverse4__default.default(ast, {
|
|
2910
|
+
JSXElement: (nodePath) => {
|
|
2911
|
+
const element = nodePath.node;
|
|
2912
|
+
const name = getJsxElementName(element.openingElement);
|
|
2913
|
+
if (!name) return;
|
|
2914
|
+
if (!this.isSubmitElement(name, element.openingElement)) return;
|
|
2915
|
+
const formParent = nodePath.findParent(
|
|
2916
|
+
(p) => p.isJSXElement() && getJsxElementName(p.node.openingElement) === "form"
|
|
2917
|
+
);
|
|
2918
|
+
if (!formParent) return;
|
|
2919
|
+
const bucket = formBuckets.get(formParent.node);
|
|
2920
|
+
if (!bucket) return;
|
|
2921
|
+
const handler = this.handlerNameFromAttr(element.openingElement, "onClick");
|
|
2922
|
+
if (!bucket.submitAction && handler) bucket.submitAction = handler;
|
|
2923
|
+
}
|
|
2924
|
+
});
|
|
2925
|
+
return Array.from(formBuckets.values()).filter((bucket) => bucket.fields.size > 0).map((bucket) => ({
|
|
2926
|
+
id: bucket.id,
|
|
2927
|
+
fields: Array.from(bucket.fields.values()),
|
|
2928
|
+
...bucket.submitAction && bucket.submitAction !== "anonymous" ? { submitAction: bucket.submitAction } : {}
|
|
2929
|
+
}));
|
|
2930
|
+
}
|
|
2931
|
+
isSubmitElement(name, opening) {
|
|
2932
|
+
const type = getStringAttr(opening, "type");
|
|
2933
|
+
if (name === "button") return type === "submit" || type === void 0;
|
|
2934
|
+
if (name === "input") return type === "submit";
|
|
2935
|
+
return false;
|
|
2936
|
+
}
|
|
2937
|
+
extractField(element, role, labelsByHtmlFor) {
|
|
2938
|
+
const opening = element.openingElement;
|
|
2939
|
+
const componentName = getJsxElementName(opening) ?? void 0;
|
|
2940
|
+
const field = {
|
|
2941
|
+
name: "",
|
|
2942
|
+
type: "text",
|
|
2943
|
+
required: false,
|
|
2944
|
+
sourceComponent: componentName
|
|
2945
|
+
};
|
|
2946
|
+
const appilotsId = getStringAttr(opening, "appilotsId");
|
|
2947
|
+
const dataTestId = getStringAttr(opening, "data-testid");
|
|
2948
|
+
const domId = getStringAttr(opening, "id");
|
|
2949
|
+
const nameAttr = getStringAttr(opening, "name");
|
|
2950
|
+
const ariaLabel = getStringAttr(opening, "aria-label");
|
|
2951
|
+
const placeholder = getStringAttr(opening, "placeholder");
|
|
2952
|
+
if (placeholder) field.placeholder = placeholder;
|
|
2953
|
+
if (ariaLabel) field.label = ariaLabel;
|
|
2954
|
+
if (domId && labelsByHtmlFor.has(domId)) field.label = labelsByHtmlFor.get(domId);
|
|
2955
|
+
if (appilotsId) {
|
|
2956
|
+
field.name = appilotsId;
|
|
2957
|
+
field.locator = mergeLocator(field.locator, {
|
|
2958
|
+
id: appilotsId,
|
|
2959
|
+
appilotsId,
|
|
2960
|
+
source: "appilotsId"
|
|
2961
|
+
});
|
|
2962
|
+
}
|
|
2963
|
+
if (dataTestId) {
|
|
2964
|
+
if (!field.name) field.name = dataTestId.replace(/^(input-|field-|txt-)/, "");
|
|
2965
|
+
field.locator = mergeLocator(field.locator, {
|
|
2966
|
+
...field.locator?.id ? {} : { id: dataTestId },
|
|
2967
|
+
testID: dataTestId,
|
|
2968
|
+
source: field.locator?.source ?? "data-testid"
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
if (nameAttr && !field.name) field.name = nameAttr;
|
|
2972
|
+
if (domId) {
|
|
2973
|
+
if (!field.name) field.name = domId;
|
|
2974
|
+
field.locator = mergeLocator(field.locator, {
|
|
2975
|
+
...field.locator?.id ? {} : { id: domId },
|
|
2976
|
+
source: field.locator?.source ?? "id"
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2979
|
+
if (ariaLabel) {
|
|
2980
|
+
field.locator = mergeLocator(field.locator, {
|
|
2981
|
+
...field.locator?.id ? {} : { id: slugify(ariaLabel) },
|
|
2982
|
+
accessibilityLabel: ariaLabel,
|
|
2983
|
+
source: field.locator?.source ?? "aria-label"
|
|
2984
|
+
});
|
|
2985
|
+
}
|
|
2986
|
+
for (const attr of opening.attributes) {
|
|
2987
|
+
if (!BabelTypes__namespace.isJSXAttribute(attr) || !BabelTypes__namespace.isJSXIdentifier(attr.name)) continue;
|
|
2988
|
+
const attrName = attr.name.name;
|
|
2989
|
+
if (attrName === "required") {
|
|
2990
|
+
if (attr.value === null) field.required = true;
|
|
2991
|
+
else if (BabelTypes__namespace.isJSXExpressionContainer(attr.value) && BabelTypes__namespace.isBooleanLiteral(attr.value.expression)) {
|
|
2992
|
+
field.required = attr.value.expression.value;
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
if ((attrName === "value" || attrName === "checked") && attr.value && BabelTypes__namespace.isJSXExpressionContainer(attr.value) && BabelTypes__namespace.isIdentifier(attr.value.expression)) {
|
|
2996
|
+
field.valueBinding = attr.value.expression.name;
|
|
2997
|
+
if (!field.name) field.name = attr.value.expression.name;
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
if (getStringAttr(opening, "aria-required") === "true") field.required = true;
|
|
3001
|
+
if (role === "select") field.type = "select";
|
|
3002
|
+
else if (role === "toggle") field.type = "toggle";
|
|
3003
|
+
else if (role === "date") field.type = "date";
|
|
3004
|
+
else {
|
|
3005
|
+
field.type = this.inferInputType(opening, field);
|
|
3006
|
+
}
|
|
3007
|
+
if (componentName === "select") {
|
|
3008
|
+
const options = this.extractSelectOptions(element);
|
|
3009
|
+
if (options.length > 0) field.options = options;
|
|
3010
|
+
}
|
|
3011
|
+
if (!field.name || isWeakInferredFieldName(field.name)) {
|
|
3012
|
+
const labelish = field.label ?? field.placeholder;
|
|
3013
|
+
if (labelish) field.name = slugify(labelish);
|
|
3014
|
+
}
|
|
3015
|
+
if (!field.locator && field.name) {
|
|
3016
|
+
field.locator = { id: field.name, label: field.label, source: "inferred" };
|
|
3017
|
+
} else if (field.locator && !field.locator.id && field.name) {
|
|
3018
|
+
field.locator = mergeLocator(field.locator, {
|
|
3019
|
+
id: field.name,
|
|
3020
|
+
label: field.label,
|
|
3021
|
+
source: field.locator.source ?? "inferred"
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
return field;
|
|
3025
|
+
}
|
|
3026
|
+
inferInputType(opening, field) {
|
|
3027
|
+
const type = getStringAttr(opening, "type");
|
|
3028
|
+
if (type === "email") return "email";
|
|
3029
|
+
if (type === "tel") return "phone";
|
|
3030
|
+
if (type === "number") return "number";
|
|
3031
|
+
if (type === "date" || type === "datetime-local" || type === "month" || type === "week") return "date";
|
|
3032
|
+
const inputMode = getStringAttr(opening, "inputMode") ?? getStringAttr(opening, "inputmode");
|
|
3033
|
+
if (inputMode === "email") return "email";
|
|
3034
|
+
if (inputMode === "tel") return "phone";
|
|
3035
|
+
if (inputMode === "numeric" || inputMode === "decimal") return "number";
|
|
3036
|
+
const combined = `${field.name} ${field.label ?? ""} ${field.placeholder ?? ""}`.toLowerCase();
|
|
3037
|
+
if (combined.includes("email")) return "email";
|
|
3038
|
+
if (combined.includes("phone") || combined.includes("tel")) return "phone";
|
|
3039
|
+
return "text";
|
|
3040
|
+
}
|
|
3041
|
+
extractSelectOptions(selectElement) {
|
|
3042
|
+
const options = [];
|
|
3043
|
+
for (const child of selectElement.children) {
|
|
3044
|
+
if (!BabelTypes__namespace.isJSXElement(child)) continue;
|
|
3045
|
+
if (getJsxElementName(child.openingElement) !== "option") continue;
|
|
3046
|
+
const value = getStringAttr(child.openingElement, "value");
|
|
3047
|
+
const label = jsxTextContent(child) || value || "";
|
|
3048
|
+
if (label && value) options.push({ label, value });
|
|
3049
|
+
}
|
|
3050
|
+
return options;
|
|
3051
|
+
}
|
|
3052
|
+
mergeForms(primary, secondary) {
|
|
3053
|
+
const out = primary.map((form) => ({ ...form, fields: [...form.fields] }));
|
|
3054
|
+
for (const form of secondary) {
|
|
3055
|
+
const existing = out.find((candidate) => candidate.id === form.id);
|
|
3056
|
+
if (!existing) {
|
|
3057
|
+
out.push({ ...form, fields: [...form.fields] });
|
|
3058
|
+
continue;
|
|
3059
|
+
}
|
|
3060
|
+
for (const field of form.fields) {
|
|
3061
|
+
const existingField = existing.fields.find((candidate) => candidate.name === field.name);
|
|
3062
|
+
if (!existingField) {
|
|
3063
|
+
existing.fields.push(field);
|
|
3064
|
+
continue;
|
|
3065
|
+
}
|
|
3066
|
+
existingField.label = existingField.label ?? field.label;
|
|
3067
|
+
existingField.placeholder = existingField.placeholder ?? field.placeholder;
|
|
3068
|
+
existingField.options = existingField.options ?? field.options;
|
|
3069
|
+
existingField.locator = existingField.locator ?? field.locator;
|
|
3070
|
+
existingField.sourceComponent = existingField.sourceComponent ?? field.sourceComponent;
|
|
3071
|
+
existingField.valueBinding = existingField.valueBinding ?? field.valueBinding;
|
|
3072
|
+
existingField.required = existingField.required || field.required;
|
|
3073
|
+
if (existingField.type === "text" && field.type !== "text") existingField.type = field.type;
|
|
3074
|
+
}
|
|
3075
|
+
existing.submitAction = existing.submitAction ?? form.submitAction;
|
|
3076
|
+
}
|
|
3077
|
+
return out;
|
|
3078
|
+
}
|
|
3079
|
+
// ── Actions ───────────────────────────────────────────────────────
|
|
3080
|
+
extractActions(ast, registerScreenMeta, handlerBehaviors) {
|
|
3081
|
+
const actions = [...registerScreenMeta?.actions ?? []];
|
|
3082
|
+
const actionIds = new Set(actions.map((a) => a.id));
|
|
3083
|
+
const actionLabels = new Map(
|
|
3084
|
+
actions.filter((a) => a.label).map((a) => [normalizeLabel(a.label), a])
|
|
3085
|
+
);
|
|
3086
|
+
traverse4__default.default(ast, {
|
|
3087
|
+
JSXElement: (nodePath) => {
|
|
3088
|
+
const element = nodePath.node;
|
|
3089
|
+
const name = getJsxElementName(element.openingElement);
|
|
3090
|
+
if (!name) return;
|
|
3091
|
+
if (classifyWebJsxComponent(name, element.openingElement) !== "button") return;
|
|
3092
|
+
const action = this.extractActionFromElement(element, nodePath);
|
|
3093
|
+
if (!action) return;
|
|
3094
|
+
const existingByLabel = action.label ? actionLabels.get(normalizeLabel(action.label)) : void 0;
|
|
3095
|
+
if (existingByLabel) {
|
|
3096
|
+
this.mergeActionMetadata(existingByLabel, action);
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
if (action.id && !actionIds.has(action.id)) {
|
|
3100
|
+
actions.push(action);
|
|
3101
|
+
actionIds.add(action.id);
|
|
3102
|
+
if (action.label) actionLabels.set(normalizeLabel(action.label), action);
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
});
|
|
3106
|
+
this.enrichActionsFromHandlers(actions, handlerBehaviors);
|
|
3107
|
+
return actions;
|
|
3108
|
+
}
|
|
3109
|
+
extractActionFromElement(element, nodePath) {
|
|
3110
|
+
const opening = element.openingElement;
|
|
3111
|
+
const componentName = getJsxElementName(opening) ?? void 0;
|
|
3112
|
+
const action = { id: "", type: "custom", sourceComponent: componentName };
|
|
3113
|
+
const ariaLabel = getStringAttr(opening, "aria-label");
|
|
3114
|
+
const label = ariaLabel ?? jsxTextContent(element) ?? getStringAttr(opening, "value") ?? getStringAttr(opening, "title");
|
|
3115
|
+
if (label) action.label = label;
|
|
3116
|
+
if (ariaLabel) {
|
|
3117
|
+
action.locator = mergeLocator(action.locator, {
|
|
3118
|
+
accessibilityLabel: ariaLabel,
|
|
3119
|
+
source: "aria-label"
|
|
3120
|
+
});
|
|
3121
|
+
}
|
|
3122
|
+
const appilotsId = getStringAttr(opening, "appilotsId");
|
|
3123
|
+
const dataTestId = getStringAttr(opening, "data-testid");
|
|
3124
|
+
const domId = getStringAttr(opening, "id");
|
|
3125
|
+
if (appilotsId) {
|
|
3126
|
+
action.id = appilotsId;
|
|
3127
|
+
action.locator = mergeLocator(action.locator, {
|
|
3128
|
+
id: appilotsId,
|
|
3129
|
+
appilotsId,
|
|
3130
|
+
source: "appilotsId"
|
|
3131
|
+
});
|
|
3132
|
+
} else if (dataTestId) {
|
|
3133
|
+
action.id = dataTestId;
|
|
3134
|
+
action.locator = mergeLocator(action.locator, {
|
|
3135
|
+
id: dataTestId,
|
|
3136
|
+
testID: dataTestId,
|
|
3137
|
+
source: "data-testid"
|
|
3138
|
+
});
|
|
3139
|
+
} else if (domId) {
|
|
3140
|
+
action.id = domId;
|
|
3141
|
+
action.locator = mergeLocator(action.locator, { id: domId, source: "id" });
|
|
3142
|
+
} else if (action.label) {
|
|
3143
|
+
action.id = slugify(action.label);
|
|
3144
|
+
}
|
|
3145
|
+
if (!action.id) return null;
|
|
3146
|
+
const to = routePathAttr(opening, "to") ?? routePathAttr(opening, "href");
|
|
3147
|
+
if (to && to.startsWith("/")) {
|
|
3148
|
+
action.type = "navigation";
|
|
3149
|
+
action.targetScreen = to;
|
|
3150
|
+
} else if (to && !to.startsWith("/") && !hasJsxAttribute(opening, "onClick")) {
|
|
3151
|
+
return null;
|
|
3152
|
+
}
|
|
3153
|
+
let handlerName;
|
|
3154
|
+
const onClickAttr = opening.attributes.find(
|
|
3155
|
+
(attr) => BabelTypes__namespace.isJSXAttribute(attr) && BabelTypes__namespace.isJSXIdentifier(attr.name) && attr.name.name === "onClick"
|
|
3156
|
+
);
|
|
3157
|
+
if (onClickAttr?.value && BabelTypes__namespace.isJSXExpressionContainer(onClickAttr.value)) {
|
|
3158
|
+
const expr = onClickAttr.value.expression;
|
|
3159
|
+
if (BabelTypes__namespace.isIdentifier(expr)) {
|
|
3160
|
+
handlerName = expr.name;
|
|
3161
|
+
action.handler = expr.name;
|
|
3162
|
+
} else if (!BabelTypes__namespace.isJSXEmptyExpression(expr)) {
|
|
3163
|
+
const inlineNavCalls = extractWebNavigationCalls(expr);
|
|
3164
|
+
const inlineNav = inlineNavCalls.find((call) => call.targetPath);
|
|
3165
|
+
if (inlineNav?.targetPath) {
|
|
3166
|
+
action.type = "navigation";
|
|
3167
|
+
action.targetScreen = inlineNav.targetPath;
|
|
3168
|
+
} else if (inlineNavCalls.some((call) => call.method === "goBack")) {
|
|
3169
|
+
action.type = "navigation";
|
|
3170
|
+
action.successSignal = {
|
|
3171
|
+
type: "goBack",
|
|
3172
|
+
description: "Action returns to the previous page"
|
|
3173
|
+
};
|
|
3174
|
+
action.appilotsInferred = {
|
|
3175
|
+
...action.appilotsInferred ?? {},
|
|
3176
|
+
expectedOutcome: "navigation"
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
const inlineHandler = firstCalledFunctionName(expr);
|
|
3180
|
+
if (inlineHandler) {
|
|
3181
|
+
handlerName = inlineHandler;
|
|
3182
|
+
action.handler = inlineHandler;
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
if (handlerName) {
|
|
3187
|
+
const lower = handlerName.toLowerCase();
|
|
3188
|
+
if (lower.includes("submit")) action.type = "submit";
|
|
3189
|
+
else if (lower.includes("navigate") && action.type === "custom") action.type = "navigation";
|
|
3190
|
+
}
|
|
3191
|
+
if (componentName && this.isSubmitElement(componentName, opening)) {
|
|
3192
|
+
const formParent = nodePath.findParent(
|
|
3193
|
+
(p) => p.isJSXElement() && getJsxElementName(p.node.openingElement) === "form"
|
|
3194
|
+
);
|
|
3195
|
+
if (formParent) {
|
|
3196
|
+
action.type = "submit";
|
|
3197
|
+
if (!action.handler) {
|
|
3198
|
+
const formHandler = this.handlerNameFromAttr(formParent.node.openingElement, "onSubmit");
|
|
3199
|
+
if (formHandler && formHandler !== "anonymous") {
|
|
3200
|
+
action.handler = formHandler;
|
|
3201
|
+
handlerName = formHandler;
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
if (hasJsxAttribute(opening, "destructive") || hasJsxAttribute(opening, "aria-destructive")) {
|
|
3207
|
+
const value = getStringAttr(opening, "destructive") ?? getStringAttr(opening, "aria-destructive");
|
|
3208
|
+
action.destructive = value !== "false";
|
|
3209
|
+
} else if (handlerName && DESTRUCTIVE_VERB2.test(handlerName) || DESTRUCTIVE_VERB2.test(action.id)) {
|
|
3210
|
+
action.destructive = true;
|
|
3211
|
+
}
|
|
3212
|
+
if (!action.locator && action.id) {
|
|
3213
|
+
action.locator = {
|
|
3214
|
+
id: action.id,
|
|
3215
|
+
label: action.label,
|
|
3216
|
+
source: action.label ? "label" : "inferred"
|
|
3217
|
+
};
|
|
3218
|
+
}
|
|
3219
|
+
return action;
|
|
3220
|
+
}
|
|
3221
|
+
mergeActionMetadata(target, source) {
|
|
3222
|
+
target.handler = target.handler ?? source.handler;
|
|
3223
|
+
target.targetScreen = target.targetScreen ?? source.targetScreen;
|
|
3224
|
+
target.description = target.description ?? source.description;
|
|
3225
|
+
target.locator = target.locator ?? source.locator;
|
|
3226
|
+
target.nativeConfirmationExpected = target.nativeConfirmationExpected || source.nativeConfirmationExpected || void 0;
|
|
3227
|
+
target.requiresConfirmation = target.requiresConfirmation || source.requiresConfirmation || void 0;
|
|
3228
|
+
target.destructive = target.destructive || source.destructive || void 0;
|
|
3229
|
+
target.effect = target.effect ?? source.effect;
|
|
3230
|
+
target.riskLevel = target.riskLevel ?? source.riskLevel;
|
|
3231
|
+
target.appilotsInferred = target.appilotsInferred ?? source.appilotsInferred;
|
|
3232
|
+
}
|
|
3233
|
+
enrichActionsFromHandlers(actions, behaviors) {
|
|
3234
|
+
for (const action of actions) {
|
|
3235
|
+
const behavior = action.handler ? behaviors.get(action.handler) : void 0;
|
|
3236
|
+
if (!behavior) continue;
|
|
3237
|
+
action.appilotsInferred = {
|
|
3238
|
+
...action.appilotsInferred ?? {},
|
|
3239
|
+
...behavior.base.appilotsInferred
|
|
3240
|
+
};
|
|
3241
|
+
if (behavior.nativeConfirmationExpected || behavior.base.nativeConfirmationExpected) {
|
|
3242
|
+
action.nativeConfirmationExpected = true;
|
|
3243
|
+
}
|
|
3244
|
+
if (behavior.targetPath && !action.targetScreen) {
|
|
3245
|
+
action.targetScreen = behavior.targetPath;
|
|
3246
|
+
if (action.type === "custom") action.type = "navigation";
|
|
3247
|
+
action.appilotsInferred = {
|
|
3248
|
+
...action.appilotsInferred ?? {},
|
|
3249
|
+
expectedOutcome: "navigation"
|
|
3250
|
+
};
|
|
3251
|
+
}
|
|
3252
|
+
if (behavior.base.successSignal && !action.successSignal) {
|
|
3253
|
+
action.successSignal = behavior.base.successSignal;
|
|
3254
|
+
}
|
|
3255
|
+
if (behavior.base.failureSignal && !action.failureSignal) {
|
|
3256
|
+
action.failureSignal = behavior.base.failureSignal;
|
|
3257
|
+
}
|
|
3258
|
+
if (behavior.base.opensModal && !action.opensModal) {
|
|
3259
|
+
action.opensModal = behavior.base.opensModal;
|
|
3260
|
+
}
|
|
3261
|
+
if (behavior.base.destructive || action.destructive === true || action.requiresConfirmation === true || action.effect === "destructive" || action.riskLevel === "high") {
|
|
3262
|
+
action.destructive = true;
|
|
3263
|
+
action.effect = action.effect ?? "destructive";
|
|
3264
|
+
action.riskLevel = action.riskLevel ?? "high";
|
|
3265
|
+
action.requiresConfirmation = action.requiresConfirmation ?? true;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
/**
|
|
3270
|
+
* Handler behavior via the shared, platform-neutral analyzer
|
|
3271
|
+
* (async/await, `.then`, state setters, toasts, destructive verbs)
|
|
3272
|
+
* plus the web-only signals: React Router navigation targets and
|
|
3273
|
+
* `window.confirm(...)` as the native confirmation dialog.
|
|
3274
|
+
*/
|
|
3275
|
+
collectWebHandlerBehaviors(ast) {
|
|
3276
|
+
const handlers = collectFunctions(ast);
|
|
3277
|
+
const out = /* @__PURE__ */ new Map();
|
|
3278
|
+
for (const [name, fn] of handlers) {
|
|
3279
|
+
const base = analyzeFunctionBehavior(name, fn, handlers);
|
|
3280
|
+
const navCalls = fn.body ? extractWebNavigationCalls(fn.body) : [];
|
|
3281
|
+
const firstNav = navCalls.find((call) => call.targetPath);
|
|
3282
|
+
const goesBack = navCalls.some((call) => call.method === "goBack");
|
|
3283
|
+
out.set(name, {
|
|
3284
|
+
base: {
|
|
3285
|
+
...base,
|
|
3286
|
+
...goesBack && !base.successSignal ? {
|
|
3287
|
+
successSignal: {
|
|
3288
|
+
type: "goBack",
|
|
3289
|
+
description: "Action returns to the previous page"
|
|
3290
|
+
}
|
|
3291
|
+
} : {}
|
|
3292
|
+
},
|
|
3293
|
+
targetPath: firstNav?.targetPath,
|
|
3294
|
+
nativeConfirmationExpected: fn.body ? containsWindowConfirm(fn.body) : false
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3297
|
+
return out;
|
|
3298
|
+
}
|
|
3299
|
+
handlerNameFromAttr(opening, attrName) {
|
|
3300
|
+
const attr = opening.attributes.find(
|
|
3301
|
+
(candidate) => BabelTypes__namespace.isJSXAttribute(candidate) && BabelTypes__namespace.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
|
|
3302
|
+
);
|
|
3303
|
+
if (!attr?.value || !BabelTypes__namespace.isJSXExpressionContainer(attr.value)) return void 0;
|
|
3304
|
+
const expr = attr.value.expression;
|
|
3305
|
+
if (BabelTypes__namespace.isIdentifier(expr)) return expr.name;
|
|
3306
|
+
if (BabelTypes__namespace.isArrowFunctionExpression(expr) || BabelTypes__namespace.isFunctionExpression(expr)) {
|
|
3307
|
+
return firstCalledFunctionName(expr) ?? "anonymous";
|
|
3308
|
+
}
|
|
3309
|
+
return void 0;
|
|
3310
|
+
}
|
|
3311
|
+
// ── Navigation targets (route paths — resolved by the orchestrator) ─
|
|
3312
|
+
extractNavigationTargets(ast) {
|
|
3313
|
+
const targets = /* @__PURE__ */ new Set();
|
|
3314
|
+
for (const call of extractWebNavigationCalls(ast)) {
|
|
3315
|
+
if (call.targetPath && call.targetPath.startsWith("/")) targets.add(call.targetPath);
|
|
3316
|
+
}
|
|
3317
|
+
traverse4__default.default(ast, {
|
|
3318
|
+
JSXOpeningElement: (nodePath) => {
|
|
3319
|
+
const element = nodePath.node;
|
|
3320
|
+
const name = getJsxElementName(element);
|
|
3321
|
+
if (name === "Link" || name === "NavLink" || name === "Navigate") {
|
|
3322
|
+
const to = routePathAttr(element, "to");
|
|
3323
|
+
if (to && to.startsWith("/")) targets.add(to);
|
|
3324
|
+
}
|
|
3325
|
+
if (name === "a") {
|
|
3326
|
+
const href = routePathAttr(element, "href");
|
|
3327
|
+
if (href && href.startsWith("/")) targets.add(href);
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
});
|
|
3331
|
+
return Array.from(targets).sort();
|
|
3332
|
+
}
|
|
3333
|
+
// ── Collections ───────────────────────────────────────────────────
|
|
3334
|
+
extractCollections(ast) {
|
|
3335
|
+
const collections = [];
|
|
3336
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3337
|
+
traverse4__default.default(ast, {
|
|
3338
|
+
JSXExpressionContainer: (nodePath) => {
|
|
3339
|
+
const expr = nodePath.node.expression;
|
|
3340
|
+
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") {
|
|
3341
|
+
return;
|
|
3342
|
+
}
|
|
3343
|
+
const callback = expr.arguments[0];
|
|
3344
|
+
if (!BabelTypes__namespace.isArrowFunctionExpression(callback) && !BabelTypes__namespace.isFunctionExpression(callback)) return;
|
|
3345
|
+
const enclosing = nodePath.findParent((p) => p.isJSXElement());
|
|
3346
|
+
const enclosingName = enclosing ? getJsxElementName(enclosing.node.openingElement) : null;
|
|
3347
|
+
const enclosingRole = enclosing && enclosingName ? classifyWebJsxComponent(enclosingName, enclosing.node.openingElement) : null;
|
|
3348
|
+
const returnsListItem = callbackReturnsTag(callback, /* @__PURE__ */ new Set(["li", "tr"]));
|
|
3349
|
+
if (enclosingName === "select") return;
|
|
3350
|
+
const isListContext = enclosingName !== null && LISTISH_TAGS.has(enclosingName) || enclosingRole === "list" || returnsListItem;
|
|
3351
|
+
if (!isListContext) return;
|
|
3352
|
+
const dataSource = expr.callee.object.name;
|
|
3353
|
+
if (seen.has(dataSource)) return;
|
|
3354
|
+
seen.add(dataSource);
|
|
3355
|
+
const itemNames = collectionItemNames(callback);
|
|
3356
|
+
const displayFields = collectionDisplayFields(callback, itemNames);
|
|
3357
|
+
const keyField = collectionKeyField(callback, itemNames);
|
|
3358
|
+
const rowAction = collectionRowAction(callback);
|
|
3359
|
+
const itemType = inferItemType(dataSource, displayFields);
|
|
3360
|
+
const identityFields = inferIdentityFields(keyField, displayFields);
|
|
3361
|
+
collections.push({
|
|
3362
|
+
id: dataSource,
|
|
3363
|
+
component: enclosingName ?? "list",
|
|
3364
|
+
...itemType ? { itemType } : {},
|
|
3365
|
+
dataSource,
|
|
3366
|
+
...keyField ? { keyField } : {},
|
|
3367
|
+
...displayFields.length > 0 ? { displayFields } : {},
|
|
3368
|
+
...rowAction ? { rowAction, rowActions: [rowAction] } : {},
|
|
3369
|
+
...identityFields.length > 0 ? { identityFields } : {}
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
});
|
|
3373
|
+
return collections;
|
|
3374
|
+
}
|
|
3375
|
+
};
|
|
3376
|
+
function isRegisterScreenCallee(callee) {
|
|
3377
|
+
return BabelTypes__namespace.isIdentifier(callee) && callee.name === "registerScreen" || BabelTypes__namespace.isMemberExpression(callee) && BabelTypes__namespace.isIdentifier(callee.property) && callee.property.name === "registerScreen";
|
|
3378
|
+
}
|
|
3379
|
+
function literalToPlain(node) {
|
|
3380
|
+
if (BabelTypes__namespace.isStringLiteral(node) || BabelTypes__namespace.isNumericLiteral(node) || BabelTypes__namespace.isBooleanLiteral(node)) {
|
|
3381
|
+
return node.value;
|
|
3382
|
+
}
|
|
3383
|
+
if (BabelTypes__namespace.isNullLiteral(node)) return null;
|
|
3384
|
+
if (BabelTypes__namespace.isArrayExpression(node)) {
|
|
3385
|
+
return node.elements.filter((el) => el !== null && BabelTypes__namespace.isExpression(el)).map((el) => literalToPlain(el)).filter((value) => value !== void 0);
|
|
3386
|
+
}
|
|
3387
|
+
if (BabelTypes__namespace.isObjectExpression(node)) {
|
|
3388
|
+
const out = {};
|
|
3389
|
+
for (const prop of node.properties) {
|
|
3390
|
+
if (!BabelTypes__namespace.isObjectProperty(prop)) continue;
|
|
3391
|
+
const key = BabelTypes__namespace.isIdentifier(prop.key) ? prop.key.name : BabelTypes__namespace.isStringLiteral(prop.key) ? prop.key.value : void 0;
|
|
3392
|
+
if (!key || !BabelTypes__namespace.isExpression(prop.value)) continue;
|
|
3393
|
+
const value = literalToPlain(prop.value);
|
|
3394
|
+
if (value !== void 0) out[key] = value;
|
|
3395
|
+
}
|
|
3396
|
+
return out;
|
|
3397
|
+
}
|
|
3398
|
+
return void 0;
|
|
3399
|
+
}
|
|
3400
|
+
function jsxTextContent(element) {
|
|
3401
|
+
const parts = [];
|
|
3402
|
+
const walk = (children) => {
|
|
3403
|
+
for (const child of children) {
|
|
3404
|
+
if (BabelTypes__namespace.isJSXText(child)) {
|
|
3405
|
+
const trimmed = child.value.replace(/\s+/g, " ").trim();
|
|
3406
|
+
if (trimmed) parts.push(trimmed);
|
|
3407
|
+
} else if (BabelTypes__namespace.isJSXExpressionContainer(child) && BabelTypes__namespace.isStringLiteral(child.expression)) {
|
|
3408
|
+
parts.push(child.expression.value);
|
|
3409
|
+
} else if (BabelTypes__namespace.isJSXElement(child)) {
|
|
3410
|
+
walk(child.children);
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
};
|
|
3414
|
+
walk(element.children);
|
|
3415
|
+
const text = parts.join(" ").trim();
|
|
3416
|
+
return text.length > 0 ? text : void 0;
|
|
3417
|
+
}
|
|
3418
|
+
function firstCalledFunctionName(node) {
|
|
3419
|
+
if (BabelTypes__namespace.isIdentifier(node)) return node.name;
|
|
3420
|
+
if (BabelTypes__namespace.isArrowFunctionExpression(node) || BabelTypes__namespace.isFunctionExpression(node)) {
|
|
3421
|
+
return firstCalledFunctionName(node.body);
|
|
3422
|
+
}
|
|
3423
|
+
if (BabelTypes__namespace.isBlockStatement(node)) {
|
|
3424
|
+
for (const statement of node.body) {
|
|
3425
|
+
const handler = firstCalledFunctionName(statement);
|
|
3426
|
+
if (handler) return handler;
|
|
3427
|
+
}
|
|
3428
|
+
return void 0;
|
|
3429
|
+
}
|
|
3430
|
+
if (BabelTypes__namespace.isExpressionStatement(node)) return firstCalledFunctionName(node.expression);
|
|
3431
|
+
if (BabelTypes__namespace.isReturnStatement(node)) {
|
|
3432
|
+
return node.argument ? firstCalledFunctionName(node.argument) : void 0;
|
|
3433
|
+
}
|
|
3434
|
+
if (BabelTypes__namespace.isAwaitExpression(node) || BabelTypes__namespace.isUnaryExpression(node)) {
|
|
3435
|
+
return firstCalledFunctionName(node.argument);
|
|
3436
|
+
}
|
|
3437
|
+
if (BabelTypes__namespace.isCallExpression(node)) {
|
|
3438
|
+
if (BabelTypes__namespace.isIdentifier(node.callee) && !/^(navigate|confirm|alert)$/.test(node.callee.name)) {
|
|
3439
|
+
return node.callee.name;
|
|
3440
|
+
}
|
|
3441
|
+
return void 0;
|
|
3442
|
+
}
|
|
3443
|
+
return void 0;
|
|
3444
|
+
}
|
|
3445
|
+
function containsWindowConfirm(body) {
|
|
3446
|
+
let found = false;
|
|
3447
|
+
traverse4__default.default(body, {
|
|
3448
|
+
noScope: true,
|
|
3449
|
+
CallExpression: (nodePath) => {
|
|
3450
|
+
const callee = nodePath.node.callee;
|
|
3451
|
+
if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "confirm") found = true;
|
|
3452
|
+
if (BabelTypes__namespace.isMemberExpression(callee) && BabelTypes__namespace.isIdentifier(callee.object) && callee.object.name === "window" && BabelTypes__namespace.isIdentifier(callee.property) && callee.property.name === "confirm") {
|
|
3453
|
+
found = true;
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
});
|
|
3457
|
+
return found;
|
|
3458
|
+
}
|
|
3459
|
+
function mergeLocator(current, next) {
|
|
3460
|
+
return { ...current ?? {}, ...next };
|
|
3461
|
+
}
|
|
3462
|
+
function routePathAttr(opening, attrName) {
|
|
3463
|
+
const literal = getStringAttr(opening, attrName);
|
|
3464
|
+
if (literal) return literal;
|
|
3465
|
+
const attr = opening.attributes.find(
|
|
3466
|
+
(candidate) => BabelTypes__namespace.isJSXAttribute(candidate) && BabelTypes__namespace.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
|
|
3467
|
+
);
|
|
3468
|
+
if (!attr?.value || !BabelTypes__namespace.isJSXExpressionContainer(attr.value)) return void 0;
|
|
3469
|
+
return staticRoutePath(attr.value.expression);
|
|
3470
|
+
}
|
|
3471
|
+
function slugify(label) {
|
|
3472
|
+
return label.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3473
|
+
}
|
|
3474
|
+
function normalizeLabel(label) {
|
|
3475
|
+
return label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3476
|
+
}
|
|
3477
|
+
function isWeakInferredFieldName(name) {
|
|
3478
|
+
return /^(text|value|input|query|search|selected|checked)$/i.test(name);
|
|
3479
|
+
}
|
|
3480
|
+
function collectionItemNames(callback) {
|
|
3481
|
+
const names = /* @__PURE__ */ new Set(["item"]);
|
|
3482
|
+
const firstParam = callback.params[0];
|
|
3483
|
+
if (BabelTypes__namespace.isIdentifier(firstParam)) names.add(firstParam.name);
|
|
3484
|
+
if (BabelTypes__namespace.isObjectPattern(firstParam)) {
|
|
3485
|
+
for (const prop of firstParam.properties) {
|
|
3486
|
+
if (BabelTypes__namespace.isObjectProperty(prop) && BabelTypes__namespace.isIdentifier(prop.key) && BabelTypes__namespace.isIdentifier(prop.value)) {
|
|
3487
|
+
names.add(prop.value.name);
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
return names;
|
|
3492
|
+
}
|
|
3493
|
+
function collectionDisplayFields(callback, itemNames) {
|
|
3494
|
+
const fields = /* @__PURE__ */ new Set();
|
|
3495
|
+
if (!callback.body) return [];
|
|
3496
|
+
traverse4__default.default(callback.body, {
|
|
3497
|
+
noScope: true,
|
|
3498
|
+
MemberExpression: (nodePath) => {
|
|
3499
|
+
const node = nodePath.node;
|
|
3500
|
+
if (BabelTypes__namespace.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes__namespace.isIdentifier(node.property)) {
|
|
3501
|
+
fields.add(node.property.name);
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
});
|
|
3505
|
+
return Array.from(fields).sort();
|
|
3506
|
+
}
|
|
3507
|
+
function collectionKeyField(callback, itemNames) {
|
|
3508
|
+
let keyField;
|
|
3509
|
+
if (!callback.body) return void 0;
|
|
3510
|
+
traverse4__default.default(callback.body, {
|
|
3511
|
+
noScope: true,
|
|
3512
|
+
JSXAttribute: (nodePath) => {
|
|
3513
|
+
const attr = nodePath.node;
|
|
3514
|
+
if (!BabelTypes__namespace.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
|
|
3515
|
+
if (!attr.value || !BabelTypes__namespace.isJSXExpressionContainer(attr.value)) return;
|
|
3516
|
+
const expr = attr.value.expression;
|
|
3517
|
+
if (BabelTypes__namespace.isMemberExpression(expr) && BabelTypes__namespace.isIdentifier(expr.object) && itemNames.has(expr.object.name) && BabelTypes__namespace.isIdentifier(expr.property)) {
|
|
3518
|
+
keyField = keyField ?? expr.property.name;
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
});
|
|
3522
|
+
return keyField;
|
|
3523
|
+
}
|
|
3524
|
+
function collectionRowAction(callback) {
|
|
3525
|
+
if (!callback.body) return void 0;
|
|
3526
|
+
const navCall = extractWebNavigationCalls(callback.body).find((call) => call.targetPath);
|
|
3527
|
+
if (!navCall?.targetPath) return void 0;
|
|
3528
|
+
return {
|
|
3529
|
+
type: "navigation",
|
|
3530
|
+
// Route path — the orchestrator resolves it to a screen name.
|
|
3531
|
+
targetScreen: navCall.targetPath,
|
|
3532
|
+
description: `Clicking a row opens ${navCall.targetPath}`
|
|
3533
|
+
};
|
|
3534
|
+
}
|
|
3535
|
+
function callbackReturnsTag(callback, tags) {
|
|
3536
|
+
let found = false;
|
|
3537
|
+
const inspect = (node) => {
|
|
3538
|
+
if (!node || found) return;
|
|
3539
|
+
if (BabelTypes__namespace.isJSXElement(node)) {
|
|
3540
|
+
const name = getJsxElementName(node.openingElement);
|
|
3541
|
+
if (name && tags.has(name)) found = true;
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3544
|
+
if (BabelTypes__namespace.isBlockStatement(node)) {
|
|
3545
|
+
for (const statement of node.body) {
|
|
3546
|
+
if (BabelTypes__namespace.isReturnStatement(statement)) inspect(statement.argument);
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
if (BabelTypes__namespace.isParenthesizedExpression(node)) inspect(node.expression);
|
|
3550
|
+
if (BabelTypes__namespace.isConditionalExpression(node)) {
|
|
3551
|
+
inspect(node.consequent);
|
|
3552
|
+
inspect(node.alternate);
|
|
3553
|
+
}
|
|
3554
|
+
};
|
|
3555
|
+
inspect(callback.body);
|
|
3556
|
+
return found;
|
|
3557
|
+
}
|
|
3558
|
+
function inferItemType(dataSource, displayFields) {
|
|
3559
|
+
const singular = dataSource.replace(/^render/i, "").replace(/(List|Items|Data|Rows|Sections)$/i, "").replace(/s$/i, "");
|
|
3560
|
+
const candidate = singular.charAt(0).toUpperCase() + singular.slice(1);
|
|
3561
|
+
if (candidate.length > 1) return candidate;
|
|
3562
|
+
if (displayFields.length > 0) return "Item";
|
|
3563
|
+
return void 0;
|
|
3564
|
+
}
|
|
3565
|
+
function inferIdentityFields(keyField, displayFields) {
|
|
3566
|
+
const out = /* @__PURE__ */ new Set();
|
|
3567
|
+
if (keyField) out.add(keyField);
|
|
3568
|
+
for (const field of displayFields) {
|
|
3569
|
+
if (/^(id|uuid|key|name|title|plate|email|slug)$/i.test(field)) out.add(field);
|
|
3570
|
+
}
|
|
3571
|
+
return Array.from(out);
|
|
3572
|
+
}
|
|
3573
|
+
var WEB_NAVIGATOR_TYPE = "route";
|
|
3574
|
+
var WebNavigationAnalyzer = class {
|
|
3575
|
+
config;
|
|
3576
|
+
navigationInclude;
|
|
3577
|
+
navigationExclude;
|
|
3578
|
+
constructor(config, options) {
|
|
3579
|
+
this.config = config;
|
|
3580
|
+
this.navigationInclude = options?.navigationInclude ?? [];
|
|
3581
|
+
this.navigationExclude = options?.navigationExclude ?? [];
|
|
3582
|
+
}
|
|
3583
|
+
async analyze() {
|
|
3584
|
+
const files = await this.findRouteFiles();
|
|
3585
|
+
const routes = [];
|
|
3586
|
+
for (const filePath of files) {
|
|
3587
|
+
try {
|
|
3588
|
+
const content = await fs$1.promises.readFile(filePath, "utf-8");
|
|
3589
|
+
if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(content)) {
|
|
3590
|
+
continue;
|
|
3591
|
+
}
|
|
3592
|
+
const ast = parseSource(content, this.config.parserPlugins);
|
|
3593
|
+
routes.push(...this.extractJsxRoutes(ast));
|
|
3594
|
+
routes.push(...this.extractObjectRoutes(ast));
|
|
3595
|
+
} catch (error) {
|
|
3596
|
+
console.warn(`[WebNavigationAnalyzer] Failed to parse ${filePath}:`, error);
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
const deduped = this.dedupeRoutes(routes);
|
|
3600
|
+
return { graph: this.buildGraph(deduped), routes: deduped };
|
|
3601
|
+
}
|
|
3602
|
+
/** Files likely to contain route configuration. */
|
|
3603
|
+
async findRouteFiles() {
|
|
3604
|
+
const patterns = [
|
|
3605
|
+
"**/*{router,routes,Router,Routes}*.{ts,tsx,js,jsx}",
|
|
3606
|
+
"**/App.{ts,tsx,js,jsx}",
|
|
3607
|
+
"**/app.{ts,tsx,js,jsx}",
|
|
3608
|
+
"**/main.{ts,tsx,js,jsx}",
|
|
3609
|
+
"**/index.{ts,tsx,js,jsx}",
|
|
3610
|
+
...this.navigationInclude
|
|
3611
|
+
];
|
|
3612
|
+
const ignore = [
|
|
3613
|
+
"**/node_modules/**",
|
|
3614
|
+
"**/dist/**",
|
|
3615
|
+
"**/build/**",
|
|
3616
|
+
...this.config.exclude || [],
|
|
3617
|
+
...this.navigationExclude
|
|
3618
|
+
];
|
|
3619
|
+
const files = await glob__default.default(patterns, { cwd: this.config.rootDir, ignore });
|
|
3620
|
+
return files.map((file) => path8__namespace.default.join(this.config.rootDir, file));
|
|
3621
|
+
}
|
|
3622
|
+
// ── JSX <Route> style ────────────────────────────────────────────
|
|
3623
|
+
extractJsxRoutes(ast) {
|
|
3624
|
+
const routes = [];
|
|
3625
|
+
const visitRoute = (element, parentPath) => {
|
|
3626
|
+
const opening = element.openingElement;
|
|
3627
|
+
const name = getJsxElementName(opening);
|
|
3628
|
+
if (name !== "Route") {
|
|
3629
|
+
for (const child of element.children) {
|
|
3630
|
+
if (BabelTypes__namespace.isJSXElement(child)) visitRoute(child, parentPath);
|
|
3631
|
+
}
|
|
3632
|
+
return;
|
|
3633
|
+
}
|
|
3634
|
+
const segment = getStringAttr(opening, "path");
|
|
3635
|
+
const isIndex = hasJsxAttribute(opening, "index") && !segment;
|
|
3636
|
+
const fullPath = this.joinPaths(parentPath, segment, isIndex);
|
|
3637
|
+
const componentName = this.componentNameFromElementAttr(opening) ?? void 0;
|
|
3638
|
+
const isLeaf = !element.children.some(
|
|
3639
|
+
(child) => BabelTypes__namespace.isJSXElement(child) && getJsxElementName(child.openingElement) === "Route"
|
|
3640
|
+
);
|
|
3641
|
+
if ((segment || isIndex) && (componentName || isLeaf)) {
|
|
3642
|
+
routes.push(this.buildRoute(fullPath, componentName, isIndex, !isLeaf));
|
|
3643
|
+
}
|
|
3644
|
+
for (const child of element.children) {
|
|
3645
|
+
if (BabelTypes__namespace.isJSXElement(child)) visitRoute(child, fullPath);
|
|
3646
|
+
}
|
|
3647
|
+
};
|
|
3648
|
+
traverse4__default.default(ast, {
|
|
3649
|
+
JSXElement: (nodePath) => {
|
|
3650
|
+
const name = getJsxElementName(nodePath.node.openingElement);
|
|
3651
|
+
if (name !== "Routes" && name !== "Route") return;
|
|
3652
|
+
if (nodePath.findParent((p) => {
|
|
3653
|
+
if (!p.isJSXElement()) return false;
|
|
3654
|
+
const parentName = getJsxElementName(p.node.openingElement);
|
|
3655
|
+
return parentName === "Routes" || parentName === "Route";
|
|
3656
|
+
})) {
|
|
3657
|
+
return;
|
|
3658
|
+
}
|
|
3659
|
+
visitRoute(nodePath.node, "");
|
|
3660
|
+
}
|
|
3661
|
+
});
|
|
3662
|
+
return routes;
|
|
3663
|
+
}
|
|
3664
|
+
/** `element={<VehicleList/>}` or `Component={VehicleList}`. */
|
|
3665
|
+
componentNameFromElementAttr(opening) {
|
|
3666
|
+
for (const attr of opening.attributes) {
|
|
3667
|
+
if (!BabelTypes__namespace.isJSXAttribute(attr) || !BabelTypes__namespace.isJSXIdentifier(attr.name)) continue;
|
|
3668
|
+
if (attr.name.name === "element" && BabelTypes__namespace.isJSXExpressionContainer(attr.value)) {
|
|
3669
|
+
const expr = attr.value.expression;
|
|
3670
|
+
if (BabelTypes__namespace.isJSXElement(expr)) return getJsxElementName(expr.openingElement);
|
|
3671
|
+
}
|
|
3672
|
+
if (attr.name.name === "Component" && BabelTypes__namespace.isJSXExpressionContainer(attr.value)) {
|
|
3673
|
+
if (BabelTypes__namespace.isIdentifier(attr.value.expression)) return attr.value.expression.name;
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
return null;
|
|
3677
|
+
}
|
|
3678
|
+
// ── createBrowserRouter([...]) / useRoutes([...]) style ──────────
|
|
3679
|
+
extractObjectRoutes(ast) {
|
|
3680
|
+
const routes = [];
|
|
3681
|
+
const ROUTER_FACTORIES = /* @__PURE__ */ new Set([
|
|
3682
|
+
"createBrowserRouter",
|
|
3683
|
+
"createHashRouter",
|
|
3684
|
+
"createMemoryRouter",
|
|
3685
|
+
"useRoutes"
|
|
3686
|
+
]);
|
|
3687
|
+
traverse4__default.default(ast, {
|
|
3688
|
+
CallExpression: (nodePath) => {
|
|
3689
|
+
const callee = nodePath.node.callee;
|
|
3690
|
+
if (!BabelTypes__namespace.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
|
|
3691
|
+
const first = nodePath.node.arguments[0];
|
|
3692
|
+
if (!BabelTypes__namespace.isArrayExpression(first)) return;
|
|
3693
|
+
this.visitRouteObjects(first, "", routes);
|
|
3694
|
+
}
|
|
3695
|
+
});
|
|
3696
|
+
return routes;
|
|
3697
|
+
}
|
|
3698
|
+
visitRouteObjects(arr, parentPath, out) {
|
|
3699
|
+
for (const element of arr.elements) {
|
|
3700
|
+
if (!BabelTypes__namespace.isObjectExpression(element)) continue;
|
|
3701
|
+
let segment;
|
|
3702
|
+
let isIndex = false;
|
|
3703
|
+
let componentName;
|
|
3704
|
+
let children;
|
|
3705
|
+
for (const prop of element.properties) {
|
|
3706
|
+
if (!BabelTypes__namespace.isObjectProperty(prop) || !BabelTypes__namespace.isIdentifier(prop.key)) continue;
|
|
3707
|
+
const key = prop.key.name;
|
|
3708
|
+
if (key === "path" && BabelTypes__namespace.isStringLiteral(prop.value)) segment = prop.value.value;
|
|
3709
|
+
if (key === "index" && BabelTypes__namespace.isBooleanLiteral(prop.value)) isIndex = prop.value.value;
|
|
3710
|
+
if (key === "element" && BabelTypes__namespace.isJSXElement(prop.value)) {
|
|
3711
|
+
componentName = getJsxElementName(prop.value.openingElement) ?? void 0;
|
|
3712
|
+
}
|
|
3713
|
+
if (key === "Component" && BabelTypes__namespace.isIdentifier(prop.value)) componentName = prop.value.name;
|
|
3714
|
+
if (key === "children" && BabelTypes__namespace.isArrayExpression(prop.value)) children = prop.value;
|
|
3715
|
+
}
|
|
3716
|
+
const fullPath = this.joinPaths(parentPath, segment, isIndex);
|
|
3717
|
+
if ((segment !== void 0 || isIndex) && (componentName || !children)) {
|
|
3718
|
+
out.push(this.buildRoute(fullPath, componentName, isIndex, Boolean(children)));
|
|
3719
|
+
}
|
|
3720
|
+
if (children) this.visitRouteObjects(children, fullPath, out);
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
// ── Shared route building ────────────────────────────────────────
|
|
3724
|
+
joinPaths(parent, segment, isIndex) {
|
|
3725
|
+
if (isIndex || segment === void 0) return parent || "/";
|
|
3726
|
+
if (segment.startsWith("/")) return this.normalizePath(segment);
|
|
3727
|
+
return this.normalizePath(`${parent === "/" ? "" : parent}/${segment}`);
|
|
3728
|
+
}
|
|
3729
|
+
normalizePath(p) {
|
|
3730
|
+
const cleaned = `/${p}`.replace(/\/+/g, "/");
|
|
3731
|
+
return cleaned.length > 1 ? cleaned.replace(/\/$/, "") : cleaned;
|
|
3732
|
+
}
|
|
3733
|
+
buildRoute(fullPath, componentName, isIndex, isLayout) {
|
|
3734
|
+
const params = this.paramsFromPath(fullPath);
|
|
3735
|
+
return {
|
|
3736
|
+
path: fullPath,
|
|
3737
|
+
screenName: componentName ?? screenNameFromPath(fullPath),
|
|
3738
|
+
...params.length > 0 ? { params } : {},
|
|
3739
|
+
...isIndex ? { index: true } : {},
|
|
3740
|
+
...isLayout ? { layout: true } : {}
|
|
3741
|
+
};
|
|
3742
|
+
}
|
|
3743
|
+
paramsFromPath(routePath) {
|
|
3744
|
+
const params = [];
|
|
3745
|
+
for (const segment of routePath.split("/")) {
|
|
3746
|
+
if (!segment.startsWith(":")) continue;
|
|
3747
|
+
const optional = segment.endsWith("?");
|
|
3748
|
+
const name = segment.slice(1, optional ? -1 : void 0);
|
|
3749
|
+
if (name) params.push({ name, type: "string", required: !optional });
|
|
3750
|
+
}
|
|
3751
|
+
return params;
|
|
3752
|
+
}
|
|
3753
|
+
/**
|
|
3754
|
+
* One entry per path. When several declarations resolve to the same
|
|
3755
|
+
* path, keep the one that best describes what the user lands on: a
|
|
3756
|
+
* page beats a layout wrapper (an `index` child and its parent layout
|
|
3757
|
+
* share a path), and a resolved component name beats a name derived
|
|
3758
|
+
* from the path.
|
|
3759
|
+
*/
|
|
3760
|
+
dedupeRoutes(routes) {
|
|
3761
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
3762
|
+
for (const route of routes) {
|
|
3763
|
+
const existing = byPath.get(route.path);
|
|
3764
|
+
if (!existing || routeScore(route) > routeScore(existing)) {
|
|
3765
|
+
byPath.set(route.path, route);
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
return Array.from(byPath.values());
|
|
3769
|
+
}
|
|
3770
|
+
buildGraph(routes) {
|
|
3771
|
+
const screens = {};
|
|
3772
|
+
const navigatorName = "router";
|
|
3773
|
+
const screenNames = routes.map((r) => r.screenName);
|
|
3774
|
+
for (const route of routes) {
|
|
3775
|
+
const others = screenNames.filter((name) => name !== route.screenName);
|
|
3776
|
+
screens[route.screenName] = {
|
|
3777
|
+
screenName: route.screenName,
|
|
3778
|
+
// Open-union value — web routes, not a RN stack/tab/drawer.
|
|
3779
|
+
navigatorType: WEB_NAVIGATOR_TYPE,
|
|
3780
|
+
parentNavigator: navigatorName,
|
|
3781
|
+
// Any route is one URL away from any other — both directions,
|
|
3782
|
+
// like the RN analyzer models tab navigators.
|
|
3783
|
+
reachableFrom: others,
|
|
3784
|
+
reachableTo: others,
|
|
3785
|
+
...route.params ? { params: route.params } : {}
|
|
3786
|
+
};
|
|
3787
|
+
}
|
|
3788
|
+
const initialRoute = routes.find((r) => r.path === "/") ?? routes.find((r) => r.index) ?? routes[0];
|
|
3789
|
+
return {
|
|
3790
|
+
screens,
|
|
3791
|
+
initialScreen: initialRoute?.screenName ?? "",
|
|
3792
|
+
navigators: routes.length > 0 ? [{
|
|
3793
|
+
name: navigatorName,
|
|
3794
|
+
type: WEB_NAVIGATOR_TYPE,
|
|
3795
|
+
screens: screenNames
|
|
3796
|
+
}] : []
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
};
|
|
3800
|
+
function screenNameFromPath(routePath) {
|
|
3801
|
+
if (routePath === "/" || routePath === "") return "Home";
|
|
3802
|
+
return routePath.split("/").filter(Boolean).map((segment) => segment.replace(/^:/, "").replace(/\?$/, "")).map(
|
|
3803
|
+
(segment) => segment.split(/[-_.]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("")
|
|
3804
|
+
).join("");
|
|
3805
|
+
}
|
|
3806
|
+
function routeScore(route) {
|
|
3807
|
+
const isPage = route.layout ? 0 : 2;
|
|
3808
|
+
const hasRealName = route.screenName === screenNameFromPath(route.path) ? 0 : 1;
|
|
3809
|
+
return isPage + hasRealName;
|
|
3810
|
+
}
|
|
3811
|
+
function resolvePathToScreen(routes, target) {
|
|
3812
|
+
const normalized = `/${target}`.replace(/\/+/g, "/").replace(/\?.*$/, "").replace(/#.*$/, "");
|
|
3813
|
+
const cleaned = normalized.length > 1 ? normalized.replace(/\/$/, "") : normalized;
|
|
3814
|
+
const exact = routes.find((r) => r.path === cleaned);
|
|
3815
|
+
if (exact) return exact.screenName;
|
|
3816
|
+
const targetSegments = cleaned.split("/").filter(Boolean);
|
|
3817
|
+
for (const route of routes) {
|
|
3818
|
+
const routeSegments = route.path.split("/").filter(Boolean);
|
|
3819
|
+
if (routeSegments.length !== targetSegments.length) continue;
|
|
3820
|
+
const matches = routeSegments.every(
|
|
3821
|
+
(seg, i) => seg.startsWith(":") || seg === "*" || seg === targetSegments[i]
|
|
3822
|
+
);
|
|
3823
|
+
if (matches) return route.screenName;
|
|
3824
|
+
}
|
|
3825
|
+
return void 0;
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3828
|
+
// src/analyzers/web/ReactWebPlatformAnalyzer.ts
|
|
3829
|
+
var ReactWebPlatformAnalyzer = class {
|
|
3830
|
+
platform = "web";
|
|
3831
|
+
async analyze(config, options) {
|
|
3832
|
+
const screenAnalyzer = new WebScreenAnalyzer(config, {
|
|
3833
|
+
screenPatterns: options.screenPatterns
|
|
3834
|
+
});
|
|
3835
|
+
const navigationAnalyzer = new WebNavigationAnalyzer(config, {
|
|
3836
|
+
navigationInclude: options.navigationInclude,
|
|
3837
|
+
navigationExclude: options.navigationExclude
|
|
3838
|
+
});
|
|
3839
|
+
console.log("[ReactWebPlatformAnalyzer] Running analyzers...");
|
|
3840
|
+
const [screenAnalysis, navigationResult] = await Promise.all([
|
|
3841
|
+
screenAnalyzer.analyze(),
|
|
3842
|
+
navigationAnalyzer.analyze()
|
|
3843
|
+
]);
|
|
3844
|
+
const routeScreenNames = new Set(navigationResult.routes.map((route) => route.screenName));
|
|
3845
|
+
const strictScreens = options.strictScreens ?? true;
|
|
3846
|
+
let screensFilteredOut = 0;
|
|
3847
|
+
const included = [];
|
|
3848
|
+
for (const candidate of screenAnalysis.candidates) {
|
|
3849
|
+
if (!strictScreens || this.isScreen(candidate, routeScreenNames)) {
|
|
3850
|
+
included.push(candidate);
|
|
3851
|
+
} else {
|
|
3852
|
+
screensFilteredOut++;
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
const screens = included.map(
|
|
3856
|
+
(candidate) => this.resolveRoutePaths(candidate.descriptor, navigationResult.routes)
|
|
3857
|
+
);
|
|
3858
|
+
console.log(
|
|
3859
|
+
`[ReactWebPlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens, ${navigationResult.routes.length} routes`
|
|
3860
|
+
);
|
|
3861
|
+
return {
|
|
3862
|
+
screens,
|
|
3863
|
+
navigation: navigationResult.graph,
|
|
3864
|
+
analyzedFiles: screenAnalysis.analyzedFiles,
|
|
3865
|
+
...screensFilteredOut > 0 ? { screensFilteredOut } : {}
|
|
3866
|
+
};
|
|
3867
|
+
}
|
|
3868
|
+
isScreen(candidate, routeScreenNames) {
|
|
3869
|
+
return candidate.hasRegisterScreen || candidate.matchesScreenPattern || routeScreenNames.has(candidate.descriptor.name);
|
|
3870
|
+
}
|
|
3871
|
+
/** Replace route-path references with screen names where the route table resolves them. */
|
|
3872
|
+
resolveRoutePaths(screen, routes) {
|
|
3873
|
+
const resolve2 = (target) => {
|
|
3874
|
+
if (!target || !target.startsWith("/")) return target;
|
|
3875
|
+
return resolvePathToScreen(routes, target) ?? target;
|
|
3876
|
+
};
|
|
3877
|
+
const navigationTargets = Array.from(
|
|
3878
|
+
new Set(
|
|
3879
|
+
screen.navigationTargets.map((target) => resolve2(target)).filter((target) => target !== screen.name)
|
|
3880
|
+
)
|
|
3881
|
+
).sort();
|
|
3882
|
+
const actions = screen.actions.map((action) => {
|
|
3883
|
+
const resolved = resolve2(action.targetScreen);
|
|
3884
|
+
return resolved === action.targetScreen ? action : { ...action, targetScreen: resolved };
|
|
3885
|
+
});
|
|
3886
|
+
const collections = screen.collections?.map((collection) => {
|
|
3887
|
+
const resolveRow = (row) => {
|
|
3888
|
+
if (!row?.targetScreen) return row;
|
|
3889
|
+
const resolved = resolve2(row.targetScreen);
|
|
3890
|
+
if (resolved === row.targetScreen) return row;
|
|
3891
|
+
return {
|
|
3892
|
+
...row,
|
|
3893
|
+
targetScreen: resolved,
|
|
3894
|
+
...row.description ? { description: `Clicking a row opens ${resolved}` } : {}
|
|
3895
|
+
};
|
|
3896
|
+
};
|
|
3897
|
+
const rowAction = resolveRow(collection.rowAction);
|
|
3898
|
+
return {
|
|
3899
|
+
...collection,
|
|
3900
|
+
...rowAction ? { rowAction } : {},
|
|
3901
|
+
...collection.rowActions ? { rowActions: collection.rowActions.map((row) => resolveRow(row)) } : {}
|
|
3902
|
+
};
|
|
3903
|
+
});
|
|
3904
|
+
return {
|
|
3905
|
+
...screen,
|
|
3906
|
+
navigationTargets,
|
|
3907
|
+
actions,
|
|
3908
|
+
...collections ? { collections } : {}
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3911
|
+
};
|
|
3912
|
+
|
|
2439
3913
|
// ../../node_modules/zod/v3/external.js
|
|
2440
3914
|
var external_exports = {};
|
|
2441
3915
|
__export(external_exports, {
|
|
@@ -2641,8 +4115,8 @@ var ZodParsedType = util.arrayToEnum([
|
|
|
2641
4115
|
"set"
|
|
2642
4116
|
]);
|
|
2643
4117
|
var getParsedType = (data) => {
|
|
2644
|
-
const
|
|
2645
|
-
switch (
|
|
4118
|
+
const t12 = typeof data;
|
|
4119
|
+
switch (t12) {
|
|
2646
4120
|
case "undefined":
|
|
2647
4121
|
return ZodParsedType.undefined;
|
|
2648
4122
|
case "string":
|
|
@@ -2914,8 +4388,8 @@ function getErrorMap() {
|
|
|
2914
4388
|
|
|
2915
4389
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
2916
4390
|
var makeIssue = (params) => {
|
|
2917
|
-
const { data, path:
|
|
2918
|
-
const fullPath = [...
|
|
4391
|
+
const { data, path: path9, errorMaps, issueData } = params;
|
|
4392
|
+
const fullPath = [...path9, ...issueData.path || []];
|
|
2919
4393
|
const fullIssue = {
|
|
2920
4394
|
...issueData,
|
|
2921
4395
|
path: fullPath
|
|
@@ -3031,11 +4505,11 @@ var errorUtil;
|
|
|
3031
4505
|
|
|
3032
4506
|
// ../../node_modules/zod/v3/types.js
|
|
3033
4507
|
var ParseInputLazyPath = class {
|
|
3034
|
-
constructor(parent, value,
|
|
4508
|
+
constructor(parent, value, path9, key) {
|
|
3035
4509
|
this._cachedPath = [];
|
|
3036
4510
|
this.parent = parent;
|
|
3037
4511
|
this.data = value;
|
|
3038
|
-
this._path =
|
|
4512
|
+
this._path = path9;
|
|
3039
4513
|
this._key = key;
|
|
3040
4514
|
}
|
|
3041
4515
|
get path() {
|
|
@@ -6476,7 +7950,7 @@ var coerce = {
|
|
|
6476
7950
|
};
|
|
6477
7951
|
var NEVER = INVALID;
|
|
6478
7952
|
|
|
6479
|
-
// ../shared/dist/chunk-
|
|
7953
|
+
// ../shared/dist/chunk-MSOZJNXB.mjs
|
|
6480
7954
|
var locatorSourceSchema = external_exports.enum([
|
|
6481
7955
|
"appilotsId",
|
|
6482
7956
|
"testID",
|
|
@@ -6661,7 +8135,19 @@ var navigationNodeSchema = external_exports.object({
|
|
|
6661
8135
|
parentNavigator: external_exports.string().optional(),
|
|
6662
8136
|
reachableFrom: external_exports.array(external_exports.string()).default([]),
|
|
6663
8137
|
reachableTo: external_exports.array(external_exports.string()).default([]),
|
|
6664
|
-
params: external_exports.array(paramDescriptorSchema).optional()
|
|
8138
|
+
params: external_exports.array(paramDescriptorSchema).optional(),
|
|
8139
|
+
/**
|
|
8140
|
+
* Web clients only (documented convention, additive — previously
|
|
8141
|
+
* round-tripped via `.passthrough()`): the screen's URL route
|
|
8142
|
+
* template, e.g. `/users/:id`. Param segments use `:name` (the
|
|
8143
|
+
* relay also accepts `[name]` / `{name}`). When present and the
|
|
8144
|
+
* request's `context.platform` is `'web'`, the relay resolves
|
|
8145
|
+
* `navigate` targets against these templates and injects the
|
|
8146
|
+
* concrete URL segments as the navigate payload's `path` — the web
|
|
8147
|
+
* equivalent of RN's nested-navigation path injection. See
|
|
8148
|
+
* docs/agent-contract.md ("Web clients" section).
|
|
8149
|
+
*/
|
|
8150
|
+
path: external_exports.string().max(500).optional()
|
|
6665
8151
|
}).passthrough();
|
|
6666
8152
|
var navigatorDescriptorSchema = external_exports.object({
|
|
6667
8153
|
name: external_exports.string(),
|
|
@@ -6708,6 +8194,41 @@ var loginSchema = external_exports.object({
|
|
|
6708
8194
|
loginSchema.extend({
|
|
6709
8195
|
name: external_exports.string().min(2, "Name must be at least 2 characters").max(100)
|
|
6710
8196
|
});
|
|
8197
|
+
external_exports.object({
|
|
8198
|
+
name: external_exports.string().min(2, "Name must be at least 2 characters").max(100).optional(),
|
|
8199
|
+
avatarUrl: external_exports.string().url("Invalid URL").max(2048).nullable().optional()
|
|
8200
|
+
}).refine((v) => v.name !== void 0 || v.avatarUrl !== void 0, {
|
|
8201
|
+
message: "At least one field must be provided"
|
|
8202
|
+
});
|
|
8203
|
+
external_exports.object({
|
|
8204
|
+
currentPassword: external_exports.string().min(1, "Current password is required"),
|
|
8205
|
+
newPassword: external_exports.string().min(8, "Password must be at least 8 characters")
|
|
8206
|
+
});
|
|
8207
|
+
var totpCodeSchema = external_exports.string().transform((v) => v.replace(/\s/g, "")).pipe(external_exports.string().regex(/^\d{6}$/, "Code must be 6 digits"));
|
|
8208
|
+
var recoveryCodeSchema = external_exports.string().transform((v) => v.toUpperCase().replace(/[^A-Z0-9]/g, "")).pipe(external_exports.string().regex(/^[23456789BCDFGHJKMNPQRSTVWXYZ]{10}$/, "Invalid recovery code"));
|
|
8209
|
+
external_exports.object({
|
|
8210
|
+
code: totpCodeSchema
|
|
8211
|
+
});
|
|
8212
|
+
external_exports.object({
|
|
8213
|
+
challengeToken: external_exports.string().min(1, "Challenge token is required"),
|
|
8214
|
+
code: totpCodeSchema.optional(),
|
|
8215
|
+
recoveryCode: recoveryCodeSchema.optional()
|
|
8216
|
+
}).refine((v) => Boolean(v.code) !== Boolean(v.recoveryCode), {
|
|
8217
|
+
message: "Provide either a TOTP code or a recovery code",
|
|
8218
|
+
path: ["code"]
|
|
8219
|
+
});
|
|
8220
|
+
external_exports.object({
|
|
8221
|
+
password: external_exports.string().min(1, "Password is required"),
|
|
8222
|
+
code: totpCodeSchema.optional(),
|
|
8223
|
+
recoveryCode: recoveryCodeSchema.optional()
|
|
8224
|
+
}).refine((v) => Boolean(v.code) !== Boolean(v.recoveryCode), {
|
|
8225
|
+
message: "Provide either a TOTP code or a recovery code",
|
|
8226
|
+
path: ["code"]
|
|
8227
|
+
});
|
|
8228
|
+
external_exports.object({
|
|
8229
|
+
password: external_exports.string().min(1, "Password is required"),
|
|
8230
|
+
code: totpCodeSchema
|
|
8231
|
+
});
|
|
6711
8232
|
var createProjectSchema = external_exports.object({
|
|
6712
8233
|
name: external_exports.string().min(1).max(100),
|
|
6713
8234
|
description: external_exports.string().max(500).optional(),
|
|
@@ -7371,6 +8892,18 @@ external_exports.object({
|
|
|
7371
8892
|
*/
|
|
7372
8893
|
isRecoveryHop: external_exports.boolean().optional()
|
|
7373
8894
|
}).strict();
|
|
8895
|
+
var relayDiagnosticsSchema = external_exports.object({
|
|
8896
|
+
unverifiedTargets: external_exports.array(external_exports.string()),
|
|
8897
|
+
guardsFired: external_exports.array(
|
|
8898
|
+
external_exports.object({ guard: external_exports.string(), detail: external_exports.string().optional() })
|
|
8899
|
+
),
|
|
8900
|
+
repairMode: external_exports.enum(["repair", "strict", "disabled"]),
|
|
8901
|
+
promptVersion: external_exports.string(),
|
|
8902
|
+
// Defaulted for producers that predate the field: before it existed
|
|
8903
|
+
// every response had reached the model, so `true` is the correct
|
|
8904
|
+
// backfill.
|
|
8905
|
+
modelInvoked: external_exports.boolean().default(true)
|
|
8906
|
+
});
|
|
7374
8907
|
var sandboxTraceHopSchema = external_exports.object({
|
|
7375
8908
|
hop: external_exports.number().int().nonnegative(),
|
|
7376
8909
|
inputPreview: external_exports.string(),
|
|
@@ -7409,7 +8942,16 @@ external_exports.object({
|
|
|
7409
8942
|
/** Hop-by-hop trace. Always at least one entry. */
|
|
7410
8943
|
trace: external_exports.array(sandboxTraceHopSchema),
|
|
7411
8944
|
/** True when the run was a no-op (e.g. project has no MCP yet). */
|
|
7412
|
-
warning: external_exports.string().optional()
|
|
8945
|
+
warning: external_exports.string().optional(),
|
|
8946
|
+
/**
|
|
8947
|
+
* SPEC-046 — guard / hallucination telemetry for the run.
|
|
8948
|
+
*
|
|
8949
|
+
* ADDITIVE AND OPTIONAL, permanently: SDK and dashboard consumers
|
|
8950
|
+
* predate it, and older API deployments will not send it. Consumers
|
|
8951
|
+
* must treat an absent block as "no information", never as "no guards
|
|
8952
|
+
* fired".
|
|
8953
|
+
*/
|
|
8954
|
+
diagnostics: relayDiagnosticsSchema.optional()
|
|
7413
8955
|
});
|
|
7414
8956
|
external_exports.object({
|
|
7415
8957
|
name: external_exports.string().min(1).max(120),
|
|
@@ -7628,7 +9170,7 @@ var manifestSchema = external_exports.object({
|
|
|
7628
9170
|
navigation: navigationGraphSchema.partial().optional()
|
|
7629
9171
|
}).passthrough();
|
|
7630
9172
|
async function loadManifest(rootDir, manifestPath) {
|
|
7631
|
-
const resolvedPath =
|
|
9173
|
+
const resolvedPath = path8__namespace.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
|
|
7632
9174
|
let raw;
|
|
7633
9175
|
try {
|
|
7634
9176
|
raw = await fs.readFile(resolvedPath, "utf-8");
|
|
@@ -7913,12 +9455,15 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
7913
9455
|
/**
|
|
7914
9456
|
* Select the `PlatformAnalyzer` implementation for a `.appilotsrc`
|
|
7915
9457
|
* `platform` value. `'react-native'` (or unset — the existing default)
|
|
7916
|
-
* gets the
|
|
7917
|
-
*
|
|
9458
|
+
* gets the RN Babel/JSX pipeline; `'web'` gets the React web (DOM +
|
|
9459
|
+
* React Router) pipeline; anything else gets the no-op generic
|
|
9460
|
+
* analyzer, relying entirely on a declared manifest. The manifest
|
|
9461
|
+
* still merges on top of every analyzer's output either way.
|
|
7918
9462
|
*/
|
|
7919
9463
|
static createPlatformAnalyzer(platform) {
|
|
7920
9464
|
const resolved = platform ?? "react-native";
|
|
7921
9465
|
if (resolved === "react-native") return new ReactNativePlatformAnalyzer();
|
|
9466
|
+
if (resolved === "web") return new ReactWebPlatformAnalyzer();
|
|
7922
9467
|
return new GenericPlatformAnalyzer(resolved);
|
|
7923
9468
|
}
|
|
7924
9469
|
/** Generate MCP documents from the project */
|
|
@@ -7971,13 +9516,13 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
7971
9516
|
};
|
|
7972
9517
|
const serialized = JSON.stringify(document, null, 2);
|
|
7973
9518
|
const checksum = this.calculateChecksum(serialized);
|
|
7974
|
-
const filePath =
|
|
9519
|
+
const filePath = path8__namespace.default.resolve(
|
|
7975
9520
|
outputDir,
|
|
7976
9521
|
`mcp-document.${this.options.format}`
|
|
7977
9522
|
);
|
|
7978
9523
|
await fs.writeFile(filePath, serialized, "utf-8");
|
|
7979
9524
|
console.log(`[MCPGenerator] Document written to: ${filePath}`);
|
|
7980
|
-
const checksumFilePath =
|
|
9525
|
+
const checksumFilePath = path8__namespace.default.resolve(outputDir, ".appilots-checksum");
|
|
7981
9526
|
await fs.writeFile(checksumFilePath, checksum, "utf-8");
|
|
7982
9527
|
console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
|
|
7983
9528
|
console.log("[MCPGenerator] Generation complete!");
|
|
@@ -7997,7 +9542,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
7997
9542
|
* after calling `generate()`.
|
|
7998
9543
|
*/
|
|
7999
9544
|
static async readPreviousChecksum(outputDir) {
|
|
8000
|
-
const checksumFilePath =
|
|
9545
|
+
const checksumFilePath = path8__namespace.default.resolve(outputDir, ".appilots-checksum");
|
|
8001
9546
|
try {
|
|
8002
9547
|
const content = await fs.readFile(checksumFilePath, "utf-8");
|
|
8003
9548
|
return content.trim() || null;
|
|
@@ -8016,7 +9561,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
8016
9561
|
*/
|
|
8017
9562
|
async getProjectInfo() {
|
|
8018
9563
|
try {
|
|
8019
|
-
const packageJsonPath =
|
|
9564
|
+
const packageJsonPath = path8__namespace.default.resolve(this.analyzerConfig.rootDir, "package.json");
|
|
8020
9565
|
const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
|
|
8021
9566
|
const packageJson = JSON.parse(packageJsonContent);
|
|
8022
9567
|
return {
|
|
@@ -8074,7 +9619,7 @@ function loadConfig() {
|
|
|
8074
9619
|
return merged;
|
|
8075
9620
|
}
|
|
8076
9621
|
function saveConfig(dir, config) {
|
|
8077
|
-
const configPath =
|
|
9622
|
+
const configPath = path8.join(dir, ".appilotsrc");
|
|
8078
9623
|
const existingConfig = getConfigPath() ? loadConfig() : null;
|
|
8079
9624
|
const mergedConfig = {
|
|
8080
9625
|
apiKey: config.apiKey || existingConfig?.apiKey || "",
|
|
@@ -8099,19 +9644,19 @@ function saveConfig(dir, config) {
|
|
|
8099
9644
|
}
|
|
8100
9645
|
}
|
|
8101
9646
|
function getConfigPath() {
|
|
8102
|
-
let currentDir =
|
|
8103
|
-
const root =
|
|
9647
|
+
let currentDir = path8.resolve(process.cwd());
|
|
9648
|
+
const root = path8.resolve("/");
|
|
8104
9649
|
while (currentDir !== root) {
|
|
8105
|
-
const configPath =
|
|
9650
|
+
const configPath = path8.join(currentDir, ".appilotsrc");
|
|
8106
9651
|
try {
|
|
8107
9652
|
if (fs$1.existsSync(configPath) && fs$1.statSync(configPath).isFile()) {
|
|
8108
9653
|
return configPath;
|
|
8109
9654
|
}
|
|
8110
9655
|
} catch {
|
|
8111
9656
|
}
|
|
8112
|
-
currentDir =
|
|
9657
|
+
currentDir = path8.resolve(currentDir, "..");
|
|
8113
9658
|
}
|
|
8114
|
-
const rootConfigPath =
|
|
9659
|
+
const rootConfigPath = path8.join(root, ".appilotsrc");
|
|
8115
9660
|
try {
|
|
8116
9661
|
if (fs$1.existsSync(rootConfigPath) && fs$1.statSync(rootConfigPath).isFile()) {
|
|
8117
9662
|
return rootConfigPath;
|
|
@@ -8405,12 +9950,16 @@ var AppilotsAPIClient = class {
|
|
|
8405
9950
|
exports.AppilotsAPIClient = AppilotsAPIClient;
|
|
8406
9951
|
exports.ComponentAnalyzer = ComponentAnalyzer;
|
|
8407
9952
|
exports.DEFAULT_MANIFEST_FILENAME = DEFAULT_MANIFEST_FILENAME;
|
|
9953
|
+
exports.DEFAULT_WEB_SCREEN_PATTERNS = DEFAULT_WEB_SCREEN_PATTERNS;
|
|
8408
9954
|
exports.FormAnalyzer = FormAnalyzer;
|
|
8409
9955
|
exports.GenericPlatformAnalyzer = GenericPlatformAnalyzer;
|
|
8410
9956
|
exports.MCPGenerator = MCPGenerator;
|
|
8411
9957
|
exports.NavigationAnalyzer = NavigationAnalyzer;
|
|
8412
9958
|
exports.ReactNativePlatformAnalyzer = ReactNativePlatformAnalyzer;
|
|
9959
|
+
exports.ReactWebPlatformAnalyzer = ReactWebPlatformAnalyzer;
|
|
8413
9960
|
exports.ScreenAnalyzer = ScreenAnalyzer;
|
|
9961
|
+
exports.WebNavigationAnalyzer = WebNavigationAnalyzer;
|
|
9962
|
+
exports.WebScreenAnalyzer = WebScreenAnalyzer;
|
|
8414
9963
|
exports.formatMetadataWarnings = formatMetadataWarnings;
|
|
8415
9964
|
exports.getConfigPath = getConfigPath;
|
|
8416
9965
|
exports.getEnvOverrides = getEnvOverrides;
|
|
@@ -8419,7 +9968,9 @@ exports.loadConfig = loadConfig;
|
|
|
8419
9968
|
exports.loadManifest = loadManifest;
|
|
8420
9969
|
exports.mergeManifestNavigation = mergeManifestNavigation;
|
|
8421
9970
|
exports.mergeManifestScreens = mergeManifestScreens;
|
|
9971
|
+
exports.resolvePathToScreen = resolvePathToScreen;
|
|
8422
9972
|
exports.saveConfig = saveConfig;
|
|
9973
|
+
exports.screenNameFromPath = screenNameFromPath;
|
|
8423
9974
|
exports.validateConfig = validateConfig;
|
|
8424
9975
|
//# sourceMappingURL=index.js.map
|
|
8425
9976
|
//# sourceMappingURL=index.js.map
|