@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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var fs = require('fs/promises');
4
- var path6 = require('path');
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 path6__namespace = /*#__PURE__*/_interopNamespace(path6);
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) => path6__namespace.default.resolve(this.config.rootDir, 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 = path6__namespace.default.resolve(this.config.rootDir, file);
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 DESTRUCTIVE_VERB2 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
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 && DESTRUCTIVE_VERB2.test(handlerName)) return true;
1232
- if (actionId && DESTRUCTIVE_VERB2.test(actionId)) return true;
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 = path6__namespace.default.basename(filePath);
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) => path6__namespace.default.join(this.config.rootDir, 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((t8) => this.typeToString(t8)).join(" | ");
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((t8) => t8.type === navigator.type);
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: (path7) => {
1941
- const component = this.extractComponentFromJSXElement(path7.node);
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: (path7) => {
2069
- this.extractStateVariables(path7.node);
2068
+ CallExpression: (path9) => {
2069
+ this.extractStateVariables(path9.node);
2070
2070
  }
2071
2071
  });
2072
2072
  traverse4__default.default(ast, {
2073
- JSXElement: (path7) => {
2074
- this.extractFormElements(path7.node);
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: (path7) => {
2175
- const test = path7.node.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 = path6__namespace.basename(filePath, path6__namespace.extname(filePath));
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 = path6__namespace.default.resolve(config.rootDir, file);
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 t8 = typeof data;
2645
- switch (t8) {
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: path7, errorMaps, issueData } = params;
2918
- const fullPath = [...path7, ...issueData.path || []];
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, path7, key) {
4508
+ constructor(parent, value, path9, key) {
3035
4509
  this._cachedPath = [];
3036
4510
  this.parent = parent;
3037
4511
  this.data = value;
3038
- this._path = path7;
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-XLOYG3DH.mjs
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 = path6__namespace.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
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 real Babel/JSX pipeline; anything else gets the no-op
7917
- * generic analyzer, relying entirely on a declared manifest.
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 = path6__namespace.default.resolve(
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 = path6__namespace.default.resolve(outputDir, ".appilots-checksum");
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 = path6__namespace.default.resolve(outputDir, ".appilots-checksum");
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 = path6__namespace.default.resolve(this.analyzerConfig.rootDir, "package.json");
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 = path6.join(dir, ".appilotsrc");
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 = path6.resolve(process.cwd());
8103
- const root = path6.resolve("/");
9647
+ let currentDir = path8.resolve(process.cwd());
9648
+ const root = path8.resolve("/");
8104
9649
  while (currentDir !== root) {
8105
- const configPath = path6.join(currentDir, ".appilotsrc");
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 = path6.resolve(currentDir, "..");
9657
+ currentDir = path8.resolve(currentDir, "..");
8113
9658
  }
8114
- const rootConfigPath = path6.join(root, ".appilotsrc");
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