@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.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import fs, { readFile, mkdir, writeFile } from 'fs/promises';
2
- import * as path6 from 'path';
3
- import path6__default, { join, resolve } from 'path';
2
+ import * as path8 from 'path';
3
+ import path8__default, { join, resolve } from 'path';
4
4
  import traverse4 from '@babel/traverse';
5
5
  import * as BabelTypes from '@babel/types';
6
6
  import glob from 'fast-glob';
@@ -392,12 +392,12 @@ var ScreenAnalyzer = class {
392
392
  cwd: this.config.rootDir,
393
393
  ignore: exclude
394
394
  });
395
- screenPatternFiles = new Set(matched.map((f) => path6__default.resolve(this.config.rootDir, f)));
395
+ screenPatternFiles = new Set(matched.map((f) => path8__default.resolve(this.config.rootDir, f)));
396
396
  }
397
397
  const screens = [];
398
398
  this.screensFilteredOut = 0;
399
399
  for (const file of files) {
400
- const filePath = path6__default.resolve(this.config.rootDir, file);
400
+ const filePath = path8__default.resolve(this.config.rootDir, file);
401
401
  try {
402
402
  const descriptor = await this.analyzeFile(filePath);
403
403
  if (!descriptor) continue;
@@ -1186,7 +1186,7 @@ var ScreenAnalyzer = class {
1186
1186
  * heuristic must hit OR the JSDoc tag must be present.
1187
1187
  */
1188
1188
  isElementDestructive(element, handlerName, actionId) {
1189
- const DESTRUCTIVE_VERB2 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
1189
+ const DESTRUCTIVE_VERB3 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
1190
1190
  for (const attr of element.attributes) {
1191
1191
  if (!BabelTypes.isJSXAttribute(attr)) continue;
1192
1192
  const attrName = BabelTypes.isJSXIdentifier(attr.name) ? attr.name.name : null;
@@ -1201,8 +1201,8 @@ var ScreenAnalyzer = class {
1201
1201
  }
1202
1202
  }
1203
1203
  }
1204
- if (handlerName && DESTRUCTIVE_VERB2.test(handlerName)) return true;
1205
- if (actionId && DESTRUCTIVE_VERB2.test(actionId)) return true;
1204
+ if (handlerName && DESTRUCTIVE_VERB3.test(handlerName)) return true;
1205
+ if (actionId && DESTRUCTIVE_VERB3.test(actionId)) return true;
1206
1206
  return false;
1207
1207
  }
1208
1208
  /**
@@ -1546,7 +1546,7 @@ var ScreenAnalyzer = class {
1546
1546
  * E.g., /src/screens/ItemListScreen.tsx -> ItemListScreen
1547
1547
  */
1548
1548
  extractScreenName(filePath) {
1549
- const basename2 = path6__default.basename(filePath);
1549
+ const basename2 = path8__default.basename(filePath);
1550
1550
  return basename2.replace(/\.(tsx?|jsx?)$/, "");
1551
1551
  }
1552
1552
  };
@@ -1601,7 +1601,7 @@ var NavigationAnalyzer = class {
1601
1601
  cwd: this.config.rootDir,
1602
1602
  ignore: excludePatterns
1603
1603
  });
1604
- return files.map((file) => path6__default.join(this.config.rootDir, file));
1604
+ return files.map((file) => path8__default.join(this.config.rootDir, file));
1605
1605
  }
1606
1606
  /** Parse navigator definitions from a file */
1607
1607
  parseNavigators(content, filePath) {
@@ -1804,7 +1804,7 @@ var NavigationAnalyzer = class {
1804
1804
  if (type.type === "TSUndefinedKeyword") return "undefined";
1805
1805
  if (type.type === "TSNullKeyword") return "null";
1806
1806
  if (type.type === "TSUnionType") {
1807
- return type.types.map((t8) => this.typeToString(t8)).join(" | ");
1807
+ return type.types.map((t12) => this.typeToString(t12)).join(" | ");
1808
1808
  }
1809
1809
  if (type.type === "TSTypeLiteral") {
1810
1810
  return "object";
@@ -1825,7 +1825,7 @@ var NavigationAnalyzer = class {
1825
1825
  /** Attach parsed type params to navigator screens */
1826
1826
  attachParamsToNavigators(navigators, types) {
1827
1827
  for (const navigator of navigators) {
1828
- const matchingType = types.find((t8) => t8.type === navigator.type);
1828
+ const matchingType = types.find((t12) => t12.type === navigator.type);
1829
1829
  if (matchingType) {
1830
1830
  for (const screen of navigator.screens) {
1831
1831
  const screenParams = matchingType.paramEntries.get(screen.name);
@@ -1910,8 +1910,8 @@ var ComponentAnalyzer = class {
1910
1910
  });
1911
1911
  const components = [];
1912
1912
  traverse4(ast, {
1913
- JSXElement: (path7) => {
1914
- const component = this.extractComponentFromJSXElement(path7.node);
1913
+ JSXElement: (path9) => {
1914
+ const component = this.extractComponentFromJSXElement(path9.node);
1915
1915
  if (component) {
1916
1916
  components.push(component);
1917
1917
  }
@@ -2038,13 +2038,13 @@ var FormAnalyzer = class {
2038
2038
  this.inputElements = [];
2039
2039
  this.submitButtons = [];
2040
2040
  traverse4(ast, {
2041
- CallExpression: (path7) => {
2042
- this.extractStateVariables(path7.node);
2041
+ CallExpression: (path9) => {
2042
+ this.extractStateVariables(path9.node);
2043
2043
  }
2044
2044
  });
2045
2045
  traverse4(ast, {
2046
- JSXElement: (path7) => {
2047
- this.extractFormElements(path7.node);
2046
+ JSXElement: (path9) => {
2047
+ this.extractFormElements(path9.node);
2048
2048
  }
2049
2049
  });
2050
2050
  const validationRules = this.extractValidationRules(ast);
@@ -2144,8 +2144,8 @@ var FormAnalyzer = class {
2144
2144
  extractValidationRules(ast) {
2145
2145
  const rules = {};
2146
2146
  traverse4(ast, {
2147
- IfStatement: (path7) => {
2148
- const test = path7.node.test;
2147
+ IfStatement: (path9) => {
2148
+ const test = path9.node.test;
2149
2149
  const rule = this.extractRuleFromCondition(test);
2150
2150
  if (rule) {
2151
2151
  const { field, description } = rule;
@@ -2199,7 +2199,7 @@ var FormAnalyzer = class {
2199
2199
  }
2200
2200
  buildForms(filePath, validationRules) {
2201
2201
  if (this.inputElements.length === 0) return [];
2202
- const fileName = path6.basename(filePath, path6.extname(filePath));
2202
+ const fileName = path8.basename(filePath, path8.extname(filePath));
2203
2203
  const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
2204
2204
  const fields = this.inputElements.map((input) => {
2205
2205
  const fieldType = this.inferFieldType(input);
@@ -2277,7 +2277,7 @@ var ReactNativePlatformAnalyzer = class {
2277
2277
  `[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
2278
2278
  );
2279
2279
  const enrichmentPromises = screenFiles.map(async (file) => {
2280
- const filePath = path6__default.resolve(config.rootDir, file);
2280
+ const filePath = path8__default.resolve(config.rootDir, file);
2281
2281
  try {
2282
2282
  const [components, forms] = await Promise.all([
2283
2283
  componentAnalyzer.analyzeFile(filePath),
@@ -2409,6 +2409,1480 @@ var GenericPlatformAnalyzer = class {
2409
2409
  }
2410
2410
  };
2411
2411
 
2412
+ // src/ast/jsx/web/classify.ts
2413
+ var VIEW_TAGS = /* @__PURE__ */ new Set([
2414
+ "div",
2415
+ "span",
2416
+ "section",
2417
+ "main",
2418
+ "article",
2419
+ "aside",
2420
+ "header",
2421
+ "footer",
2422
+ "nav",
2423
+ "fieldset",
2424
+ "form",
2425
+ "p"
2426
+ ]);
2427
+ var LIST_TAGS = /* @__PURE__ */ new Set(["ul", "ol", "dl", "table", "tbody"]);
2428
+ var BUTTON_TAGS = /* @__PURE__ */ new Set(["button"]);
2429
+ var INPUT_TAGS = /* @__PURE__ */ new Set(["input", "textarea"]);
2430
+ var MODAL_TAGS = /* @__PURE__ */ new Set(["dialog"]);
2431
+ var BUTTON_COMPONENTS2 = /* @__PURE__ */ new Set(["Button", "IconButton", "Link", "NavLink"]);
2432
+ var INPUT_COMPONENTS2 = /* @__PURE__ */ new Set(["Input", "TextField", "TextArea", "Textarea"]);
2433
+ var LIST_COMPONENTS2 = /* @__PURE__ */ new Set(["List", "Table", "DataGrid", "DataTable"]);
2434
+ var MODAL_COMPONENTS2 = /* @__PURE__ */ new Set(["Modal", "Dialog", "Drawer", "Popover", "BottomSheet"]);
2435
+ var ARIA_ROLE_TO_SEMANTIC = {
2436
+ button: "button",
2437
+ link: "button",
2438
+ tab: "button",
2439
+ menuitem: "button",
2440
+ textbox: "input",
2441
+ searchbox: "input",
2442
+ spinbutton: "input",
2443
+ listbox: "select",
2444
+ combobox: "select",
2445
+ radiogroup: "select",
2446
+ radio: "select",
2447
+ option: "select",
2448
+ checkbox: "toggle",
2449
+ switch: "toggle",
2450
+ dialog: "modal",
2451
+ alertdialog: "modal",
2452
+ list: "list",
2453
+ table: "list",
2454
+ grid: "list",
2455
+ form: "view"
2456
+ };
2457
+ var INPUT_TYPE_TO_SEMANTIC = {
2458
+ checkbox: "toggle",
2459
+ radio: "select",
2460
+ date: "date",
2461
+ "datetime-local": "date",
2462
+ month: "date",
2463
+ week: "date",
2464
+ time: "date",
2465
+ submit: "button",
2466
+ button: "button",
2467
+ reset: "button",
2468
+ image: "button",
2469
+ hidden: "custom",
2470
+ range: "input",
2471
+ file: "input",
2472
+ color: "input"
2473
+ };
2474
+ function classifyWebJsxComponent(name, element) {
2475
+ if (element) {
2476
+ const role = getStringAttr(element, "role");
2477
+ if (role && ARIA_ROLE_TO_SEMANTIC[role]) return ARIA_ROLE_TO_SEMANTIC[role];
2478
+ }
2479
+ if (/^[a-z]/.test(name)) {
2480
+ if (name === "input") {
2481
+ const type = element ? getStringAttr(element, "type") : void 0;
2482
+ if (type && INPUT_TYPE_TO_SEMANTIC[type]) return INPUT_TYPE_TO_SEMANTIC[type];
2483
+ return "input";
2484
+ }
2485
+ if (INPUT_TAGS.has(name)) return "input";
2486
+ if (name === "select") return "select";
2487
+ if (BUTTON_TAGS.has(name)) return "button";
2488
+ if (name === "a") {
2489
+ if (element && (hasJsxAttribute(element, "href") || hasJsxAttribute(element, "onClick"))) {
2490
+ return "button";
2491
+ }
2492
+ return "view";
2493
+ }
2494
+ if (LIST_TAGS.has(name)) return "list";
2495
+ if (MODAL_TAGS.has(name)) return "modal";
2496
+ if (VIEW_TAGS.has(name)) return "view";
2497
+ return "custom";
2498
+ }
2499
+ if (LIST_COMPONENTS2.has(name)) return "list";
2500
+ if (MODAL_COMPONENTS2.has(name)) return "modal";
2501
+ if (/date/i.test(name)) return "date";
2502
+ if (/(select|picker|dropdown|radio)/i.test(name)) return "select";
2503
+ if (/(checkbox|switch|toggle)/i.test(name)) return "toggle";
2504
+ if (INPUT_COMPONENTS2.has(name)) return "input";
2505
+ if (BUTTON_COMPONENTS2.has(name)) return "button";
2506
+ if (element) {
2507
+ const hasOptions = hasJsxAttribute(element, "options");
2508
+ const hasValue = hasJsxAttribute(element, "value");
2509
+ const hasChecked = hasJsxAttribute(element, "checked") || hasJsxAttribute(element, "selected");
2510
+ const hasOnChange = hasJsxAttribute(element, "onChange") || hasJsxAttribute(element, "onValueChange");
2511
+ const hasOnClick = hasJsxAttribute(element, "onClick");
2512
+ if (hasOptions && (hasValue || hasOnChange)) return "select";
2513
+ if (hasChecked && hasOnChange) return "toggle";
2514
+ if (hasOnChange && (hasJsxAttribute(element, "label") || hasJsxAttribute(element, "placeholder"))) {
2515
+ return "input";
2516
+ }
2517
+ if (hasOnClick) return "button";
2518
+ if (getStringAttr(element, "open") || getExpressionIdentifierAttr(element, "open") || getExpressionIdentifierAttr(element, "isOpen")) {
2519
+ return "modal";
2520
+ }
2521
+ }
2522
+ return "custom";
2523
+ }
2524
+ function getJsxElementName(openingElement) {
2525
+ return getJsxName(openingElement.name);
2526
+ }
2527
+ function getJsxName(name) {
2528
+ if (BabelTypes.isJSXIdentifier(name)) return name.name;
2529
+ if (BabelTypes.isJSXNamespacedName(name)) return `${name.namespace.name}:${name.name.name}`;
2530
+ if (BabelTypes.isJSXMemberExpression(name)) {
2531
+ const objectName = getJsxName(name.object);
2532
+ const propertyName = BabelTypes.isJSXIdentifier(name.property) ? name.property.name : null;
2533
+ return objectName && propertyName ? `${objectName}.${propertyName}` : null;
2534
+ }
2535
+ return null;
2536
+ }
2537
+ var ROUTERISH_OBJECTS = /^(router|history|navigation)$/;
2538
+ function staticRoutePath(node) {
2539
+ if (!node) return void 0;
2540
+ if (BabelTypes.isStringLiteral(node)) return node.value;
2541
+ if (BabelTypes.isTemplateLiteral(node)) {
2542
+ let path9 = "";
2543
+ node.quasis.forEach((quasi, index) => {
2544
+ path9 += quasi.value.cooked ?? quasi.value.raw;
2545
+ const expr = node.expressions[index];
2546
+ if (expr) path9 += `:${paramNameOf(expr)}`;
2547
+ });
2548
+ return path9;
2549
+ }
2550
+ return void 0;
2551
+ }
2552
+ function paramNameOf(expr) {
2553
+ if (BabelTypes.isIdentifier(expr)) return expr.name;
2554
+ if (BabelTypes.isMemberExpression(expr) && BabelTypes.isIdentifier(expr.property)) return expr.property.name;
2555
+ return "param";
2556
+ }
2557
+ function extractWebNavigationCalls(ast) {
2558
+ const calls = [];
2559
+ const inspect = (node) => {
2560
+ if (BabelTypes.isCallExpression(node)) {
2561
+ if (BabelTypes.isIdentifier(node.callee) && node.callee.name === "navigate") {
2562
+ const first = node.arguments[0];
2563
+ const targetPath = staticRoutePath(first);
2564
+ if (targetPath !== void 0) {
2565
+ calls.push({
2566
+ method: hasReplaceOption(node.arguments[1]) ? "replace" : "navigate",
2567
+ targetPath
2568
+ });
2569
+ } else if (BabelTypes.isNumericLiteral(first) || BabelTypes.isUnaryExpression(first) && first.operator === "-") {
2570
+ calls.push({ method: "goBack" });
2571
+ }
2572
+ return;
2573
+ }
2574
+ if (BabelTypes.isMemberExpression(node.callee) && BabelTypes.isIdentifier(node.callee.object) && ROUTERISH_OBJECTS.test(node.callee.object.name) && BabelTypes.isIdentifier(node.callee.property)) {
2575
+ const method = node.callee.property.name;
2576
+ const targetPath = staticRoutePath(node.arguments[0]);
2577
+ if ((method === "push" || method === "navigate") && targetPath !== void 0) {
2578
+ calls.push({ method: "navigate", targetPath });
2579
+ } else if (method === "replace" && targetPath !== void 0) {
2580
+ calls.push({ method: "replace", targetPath });
2581
+ } else if (method === "back" || method === "goBack") {
2582
+ calls.push({ method: "goBack" });
2583
+ }
2584
+ }
2585
+ return;
2586
+ }
2587
+ if (BabelTypes.isAssignmentExpression(node) && BabelTypes.isMemberExpression(node.left) && BabelTypes.isIdentifier(node.left.property) && node.left.property.name === "href" && isLocationExpression(node.left.object) && BabelTypes.isStringLiteral(node.right) && node.right.value.startsWith("/")) {
2588
+ calls.push({ method: "navigate", targetPath: node.right.value });
2589
+ }
2590
+ };
2591
+ traverse4(ast, {
2592
+ noScope: !BabelTypes.isFile(ast),
2593
+ enter: (nodePath) => inspect(nodePath.node)
2594
+ });
2595
+ return calls;
2596
+ }
2597
+ function hasReplaceOption(arg) {
2598
+ if (!arg || !BabelTypes.isObjectExpression(arg)) return false;
2599
+ return arg.properties.some(
2600
+ (prop) => BabelTypes.isObjectProperty(prop) && BabelTypes.isIdentifier(prop.key) && prop.key.name === "replace" && BabelTypes.isBooleanLiteral(prop.value) && prop.value.value === true
2601
+ );
2602
+ }
2603
+ function isLocationExpression(node) {
2604
+ if (BabelTypes.isIdentifier(node)) return node.name === "location";
2605
+ return BabelTypes.isMemberExpression(node) && BabelTypes.isIdentifier(node.object) && node.object.name === "window" && BabelTypes.isIdentifier(node.property) && node.property.name === "location";
2606
+ }
2607
+
2608
+ // src/analyzers/web/WebScreenAnalyzer.ts
2609
+ var DEFAULT_WEB_SCREEN_PATTERNS = [
2610
+ "**/pages/**/*.{ts,tsx,js,jsx}",
2611
+ "**/routes/**/*.{ts,tsx,js,jsx}",
2612
+ "**/views/**/*.{ts,tsx,js,jsx}",
2613
+ "**/app/**/*.{ts,tsx,js,jsx}",
2614
+ "**/*Page.{ts,tsx,js,jsx}",
2615
+ "**/*Screen.{ts,tsx,js,jsx}",
2616
+ "**/*View.{ts,tsx,js,jsx}"
2617
+ ];
2618
+ var DESTRUCTIVE_VERB2 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
2619
+ var LISTISH_TAGS = /* @__PURE__ */ new Set(["ul", "ol", "dl", "table", "tbody"]);
2620
+ var WebScreenAnalyzer = class {
2621
+ config;
2622
+ verbose = process.env.VERBOSE === "true";
2623
+ screenPatterns;
2624
+ constructor(config, options) {
2625
+ this.config = config;
2626
+ this.screenPatterns = options?.screenPatterns ?? DEFAULT_WEB_SCREEN_PATTERNS;
2627
+ }
2628
+ async analyze() {
2629
+ const {
2630
+ include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
2631
+ exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
2632
+ } = this.config;
2633
+ const files = await glob(include, { cwd: this.config.rootDir, ignore: exclude });
2634
+ const patternMatches = await glob(this.screenPatterns, {
2635
+ cwd: this.config.rootDir,
2636
+ ignore: exclude
2637
+ });
2638
+ const patternSet = new Set(patternMatches.map((f) => path8__default.resolve(this.config.rootDir, f)));
2639
+ const candidates = [];
2640
+ for (const file of files) {
2641
+ const filePath = path8__default.resolve(this.config.rootDir, file);
2642
+ try {
2643
+ const candidate = await this.analyzeFile(filePath);
2644
+ if (!candidate) continue;
2645
+ candidate.matchesScreenPattern = patternSet.has(filePath);
2646
+ candidates.push(candidate);
2647
+ if (this.verbose) {
2648
+ console.log(`[WebScreenAnalyzer] \u2713 Analyzed: ${candidate.descriptor.name} (${file})`);
2649
+ }
2650
+ } catch (error) {
2651
+ if (this.verbose) {
2652
+ console.warn(
2653
+ `[WebScreenAnalyzer] Failed to parse ${file}:`,
2654
+ error instanceof Error ? error.message : error
2655
+ );
2656
+ }
2657
+ }
2658
+ }
2659
+ return { candidates, analyzedFiles: files.length };
2660
+ }
2661
+ async analyzeFile(filePath) {
2662
+ const source = await fs.readFile(filePath, "utf-8");
2663
+ const ast = parseSource(source, this.config.parserPlugins);
2664
+ const registerScreenMeta = this.extractRegisterScreenMetadata(ast);
2665
+ const componentName = this.extractComponentName(ast);
2666
+ const labelsByHtmlFor = this.collectHtmlForLabels(ast);
2667
+ const handlerBehaviors = this.collectWebHandlerBehaviors(ast);
2668
+ const forms = this.mergeForms(
2669
+ registerScreenMeta?.forms ?? [],
2670
+ this.extractForms(ast, labelsByHtmlFor)
2671
+ );
2672
+ const actions = this.extractActions(ast, registerScreenMeta, handlerBehaviors);
2673
+ const components = this.extractComponents(ast);
2674
+ const collections = this.extractCollections(ast);
2675
+ const navigationTargets = this.extractNavigationTargets(ast);
2676
+ const permissionsFromJsDoc = extractPermissionsFromJsDoc(source);
2677
+ const destructiveTags = extractDestructiveJsDocTargets(source);
2678
+ if (destructiveTags) {
2679
+ for (const action of actions) {
2680
+ if (destructiveTags === "*" || destructiveTags.has(action.id)) {
2681
+ action.destructive = true;
2682
+ }
2683
+ }
2684
+ }
2685
+ const name = registerScreenMeta?.name || componentName || path8__default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
2686
+ const descriptor = {
2687
+ name,
2688
+ filePath,
2689
+ title: registerScreenMeta?.title,
2690
+ description: registerScreenMeta?.description,
2691
+ components,
2692
+ forms,
2693
+ actions,
2694
+ navigationTargets,
2695
+ ...collections.length > 0 ? { collections } : {},
2696
+ ...registerScreenMeta?.suggestedPrompts && registerScreenMeta.suggestedPrompts.length > 0 ? { suggestedPrompts: registerScreenMeta.suggestedPrompts } : {},
2697
+ ...permissionsFromJsDoc ? {
2698
+ permissions: permissionsFromJsDoc,
2699
+ ...permissionsFromJsDoc.isPii ? { isPii: true } : {}
2700
+ } : {}
2701
+ };
2702
+ return {
2703
+ descriptor,
2704
+ hasRegisterScreen: registerScreenMeta !== null || this.detectRegisterScreenCall(ast),
2705
+ matchesScreenPattern: false
2706
+ };
2707
+ }
2708
+ // ── registerScreen ────────────────────────────────────────────────
2709
+ detectRegisterScreenCall(ast) {
2710
+ let found = false;
2711
+ traverse4(ast, {
2712
+ CallExpression: (nodePath) => {
2713
+ if (found) return;
2714
+ if (isRegisterScreenCallee(nodePath.node.callee)) {
2715
+ found = true;
2716
+ nodePath.stop();
2717
+ }
2718
+ }
2719
+ });
2720
+ return found;
2721
+ }
2722
+ extractRegisterScreenMetadata(ast) {
2723
+ let plain = null;
2724
+ traverse4(ast, {
2725
+ CallExpression: (nodePath) => {
2726
+ if (!isRegisterScreenCallee(nodePath.node.callee)) return;
2727
+ const arg = nodePath.node.arguments[0];
2728
+ if (BabelTypes.isObjectExpression(arg)) {
2729
+ plain = literalToPlain(arg);
2730
+ }
2731
+ }
2732
+ });
2733
+ if (!plain) return null;
2734
+ const meta = plain;
2735
+ const result = {
2736
+ name: typeof meta.name === "string" ? meta.name : "",
2737
+ actions: [],
2738
+ forms: [],
2739
+ navigationTargets: [],
2740
+ components: []
2741
+ };
2742
+ if (typeof meta.title === "string") result.title = meta.title;
2743
+ if (typeof meta.description === "string") result.description = meta.description;
2744
+ if (Array.isArray(meta.suggestedPrompts)) {
2745
+ const prompts = meta.suggestedPrompts.filter((p) => typeof p === "string").map((p) => p.trim()).filter((p) => p.length > 0);
2746
+ if (prompts.length > 0) result.suggestedPrompts = prompts;
2747
+ }
2748
+ if (Array.isArray(meta.actions)) {
2749
+ 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 }));
2750
+ }
2751
+ if (Array.isArray(meta.fields)) {
2752
+ const fields = meta.fields.filter((f) => typeof f === "object" && f !== null).filter((f) => typeof f.id === "string" && f.id.length > 0).map(
2753
+ (f) => ({
2754
+ name: f.id,
2755
+ type: typeof f.type === "string" ? f.type : "text",
2756
+ required: f.required === true,
2757
+ ...typeof f.label === "string" ? { label: f.label } : {},
2758
+ ...typeof f.placeholder === "string" ? { placeholder: f.placeholder } : {},
2759
+ ...f.defaultValue !== void 0 ? { defaultValue: f.defaultValue } : {},
2760
+ ...Array.isArray(f.options) ? { options: f.options } : {}
2761
+ })
2762
+ );
2763
+ if (fields.length > 0) result.forms = [{ id: "default", fields }];
2764
+ }
2765
+ return result;
2766
+ }
2767
+ // ── Component name / structure ────────────────────────────────────
2768
+ /** Default-exported component name, else the first exported capitalized function. */
2769
+ extractComponentName(ast) {
2770
+ let defaultName = "";
2771
+ let firstExported = "";
2772
+ traverse4(ast, {
2773
+ ExportDefaultDeclaration: (nodePath) => {
2774
+ const declaration = nodePath.node.declaration;
2775
+ if (BabelTypes.isFunctionDeclaration(declaration) && declaration.id?.name) {
2776
+ defaultName = declaration.id.name;
2777
+ } else if (BabelTypes.isIdentifier(declaration)) {
2778
+ defaultName = declaration.name;
2779
+ }
2780
+ },
2781
+ ExportNamedDeclaration: (nodePath) => {
2782
+ if (firstExported) return;
2783
+ const declaration = nodePath.node.declaration;
2784
+ if (BabelTypes.isFunctionDeclaration(declaration) && declaration.id && /^[A-Z]/.test(declaration.id.name)) {
2785
+ firstExported = declaration.id.name;
2786
+ } else if (BabelTypes.isVariableDeclaration(declaration)) {
2787
+ for (const declarator of declaration.declarations) {
2788
+ if (BabelTypes.isIdentifier(declarator.id) && /^[A-Z]/.test(declarator.id.name) && (BabelTypes.isArrowFunctionExpression(declarator.init) || BabelTypes.isFunctionExpression(declarator.init))) {
2789
+ firstExported = declarator.id.name;
2790
+ break;
2791
+ }
2792
+ }
2793
+ }
2794
+ }
2795
+ });
2796
+ return defaultName || firstExported;
2797
+ }
2798
+ extractComponents(ast) {
2799
+ const components = [];
2800
+ const seen = /* @__PURE__ */ new Set();
2801
+ traverse4(ast, {
2802
+ JSXOpeningElement: (nodePath) => {
2803
+ const element = nodePath.node;
2804
+ const name = getJsxElementName(element);
2805
+ if (!name || seen.has(name)) return;
2806
+ seen.add(name);
2807
+ const role = classifyWebJsxComponent(name, element);
2808
+ const type = role === "select" || role === "toggle" || role === "date" ? "input" : role === "input" || role === "button" || role === "list" || role === "modal" || role === "view" ? role : "custom";
2809
+ const component = { name, type };
2810
+ const testId = getStringAttr(element, "data-testid") ?? getStringAttr(element, "testID");
2811
+ const ariaLabel = getStringAttr(element, "aria-label");
2812
+ if (testId) component.testID = testId;
2813
+ if (ariaLabel) component.accessibilityLabel = ariaLabel;
2814
+ components.push(component);
2815
+ }
2816
+ });
2817
+ return components;
2818
+ }
2819
+ // ── <label htmlFor> association ───────────────────────────────────
2820
+ collectHtmlForLabels(ast) {
2821
+ const labels = /* @__PURE__ */ new Map();
2822
+ traverse4(ast, {
2823
+ JSXElement: (nodePath) => {
2824
+ const element = nodePath.node;
2825
+ if (getJsxElementName(element.openingElement) !== "label") return;
2826
+ const htmlFor = getStringAttr(element.openingElement, "htmlFor");
2827
+ if (!htmlFor) return;
2828
+ const text = jsxTextContent(element);
2829
+ if (text) labels.set(htmlFor, text);
2830
+ }
2831
+ });
2832
+ return labels;
2833
+ }
2834
+ // ── Forms ─────────────────────────────────────────────────────────
2835
+ extractForms(ast, labelsByHtmlFor) {
2836
+ const formBuckets = /* @__PURE__ */ new Map();
2837
+ const usedIds = /* @__PURE__ */ new Set();
2838
+ let formCount = 0;
2839
+ const uniqueId = (preferred) => {
2840
+ if (!usedIds.has(preferred)) {
2841
+ usedIds.add(preferred);
2842
+ return preferred;
2843
+ }
2844
+ let suffix = 2;
2845
+ while (usedIds.has(`${preferred}-${suffix}`)) suffix += 1;
2846
+ const id = `${preferred}-${suffix}`;
2847
+ usedIds.add(id);
2848
+ return id;
2849
+ };
2850
+ const bucketFor = (formElement) => {
2851
+ let bucket = formBuckets.get(formElement);
2852
+ if (bucket) return bucket;
2853
+ formCount += 1;
2854
+ let preferred = "default";
2855
+ let submitAction;
2856
+ if (formElement) {
2857
+ const opening = formElement.openingElement;
2858
+ preferred = getStringAttr(opening, "id") ?? getStringAttr(opening, "name") ?? getStringAttr(opening, "data-testid") ?? (formCount === 1 ? "default" : `form-${formCount}`);
2859
+ submitAction = this.handlerNameFromAttr(opening, "onSubmit");
2860
+ }
2861
+ bucket = { id: uniqueId(preferred), fields: /* @__PURE__ */ new Map(), submitAction };
2862
+ formBuckets.set(formElement, bucket);
2863
+ return bucket;
2864
+ };
2865
+ traverse4(ast, {
2866
+ JSXElement: (nodePath) => {
2867
+ const element = nodePath.node;
2868
+ const name = getJsxElementName(element.openingElement);
2869
+ if (!name) return;
2870
+ const role = classifyWebJsxComponent(name, element.openingElement);
2871
+ if (!["input", "select", "toggle", "date"].includes(role)) return;
2872
+ if (name === "option") return;
2873
+ const field = this.extractField(element, role, labelsByHtmlFor);
2874
+ if (!field.name) return;
2875
+ const formParent = nodePath.findParent(
2876
+ (p) => p.isJSXElement() && (getJsxElementName(p.node.openingElement) === "form" || getStringAttr(p.node.openingElement, "role") === "form")
2877
+ );
2878
+ const bucket = bucketFor(formParent ? formParent.node : null);
2879
+ if (!bucket.fields.has(field.name)) bucket.fields.set(field.name, field);
2880
+ }
2881
+ });
2882
+ traverse4(ast, {
2883
+ JSXElement: (nodePath) => {
2884
+ const element = nodePath.node;
2885
+ const name = getJsxElementName(element.openingElement);
2886
+ if (!name) return;
2887
+ if (!this.isSubmitElement(name, element.openingElement)) return;
2888
+ const formParent = nodePath.findParent(
2889
+ (p) => p.isJSXElement() && getJsxElementName(p.node.openingElement) === "form"
2890
+ );
2891
+ if (!formParent) return;
2892
+ const bucket = formBuckets.get(formParent.node);
2893
+ if (!bucket) return;
2894
+ const handler = this.handlerNameFromAttr(element.openingElement, "onClick");
2895
+ if (!bucket.submitAction && handler) bucket.submitAction = handler;
2896
+ }
2897
+ });
2898
+ return Array.from(formBuckets.values()).filter((bucket) => bucket.fields.size > 0).map((bucket) => ({
2899
+ id: bucket.id,
2900
+ fields: Array.from(bucket.fields.values()),
2901
+ ...bucket.submitAction && bucket.submitAction !== "anonymous" ? { submitAction: bucket.submitAction } : {}
2902
+ }));
2903
+ }
2904
+ isSubmitElement(name, opening) {
2905
+ const type = getStringAttr(opening, "type");
2906
+ if (name === "button") return type === "submit" || type === void 0;
2907
+ if (name === "input") return type === "submit";
2908
+ return false;
2909
+ }
2910
+ extractField(element, role, labelsByHtmlFor) {
2911
+ const opening = element.openingElement;
2912
+ const componentName = getJsxElementName(opening) ?? void 0;
2913
+ const field = {
2914
+ name: "",
2915
+ type: "text",
2916
+ required: false,
2917
+ sourceComponent: componentName
2918
+ };
2919
+ const appilotsId = getStringAttr(opening, "appilotsId");
2920
+ const dataTestId = getStringAttr(opening, "data-testid");
2921
+ const domId = getStringAttr(opening, "id");
2922
+ const nameAttr = getStringAttr(opening, "name");
2923
+ const ariaLabel = getStringAttr(opening, "aria-label");
2924
+ const placeholder = getStringAttr(opening, "placeholder");
2925
+ if (placeholder) field.placeholder = placeholder;
2926
+ if (ariaLabel) field.label = ariaLabel;
2927
+ if (domId && labelsByHtmlFor.has(domId)) field.label = labelsByHtmlFor.get(domId);
2928
+ if (appilotsId) {
2929
+ field.name = appilotsId;
2930
+ field.locator = mergeLocator(field.locator, {
2931
+ id: appilotsId,
2932
+ appilotsId,
2933
+ source: "appilotsId"
2934
+ });
2935
+ }
2936
+ if (dataTestId) {
2937
+ if (!field.name) field.name = dataTestId.replace(/^(input-|field-|txt-)/, "");
2938
+ field.locator = mergeLocator(field.locator, {
2939
+ ...field.locator?.id ? {} : { id: dataTestId },
2940
+ testID: dataTestId,
2941
+ source: field.locator?.source ?? "data-testid"
2942
+ });
2943
+ }
2944
+ if (nameAttr && !field.name) field.name = nameAttr;
2945
+ if (domId) {
2946
+ if (!field.name) field.name = domId;
2947
+ field.locator = mergeLocator(field.locator, {
2948
+ ...field.locator?.id ? {} : { id: domId },
2949
+ source: field.locator?.source ?? "id"
2950
+ });
2951
+ }
2952
+ if (ariaLabel) {
2953
+ field.locator = mergeLocator(field.locator, {
2954
+ ...field.locator?.id ? {} : { id: slugify(ariaLabel) },
2955
+ accessibilityLabel: ariaLabel,
2956
+ source: field.locator?.source ?? "aria-label"
2957
+ });
2958
+ }
2959
+ for (const attr of opening.attributes) {
2960
+ if (!BabelTypes.isJSXAttribute(attr) || !BabelTypes.isJSXIdentifier(attr.name)) continue;
2961
+ const attrName = attr.name.name;
2962
+ if (attrName === "required") {
2963
+ if (attr.value === null) field.required = true;
2964
+ else if (BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isBooleanLiteral(attr.value.expression)) {
2965
+ field.required = attr.value.expression.value;
2966
+ }
2967
+ }
2968
+ if ((attrName === "value" || attrName === "checked") && attr.value && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
2969
+ field.valueBinding = attr.value.expression.name;
2970
+ if (!field.name) field.name = attr.value.expression.name;
2971
+ }
2972
+ }
2973
+ if (getStringAttr(opening, "aria-required") === "true") field.required = true;
2974
+ if (role === "select") field.type = "select";
2975
+ else if (role === "toggle") field.type = "toggle";
2976
+ else if (role === "date") field.type = "date";
2977
+ else {
2978
+ field.type = this.inferInputType(opening, field);
2979
+ }
2980
+ if (componentName === "select") {
2981
+ const options = this.extractSelectOptions(element);
2982
+ if (options.length > 0) field.options = options;
2983
+ }
2984
+ if (!field.name || isWeakInferredFieldName(field.name)) {
2985
+ const labelish = field.label ?? field.placeholder;
2986
+ if (labelish) field.name = slugify(labelish);
2987
+ }
2988
+ if (!field.locator && field.name) {
2989
+ field.locator = { id: field.name, label: field.label, source: "inferred" };
2990
+ } else if (field.locator && !field.locator.id && field.name) {
2991
+ field.locator = mergeLocator(field.locator, {
2992
+ id: field.name,
2993
+ label: field.label,
2994
+ source: field.locator.source ?? "inferred"
2995
+ });
2996
+ }
2997
+ return field;
2998
+ }
2999
+ inferInputType(opening, field) {
3000
+ const type = getStringAttr(opening, "type");
3001
+ if (type === "email") return "email";
3002
+ if (type === "tel") return "phone";
3003
+ if (type === "number") return "number";
3004
+ if (type === "date" || type === "datetime-local" || type === "month" || type === "week") return "date";
3005
+ const inputMode = getStringAttr(opening, "inputMode") ?? getStringAttr(opening, "inputmode");
3006
+ if (inputMode === "email") return "email";
3007
+ if (inputMode === "tel") return "phone";
3008
+ if (inputMode === "numeric" || inputMode === "decimal") return "number";
3009
+ const combined = `${field.name} ${field.label ?? ""} ${field.placeholder ?? ""}`.toLowerCase();
3010
+ if (combined.includes("email")) return "email";
3011
+ if (combined.includes("phone") || combined.includes("tel")) return "phone";
3012
+ return "text";
3013
+ }
3014
+ extractSelectOptions(selectElement) {
3015
+ const options = [];
3016
+ for (const child of selectElement.children) {
3017
+ if (!BabelTypes.isJSXElement(child)) continue;
3018
+ if (getJsxElementName(child.openingElement) !== "option") continue;
3019
+ const value = getStringAttr(child.openingElement, "value");
3020
+ const label = jsxTextContent(child) || value || "";
3021
+ if (label && value) options.push({ label, value });
3022
+ }
3023
+ return options;
3024
+ }
3025
+ mergeForms(primary, secondary) {
3026
+ const out = primary.map((form) => ({ ...form, fields: [...form.fields] }));
3027
+ for (const form of secondary) {
3028
+ const existing = out.find((candidate) => candidate.id === form.id);
3029
+ if (!existing) {
3030
+ out.push({ ...form, fields: [...form.fields] });
3031
+ continue;
3032
+ }
3033
+ for (const field of form.fields) {
3034
+ const existingField = existing.fields.find((candidate) => candidate.name === field.name);
3035
+ if (!existingField) {
3036
+ existing.fields.push(field);
3037
+ continue;
3038
+ }
3039
+ existingField.label = existingField.label ?? field.label;
3040
+ existingField.placeholder = existingField.placeholder ?? field.placeholder;
3041
+ existingField.options = existingField.options ?? field.options;
3042
+ existingField.locator = existingField.locator ?? field.locator;
3043
+ existingField.sourceComponent = existingField.sourceComponent ?? field.sourceComponent;
3044
+ existingField.valueBinding = existingField.valueBinding ?? field.valueBinding;
3045
+ existingField.required = existingField.required || field.required;
3046
+ if (existingField.type === "text" && field.type !== "text") existingField.type = field.type;
3047
+ }
3048
+ existing.submitAction = existing.submitAction ?? form.submitAction;
3049
+ }
3050
+ return out;
3051
+ }
3052
+ // ── Actions ───────────────────────────────────────────────────────
3053
+ extractActions(ast, registerScreenMeta, handlerBehaviors) {
3054
+ const actions = [...registerScreenMeta?.actions ?? []];
3055
+ const actionIds = new Set(actions.map((a) => a.id));
3056
+ const actionLabels = new Map(
3057
+ actions.filter((a) => a.label).map((a) => [normalizeLabel(a.label), a])
3058
+ );
3059
+ traverse4(ast, {
3060
+ JSXElement: (nodePath) => {
3061
+ const element = nodePath.node;
3062
+ const name = getJsxElementName(element.openingElement);
3063
+ if (!name) return;
3064
+ if (classifyWebJsxComponent(name, element.openingElement) !== "button") return;
3065
+ const action = this.extractActionFromElement(element, nodePath);
3066
+ if (!action) return;
3067
+ const existingByLabel = action.label ? actionLabels.get(normalizeLabel(action.label)) : void 0;
3068
+ if (existingByLabel) {
3069
+ this.mergeActionMetadata(existingByLabel, action);
3070
+ return;
3071
+ }
3072
+ if (action.id && !actionIds.has(action.id)) {
3073
+ actions.push(action);
3074
+ actionIds.add(action.id);
3075
+ if (action.label) actionLabels.set(normalizeLabel(action.label), action);
3076
+ }
3077
+ }
3078
+ });
3079
+ this.enrichActionsFromHandlers(actions, handlerBehaviors);
3080
+ return actions;
3081
+ }
3082
+ extractActionFromElement(element, nodePath) {
3083
+ const opening = element.openingElement;
3084
+ const componentName = getJsxElementName(opening) ?? void 0;
3085
+ const action = { id: "", type: "custom", sourceComponent: componentName };
3086
+ const ariaLabel = getStringAttr(opening, "aria-label");
3087
+ const label = ariaLabel ?? jsxTextContent(element) ?? getStringAttr(opening, "value") ?? getStringAttr(opening, "title");
3088
+ if (label) action.label = label;
3089
+ if (ariaLabel) {
3090
+ action.locator = mergeLocator(action.locator, {
3091
+ accessibilityLabel: ariaLabel,
3092
+ source: "aria-label"
3093
+ });
3094
+ }
3095
+ const appilotsId = getStringAttr(opening, "appilotsId");
3096
+ const dataTestId = getStringAttr(opening, "data-testid");
3097
+ const domId = getStringAttr(opening, "id");
3098
+ if (appilotsId) {
3099
+ action.id = appilotsId;
3100
+ action.locator = mergeLocator(action.locator, {
3101
+ id: appilotsId,
3102
+ appilotsId,
3103
+ source: "appilotsId"
3104
+ });
3105
+ } else if (dataTestId) {
3106
+ action.id = dataTestId;
3107
+ action.locator = mergeLocator(action.locator, {
3108
+ id: dataTestId,
3109
+ testID: dataTestId,
3110
+ source: "data-testid"
3111
+ });
3112
+ } else if (domId) {
3113
+ action.id = domId;
3114
+ action.locator = mergeLocator(action.locator, { id: domId, source: "id" });
3115
+ } else if (action.label) {
3116
+ action.id = slugify(action.label);
3117
+ }
3118
+ if (!action.id) return null;
3119
+ const to = routePathAttr(opening, "to") ?? routePathAttr(opening, "href");
3120
+ if (to && to.startsWith("/")) {
3121
+ action.type = "navigation";
3122
+ action.targetScreen = to;
3123
+ } else if (to && !to.startsWith("/") && !hasJsxAttribute(opening, "onClick")) {
3124
+ return null;
3125
+ }
3126
+ let handlerName;
3127
+ const onClickAttr = opening.attributes.find(
3128
+ (attr) => BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name) && attr.name.name === "onClick"
3129
+ );
3130
+ if (onClickAttr?.value && BabelTypes.isJSXExpressionContainer(onClickAttr.value)) {
3131
+ const expr = onClickAttr.value.expression;
3132
+ if (BabelTypes.isIdentifier(expr)) {
3133
+ handlerName = expr.name;
3134
+ action.handler = expr.name;
3135
+ } else if (!BabelTypes.isJSXEmptyExpression(expr)) {
3136
+ const inlineNavCalls = extractWebNavigationCalls(expr);
3137
+ const inlineNav = inlineNavCalls.find((call) => call.targetPath);
3138
+ if (inlineNav?.targetPath) {
3139
+ action.type = "navigation";
3140
+ action.targetScreen = inlineNav.targetPath;
3141
+ } else if (inlineNavCalls.some((call) => call.method === "goBack")) {
3142
+ action.type = "navigation";
3143
+ action.successSignal = {
3144
+ type: "goBack",
3145
+ description: "Action returns to the previous page"
3146
+ };
3147
+ action.appilotsInferred = {
3148
+ ...action.appilotsInferred ?? {},
3149
+ expectedOutcome: "navigation"
3150
+ };
3151
+ }
3152
+ const inlineHandler = firstCalledFunctionName(expr);
3153
+ if (inlineHandler) {
3154
+ handlerName = inlineHandler;
3155
+ action.handler = inlineHandler;
3156
+ }
3157
+ }
3158
+ }
3159
+ if (handlerName) {
3160
+ const lower = handlerName.toLowerCase();
3161
+ if (lower.includes("submit")) action.type = "submit";
3162
+ else if (lower.includes("navigate") && action.type === "custom") action.type = "navigation";
3163
+ }
3164
+ if (componentName && this.isSubmitElement(componentName, opening)) {
3165
+ const formParent = nodePath.findParent(
3166
+ (p) => p.isJSXElement() && getJsxElementName(p.node.openingElement) === "form"
3167
+ );
3168
+ if (formParent) {
3169
+ action.type = "submit";
3170
+ if (!action.handler) {
3171
+ const formHandler = this.handlerNameFromAttr(formParent.node.openingElement, "onSubmit");
3172
+ if (formHandler && formHandler !== "anonymous") {
3173
+ action.handler = formHandler;
3174
+ handlerName = formHandler;
3175
+ }
3176
+ }
3177
+ }
3178
+ }
3179
+ if (hasJsxAttribute(opening, "destructive") || hasJsxAttribute(opening, "aria-destructive")) {
3180
+ const value = getStringAttr(opening, "destructive") ?? getStringAttr(opening, "aria-destructive");
3181
+ action.destructive = value !== "false";
3182
+ } else if (handlerName && DESTRUCTIVE_VERB2.test(handlerName) || DESTRUCTIVE_VERB2.test(action.id)) {
3183
+ action.destructive = true;
3184
+ }
3185
+ if (!action.locator && action.id) {
3186
+ action.locator = {
3187
+ id: action.id,
3188
+ label: action.label,
3189
+ source: action.label ? "label" : "inferred"
3190
+ };
3191
+ }
3192
+ return action;
3193
+ }
3194
+ mergeActionMetadata(target, source) {
3195
+ target.handler = target.handler ?? source.handler;
3196
+ target.targetScreen = target.targetScreen ?? source.targetScreen;
3197
+ target.description = target.description ?? source.description;
3198
+ target.locator = target.locator ?? source.locator;
3199
+ target.nativeConfirmationExpected = target.nativeConfirmationExpected || source.nativeConfirmationExpected || void 0;
3200
+ target.requiresConfirmation = target.requiresConfirmation || source.requiresConfirmation || void 0;
3201
+ target.destructive = target.destructive || source.destructive || void 0;
3202
+ target.effect = target.effect ?? source.effect;
3203
+ target.riskLevel = target.riskLevel ?? source.riskLevel;
3204
+ target.appilotsInferred = target.appilotsInferred ?? source.appilotsInferred;
3205
+ }
3206
+ enrichActionsFromHandlers(actions, behaviors) {
3207
+ for (const action of actions) {
3208
+ const behavior = action.handler ? behaviors.get(action.handler) : void 0;
3209
+ if (!behavior) continue;
3210
+ action.appilotsInferred = {
3211
+ ...action.appilotsInferred ?? {},
3212
+ ...behavior.base.appilotsInferred
3213
+ };
3214
+ if (behavior.nativeConfirmationExpected || behavior.base.nativeConfirmationExpected) {
3215
+ action.nativeConfirmationExpected = true;
3216
+ }
3217
+ if (behavior.targetPath && !action.targetScreen) {
3218
+ action.targetScreen = behavior.targetPath;
3219
+ if (action.type === "custom") action.type = "navigation";
3220
+ action.appilotsInferred = {
3221
+ ...action.appilotsInferred ?? {},
3222
+ expectedOutcome: "navigation"
3223
+ };
3224
+ }
3225
+ if (behavior.base.successSignal && !action.successSignal) {
3226
+ action.successSignal = behavior.base.successSignal;
3227
+ }
3228
+ if (behavior.base.failureSignal && !action.failureSignal) {
3229
+ action.failureSignal = behavior.base.failureSignal;
3230
+ }
3231
+ if (behavior.base.opensModal && !action.opensModal) {
3232
+ action.opensModal = behavior.base.opensModal;
3233
+ }
3234
+ if (behavior.base.destructive || action.destructive === true || action.requiresConfirmation === true || action.effect === "destructive" || action.riskLevel === "high") {
3235
+ action.destructive = true;
3236
+ action.effect = action.effect ?? "destructive";
3237
+ action.riskLevel = action.riskLevel ?? "high";
3238
+ action.requiresConfirmation = action.requiresConfirmation ?? true;
3239
+ }
3240
+ }
3241
+ }
3242
+ /**
3243
+ * Handler behavior via the shared, platform-neutral analyzer
3244
+ * (async/await, `.then`, state setters, toasts, destructive verbs)
3245
+ * plus the web-only signals: React Router navigation targets and
3246
+ * `window.confirm(...)` as the native confirmation dialog.
3247
+ */
3248
+ collectWebHandlerBehaviors(ast) {
3249
+ const handlers = collectFunctions(ast);
3250
+ const out = /* @__PURE__ */ new Map();
3251
+ for (const [name, fn] of handlers) {
3252
+ const base = analyzeFunctionBehavior(name, fn, handlers);
3253
+ const navCalls = fn.body ? extractWebNavigationCalls(fn.body) : [];
3254
+ const firstNav = navCalls.find((call) => call.targetPath);
3255
+ const goesBack = navCalls.some((call) => call.method === "goBack");
3256
+ out.set(name, {
3257
+ base: {
3258
+ ...base,
3259
+ ...goesBack && !base.successSignal ? {
3260
+ successSignal: {
3261
+ type: "goBack",
3262
+ description: "Action returns to the previous page"
3263
+ }
3264
+ } : {}
3265
+ },
3266
+ targetPath: firstNav?.targetPath,
3267
+ nativeConfirmationExpected: fn.body ? containsWindowConfirm(fn.body) : false
3268
+ });
3269
+ }
3270
+ return out;
3271
+ }
3272
+ handlerNameFromAttr(opening, attrName) {
3273
+ const attr = opening.attributes.find(
3274
+ (candidate) => BabelTypes.isJSXAttribute(candidate) && BabelTypes.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
3275
+ );
3276
+ if (!attr?.value || !BabelTypes.isJSXExpressionContainer(attr.value)) return void 0;
3277
+ const expr = attr.value.expression;
3278
+ if (BabelTypes.isIdentifier(expr)) return expr.name;
3279
+ if (BabelTypes.isArrowFunctionExpression(expr) || BabelTypes.isFunctionExpression(expr)) {
3280
+ return firstCalledFunctionName(expr) ?? "anonymous";
3281
+ }
3282
+ return void 0;
3283
+ }
3284
+ // ── Navigation targets (route paths — resolved by the orchestrator) ─
3285
+ extractNavigationTargets(ast) {
3286
+ const targets = /* @__PURE__ */ new Set();
3287
+ for (const call of extractWebNavigationCalls(ast)) {
3288
+ if (call.targetPath && call.targetPath.startsWith("/")) targets.add(call.targetPath);
3289
+ }
3290
+ traverse4(ast, {
3291
+ JSXOpeningElement: (nodePath) => {
3292
+ const element = nodePath.node;
3293
+ const name = getJsxElementName(element);
3294
+ if (name === "Link" || name === "NavLink" || name === "Navigate") {
3295
+ const to = routePathAttr(element, "to");
3296
+ if (to && to.startsWith("/")) targets.add(to);
3297
+ }
3298
+ if (name === "a") {
3299
+ const href = routePathAttr(element, "href");
3300
+ if (href && href.startsWith("/")) targets.add(href);
3301
+ }
3302
+ }
3303
+ });
3304
+ return Array.from(targets).sort();
3305
+ }
3306
+ // ── Collections ───────────────────────────────────────────────────
3307
+ extractCollections(ast) {
3308
+ const collections = [];
3309
+ const seen = /* @__PURE__ */ new Set();
3310
+ traverse4(ast, {
3311
+ JSXExpressionContainer: (nodePath) => {
3312
+ const expr = nodePath.node.expression;
3313
+ if (!BabelTypes.isCallExpression(expr) || !BabelTypes.isMemberExpression(expr.callee) || !BabelTypes.isIdentifier(expr.callee.object) || !BabelTypes.isIdentifier(expr.callee.property) || expr.callee.property.name !== "map") {
3314
+ return;
3315
+ }
3316
+ const callback = expr.arguments[0];
3317
+ if (!BabelTypes.isArrowFunctionExpression(callback) && !BabelTypes.isFunctionExpression(callback)) return;
3318
+ const enclosing = nodePath.findParent((p) => p.isJSXElement());
3319
+ const enclosingName = enclosing ? getJsxElementName(enclosing.node.openingElement) : null;
3320
+ const enclosingRole = enclosing && enclosingName ? classifyWebJsxComponent(enclosingName, enclosing.node.openingElement) : null;
3321
+ const returnsListItem = callbackReturnsTag(callback, /* @__PURE__ */ new Set(["li", "tr"]));
3322
+ if (enclosingName === "select") return;
3323
+ const isListContext = enclosingName !== null && LISTISH_TAGS.has(enclosingName) || enclosingRole === "list" || returnsListItem;
3324
+ if (!isListContext) return;
3325
+ const dataSource = expr.callee.object.name;
3326
+ if (seen.has(dataSource)) return;
3327
+ seen.add(dataSource);
3328
+ const itemNames = collectionItemNames(callback);
3329
+ const displayFields = collectionDisplayFields(callback, itemNames);
3330
+ const keyField = collectionKeyField(callback, itemNames);
3331
+ const rowAction = collectionRowAction(callback);
3332
+ const itemType = inferItemType(dataSource, displayFields);
3333
+ const identityFields = inferIdentityFields(keyField, displayFields);
3334
+ collections.push({
3335
+ id: dataSource,
3336
+ component: enclosingName ?? "list",
3337
+ ...itemType ? { itemType } : {},
3338
+ dataSource,
3339
+ ...keyField ? { keyField } : {},
3340
+ ...displayFields.length > 0 ? { displayFields } : {},
3341
+ ...rowAction ? { rowAction, rowActions: [rowAction] } : {},
3342
+ ...identityFields.length > 0 ? { identityFields } : {}
3343
+ });
3344
+ }
3345
+ });
3346
+ return collections;
3347
+ }
3348
+ };
3349
+ function isRegisterScreenCallee(callee) {
3350
+ return BabelTypes.isIdentifier(callee) && callee.name === "registerScreen" || BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.property) && callee.property.name === "registerScreen";
3351
+ }
3352
+ function literalToPlain(node) {
3353
+ if (BabelTypes.isStringLiteral(node) || BabelTypes.isNumericLiteral(node) || BabelTypes.isBooleanLiteral(node)) {
3354
+ return node.value;
3355
+ }
3356
+ if (BabelTypes.isNullLiteral(node)) return null;
3357
+ if (BabelTypes.isArrayExpression(node)) {
3358
+ return node.elements.filter((el) => el !== null && BabelTypes.isExpression(el)).map((el) => literalToPlain(el)).filter((value) => value !== void 0);
3359
+ }
3360
+ if (BabelTypes.isObjectExpression(node)) {
3361
+ const out = {};
3362
+ for (const prop of node.properties) {
3363
+ if (!BabelTypes.isObjectProperty(prop)) continue;
3364
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : BabelTypes.isStringLiteral(prop.key) ? prop.key.value : void 0;
3365
+ if (!key || !BabelTypes.isExpression(prop.value)) continue;
3366
+ const value = literalToPlain(prop.value);
3367
+ if (value !== void 0) out[key] = value;
3368
+ }
3369
+ return out;
3370
+ }
3371
+ return void 0;
3372
+ }
3373
+ function jsxTextContent(element) {
3374
+ const parts = [];
3375
+ const walk = (children) => {
3376
+ for (const child of children) {
3377
+ if (BabelTypes.isJSXText(child)) {
3378
+ const trimmed = child.value.replace(/\s+/g, " ").trim();
3379
+ if (trimmed) parts.push(trimmed);
3380
+ } else if (BabelTypes.isJSXExpressionContainer(child) && BabelTypes.isStringLiteral(child.expression)) {
3381
+ parts.push(child.expression.value);
3382
+ } else if (BabelTypes.isJSXElement(child)) {
3383
+ walk(child.children);
3384
+ }
3385
+ }
3386
+ };
3387
+ walk(element.children);
3388
+ const text = parts.join(" ").trim();
3389
+ return text.length > 0 ? text : void 0;
3390
+ }
3391
+ function firstCalledFunctionName(node) {
3392
+ if (BabelTypes.isIdentifier(node)) return node.name;
3393
+ if (BabelTypes.isArrowFunctionExpression(node) || BabelTypes.isFunctionExpression(node)) {
3394
+ return firstCalledFunctionName(node.body);
3395
+ }
3396
+ if (BabelTypes.isBlockStatement(node)) {
3397
+ for (const statement of node.body) {
3398
+ const handler = firstCalledFunctionName(statement);
3399
+ if (handler) return handler;
3400
+ }
3401
+ return void 0;
3402
+ }
3403
+ if (BabelTypes.isExpressionStatement(node)) return firstCalledFunctionName(node.expression);
3404
+ if (BabelTypes.isReturnStatement(node)) {
3405
+ return node.argument ? firstCalledFunctionName(node.argument) : void 0;
3406
+ }
3407
+ if (BabelTypes.isAwaitExpression(node) || BabelTypes.isUnaryExpression(node)) {
3408
+ return firstCalledFunctionName(node.argument);
3409
+ }
3410
+ if (BabelTypes.isCallExpression(node)) {
3411
+ if (BabelTypes.isIdentifier(node.callee) && !/^(navigate|confirm|alert)$/.test(node.callee.name)) {
3412
+ return node.callee.name;
3413
+ }
3414
+ return void 0;
3415
+ }
3416
+ return void 0;
3417
+ }
3418
+ function containsWindowConfirm(body) {
3419
+ let found = false;
3420
+ traverse4(body, {
3421
+ noScope: true,
3422
+ CallExpression: (nodePath) => {
3423
+ const callee = nodePath.node.callee;
3424
+ if (BabelTypes.isIdentifier(callee) && callee.name === "confirm") found = true;
3425
+ if (BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.object) && callee.object.name === "window" && BabelTypes.isIdentifier(callee.property) && callee.property.name === "confirm") {
3426
+ found = true;
3427
+ }
3428
+ }
3429
+ });
3430
+ return found;
3431
+ }
3432
+ function mergeLocator(current, next) {
3433
+ return { ...current ?? {}, ...next };
3434
+ }
3435
+ function routePathAttr(opening, attrName) {
3436
+ const literal = getStringAttr(opening, attrName);
3437
+ if (literal) return literal;
3438
+ const attr = opening.attributes.find(
3439
+ (candidate) => BabelTypes.isJSXAttribute(candidate) && BabelTypes.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
3440
+ );
3441
+ if (!attr?.value || !BabelTypes.isJSXExpressionContainer(attr.value)) return void 0;
3442
+ return staticRoutePath(attr.value.expression);
3443
+ }
3444
+ function slugify(label) {
3445
+ return label.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3446
+ }
3447
+ function normalizeLabel(label) {
3448
+ return label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]/g, "");
3449
+ }
3450
+ function isWeakInferredFieldName(name) {
3451
+ return /^(text|value|input|query|search|selected|checked)$/i.test(name);
3452
+ }
3453
+ function collectionItemNames(callback) {
3454
+ const names = /* @__PURE__ */ new Set(["item"]);
3455
+ const firstParam = callback.params[0];
3456
+ if (BabelTypes.isIdentifier(firstParam)) names.add(firstParam.name);
3457
+ if (BabelTypes.isObjectPattern(firstParam)) {
3458
+ for (const prop of firstParam.properties) {
3459
+ if (BabelTypes.isObjectProperty(prop) && BabelTypes.isIdentifier(prop.key) && BabelTypes.isIdentifier(prop.value)) {
3460
+ names.add(prop.value.name);
3461
+ }
3462
+ }
3463
+ }
3464
+ return names;
3465
+ }
3466
+ function collectionDisplayFields(callback, itemNames) {
3467
+ const fields = /* @__PURE__ */ new Set();
3468
+ if (!callback.body) return [];
3469
+ traverse4(callback.body, {
3470
+ noScope: true,
3471
+ MemberExpression: (nodePath) => {
3472
+ const node = nodePath.node;
3473
+ if (BabelTypes.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes.isIdentifier(node.property)) {
3474
+ fields.add(node.property.name);
3475
+ }
3476
+ }
3477
+ });
3478
+ return Array.from(fields).sort();
3479
+ }
3480
+ function collectionKeyField(callback, itemNames) {
3481
+ let keyField;
3482
+ if (!callback.body) return void 0;
3483
+ traverse4(callback.body, {
3484
+ noScope: true,
3485
+ JSXAttribute: (nodePath) => {
3486
+ const attr = nodePath.node;
3487
+ if (!BabelTypes.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
3488
+ if (!attr.value || !BabelTypes.isJSXExpressionContainer(attr.value)) return;
3489
+ const expr = attr.value.expression;
3490
+ if (BabelTypes.isMemberExpression(expr) && BabelTypes.isIdentifier(expr.object) && itemNames.has(expr.object.name) && BabelTypes.isIdentifier(expr.property)) {
3491
+ keyField = keyField ?? expr.property.name;
3492
+ }
3493
+ }
3494
+ });
3495
+ return keyField;
3496
+ }
3497
+ function collectionRowAction(callback) {
3498
+ if (!callback.body) return void 0;
3499
+ const navCall = extractWebNavigationCalls(callback.body).find((call) => call.targetPath);
3500
+ if (!navCall?.targetPath) return void 0;
3501
+ return {
3502
+ type: "navigation",
3503
+ // Route path — the orchestrator resolves it to a screen name.
3504
+ targetScreen: navCall.targetPath,
3505
+ description: `Clicking a row opens ${navCall.targetPath}`
3506
+ };
3507
+ }
3508
+ function callbackReturnsTag(callback, tags) {
3509
+ let found = false;
3510
+ const inspect = (node) => {
3511
+ if (!node || found) return;
3512
+ if (BabelTypes.isJSXElement(node)) {
3513
+ const name = getJsxElementName(node.openingElement);
3514
+ if (name && tags.has(name)) found = true;
3515
+ return;
3516
+ }
3517
+ if (BabelTypes.isBlockStatement(node)) {
3518
+ for (const statement of node.body) {
3519
+ if (BabelTypes.isReturnStatement(statement)) inspect(statement.argument);
3520
+ }
3521
+ }
3522
+ if (BabelTypes.isParenthesizedExpression(node)) inspect(node.expression);
3523
+ if (BabelTypes.isConditionalExpression(node)) {
3524
+ inspect(node.consequent);
3525
+ inspect(node.alternate);
3526
+ }
3527
+ };
3528
+ inspect(callback.body);
3529
+ return found;
3530
+ }
3531
+ function inferItemType(dataSource, displayFields) {
3532
+ const singular = dataSource.replace(/^render/i, "").replace(/(List|Items|Data|Rows|Sections)$/i, "").replace(/s$/i, "");
3533
+ const candidate = singular.charAt(0).toUpperCase() + singular.slice(1);
3534
+ if (candidate.length > 1) return candidate;
3535
+ if (displayFields.length > 0) return "Item";
3536
+ return void 0;
3537
+ }
3538
+ function inferIdentityFields(keyField, displayFields) {
3539
+ const out = /* @__PURE__ */ new Set();
3540
+ if (keyField) out.add(keyField);
3541
+ for (const field of displayFields) {
3542
+ if (/^(id|uuid|key|name|title|plate|email|slug)$/i.test(field)) out.add(field);
3543
+ }
3544
+ return Array.from(out);
3545
+ }
3546
+ var WEB_NAVIGATOR_TYPE = "route";
3547
+ var WebNavigationAnalyzer = class {
3548
+ config;
3549
+ navigationInclude;
3550
+ navigationExclude;
3551
+ constructor(config, options) {
3552
+ this.config = config;
3553
+ this.navigationInclude = options?.navigationInclude ?? [];
3554
+ this.navigationExclude = options?.navigationExclude ?? [];
3555
+ }
3556
+ async analyze() {
3557
+ const files = await this.findRouteFiles();
3558
+ const routes = [];
3559
+ for (const filePath of files) {
3560
+ try {
3561
+ const content = await promises.readFile(filePath, "utf-8");
3562
+ if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(content)) {
3563
+ continue;
3564
+ }
3565
+ const ast = parseSource(content, this.config.parserPlugins);
3566
+ routes.push(...this.extractJsxRoutes(ast));
3567
+ routes.push(...this.extractObjectRoutes(ast));
3568
+ } catch (error) {
3569
+ console.warn(`[WebNavigationAnalyzer] Failed to parse ${filePath}:`, error);
3570
+ }
3571
+ }
3572
+ const deduped = this.dedupeRoutes(routes);
3573
+ return { graph: this.buildGraph(deduped), routes: deduped };
3574
+ }
3575
+ /** Files likely to contain route configuration. */
3576
+ async findRouteFiles() {
3577
+ const patterns = [
3578
+ "**/*{router,routes,Router,Routes}*.{ts,tsx,js,jsx}",
3579
+ "**/App.{ts,tsx,js,jsx}",
3580
+ "**/app.{ts,tsx,js,jsx}",
3581
+ "**/main.{ts,tsx,js,jsx}",
3582
+ "**/index.{ts,tsx,js,jsx}",
3583
+ ...this.navigationInclude
3584
+ ];
3585
+ const ignore = [
3586
+ "**/node_modules/**",
3587
+ "**/dist/**",
3588
+ "**/build/**",
3589
+ ...this.config.exclude || [],
3590
+ ...this.navigationExclude
3591
+ ];
3592
+ const files = await glob(patterns, { cwd: this.config.rootDir, ignore });
3593
+ return files.map((file) => path8__default.join(this.config.rootDir, file));
3594
+ }
3595
+ // ── JSX <Route> style ────────────────────────────────────────────
3596
+ extractJsxRoutes(ast) {
3597
+ const routes = [];
3598
+ const visitRoute = (element, parentPath) => {
3599
+ const opening = element.openingElement;
3600
+ const name = getJsxElementName(opening);
3601
+ if (name !== "Route") {
3602
+ for (const child of element.children) {
3603
+ if (BabelTypes.isJSXElement(child)) visitRoute(child, parentPath);
3604
+ }
3605
+ return;
3606
+ }
3607
+ const segment = getStringAttr(opening, "path");
3608
+ const isIndex = hasJsxAttribute(opening, "index") && !segment;
3609
+ const fullPath = this.joinPaths(parentPath, segment, isIndex);
3610
+ const componentName = this.componentNameFromElementAttr(opening) ?? void 0;
3611
+ const isLeaf = !element.children.some(
3612
+ (child) => BabelTypes.isJSXElement(child) && getJsxElementName(child.openingElement) === "Route"
3613
+ );
3614
+ if ((segment || isIndex) && (componentName || isLeaf)) {
3615
+ routes.push(this.buildRoute(fullPath, componentName, isIndex, !isLeaf));
3616
+ }
3617
+ for (const child of element.children) {
3618
+ if (BabelTypes.isJSXElement(child)) visitRoute(child, fullPath);
3619
+ }
3620
+ };
3621
+ traverse4(ast, {
3622
+ JSXElement: (nodePath) => {
3623
+ const name = getJsxElementName(nodePath.node.openingElement);
3624
+ if (name !== "Routes" && name !== "Route") return;
3625
+ if (nodePath.findParent((p) => {
3626
+ if (!p.isJSXElement()) return false;
3627
+ const parentName = getJsxElementName(p.node.openingElement);
3628
+ return parentName === "Routes" || parentName === "Route";
3629
+ })) {
3630
+ return;
3631
+ }
3632
+ visitRoute(nodePath.node, "");
3633
+ }
3634
+ });
3635
+ return routes;
3636
+ }
3637
+ /** `element={<VehicleList/>}` or `Component={VehicleList}`. */
3638
+ componentNameFromElementAttr(opening) {
3639
+ for (const attr of opening.attributes) {
3640
+ if (!BabelTypes.isJSXAttribute(attr) || !BabelTypes.isJSXIdentifier(attr.name)) continue;
3641
+ if (attr.name.name === "element" && BabelTypes.isJSXExpressionContainer(attr.value)) {
3642
+ const expr = attr.value.expression;
3643
+ if (BabelTypes.isJSXElement(expr)) return getJsxElementName(expr.openingElement);
3644
+ }
3645
+ if (attr.name.name === "Component" && BabelTypes.isJSXExpressionContainer(attr.value)) {
3646
+ if (BabelTypes.isIdentifier(attr.value.expression)) return attr.value.expression.name;
3647
+ }
3648
+ }
3649
+ return null;
3650
+ }
3651
+ // ── createBrowserRouter([...]) / useRoutes([...]) style ──────────
3652
+ extractObjectRoutes(ast) {
3653
+ const routes = [];
3654
+ const ROUTER_FACTORIES = /* @__PURE__ */ new Set([
3655
+ "createBrowserRouter",
3656
+ "createHashRouter",
3657
+ "createMemoryRouter",
3658
+ "useRoutes"
3659
+ ]);
3660
+ traverse4(ast, {
3661
+ CallExpression: (nodePath) => {
3662
+ const callee = nodePath.node.callee;
3663
+ if (!BabelTypes.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
3664
+ const first = nodePath.node.arguments[0];
3665
+ if (!BabelTypes.isArrayExpression(first)) return;
3666
+ this.visitRouteObjects(first, "", routes);
3667
+ }
3668
+ });
3669
+ return routes;
3670
+ }
3671
+ visitRouteObjects(arr, parentPath, out) {
3672
+ for (const element of arr.elements) {
3673
+ if (!BabelTypes.isObjectExpression(element)) continue;
3674
+ let segment;
3675
+ let isIndex = false;
3676
+ let componentName;
3677
+ let children;
3678
+ for (const prop of element.properties) {
3679
+ if (!BabelTypes.isObjectProperty(prop) || !BabelTypes.isIdentifier(prop.key)) continue;
3680
+ const key = prop.key.name;
3681
+ if (key === "path" && BabelTypes.isStringLiteral(prop.value)) segment = prop.value.value;
3682
+ if (key === "index" && BabelTypes.isBooleanLiteral(prop.value)) isIndex = prop.value.value;
3683
+ if (key === "element" && BabelTypes.isJSXElement(prop.value)) {
3684
+ componentName = getJsxElementName(prop.value.openingElement) ?? void 0;
3685
+ }
3686
+ if (key === "Component" && BabelTypes.isIdentifier(prop.value)) componentName = prop.value.name;
3687
+ if (key === "children" && BabelTypes.isArrayExpression(prop.value)) children = prop.value;
3688
+ }
3689
+ const fullPath = this.joinPaths(parentPath, segment, isIndex);
3690
+ if ((segment !== void 0 || isIndex) && (componentName || !children)) {
3691
+ out.push(this.buildRoute(fullPath, componentName, isIndex, Boolean(children)));
3692
+ }
3693
+ if (children) this.visitRouteObjects(children, fullPath, out);
3694
+ }
3695
+ }
3696
+ // ── Shared route building ────────────────────────────────────────
3697
+ joinPaths(parent, segment, isIndex) {
3698
+ if (isIndex || segment === void 0) return parent || "/";
3699
+ if (segment.startsWith("/")) return this.normalizePath(segment);
3700
+ return this.normalizePath(`${parent === "/" ? "" : parent}/${segment}`);
3701
+ }
3702
+ normalizePath(p) {
3703
+ const cleaned = `/${p}`.replace(/\/+/g, "/");
3704
+ return cleaned.length > 1 ? cleaned.replace(/\/$/, "") : cleaned;
3705
+ }
3706
+ buildRoute(fullPath, componentName, isIndex, isLayout) {
3707
+ const params = this.paramsFromPath(fullPath);
3708
+ return {
3709
+ path: fullPath,
3710
+ screenName: componentName ?? screenNameFromPath(fullPath),
3711
+ ...params.length > 0 ? { params } : {},
3712
+ ...isIndex ? { index: true } : {},
3713
+ ...isLayout ? { layout: true } : {}
3714
+ };
3715
+ }
3716
+ paramsFromPath(routePath) {
3717
+ const params = [];
3718
+ for (const segment of routePath.split("/")) {
3719
+ if (!segment.startsWith(":")) continue;
3720
+ const optional = segment.endsWith("?");
3721
+ const name = segment.slice(1, optional ? -1 : void 0);
3722
+ if (name) params.push({ name, type: "string", required: !optional });
3723
+ }
3724
+ return params;
3725
+ }
3726
+ /**
3727
+ * One entry per path. When several declarations resolve to the same
3728
+ * path, keep the one that best describes what the user lands on: a
3729
+ * page beats a layout wrapper (an `index` child and its parent layout
3730
+ * share a path), and a resolved component name beats a name derived
3731
+ * from the path.
3732
+ */
3733
+ dedupeRoutes(routes) {
3734
+ const byPath = /* @__PURE__ */ new Map();
3735
+ for (const route of routes) {
3736
+ const existing = byPath.get(route.path);
3737
+ if (!existing || routeScore(route) > routeScore(existing)) {
3738
+ byPath.set(route.path, route);
3739
+ }
3740
+ }
3741
+ return Array.from(byPath.values());
3742
+ }
3743
+ buildGraph(routes) {
3744
+ const screens = {};
3745
+ const navigatorName = "router";
3746
+ const screenNames = routes.map((r) => r.screenName);
3747
+ for (const route of routes) {
3748
+ const others = screenNames.filter((name) => name !== route.screenName);
3749
+ screens[route.screenName] = {
3750
+ screenName: route.screenName,
3751
+ // Open-union value — web routes, not a RN stack/tab/drawer.
3752
+ navigatorType: WEB_NAVIGATOR_TYPE,
3753
+ parentNavigator: navigatorName,
3754
+ // Any route is one URL away from any other — both directions,
3755
+ // like the RN analyzer models tab navigators.
3756
+ reachableFrom: others,
3757
+ reachableTo: others,
3758
+ ...route.params ? { params: route.params } : {}
3759
+ };
3760
+ }
3761
+ const initialRoute = routes.find((r) => r.path === "/") ?? routes.find((r) => r.index) ?? routes[0];
3762
+ return {
3763
+ screens,
3764
+ initialScreen: initialRoute?.screenName ?? "",
3765
+ navigators: routes.length > 0 ? [{
3766
+ name: navigatorName,
3767
+ type: WEB_NAVIGATOR_TYPE,
3768
+ screens: screenNames
3769
+ }] : []
3770
+ };
3771
+ }
3772
+ };
3773
+ function screenNameFromPath(routePath) {
3774
+ if (routePath === "/" || routePath === "") return "Home";
3775
+ return routePath.split("/").filter(Boolean).map((segment) => segment.replace(/^:/, "").replace(/\?$/, "")).map(
3776
+ (segment) => segment.split(/[-_.]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("")
3777
+ ).join("");
3778
+ }
3779
+ function routeScore(route) {
3780
+ const isPage = route.layout ? 0 : 2;
3781
+ const hasRealName = route.screenName === screenNameFromPath(route.path) ? 0 : 1;
3782
+ return isPage + hasRealName;
3783
+ }
3784
+ function resolvePathToScreen(routes, target) {
3785
+ const normalized = `/${target}`.replace(/\/+/g, "/").replace(/\?.*$/, "").replace(/#.*$/, "");
3786
+ const cleaned = normalized.length > 1 ? normalized.replace(/\/$/, "") : normalized;
3787
+ const exact = routes.find((r) => r.path === cleaned);
3788
+ if (exact) return exact.screenName;
3789
+ const targetSegments = cleaned.split("/").filter(Boolean);
3790
+ for (const route of routes) {
3791
+ const routeSegments = route.path.split("/").filter(Boolean);
3792
+ if (routeSegments.length !== targetSegments.length) continue;
3793
+ const matches = routeSegments.every(
3794
+ (seg, i) => seg.startsWith(":") || seg === "*" || seg === targetSegments[i]
3795
+ );
3796
+ if (matches) return route.screenName;
3797
+ }
3798
+ return void 0;
3799
+ }
3800
+
3801
+ // src/analyzers/web/ReactWebPlatformAnalyzer.ts
3802
+ var ReactWebPlatformAnalyzer = class {
3803
+ platform = "web";
3804
+ async analyze(config, options) {
3805
+ const screenAnalyzer = new WebScreenAnalyzer(config, {
3806
+ screenPatterns: options.screenPatterns
3807
+ });
3808
+ const navigationAnalyzer = new WebNavigationAnalyzer(config, {
3809
+ navigationInclude: options.navigationInclude,
3810
+ navigationExclude: options.navigationExclude
3811
+ });
3812
+ console.log("[ReactWebPlatformAnalyzer] Running analyzers...");
3813
+ const [screenAnalysis, navigationResult] = await Promise.all([
3814
+ screenAnalyzer.analyze(),
3815
+ navigationAnalyzer.analyze()
3816
+ ]);
3817
+ const routeScreenNames = new Set(navigationResult.routes.map((route) => route.screenName));
3818
+ const strictScreens = options.strictScreens ?? true;
3819
+ let screensFilteredOut = 0;
3820
+ const included = [];
3821
+ for (const candidate of screenAnalysis.candidates) {
3822
+ if (!strictScreens || this.isScreen(candidate, routeScreenNames)) {
3823
+ included.push(candidate);
3824
+ } else {
3825
+ screensFilteredOut++;
3826
+ }
3827
+ }
3828
+ const screens = included.map(
3829
+ (candidate) => this.resolveRoutePaths(candidate.descriptor, navigationResult.routes)
3830
+ );
3831
+ console.log(
3832
+ `[ReactWebPlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens, ${navigationResult.routes.length} routes`
3833
+ );
3834
+ return {
3835
+ screens,
3836
+ navigation: navigationResult.graph,
3837
+ analyzedFiles: screenAnalysis.analyzedFiles,
3838
+ ...screensFilteredOut > 0 ? { screensFilteredOut } : {}
3839
+ };
3840
+ }
3841
+ isScreen(candidate, routeScreenNames) {
3842
+ return candidate.hasRegisterScreen || candidate.matchesScreenPattern || routeScreenNames.has(candidate.descriptor.name);
3843
+ }
3844
+ /** Replace route-path references with screen names where the route table resolves them. */
3845
+ resolveRoutePaths(screen, routes) {
3846
+ const resolve2 = (target) => {
3847
+ if (!target || !target.startsWith("/")) return target;
3848
+ return resolvePathToScreen(routes, target) ?? target;
3849
+ };
3850
+ const navigationTargets = Array.from(
3851
+ new Set(
3852
+ screen.navigationTargets.map((target) => resolve2(target)).filter((target) => target !== screen.name)
3853
+ )
3854
+ ).sort();
3855
+ const actions = screen.actions.map((action) => {
3856
+ const resolved = resolve2(action.targetScreen);
3857
+ return resolved === action.targetScreen ? action : { ...action, targetScreen: resolved };
3858
+ });
3859
+ const collections = screen.collections?.map((collection) => {
3860
+ const resolveRow = (row) => {
3861
+ if (!row?.targetScreen) return row;
3862
+ const resolved = resolve2(row.targetScreen);
3863
+ if (resolved === row.targetScreen) return row;
3864
+ return {
3865
+ ...row,
3866
+ targetScreen: resolved,
3867
+ ...row.description ? { description: `Clicking a row opens ${resolved}` } : {}
3868
+ };
3869
+ };
3870
+ const rowAction = resolveRow(collection.rowAction);
3871
+ return {
3872
+ ...collection,
3873
+ ...rowAction ? { rowAction } : {},
3874
+ ...collection.rowActions ? { rowActions: collection.rowActions.map((row) => resolveRow(row)) } : {}
3875
+ };
3876
+ });
3877
+ return {
3878
+ ...screen,
3879
+ navigationTargets,
3880
+ actions,
3881
+ ...collections ? { collections } : {}
3882
+ };
3883
+ }
3884
+ };
3885
+
2412
3886
  // ../../node_modules/zod/v3/external.js
2413
3887
  var external_exports = {};
2414
3888
  __export(external_exports, {
@@ -2614,8 +4088,8 @@ var ZodParsedType = util.arrayToEnum([
2614
4088
  "set"
2615
4089
  ]);
2616
4090
  var getParsedType = (data) => {
2617
- const t8 = typeof data;
2618
- switch (t8) {
4091
+ const t12 = typeof data;
4092
+ switch (t12) {
2619
4093
  case "undefined":
2620
4094
  return ZodParsedType.undefined;
2621
4095
  case "string":
@@ -2887,8 +4361,8 @@ function getErrorMap() {
2887
4361
 
2888
4362
  // ../../node_modules/zod/v3/helpers/parseUtil.js
2889
4363
  var makeIssue = (params) => {
2890
- const { data, path: path7, errorMaps, issueData } = params;
2891
- const fullPath = [...path7, ...issueData.path || []];
4364
+ const { data, path: path9, errorMaps, issueData } = params;
4365
+ const fullPath = [...path9, ...issueData.path || []];
2892
4366
  const fullIssue = {
2893
4367
  ...issueData,
2894
4368
  path: fullPath
@@ -3004,11 +4478,11 @@ var errorUtil;
3004
4478
 
3005
4479
  // ../../node_modules/zod/v3/types.js
3006
4480
  var ParseInputLazyPath = class {
3007
- constructor(parent, value, path7, key) {
4481
+ constructor(parent, value, path9, key) {
3008
4482
  this._cachedPath = [];
3009
4483
  this.parent = parent;
3010
4484
  this.data = value;
3011
- this._path = path7;
4485
+ this._path = path9;
3012
4486
  this._key = key;
3013
4487
  }
3014
4488
  get path() {
@@ -6449,7 +7923,7 @@ var coerce = {
6449
7923
  };
6450
7924
  var NEVER = INVALID;
6451
7925
 
6452
- // ../shared/dist/chunk-XLOYG3DH.mjs
7926
+ // ../shared/dist/chunk-MSOZJNXB.mjs
6453
7927
  var locatorSourceSchema = external_exports.enum([
6454
7928
  "appilotsId",
6455
7929
  "testID",
@@ -6634,7 +8108,19 @@ var navigationNodeSchema = external_exports.object({
6634
8108
  parentNavigator: external_exports.string().optional(),
6635
8109
  reachableFrom: external_exports.array(external_exports.string()).default([]),
6636
8110
  reachableTo: external_exports.array(external_exports.string()).default([]),
6637
- params: external_exports.array(paramDescriptorSchema).optional()
8111
+ params: external_exports.array(paramDescriptorSchema).optional(),
8112
+ /**
8113
+ * Web clients only (documented convention, additive — previously
8114
+ * round-tripped via `.passthrough()`): the screen's URL route
8115
+ * template, e.g. `/users/:id`. Param segments use `:name` (the
8116
+ * relay also accepts `[name]` / `{name}`). When present and the
8117
+ * request's `context.platform` is `'web'`, the relay resolves
8118
+ * `navigate` targets against these templates and injects the
8119
+ * concrete URL segments as the navigate payload's `path` — the web
8120
+ * equivalent of RN's nested-navigation path injection. See
8121
+ * docs/agent-contract.md ("Web clients" section).
8122
+ */
8123
+ path: external_exports.string().max(500).optional()
6638
8124
  }).passthrough();
6639
8125
  var navigatorDescriptorSchema = external_exports.object({
6640
8126
  name: external_exports.string(),
@@ -6681,6 +8167,41 @@ var loginSchema = external_exports.object({
6681
8167
  loginSchema.extend({
6682
8168
  name: external_exports.string().min(2, "Name must be at least 2 characters").max(100)
6683
8169
  });
8170
+ external_exports.object({
8171
+ name: external_exports.string().min(2, "Name must be at least 2 characters").max(100).optional(),
8172
+ avatarUrl: external_exports.string().url("Invalid URL").max(2048).nullable().optional()
8173
+ }).refine((v) => v.name !== void 0 || v.avatarUrl !== void 0, {
8174
+ message: "At least one field must be provided"
8175
+ });
8176
+ external_exports.object({
8177
+ currentPassword: external_exports.string().min(1, "Current password is required"),
8178
+ newPassword: external_exports.string().min(8, "Password must be at least 8 characters")
8179
+ });
8180
+ var totpCodeSchema = external_exports.string().transform((v) => v.replace(/\s/g, "")).pipe(external_exports.string().regex(/^\d{6}$/, "Code must be 6 digits"));
8181
+ var recoveryCodeSchema = external_exports.string().transform((v) => v.toUpperCase().replace(/[^A-Z0-9]/g, "")).pipe(external_exports.string().regex(/^[23456789BCDFGHJKMNPQRSTVWXYZ]{10}$/, "Invalid recovery code"));
8182
+ external_exports.object({
8183
+ code: totpCodeSchema
8184
+ });
8185
+ external_exports.object({
8186
+ challengeToken: external_exports.string().min(1, "Challenge token is required"),
8187
+ code: totpCodeSchema.optional(),
8188
+ recoveryCode: recoveryCodeSchema.optional()
8189
+ }).refine((v) => Boolean(v.code) !== Boolean(v.recoveryCode), {
8190
+ message: "Provide either a TOTP code or a recovery code",
8191
+ path: ["code"]
8192
+ });
8193
+ external_exports.object({
8194
+ password: external_exports.string().min(1, "Password is required"),
8195
+ code: totpCodeSchema.optional(),
8196
+ recoveryCode: recoveryCodeSchema.optional()
8197
+ }).refine((v) => Boolean(v.code) !== Boolean(v.recoveryCode), {
8198
+ message: "Provide either a TOTP code or a recovery code",
8199
+ path: ["code"]
8200
+ });
8201
+ external_exports.object({
8202
+ password: external_exports.string().min(1, "Password is required"),
8203
+ code: totpCodeSchema
8204
+ });
6684
8205
  var createProjectSchema = external_exports.object({
6685
8206
  name: external_exports.string().min(1).max(100),
6686
8207
  description: external_exports.string().max(500).optional(),
@@ -7344,6 +8865,18 @@ external_exports.object({
7344
8865
  */
7345
8866
  isRecoveryHop: external_exports.boolean().optional()
7346
8867
  }).strict();
8868
+ var relayDiagnosticsSchema = external_exports.object({
8869
+ unverifiedTargets: external_exports.array(external_exports.string()),
8870
+ guardsFired: external_exports.array(
8871
+ external_exports.object({ guard: external_exports.string(), detail: external_exports.string().optional() })
8872
+ ),
8873
+ repairMode: external_exports.enum(["repair", "strict", "disabled"]),
8874
+ promptVersion: external_exports.string(),
8875
+ // Defaulted for producers that predate the field: before it existed
8876
+ // every response had reached the model, so `true` is the correct
8877
+ // backfill.
8878
+ modelInvoked: external_exports.boolean().default(true)
8879
+ });
7347
8880
  var sandboxTraceHopSchema = external_exports.object({
7348
8881
  hop: external_exports.number().int().nonnegative(),
7349
8882
  inputPreview: external_exports.string(),
@@ -7382,7 +8915,16 @@ external_exports.object({
7382
8915
  /** Hop-by-hop trace. Always at least one entry. */
7383
8916
  trace: external_exports.array(sandboxTraceHopSchema),
7384
8917
  /** True when the run was a no-op (e.g. project has no MCP yet). */
7385
- warning: external_exports.string().optional()
8918
+ warning: external_exports.string().optional(),
8919
+ /**
8920
+ * SPEC-046 — guard / hallucination telemetry for the run.
8921
+ *
8922
+ * ADDITIVE AND OPTIONAL, permanently: SDK and dashboard consumers
8923
+ * predate it, and older API deployments will not send it. Consumers
8924
+ * must treat an absent block as "no information", never as "no guards
8925
+ * fired".
8926
+ */
8927
+ diagnostics: relayDiagnosticsSchema.optional()
7386
8928
  });
7387
8929
  external_exports.object({
7388
8930
  name: external_exports.string().min(1).max(120),
@@ -7601,7 +9143,7 @@ var manifestSchema = external_exports.object({
7601
9143
  navigation: navigationGraphSchema.partial().optional()
7602
9144
  }).passthrough();
7603
9145
  async function loadManifest(rootDir, manifestPath) {
7604
- const resolvedPath = path6__default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
9146
+ const resolvedPath = path8__default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
7605
9147
  let raw;
7606
9148
  try {
7607
9149
  raw = await readFile(resolvedPath, "utf-8");
@@ -7886,12 +9428,15 @@ var MCPGenerator = class _MCPGenerator {
7886
9428
  /**
7887
9429
  * Select the `PlatformAnalyzer` implementation for a `.appilotsrc`
7888
9430
  * `platform` value. `'react-native'` (or unset — the existing default)
7889
- * gets the real Babel/JSX pipeline; anything else gets the no-op
7890
- * generic analyzer, relying entirely on a declared manifest.
9431
+ * gets the RN Babel/JSX pipeline; `'web'` gets the React web (DOM +
9432
+ * React Router) pipeline; anything else gets the no-op generic
9433
+ * analyzer, relying entirely on a declared manifest. The manifest
9434
+ * still merges on top of every analyzer's output either way.
7891
9435
  */
7892
9436
  static createPlatformAnalyzer(platform) {
7893
9437
  const resolved = platform ?? "react-native";
7894
9438
  if (resolved === "react-native") return new ReactNativePlatformAnalyzer();
9439
+ if (resolved === "web") return new ReactWebPlatformAnalyzer();
7895
9440
  return new GenericPlatformAnalyzer(resolved);
7896
9441
  }
7897
9442
  /** Generate MCP documents from the project */
@@ -7944,13 +9489,13 @@ var MCPGenerator = class _MCPGenerator {
7944
9489
  };
7945
9490
  const serialized = JSON.stringify(document, null, 2);
7946
9491
  const checksum = this.calculateChecksum(serialized);
7947
- const filePath = path6__default.resolve(
9492
+ const filePath = path8__default.resolve(
7948
9493
  outputDir,
7949
9494
  `mcp-document.${this.options.format}`
7950
9495
  );
7951
9496
  await writeFile(filePath, serialized, "utf-8");
7952
9497
  console.log(`[MCPGenerator] Document written to: ${filePath}`);
7953
- const checksumFilePath = path6__default.resolve(outputDir, ".appilots-checksum");
9498
+ const checksumFilePath = path8__default.resolve(outputDir, ".appilots-checksum");
7954
9499
  await writeFile(checksumFilePath, checksum, "utf-8");
7955
9500
  console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
7956
9501
  console.log("[MCPGenerator] Generation complete!");
@@ -7970,7 +9515,7 @@ var MCPGenerator = class _MCPGenerator {
7970
9515
  * after calling `generate()`.
7971
9516
  */
7972
9517
  static async readPreviousChecksum(outputDir) {
7973
- const checksumFilePath = path6__default.resolve(outputDir, ".appilots-checksum");
9518
+ const checksumFilePath = path8__default.resolve(outputDir, ".appilots-checksum");
7974
9519
  try {
7975
9520
  const content = await readFile(checksumFilePath, "utf-8");
7976
9521
  return content.trim() || null;
@@ -7989,7 +9534,7 @@ var MCPGenerator = class _MCPGenerator {
7989
9534
  */
7990
9535
  async getProjectInfo() {
7991
9536
  try {
7992
- const packageJsonPath = path6__default.resolve(this.analyzerConfig.rootDir, "package.json");
9537
+ const packageJsonPath = path8__default.resolve(this.analyzerConfig.rootDir, "package.json");
7993
9538
  const packageJsonContent = await readFile(packageJsonPath, "utf-8");
7994
9539
  const packageJson = JSON.parse(packageJsonContent);
7995
9540
  return {
@@ -8375,6 +9920,6 @@ var AppilotsAPIClient = class {
8375
9920
  }
8376
9921
  };
8377
9922
 
8378
- export { AppilotsAPIClient, ComponentAnalyzer, DEFAULT_MANIFEST_FILENAME, FormAnalyzer, GenericPlatformAnalyzer, MCPGenerator, NavigationAnalyzer, ReactNativePlatformAnalyzer, ScreenAnalyzer, formatMetadataWarnings, getConfigPath, getEnvOverrides, lintActionMetadata, loadConfig, loadManifest, mergeManifestNavigation, mergeManifestScreens, saveConfig, validateConfig };
9923
+ export { AppilotsAPIClient, ComponentAnalyzer, DEFAULT_MANIFEST_FILENAME, DEFAULT_WEB_SCREEN_PATTERNS, FormAnalyzer, GenericPlatformAnalyzer, MCPGenerator, NavigationAnalyzer, ReactNativePlatformAnalyzer, ReactWebPlatformAnalyzer, ScreenAnalyzer, WebNavigationAnalyzer, WebScreenAnalyzer, formatMetadataWarnings, getConfigPath, getEnvOverrides, lintActionMetadata, loadConfig, loadManifest, mergeManifestNavigation, mergeManifestScreens, resolvePathToScreen, saveConfig, screenNameFromPath, validateConfig };
8379
9924
  //# sourceMappingURL=index.mjs.map
8380
9925
  //# sourceMappingURL=index.mjs.map