@appilots/cli 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -532,11 +532,11 @@ function initCommand() {
532
532
 
533
533
  // src/cli/commands/sync.ts
534
534
  var import_commander2 = require("commander");
535
- var import_promises4 = require("fs/promises");
535
+ var import_promises5 = require("fs/promises");
536
536
  var import_node_path4 = require("path");
537
537
 
538
538
  // src/generators/MCPGenerator.ts
539
- var import_promises3 = require("fs/promises");
539
+ var import_promises4 = require("fs/promises");
540
540
  var import_node_crypto = require("crypto");
541
541
  var import_node_path3 = __toESM(require("path"));
542
542
 
@@ -1955,7 +1955,7 @@ var ScreenAnalyzer = class {
1955
1955
  * heuristic must hit OR the JSDoc tag must be present.
1956
1956
  */
1957
1957
  isElementDestructive(element, handlerName, actionId) {
1958
- const DESTRUCTIVE_VERB2 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
1958
+ const DESTRUCTIVE_VERB3 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
1959
1959
  for (const attr of element.attributes) {
1960
1960
  if (!BabelTypes.isJSXAttribute(attr)) continue;
1961
1961
  const attrName = BabelTypes.isJSXIdentifier(attr.name) ? attr.name.name : null;
@@ -1970,8 +1970,8 @@ var ScreenAnalyzer = class {
1970
1970
  }
1971
1971
  }
1972
1972
  }
1973
- if (handlerName && DESTRUCTIVE_VERB2.test(handlerName)) return true;
1974
- if (actionId && DESTRUCTIVE_VERB2.test(actionId)) return true;
1973
+ if (handlerName && DESTRUCTIVE_VERB3.test(handlerName)) return true;
1974
+ if (actionId && DESTRUCTIVE_VERB3.test(actionId)) return true;
1975
1975
  return false;
1976
1976
  }
1977
1977
  /**
@@ -2581,7 +2581,7 @@ var NavigationAnalyzer = class {
2581
2581
  if (type.type === "TSUndefinedKeyword") return "undefined";
2582
2582
  if (type.type === "TSNullKeyword") return "null";
2583
2583
  if (type.type === "TSUnionType") {
2584
- return type.types.map((t8) => this.typeToString(t8)).join(" | ");
2584
+ return type.types.map((t12) => this.typeToString(t12)).join(" | ");
2585
2585
  }
2586
2586
  if (type.type === "TSTypeLiteral") {
2587
2587
  return "object";
@@ -2602,7 +2602,7 @@ var NavigationAnalyzer = class {
2602
2602
  /** Attach parsed type params to navigator screens */
2603
2603
  attachParamsToNavigators(navigators, types) {
2604
2604
  for (const navigator of navigators) {
2605
- const matchingType = types.find((t8) => t8.type === navigator.type);
2605
+ const matchingType = types.find((t12) => t12.type === navigator.type);
2606
2606
  if (matchingType) {
2607
2607
  for (const screen of navigator.screens) {
2608
2608
  const screenParams = matchingType.paramEntries.get(screen.name);
@@ -2693,8 +2693,8 @@ var ComponentAnalyzer = class {
2693
2693
  });
2694
2694
  const components = [];
2695
2695
  (0, import_traverse6.default)(ast, {
2696
- JSXElement: (path7) => {
2697
- const component = this.extractComponentFromJSXElement(path7.node);
2696
+ JSXElement: (path9) => {
2697
+ const component = this.extractComponentFromJSXElement(path9.node);
2698
2698
  if (component) {
2699
2699
  components.push(component);
2700
2700
  }
@@ -2828,13 +2828,13 @@ var FormAnalyzer = class {
2828
2828
  this.inputElements = [];
2829
2829
  this.submitButtons = [];
2830
2830
  (0, import_traverse7.default)(ast, {
2831
- CallExpression: (path7) => {
2832
- this.extractStateVariables(path7.node);
2831
+ CallExpression: (path9) => {
2832
+ this.extractStateVariables(path9.node);
2833
2833
  }
2834
2834
  });
2835
2835
  (0, import_traverse7.default)(ast, {
2836
- JSXElement: (path7) => {
2837
- this.extractFormElements(path7.node);
2836
+ JSXElement: (path9) => {
2837
+ this.extractFormElements(path9.node);
2838
2838
  }
2839
2839
  });
2840
2840
  const validationRules = this.extractValidationRules(ast);
@@ -2934,8 +2934,8 @@ var FormAnalyzer = class {
2934
2934
  extractValidationRules(ast) {
2935
2935
  const rules = {};
2936
2936
  (0, import_traverse7.default)(ast, {
2937
- IfStatement: (path7) => {
2938
- const test = path7.node.test;
2937
+ IfStatement: (path9) => {
2938
+ const test = path9.node.test;
2939
2939
  const rule = this.extractRuleFromCondition(test);
2940
2940
  if (rule) {
2941
2941
  const { field, description } = rule;
@@ -2985,224 +2985,1719 @@ var FormAnalyzer = class {
2985
2985
  }
2986
2986
  }
2987
2987
  }
2988
- return fieldName && description ? { field: fieldName, description } : null;
2988
+ return fieldName && description ? { field: fieldName, description } : null;
2989
+ }
2990
+ buildForms(filePath, validationRules) {
2991
+ if (this.inputElements.length === 0) return [];
2992
+ const fileName = path3.basename(filePath, path3.extname(filePath));
2993
+ const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
2994
+ const fields = this.inputElements.map((input) => {
2995
+ const fieldType = this.inferFieldType(input);
2996
+ const required = this.isFieldRequired(input.varName, validationRules);
2997
+ return {
2998
+ name: input.appilotsId || input.varName || input.testID || "field",
2999
+ label: input.label,
3000
+ type: fieldType,
3001
+ required,
3002
+ placeholder: input.placeholder
3003
+ };
3004
+ });
3005
+ const lastButton = this.submitButtons[this.submitButtons.length - 1];
3006
+ const submitAction = lastButton?.handler;
3007
+ return [
3008
+ {
3009
+ id: formId,
3010
+ fields,
3011
+ submitAction,
3012
+ validationRules: Object.keys(validationRules).length > 0 ? validationRules : void 0
3013
+ }
3014
+ ];
3015
+ }
3016
+ inferFieldType(input) {
3017
+ const lowerVarName = input.varName.toLowerCase();
3018
+ const lowerLabel = input.label?.toLowerCase() || "";
3019
+ const lowerPlaceholder = input.placeholder?.toLowerCase() || "";
3020
+ const combined = `${lowerVarName} ${lowerLabel} ${lowerPlaceholder}`;
3021
+ if (input.keyboardType === "email-address" || input.keyboardType === "email") {
3022
+ return "email";
3023
+ }
3024
+ if (input.keyboardType === "phone-pad" || input.keyboardType === "numeric") {
3025
+ return input.keyboardType === "numeric" ? "number" : "phone";
3026
+ }
3027
+ if (combined.includes("email")) return "email";
3028
+ if (combined.includes("phone") || combined.includes("tel")) return "phone";
3029
+ if (combined.includes("password")) return "text";
3030
+ if (combined.includes("number") || combined.includes("numeric")) return "number";
3031
+ if (combined.includes("date")) return "date";
3032
+ if (combined.includes("toggle") || combined.includes("check")) return "toggle";
3033
+ if (combined.includes("select") || combined.includes("choice")) return "select";
3034
+ return "text";
3035
+ }
3036
+ isFieldRequired(fieldName, rules) {
3037
+ const rule = rules[fieldName];
3038
+ return rule !== void 0 && rule.includes("required");
3039
+ }
3040
+ };
3041
+
3042
+ // src/analyzers/ReactNativePlatformAnalyzer.ts
3043
+ var ReactNativePlatformAnalyzer = class {
3044
+ platform = "react-native";
3045
+ async analyze(config, options) {
3046
+ const screenAnalyzer = new ScreenAnalyzer(config, {
3047
+ strictScreens: options.strictScreens ?? true,
3048
+ screenPatterns: options.screenPatterns
3049
+ });
3050
+ const navigationAnalyzer = new NavigationAnalyzer(config, {
3051
+ navigationInclude: options.navigationInclude,
3052
+ navigationExclude: options.navigationExclude
3053
+ });
3054
+ const componentAnalyzer = new ComponentAnalyzer(config);
3055
+ const formAnalyzer = new FormAnalyzer(config);
3056
+ console.log("[ReactNativePlatformAnalyzer] Running analyzers...");
3057
+ const [screens, navigation] = await Promise.all([
3058
+ screenAnalyzer.analyze(),
3059
+ navigationAnalyzer.analyze()
3060
+ ]);
3061
+ console.log(
3062
+ `[ReactNativePlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens`
3063
+ );
3064
+ const screenFiles = await (0, import_fast_glob3.default)(config.include || ["**/*.tsx", "**/*.ts"], {
3065
+ cwd: config.rootDir,
3066
+ ignore: config.exclude || ["**/node_modules/**"]
3067
+ });
3068
+ console.log(
3069
+ `[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
3070
+ );
3071
+ const enrichmentPromises = screenFiles.map(async (file) => {
3072
+ const filePath = import_node_path.default.resolve(config.rootDir, file);
3073
+ try {
3074
+ const [components, forms] = await Promise.all([
3075
+ componentAnalyzer.analyzeFile(filePath),
3076
+ formAnalyzer.analyzeFile(filePath)
3077
+ ]);
3078
+ return { filePath, components, forms };
3079
+ } catch (error2) {
3080
+ console.warn(`[ReactNativePlatformAnalyzer] Failed to analyze ${file}:`, error2);
3081
+ return { filePath, components: [], forms: [] };
3082
+ }
3083
+ });
3084
+ const enrichmentResults = await Promise.all(enrichmentPromises);
3085
+ const enrichmentMap = /* @__PURE__ */ new Map();
3086
+ for (const result of enrichmentResults) {
3087
+ enrichmentMap.set(result.filePath, {
3088
+ components: result.components,
3089
+ forms: result.forms
3090
+ });
3091
+ }
3092
+ const enrichedScreens = screens.map((screen) => {
3093
+ const enrichment = enrichmentMap.get(screen.filePath);
3094
+ if (enrichment) {
3095
+ const existingComponentNames = new Set(screen.components.map((c) => c.name));
3096
+ const newComponents = enrichment.components.filter(
3097
+ (c) => !existingComponentNames.has(c.name)
3098
+ );
3099
+ const mergedForms = screen.forms.map((form) => ({
3100
+ ...form,
3101
+ fields: [...form.fields]
3102
+ }));
3103
+ for (const newForm of enrichment.forms) {
3104
+ const existingForm = mergedForms.find((form) => form.id === newForm.id) ?? this.findFormWithSharedFields(mergedForms, newForm);
3105
+ if (existingForm) {
3106
+ this.mergeForm(existingForm, newForm);
3107
+ } else {
3108
+ mergedForms.push({
3109
+ ...newForm,
3110
+ fields: [...newForm.fields],
3111
+ submitAction: this.namedSubmitAction(newForm.submitAction)
3112
+ });
3113
+ }
3114
+ }
3115
+ return {
3116
+ ...screen,
3117
+ components: [...screen.components, ...newComponents],
3118
+ forms: mergedForms
3119
+ };
3120
+ }
3121
+ return screen;
3122
+ });
3123
+ return {
3124
+ screens: enrichedScreens,
3125
+ navigation,
3126
+ analyzedFiles: screenFiles.length,
3127
+ ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
3128
+ };
3129
+ }
3130
+ mergeForm(target, source) {
3131
+ for (const field of source.fields) {
3132
+ const existingField = this.findEquivalentField(target.fields, field);
3133
+ if (existingField) {
3134
+ this.mergeField(existingField, field);
3135
+ } else {
3136
+ target.fields.push(field);
3137
+ }
3138
+ }
3139
+ target.submitAction = target.submitAction ?? this.namedSubmitAction(source.submitAction);
3140
+ target.validationRules = source.validationRules ? { ...source.validationRules, ...target.validationRules ?? {} } : target.validationRules;
3141
+ }
3142
+ findEquivalentField(fields, incoming) {
3143
+ return fields.find((field) => {
3144
+ if (field.name && incoming.name && field.name === incoming.name) return true;
3145
+ if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
3146
+ if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
3147
+ if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
3148
+ if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
3149
+ return true;
3150
+ }
3151
+ return false;
3152
+ });
3153
+ }
3154
+ mergeField(target, source) {
3155
+ if (this.isWeakInferredFieldName(target.name) && !this.isWeakInferredFieldName(source.name)) {
3156
+ target.name = source.name;
3157
+ }
3158
+ target.label = target.label ?? source.label;
3159
+ target.placeholder = target.placeholder ?? source.placeholder;
3160
+ target.options = target.options ?? source.options;
3161
+ target.locator = target.locator ?? source.locator;
3162
+ target.sourceComponent = target.sourceComponent ?? source.sourceComponent;
3163
+ target.valueBinding = target.valueBinding ?? source.valueBinding;
3164
+ target.errorBinding = target.errorBinding ?? source.errorBinding;
3165
+ target.required = target.required || source.required;
3166
+ if (target.type === "text" && source.type !== "text") {
3167
+ target.type = source.type;
3168
+ }
3169
+ }
3170
+ namedSubmitAction(submitAction) {
3171
+ return submitAction && submitAction !== "anonymous" ? submitAction : void 0;
3172
+ }
3173
+ isWeakInferredFieldName(name) {
3174
+ return /^(text|value|input|query|search|selected|checked)$/i.test(name);
3175
+ }
3176
+ findFormWithSharedFields(forms, incoming) {
3177
+ if (incoming.fields.length === 0) return void 0;
3178
+ let best;
3179
+ for (const form of forms) {
3180
+ const overlap = incoming.fields.filter((field) => this.findEquivalentField(form.fields, field)).length;
3181
+ if (overlap > 0 && (!best || overlap > best.overlap)) {
3182
+ best = { form, overlap };
3183
+ }
3184
+ }
3185
+ return best?.form;
3186
+ }
3187
+ };
3188
+
3189
+ // src/analyzers/GenericPlatformAnalyzer.ts
3190
+ var GenericPlatformAnalyzer = class {
3191
+ constructor(platform) {
3192
+ this.platform = platform;
3193
+ }
3194
+ platform;
3195
+ async analyze(_config, _options) {
3196
+ return {
3197
+ screens: [],
3198
+ navigation: { screens: {}, initialScreen: "", navigators: [] },
3199
+ analyzedFiles: 0
3200
+ };
3201
+ }
3202
+ };
3203
+
3204
+ // src/analyzers/web/WebScreenAnalyzer.ts
3205
+ var import_promises2 = __toESM(require("fs/promises"));
3206
+ var import_path5 = __toESM(require("path"));
3207
+ var import_fast_glob4 = __toESM(require("fast-glob"));
3208
+ var import_traverse9 = __toESM(require("@babel/traverse"));
3209
+ var t10 = __toESM(require("@babel/types"));
3210
+
3211
+ // src/ast/jsx/web/classify.ts
3212
+ var VIEW_TAGS = /* @__PURE__ */ new Set([
3213
+ "div",
3214
+ "span",
3215
+ "section",
3216
+ "main",
3217
+ "article",
3218
+ "aside",
3219
+ "header",
3220
+ "footer",
3221
+ "nav",
3222
+ "fieldset",
3223
+ "form",
3224
+ "p"
3225
+ ]);
3226
+ var LIST_TAGS = /* @__PURE__ */ new Set(["ul", "ol", "dl", "table", "tbody"]);
3227
+ var BUTTON_TAGS = /* @__PURE__ */ new Set(["button"]);
3228
+ var INPUT_TAGS = /* @__PURE__ */ new Set(["input", "textarea"]);
3229
+ var MODAL_TAGS = /* @__PURE__ */ new Set(["dialog"]);
3230
+ var BUTTON_COMPONENTS2 = /* @__PURE__ */ new Set(["Button", "IconButton", "Link", "NavLink"]);
3231
+ var INPUT_COMPONENTS2 = /* @__PURE__ */ new Set(["Input", "TextField", "TextArea", "Textarea"]);
3232
+ var LIST_COMPONENTS2 = /* @__PURE__ */ new Set(["List", "Table", "DataGrid", "DataTable"]);
3233
+ var MODAL_COMPONENTS2 = /* @__PURE__ */ new Set(["Modal", "Dialog", "Drawer", "Popover", "BottomSheet"]);
3234
+ var ARIA_ROLE_TO_SEMANTIC = {
3235
+ button: "button",
3236
+ link: "button",
3237
+ tab: "button",
3238
+ menuitem: "button",
3239
+ textbox: "input",
3240
+ searchbox: "input",
3241
+ spinbutton: "input",
3242
+ listbox: "select",
3243
+ combobox: "select",
3244
+ radiogroup: "select",
3245
+ radio: "select",
3246
+ option: "select",
3247
+ checkbox: "toggle",
3248
+ switch: "toggle",
3249
+ dialog: "modal",
3250
+ alertdialog: "modal",
3251
+ list: "list",
3252
+ table: "list",
3253
+ grid: "list",
3254
+ form: "view"
3255
+ };
3256
+ var INPUT_TYPE_TO_SEMANTIC = {
3257
+ checkbox: "toggle",
3258
+ radio: "select",
3259
+ date: "date",
3260
+ "datetime-local": "date",
3261
+ month: "date",
3262
+ week: "date",
3263
+ time: "date",
3264
+ submit: "button",
3265
+ button: "button",
3266
+ reset: "button",
3267
+ image: "button",
3268
+ hidden: "custom",
3269
+ range: "input",
3270
+ file: "input",
3271
+ color: "input"
3272
+ };
3273
+ function classifyWebJsxComponent(name, element) {
3274
+ if (element) {
3275
+ const role = getStringAttr(element, "role");
3276
+ if (role && ARIA_ROLE_TO_SEMANTIC[role]) return ARIA_ROLE_TO_SEMANTIC[role];
3277
+ }
3278
+ if (/^[a-z]/.test(name)) {
3279
+ if (name === "input") {
3280
+ const type = element ? getStringAttr(element, "type") : void 0;
3281
+ if (type && INPUT_TYPE_TO_SEMANTIC[type]) return INPUT_TYPE_TO_SEMANTIC[type];
3282
+ return "input";
3283
+ }
3284
+ if (INPUT_TAGS.has(name)) return "input";
3285
+ if (name === "select") return "select";
3286
+ if (BUTTON_TAGS.has(name)) return "button";
3287
+ if (name === "a") {
3288
+ if (element && (hasJsxAttribute(element, "href") || hasJsxAttribute(element, "onClick"))) {
3289
+ return "button";
3290
+ }
3291
+ return "view";
3292
+ }
3293
+ if (LIST_TAGS.has(name)) return "list";
3294
+ if (MODAL_TAGS.has(name)) return "modal";
3295
+ if (VIEW_TAGS.has(name)) return "view";
3296
+ return "custom";
3297
+ }
3298
+ if (LIST_COMPONENTS2.has(name)) return "list";
3299
+ if (MODAL_COMPONENTS2.has(name)) return "modal";
3300
+ if (/date/i.test(name)) return "date";
3301
+ if (/(select|picker|dropdown|radio)/i.test(name)) return "select";
3302
+ if (/(checkbox|switch|toggle)/i.test(name)) return "toggle";
3303
+ if (INPUT_COMPONENTS2.has(name)) return "input";
3304
+ if (BUTTON_COMPONENTS2.has(name)) return "button";
3305
+ if (element) {
3306
+ const hasOptions = hasJsxAttribute(element, "options");
3307
+ const hasValue = hasJsxAttribute(element, "value");
3308
+ const hasChecked = hasJsxAttribute(element, "checked") || hasJsxAttribute(element, "selected");
3309
+ const hasOnChange = hasJsxAttribute(element, "onChange") || hasJsxAttribute(element, "onValueChange");
3310
+ const hasOnClick = hasJsxAttribute(element, "onClick");
3311
+ if (hasOptions && (hasValue || hasOnChange)) return "select";
3312
+ if (hasChecked && hasOnChange) return "toggle";
3313
+ if (hasOnChange && (hasJsxAttribute(element, "label") || hasJsxAttribute(element, "placeholder"))) {
3314
+ return "input";
3315
+ }
3316
+ if (hasOnClick) return "button";
3317
+ if (getStringAttr(element, "open") || getExpressionIdentifierAttr(element, "open") || getExpressionIdentifierAttr(element, "isOpen")) {
3318
+ return "modal";
3319
+ }
3320
+ }
3321
+ return "custom";
3322
+ }
3323
+
3324
+ // src/ast/jsx/names.ts
3325
+ var t8 = __toESM(require("@babel/types"));
3326
+ function getJsxElementName(openingElement) {
3327
+ return getJsxName(openingElement.name);
3328
+ }
3329
+ function getJsxName(name) {
3330
+ if (t8.isJSXIdentifier(name)) return name.name;
3331
+ if (t8.isJSXNamespacedName(name)) return `${name.namespace.name}:${name.name.name}`;
3332
+ if (t8.isJSXMemberExpression(name)) {
3333
+ const objectName = getJsxName(name.object);
3334
+ const propertyName = t8.isJSXIdentifier(name.property) ? name.property.name : null;
3335
+ return objectName && propertyName ? `${objectName}.${propertyName}` : null;
3336
+ }
3337
+ return null;
3338
+ }
3339
+
3340
+ // src/ast/navigation/web-calls.ts
3341
+ var import_traverse8 = __toESM(require("@babel/traverse"));
3342
+ var t9 = __toESM(require("@babel/types"));
3343
+ var ROUTERISH_OBJECTS = /^(router|history|navigation)$/;
3344
+ function staticRoutePath(node) {
3345
+ if (!node) return void 0;
3346
+ if (t9.isStringLiteral(node)) return node.value;
3347
+ if (t9.isTemplateLiteral(node)) {
3348
+ let path9 = "";
3349
+ node.quasis.forEach((quasi, index) => {
3350
+ path9 += quasi.value.cooked ?? quasi.value.raw;
3351
+ const expr = node.expressions[index];
3352
+ if (expr) path9 += `:${paramNameOf(expr)}`;
3353
+ });
3354
+ return path9;
3355
+ }
3356
+ return void 0;
3357
+ }
3358
+ function paramNameOf(expr) {
3359
+ if (t9.isIdentifier(expr)) return expr.name;
3360
+ if (t9.isMemberExpression(expr) && t9.isIdentifier(expr.property)) return expr.property.name;
3361
+ return "param";
3362
+ }
3363
+ function extractWebNavigationCalls(ast) {
3364
+ const calls = [];
3365
+ const inspect = (node) => {
3366
+ if (t9.isCallExpression(node)) {
3367
+ if (t9.isIdentifier(node.callee) && node.callee.name === "navigate") {
3368
+ const first = node.arguments[0];
3369
+ const targetPath = staticRoutePath(first);
3370
+ if (targetPath !== void 0) {
3371
+ calls.push({
3372
+ method: hasReplaceOption(node.arguments[1]) ? "replace" : "navigate",
3373
+ targetPath
3374
+ });
3375
+ } else if (t9.isNumericLiteral(first) || t9.isUnaryExpression(first) && first.operator === "-") {
3376
+ calls.push({ method: "goBack" });
3377
+ }
3378
+ return;
3379
+ }
3380
+ if (t9.isMemberExpression(node.callee) && t9.isIdentifier(node.callee.object) && ROUTERISH_OBJECTS.test(node.callee.object.name) && t9.isIdentifier(node.callee.property)) {
3381
+ const method = node.callee.property.name;
3382
+ const targetPath = staticRoutePath(node.arguments[0]);
3383
+ if ((method === "push" || method === "navigate") && targetPath !== void 0) {
3384
+ calls.push({ method: "navigate", targetPath });
3385
+ } else if (method === "replace" && targetPath !== void 0) {
3386
+ calls.push({ method: "replace", targetPath });
3387
+ } else if (method === "back" || method === "goBack") {
3388
+ calls.push({ method: "goBack" });
3389
+ }
3390
+ }
3391
+ return;
3392
+ }
3393
+ if (t9.isAssignmentExpression(node) && t9.isMemberExpression(node.left) && t9.isIdentifier(node.left.property) && node.left.property.name === "href" && isLocationExpression(node.left.object) && t9.isStringLiteral(node.right) && node.right.value.startsWith("/")) {
3394
+ calls.push({ method: "navigate", targetPath: node.right.value });
3395
+ }
3396
+ };
3397
+ (0, import_traverse8.default)(ast, {
3398
+ noScope: !t9.isFile(ast),
3399
+ enter: (nodePath) => inspect(nodePath.node)
3400
+ });
3401
+ return calls;
3402
+ }
3403
+ function hasReplaceOption(arg) {
3404
+ if (!arg || !t9.isObjectExpression(arg)) return false;
3405
+ return arg.properties.some(
3406
+ (prop) => t9.isObjectProperty(prop) && t9.isIdentifier(prop.key) && prop.key.name === "replace" && t9.isBooleanLiteral(prop.value) && prop.value.value === true
3407
+ );
3408
+ }
3409
+ function isLocationExpression(node) {
3410
+ if (t9.isIdentifier(node)) return node.name === "location";
3411
+ return t9.isMemberExpression(node) && t9.isIdentifier(node.object) && node.object.name === "window" && t9.isIdentifier(node.property) && node.property.name === "location";
3412
+ }
3413
+
3414
+ // src/analyzers/web/WebScreenAnalyzer.ts
3415
+ var DEFAULT_WEB_SCREEN_PATTERNS = [
3416
+ "**/pages/**/*.{ts,tsx,js,jsx}",
3417
+ "**/routes/**/*.{ts,tsx,js,jsx}",
3418
+ "**/views/**/*.{ts,tsx,js,jsx}",
3419
+ "**/app/**/*.{ts,tsx,js,jsx}",
3420
+ "**/*Page.{ts,tsx,js,jsx}",
3421
+ "**/*Screen.{ts,tsx,js,jsx}",
3422
+ "**/*View.{ts,tsx,js,jsx}"
3423
+ ];
3424
+ var DESTRUCTIVE_VERB2 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
3425
+ var LISTISH_TAGS = /* @__PURE__ */ new Set(["ul", "ol", "dl", "table", "tbody"]);
3426
+ var WebScreenAnalyzer = class {
3427
+ config;
3428
+ verbose = process.env.VERBOSE === "true";
3429
+ screenPatterns;
3430
+ constructor(config, options) {
3431
+ this.config = config;
3432
+ this.screenPatterns = options?.screenPatterns ?? DEFAULT_WEB_SCREEN_PATTERNS;
3433
+ }
3434
+ async analyze() {
3435
+ const {
3436
+ include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
3437
+ exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
3438
+ } = this.config;
3439
+ const files = await (0, import_fast_glob4.default)(include, { cwd: this.config.rootDir, ignore: exclude });
3440
+ const patternMatches = await (0, import_fast_glob4.default)(this.screenPatterns, {
3441
+ cwd: this.config.rootDir,
3442
+ ignore: exclude
3443
+ });
3444
+ const patternSet = new Set(patternMatches.map((f) => import_path5.default.resolve(this.config.rootDir, f)));
3445
+ const candidates = [];
3446
+ for (const file of files) {
3447
+ const filePath = import_path5.default.resolve(this.config.rootDir, file);
3448
+ try {
3449
+ const candidate = await this.analyzeFile(filePath);
3450
+ if (!candidate) continue;
3451
+ candidate.matchesScreenPattern = patternSet.has(filePath);
3452
+ candidates.push(candidate);
3453
+ if (this.verbose) {
3454
+ console.log(`[WebScreenAnalyzer] \u2713 Analyzed: ${candidate.descriptor.name} (${file})`);
3455
+ }
3456
+ } catch (error2) {
3457
+ if (this.verbose) {
3458
+ console.warn(
3459
+ `[WebScreenAnalyzer] Failed to parse ${file}:`,
3460
+ error2 instanceof Error ? error2.message : error2
3461
+ );
3462
+ }
3463
+ }
3464
+ }
3465
+ return { candidates, analyzedFiles: files.length };
3466
+ }
3467
+ async analyzeFile(filePath) {
3468
+ const source = await import_promises2.default.readFile(filePath, "utf-8");
3469
+ const ast = parseSource(source, this.config.parserPlugins);
3470
+ const registerScreenMeta = this.extractRegisterScreenMetadata(ast);
3471
+ const componentName = this.extractComponentName(ast);
3472
+ const labelsByHtmlFor = this.collectHtmlForLabels(ast);
3473
+ const handlerBehaviors = this.collectWebHandlerBehaviors(ast);
3474
+ const forms = this.mergeForms(
3475
+ registerScreenMeta?.forms ?? [],
3476
+ this.extractForms(ast, labelsByHtmlFor)
3477
+ );
3478
+ const actions = this.extractActions(ast, registerScreenMeta, handlerBehaviors);
3479
+ const components = this.extractComponents(ast);
3480
+ const collections = this.extractCollections(ast);
3481
+ const navigationTargets = this.extractNavigationTargets(ast);
3482
+ const permissionsFromJsDoc = extractPermissionsFromJsDoc(source);
3483
+ const destructiveTags = extractDestructiveJsDocTargets(source);
3484
+ if (destructiveTags) {
3485
+ for (const action of actions) {
3486
+ if (destructiveTags === "*" || destructiveTags.has(action.id)) {
3487
+ action.destructive = true;
3488
+ }
3489
+ }
3490
+ }
3491
+ const name = registerScreenMeta?.name || componentName || import_path5.default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
3492
+ const descriptor = {
3493
+ name,
3494
+ filePath,
3495
+ title: registerScreenMeta?.title,
3496
+ description: registerScreenMeta?.description,
3497
+ components,
3498
+ forms,
3499
+ actions,
3500
+ navigationTargets,
3501
+ ...collections.length > 0 ? { collections } : {},
3502
+ ...registerScreenMeta?.suggestedPrompts && registerScreenMeta.suggestedPrompts.length > 0 ? { suggestedPrompts: registerScreenMeta.suggestedPrompts } : {},
3503
+ ...permissionsFromJsDoc ? {
3504
+ permissions: permissionsFromJsDoc,
3505
+ ...permissionsFromJsDoc.isPii ? { isPii: true } : {}
3506
+ } : {}
3507
+ };
3508
+ return {
3509
+ descriptor,
3510
+ hasRegisterScreen: registerScreenMeta !== null || this.detectRegisterScreenCall(ast),
3511
+ matchesScreenPattern: false
3512
+ };
3513
+ }
3514
+ // ── registerScreen ────────────────────────────────────────────────
3515
+ detectRegisterScreenCall(ast) {
3516
+ let found = false;
3517
+ (0, import_traverse9.default)(ast, {
3518
+ CallExpression: (nodePath) => {
3519
+ if (found) return;
3520
+ if (isRegisterScreenCallee(nodePath.node.callee)) {
3521
+ found = true;
3522
+ nodePath.stop();
3523
+ }
3524
+ }
3525
+ });
3526
+ return found;
3527
+ }
3528
+ extractRegisterScreenMetadata(ast) {
3529
+ let plain = null;
3530
+ (0, import_traverse9.default)(ast, {
3531
+ CallExpression: (nodePath) => {
3532
+ if (!isRegisterScreenCallee(nodePath.node.callee)) return;
3533
+ const arg = nodePath.node.arguments[0];
3534
+ if (t10.isObjectExpression(arg)) {
3535
+ plain = literalToPlain(arg);
3536
+ }
3537
+ }
3538
+ });
3539
+ if (!plain) return null;
3540
+ const meta = plain;
3541
+ const result = {
3542
+ name: typeof meta.name === "string" ? meta.name : "",
3543
+ actions: [],
3544
+ forms: [],
3545
+ navigationTargets: [],
3546
+ components: []
3547
+ };
3548
+ if (typeof meta.title === "string") result.title = meta.title;
3549
+ if (typeof meta.description === "string") result.description = meta.description;
3550
+ if (Array.isArray(meta.suggestedPrompts)) {
3551
+ const prompts = meta.suggestedPrompts.filter((p) => typeof p === "string").map((p) => p.trim()).filter((p) => p.length > 0);
3552
+ if (prompts.length > 0) result.suggestedPrompts = prompts;
3553
+ }
3554
+ if (Array.isArray(meta.actions)) {
3555
+ 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 }));
3556
+ }
3557
+ if (Array.isArray(meta.fields)) {
3558
+ const fields = meta.fields.filter((f) => typeof f === "object" && f !== null).filter((f) => typeof f.id === "string" && f.id.length > 0).map(
3559
+ (f) => ({
3560
+ name: f.id,
3561
+ type: typeof f.type === "string" ? f.type : "text",
3562
+ required: f.required === true,
3563
+ ...typeof f.label === "string" ? { label: f.label } : {},
3564
+ ...typeof f.placeholder === "string" ? { placeholder: f.placeholder } : {},
3565
+ ...f.defaultValue !== void 0 ? { defaultValue: f.defaultValue } : {},
3566
+ ...Array.isArray(f.options) ? { options: f.options } : {}
3567
+ })
3568
+ );
3569
+ if (fields.length > 0) result.forms = [{ id: "default", fields }];
3570
+ }
3571
+ return result;
3572
+ }
3573
+ // ── Component name / structure ────────────────────────────────────
3574
+ /** Default-exported component name, else the first exported capitalized function. */
3575
+ extractComponentName(ast) {
3576
+ let defaultName = "";
3577
+ let firstExported = "";
3578
+ (0, import_traverse9.default)(ast, {
3579
+ ExportDefaultDeclaration: (nodePath) => {
3580
+ const declaration = nodePath.node.declaration;
3581
+ if (t10.isFunctionDeclaration(declaration) && declaration.id?.name) {
3582
+ defaultName = declaration.id.name;
3583
+ } else if (t10.isIdentifier(declaration)) {
3584
+ defaultName = declaration.name;
3585
+ }
3586
+ },
3587
+ ExportNamedDeclaration: (nodePath) => {
3588
+ if (firstExported) return;
3589
+ const declaration = nodePath.node.declaration;
3590
+ if (t10.isFunctionDeclaration(declaration) && declaration.id && /^[A-Z]/.test(declaration.id.name)) {
3591
+ firstExported = declaration.id.name;
3592
+ } else if (t10.isVariableDeclaration(declaration)) {
3593
+ for (const declarator of declaration.declarations) {
3594
+ if (t10.isIdentifier(declarator.id) && /^[A-Z]/.test(declarator.id.name) && (t10.isArrowFunctionExpression(declarator.init) || t10.isFunctionExpression(declarator.init))) {
3595
+ firstExported = declarator.id.name;
3596
+ break;
3597
+ }
3598
+ }
3599
+ }
3600
+ }
3601
+ });
3602
+ return defaultName || firstExported;
3603
+ }
3604
+ extractComponents(ast) {
3605
+ const components = [];
3606
+ const seen = /* @__PURE__ */ new Set();
3607
+ (0, import_traverse9.default)(ast, {
3608
+ JSXOpeningElement: (nodePath) => {
3609
+ const element = nodePath.node;
3610
+ const name = getJsxElementName(element);
3611
+ if (!name || seen.has(name)) return;
3612
+ seen.add(name);
3613
+ const role = classifyWebJsxComponent(name, element);
3614
+ const type = role === "select" || role === "toggle" || role === "date" ? "input" : role === "input" || role === "button" || role === "list" || role === "modal" || role === "view" ? role : "custom";
3615
+ const component = { name, type };
3616
+ const testId = getStringAttr(element, "data-testid") ?? getStringAttr(element, "testID");
3617
+ const ariaLabel = getStringAttr(element, "aria-label");
3618
+ if (testId) component.testID = testId;
3619
+ if (ariaLabel) component.accessibilityLabel = ariaLabel;
3620
+ components.push(component);
3621
+ }
3622
+ });
3623
+ return components;
3624
+ }
3625
+ // ── <label htmlFor> association ───────────────────────────────────
3626
+ collectHtmlForLabels(ast) {
3627
+ const labels = /* @__PURE__ */ new Map();
3628
+ (0, import_traverse9.default)(ast, {
3629
+ JSXElement: (nodePath) => {
3630
+ const element = nodePath.node;
3631
+ if (getJsxElementName(element.openingElement) !== "label") return;
3632
+ const htmlFor = getStringAttr(element.openingElement, "htmlFor");
3633
+ if (!htmlFor) return;
3634
+ const text = jsxTextContent(element);
3635
+ if (text) labels.set(htmlFor, text);
3636
+ }
3637
+ });
3638
+ return labels;
3639
+ }
3640
+ // ── Forms ─────────────────────────────────────────────────────────
3641
+ extractForms(ast, labelsByHtmlFor) {
3642
+ const formBuckets = /* @__PURE__ */ new Map();
3643
+ const usedIds = /* @__PURE__ */ new Set();
3644
+ let formCount = 0;
3645
+ const uniqueId = (preferred) => {
3646
+ if (!usedIds.has(preferred)) {
3647
+ usedIds.add(preferred);
3648
+ return preferred;
3649
+ }
3650
+ let suffix = 2;
3651
+ while (usedIds.has(`${preferred}-${suffix}`)) suffix += 1;
3652
+ const id = `${preferred}-${suffix}`;
3653
+ usedIds.add(id);
3654
+ return id;
3655
+ };
3656
+ const bucketFor = (formElement) => {
3657
+ let bucket = formBuckets.get(formElement);
3658
+ if (bucket) return bucket;
3659
+ formCount += 1;
3660
+ let preferred = "default";
3661
+ let submitAction;
3662
+ if (formElement) {
3663
+ const opening = formElement.openingElement;
3664
+ preferred = getStringAttr(opening, "id") ?? getStringAttr(opening, "name") ?? getStringAttr(opening, "data-testid") ?? (formCount === 1 ? "default" : `form-${formCount}`);
3665
+ submitAction = this.handlerNameFromAttr(opening, "onSubmit");
3666
+ }
3667
+ bucket = { id: uniqueId(preferred), fields: /* @__PURE__ */ new Map(), submitAction };
3668
+ formBuckets.set(formElement, bucket);
3669
+ return bucket;
3670
+ };
3671
+ (0, import_traverse9.default)(ast, {
3672
+ JSXElement: (nodePath) => {
3673
+ const element = nodePath.node;
3674
+ const name = getJsxElementName(element.openingElement);
3675
+ if (!name) return;
3676
+ const role = classifyWebJsxComponent(name, element.openingElement);
3677
+ if (!["input", "select", "toggle", "date"].includes(role)) return;
3678
+ if (name === "option") return;
3679
+ const field = this.extractField(element, role, labelsByHtmlFor);
3680
+ if (!field.name) return;
3681
+ const formParent = nodePath.findParent(
3682
+ (p) => p.isJSXElement() && (getJsxElementName(p.node.openingElement) === "form" || getStringAttr(p.node.openingElement, "role") === "form")
3683
+ );
3684
+ const bucket = bucketFor(formParent ? formParent.node : null);
3685
+ if (!bucket.fields.has(field.name)) bucket.fields.set(field.name, field);
3686
+ }
3687
+ });
3688
+ (0, import_traverse9.default)(ast, {
3689
+ JSXElement: (nodePath) => {
3690
+ const element = nodePath.node;
3691
+ const name = getJsxElementName(element.openingElement);
3692
+ if (!name) return;
3693
+ if (!this.isSubmitElement(name, element.openingElement)) return;
3694
+ const formParent = nodePath.findParent(
3695
+ (p) => p.isJSXElement() && getJsxElementName(p.node.openingElement) === "form"
3696
+ );
3697
+ if (!formParent) return;
3698
+ const bucket = formBuckets.get(formParent.node);
3699
+ if (!bucket) return;
3700
+ const handler = this.handlerNameFromAttr(element.openingElement, "onClick");
3701
+ if (!bucket.submitAction && handler) bucket.submitAction = handler;
3702
+ }
3703
+ });
3704
+ return Array.from(formBuckets.values()).filter((bucket) => bucket.fields.size > 0).map((bucket) => ({
3705
+ id: bucket.id,
3706
+ fields: Array.from(bucket.fields.values()),
3707
+ ...bucket.submitAction && bucket.submitAction !== "anonymous" ? { submitAction: bucket.submitAction } : {}
3708
+ }));
3709
+ }
3710
+ isSubmitElement(name, opening) {
3711
+ const type = getStringAttr(opening, "type");
3712
+ if (name === "button") return type === "submit" || type === void 0;
3713
+ if (name === "input") return type === "submit";
3714
+ return false;
3715
+ }
3716
+ extractField(element, role, labelsByHtmlFor) {
3717
+ const opening = element.openingElement;
3718
+ const componentName = getJsxElementName(opening) ?? void 0;
3719
+ const field = {
3720
+ name: "",
3721
+ type: "text",
3722
+ required: false,
3723
+ sourceComponent: componentName
3724
+ };
3725
+ const appilotsId = getStringAttr(opening, "appilotsId");
3726
+ const dataTestId = getStringAttr(opening, "data-testid");
3727
+ const domId = getStringAttr(opening, "id");
3728
+ const nameAttr = getStringAttr(opening, "name");
3729
+ const ariaLabel = getStringAttr(opening, "aria-label");
3730
+ const placeholder = getStringAttr(opening, "placeholder");
3731
+ if (placeholder) field.placeholder = placeholder;
3732
+ if (ariaLabel) field.label = ariaLabel;
3733
+ if (domId && labelsByHtmlFor.has(domId)) field.label = labelsByHtmlFor.get(domId);
3734
+ if (appilotsId) {
3735
+ field.name = appilotsId;
3736
+ field.locator = mergeLocator(field.locator, {
3737
+ id: appilotsId,
3738
+ appilotsId,
3739
+ source: "appilotsId"
3740
+ });
3741
+ }
3742
+ if (dataTestId) {
3743
+ if (!field.name) field.name = dataTestId.replace(/^(input-|field-|txt-)/, "");
3744
+ field.locator = mergeLocator(field.locator, {
3745
+ ...field.locator?.id ? {} : { id: dataTestId },
3746
+ testID: dataTestId,
3747
+ source: field.locator?.source ?? "data-testid"
3748
+ });
3749
+ }
3750
+ if (nameAttr && !field.name) field.name = nameAttr;
3751
+ if (domId) {
3752
+ if (!field.name) field.name = domId;
3753
+ field.locator = mergeLocator(field.locator, {
3754
+ ...field.locator?.id ? {} : { id: domId },
3755
+ source: field.locator?.source ?? "id"
3756
+ });
3757
+ }
3758
+ if (ariaLabel) {
3759
+ field.locator = mergeLocator(field.locator, {
3760
+ ...field.locator?.id ? {} : { id: slugify(ariaLabel) },
3761
+ accessibilityLabel: ariaLabel,
3762
+ source: field.locator?.source ?? "aria-label"
3763
+ });
3764
+ }
3765
+ for (const attr of opening.attributes) {
3766
+ if (!t10.isJSXAttribute(attr) || !t10.isJSXIdentifier(attr.name)) continue;
3767
+ const attrName = attr.name.name;
3768
+ if (attrName === "required") {
3769
+ if (attr.value === null) field.required = true;
3770
+ else if (t10.isJSXExpressionContainer(attr.value) && t10.isBooleanLiteral(attr.value.expression)) {
3771
+ field.required = attr.value.expression.value;
3772
+ }
3773
+ }
3774
+ if ((attrName === "value" || attrName === "checked") && attr.value && t10.isJSXExpressionContainer(attr.value) && t10.isIdentifier(attr.value.expression)) {
3775
+ field.valueBinding = attr.value.expression.name;
3776
+ if (!field.name) field.name = attr.value.expression.name;
3777
+ }
3778
+ }
3779
+ if (getStringAttr(opening, "aria-required") === "true") field.required = true;
3780
+ if (role === "select") field.type = "select";
3781
+ else if (role === "toggle") field.type = "toggle";
3782
+ else if (role === "date") field.type = "date";
3783
+ else {
3784
+ field.type = this.inferInputType(opening, field);
3785
+ }
3786
+ if (componentName === "select") {
3787
+ const options = this.extractSelectOptions(element);
3788
+ if (options.length > 0) field.options = options;
3789
+ }
3790
+ if (!field.name || isWeakInferredFieldName(field.name)) {
3791
+ const labelish = field.label ?? field.placeholder;
3792
+ if (labelish) field.name = slugify(labelish);
3793
+ }
3794
+ if (!field.locator && field.name) {
3795
+ field.locator = { id: field.name, label: field.label, source: "inferred" };
3796
+ } else if (field.locator && !field.locator.id && field.name) {
3797
+ field.locator = mergeLocator(field.locator, {
3798
+ id: field.name,
3799
+ label: field.label,
3800
+ source: field.locator.source ?? "inferred"
3801
+ });
3802
+ }
3803
+ return field;
3804
+ }
3805
+ inferInputType(opening, field) {
3806
+ const type = getStringAttr(opening, "type");
3807
+ if (type === "email") return "email";
3808
+ if (type === "tel") return "phone";
3809
+ if (type === "number") return "number";
3810
+ if (type === "date" || type === "datetime-local" || type === "month" || type === "week") return "date";
3811
+ const inputMode = getStringAttr(opening, "inputMode") ?? getStringAttr(opening, "inputmode");
3812
+ if (inputMode === "email") return "email";
3813
+ if (inputMode === "tel") return "phone";
3814
+ if (inputMode === "numeric" || inputMode === "decimal") return "number";
3815
+ const combined = `${field.name} ${field.label ?? ""} ${field.placeholder ?? ""}`.toLowerCase();
3816
+ if (combined.includes("email")) return "email";
3817
+ if (combined.includes("phone") || combined.includes("tel")) return "phone";
3818
+ return "text";
3819
+ }
3820
+ extractSelectOptions(selectElement) {
3821
+ const options = [];
3822
+ for (const child of selectElement.children) {
3823
+ if (!t10.isJSXElement(child)) continue;
3824
+ if (getJsxElementName(child.openingElement) !== "option") continue;
3825
+ const value = getStringAttr(child.openingElement, "value");
3826
+ const label = jsxTextContent(child) || value || "";
3827
+ if (label && value) options.push({ label, value });
3828
+ }
3829
+ return options;
3830
+ }
3831
+ mergeForms(primary, secondary) {
3832
+ const out = primary.map((form) => ({ ...form, fields: [...form.fields] }));
3833
+ for (const form of secondary) {
3834
+ const existing = out.find((candidate) => candidate.id === form.id);
3835
+ if (!existing) {
3836
+ out.push({ ...form, fields: [...form.fields] });
3837
+ continue;
3838
+ }
3839
+ for (const field of form.fields) {
3840
+ const existingField = existing.fields.find((candidate) => candidate.name === field.name);
3841
+ if (!existingField) {
3842
+ existing.fields.push(field);
3843
+ continue;
3844
+ }
3845
+ existingField.label = existingField.label ?? field.label;
3846
+ existingField.placeholder = existingField.placeholder ?? field.placeholder;
3847
+ existingField.options = existingField.options ?? field.options;
3848
+ existingField.locator = existingField.locator ?? field.locator;
3849
+ existingField.sourceComponent = existingField.sourceComponent ?? field.sourceComponent;
3850
+ existingField.valueBinding = existingField.valueBinding ?? field.valueBinding;
3851
+ existingField.required = existingField.required || field.required;
3852
+ if (existingField.type === "text" && field.type !== "text") existingField.type = field.type;
3853
+ }
3854
+ existing.submitAction = existing.submitAction ?? form.submitAction;
3855
+ }
3856
+ return out;
3857
+ }
3858
+ // ── Actions ───────────────────────────────────────────────────────
3859
+ extractActions(ast, registerScreenMeta, handlerBehaviors) {
3860
+ const actions = [...registerScreenMeta?.actions ?? []];
3861
+ const actionIds = new Set(actions.map((a) => a.id));
3862
+ const actionLabels = new Map(
3863
+ actions.filter((a) => a.label).map((a) => [normalizeLabel(a.label), a])
3864
+ );
3865
+ (0, import_traverse9.default)(ast, {
3866
+ JSXElement: (nodePath) => {
3867
+ const element = nodePath.node;
3868
+ const name = getJsxElementName(element.openingElement);
3869
+ if (!name) return;
3870
+ if (classifyWebJsxComponent(name, element.openingElement) !== "button") return;
3871
+ const action = this.extractActionFromElement(element, nodePath);
3872
+ if (!action) return;
3873
+ const existingByLabel = action.label ? actionLabels.get(normalizeLabel(action.label)) : void 0;
3874
+ if (existingByLabel) {
3875
+ this.mergeActionMetadata(existingByLabel, action);
3876
+ return;
3877
+ }
3878
+ if (action.id && !actionIds.has(action.id)) {
3879
+ actions.push(action);
3880
+ actionIds.add(action.id);
3881
+ if (action.label) actionLabels.set(normalizeLabel(action.label), action);
3882
+ }
3883
+ }
3884
+ });
3885
+ this.enrichActionsFromHandlers(actions, handlerBehaviors);
3886
+ return actions;
3887
+ }
3888
+ extractActionFromElement(element, nodePath) {
3889
+ const opening = element.openingElement;
3890
+ const componentName = getJsxElementName(opening) ?? void 0;
3891
+ const action = { id: "", type: "custom", sourceComponent: componentName };
3892
+ const ariaLabel = getStringAttr(opening, "aria-label");
3893
+ const label = ariaLabel ?? jsxTextContent(element) ?? getStringAttr(opening, "value") ?? getStringAttr(opening, "title");
3894
+ if (label) action.label = label;
3895
+ if (ariaLabel) {
3896
+ action.locator = mergeLocator(action.locator, {
3897
+ accessibilityLabel: ariaLabel,
3898
+ source: "aria-label"
3899
+ });
3900
+ }
3901
+ const appilotsId = getStringAttr(opening, "appilotsId");
3902
+ const dataTestId = getStringAttr(opening, "data-testid");
3903
+ const domId = getStringAttr(opening, "id");
3904
+ if (appilotsId) {
3905
+ action.id = appilotsId;
3906
+ action.locator = mergeLocator(action.locator, {
3907
+ id: appilotsId,
3908
+ appilotsId,
3909
+ source: "appilotsId"
3910
+ });
3911
+ } else if (dataTestId) {
3912
+ action.id = dataTestId;
3913
+ action.locator = mergeLocator(action.locator, {
3914
+ id: dataTestId,
3915
+ testID: dataTestId,
3916
+ source: "data-testid"
3917
+ });
3918
+ } else if (domId) {
3919
+ action.id = domId;
3920
+ action.locator = mergeLocator(action.locator, { id: domId, source: "id" });
3921
+ } else if (action.label) {
3922
+ action.id = slugify(action.label);
3923
+ }
3924
+ if (!action.id) return null;
3925
+ const to = routePathAttr(opening, "to") ?? routePathAttr(opening, "href");
3926
+ if (to && to.startsWith("/")) {
3927
+ action.type = "navigation";
3928
+ action.targetScreen = to;
3929
+ } else if (to && !to.startsWith("/") && !hasJsxAttribute(opening, "onClick")) {
3930
+ return null;
3931
+ }
3932
+ let handlerName;
3933
+ const onClickAttr = opening.attributes.find(
3934
+ (attr) => t10.isJSXAttribute(attr) && t10.isJSXIdentifier(attr.name) && attr.name.name === "onClick"
3935
+ );
3936
+ if (onClickAttr?.value && t10.isJSXExpressionContainer(onClickAttr.value)) {
3937
+ const expr = onClickAttr.value.expression;
3938
+ if (t10.isIdentifier(expr)) {
3939
+ handlerName = expr.name;
3940
+ action.handler = expr.name;
3941
+ } else if (!t10.isJSXEmptyExpression(expr)) {
3942
+ const inlineNavCalls = extractWebNavigationCalls(expr);
3943
+ const inlineNav = inlineNavCalls.find((call) => call.targetPath);
3944
+ if (inlineNav?.targetPath) {
3945
+ action.type = "navigation";
3946
+ action.targetScreen = inlineNav.targetPath;
3947
+ } else if (inlineNavCalls.some((call) => call.method === "goBack")) {
3948
+ action.type = "navigation";
3949
+ action.successSignal = {
3950
+ type: "goBack",
3951
+ description: "Action returns to the previous page"
3952
+ };
3953
+ action.appilotsInferred = {
3954
+ ...action.appilotsInferred ?? {},
3955
+ expectedOutcome: "navigation"
3956
+ };
3957
+ }
3958
+ const inlineHandler = firstCalledFunctionName(expr);
3959
+ if (inlineHandler) {
3960
+ handlerName = inlineHandler;
3961
+ action.handler = inlineHandler;
3962
+ }
3963
+ }
3964
+ }
3965
+ if (handlerName) {
3966
+ const lower = handlerName.toLowerCase();
3967
+ if (lower.includes("submit")) action.type = "submit";
3968
+ else if (lower.includes("navigate") && action.type === "custom") action.type = "navigation";
3969
+ }
3970
+ if (componentName && this.isSubmitElement(componentName, opening)) {
3971
+ const formParent = nodePath.findParent(
3972
+ (p) => p.isJSXElement() && getJsxElementName(p.node.openingElement) === "form"
3973
+ );
3974
+ if (formParent) {
3975
+ action.type = "submit";
3976
+ if (!action.handler) {
3977
+ const formHandler = this.handlerNameFromAttr(formParent.node.openingElement, "onSubmit");
3978
+ if (formHandler && formHandler !== "anonymous") {
3979
+ action.handler = formHandler;
3980
+ handlerName = formHandler;
3981
+ }
3982
+ }
3983
+ }
3984
+ }
3985
+ if (hasJsxAttribute(opening, "destructive") || hasJsxAttribute(opening, "aria-destructive")) {
3986
+ const value = getStringAttr(opening, "destructive") ?? getStringAttr(opening, "aria-destructive");
3987
+ action.destructive = value !== "false";
3988
+ } else if (handlerName && DESTRUCTIVE_VERB2.test(handlerName) || DESTRUCTIVE_VERB2.test(action.id)) {
3989
+ action.destructive = true;
3990
+ }
3991
+ if (!action.locator && action.id) {
3992
+ action.locator = {
3993
+ id: action.id,
3994
+ label: action.label,
3995
+ source: action.label ? "label" : "inferred"
3996
+ };
3997
+ }
3998
+ return action;
3999
+ }
4000
+ mergeActionMetadata(target, source) {
4001
+ target.handler = target.handler ?? source.handler;
4002
+ target.targetScreen = target.targetScreen ?? source.targetScreen;
4003
+ target.description = target.description ?? source.description;
4004
+ target.locator = target.locator ?? source.locator;
4005
+ target.nativeConfirmationExpected = target.nativeConfirmationExpected || source.nativeConfirmationExpected || void 0;
4006
+ target.requiresConfirmation = target.requiresConfirmation || source.requiresConfirmation || void 0;
4007
+ target.destructive = target.destructive || source.destructive || void 0;
4008
+ target.effect = target.effect ?? source.effect;
4009
+ target.riskLevel = target.riskLevel ?? source.riskLevel;
4010
+ target.appilotsInferred = target.appilotsInferred ?? source.appilotsInferred;
4011
+ }
4012
+ enrichActionsFromHandlers(actions, behaviors) {
4013
+ for (const action of actions) {
4014
+ const behavior = action.handler ? behaviors.get(action.handler) : void 0;
4015
+ if (!behavior) continue;
4016
+ action.appilotsInferred = {
4017
+ ...action.appilotsInferred ?? {},
4018
+ ...behavior.base.appilotsInferred
4019
+ };
4020
+ if (behavior.nativeConfirmationExpected || behavior.base.nativeConfirmationExpected) {
4021
+ action.nativeConfirmationExpected = true;
4022
+ }
4023
+ if (behavior.targetPath && !action.targetScreen) {
4024
+ action.targetScreen = behavior.targetPath;
4025
+ if (action.type === "custom") action.type = "navigation";
4026
+ action.appilotsInferred = {
4027
+ ...action.appilotsInferred ?? {},
4028
+ expectedOutcome: "navigation"
4029
+ };
4030
+ }
4031
+ if (behavior.base.successSignal && !action.successSignal) {
4032
+ action.successSignal = behavior.base.successSignal;
4033
+ }
4034
+ if (behavior.base.failureSignal && !action.failureSignal) {
4035
+ action.failureSignal = behavior.base.failureSignal;
4036
+ }
4037
+ if (behavior.base.opensModal && !action.opensModal) {
4038
+ action.opensModal = behavior.base.opensModal;
4039
+ }
4040
+ if (behavior.base.destructive || action.destructive === true || action.requiresConfirmation === true || action.effect === "destructive" || action.riskLevel === "high") {
4041
+ action.destructive = true;
4042
+ action.effect = action.effect ?? "destructive";
4043
+ action.riskLevel = action.riskLevel ?? "high";
4044
+ action.requiresConfirmation = action.requiresConfirmation ?? true;
4045
+ }
4046
+ }
4047
+ }
4048
+ /**
4049
+ * Handler behavior via the shared, platform-neutral analyzer
4050
+ * (async/await, `.then`, state setters, toasts, destructive verbs)
4051
+ * plus the web-only signals: React Router navigation targets and
4052
+ * `window.confirm(...)` as the native confirmation dialog.
4053
+ */
4054
+ collectWebHandlerBehaviors(ast) {
4055
+ const handlers = collectFunctions(ast);
4056
+ const out = /* @__PURE__ */ new Map();
4057
+ for (const [name, fn] of handlers) {
4058
+ const base = analyzeFunctionBehavior(name, fn, handlers);
4059
+ const navCalls = fn.body ? extractWebNavigationCalls(fn.body) : [];
4060
+ const firstNav = navCalls.find((call) => call.targetPath);
4061
+ const goesBack = navCalls.some((call) => call.method === "goBack");
4062
+ out.set(name, {
4063
+ base: {
4064
+ ...base,
4065
+ ...goesBack && !base.successSignal ? {
4066
+ successSignal: {
4067
+ type: "goBack",
4068
+ description: "Action returns to the previous page"
4069
+ }
4070
+ } : {}
4071
+ },
4072
+ targetPath: firstNav?.targetPath,
4073
+ nativeConfirmationExpected: fn.body ? containsWindowConfirm(fn.body) : false
4074
+ });
4075
+ }
4076
+ return out;
4077
+ }
4078
+ handlerNameFromAttr(opening, attrName) {
4079
+ const attr = opening.attributes.find(
4080
+ (candidate) => t10.isJSXAttribute(candidate) && t10.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
4081
+ );
4082
+ if (!attr?.value || !t10.isJSXExpressionContainer(attr.value)) return void 0;
4083
+ const expr = attr.value.expression;
4084
+ if (t10.isIdentifier(expr)) return expr.name;
4085
+ if (t10.isArrowFunctionExpression(expr) || t10.isFunctionExpression(expr)) {
4086
+ return firstCalledFunctionName(expr) ?? "anonymous";
4087
+ }
4088
+ return void 0;
4089
+ }
4090
+ // ── Navigation targets (route paths — resolved by the orchestrator) ─
4091
+ extractNavigationTargets(ast) {
4092
+ const targets = /* @__PURE__ */ new Set();
4093
+ for (const call of extractWebNavigationCalls(ast)) {
4094
+ if (call.targetPath && call.targetPath.startsWith("/")) targets.add(call.targetPath);
4095
+ }
4096
+ (0, import_traverse9.default)(ast, {
4097
+ JSXOpeningElement: (nodePath) => {
4098
+ const element = nodePath.node;
4099
+ const name = getJsxElementName(element);
4100
+ if (name === "Link" || name === "NavLink" || name === "Navigate") {
4101
+ const to = routePathAttr(element, "to");
4102
+ if (to && to.startsWith("/")) targets.add(to);
4103
+ }
4104
+ if (name === "a") {
4105
+ const href = routePathAttr(element, "href");
4106
+ if (href && href.startsWith("/")) targets.add(href);
4107
+ }
4108
+ }
4109
+ });
4110
+ return Array.from(targets).sort();
4111
+ }
4112
+ // ── Collections ───────────────────────────────────────────────────
4113
+ extractCollections(ast) {
4114
+ const collections = [];
4115
+ const seen = /* @__PURE__ */ new Set();
4116
+ (0, import_traverse9.default)(ast, {
4117
+ JSXExpressionContainer: (nodePath) => {
4118
+ const expr = nodePath.node.expression;
4119
+ if (!t10.isCallExpression(expr) || !t10.isMemberExpression(expr.callee) || !t10.isIdentifier(expr.callee.object) || !t10.isIdentifier(expr.callee.property) || expr.callee.property.name !== "map") {
4120
+ return;
4121
+ }
4122
+ const callback = expr.arguments[0];
4123
+ if (!t10.isArrowFunctionExpression(callback) && !t10.isFunctionExpression(callback)) return;
4124
+ const enclosing = nodePath.findParent((p) => p.isJSXElement());
4125
+ const enclosingName = enclosing ? getJsxElementName(enclosing.node.openingElement) : null;
4126
+ const enclosingRole = enclosing && enclosingName ? classifyWebJsxComponent(enclosingName, enclosing.node.openingElement) : null;
4127
+ const returnsListItem = callbackReturnsTag(callback, /* @__PURE__ */ new Set(["li", "tr"]));
4128
+ if (enclosingName === "select") return;
4129
+ const isListContext = enclosingName !== null && LISTISH_TAGS.has(enclosingName) || enclosingRole === "list" || returnsListItem;
4130
+ if (!isListContext) return;
4131
+ const dataSource = expr.callee.object.name;
4132
+ if (seen.has(dataSource)) return;
4133
+ seen.add(dataSource);
4134
+ const itemNames = collectionItemNames(callback);
4135
+ const displayFields = collectionDisplayFields(callback, itemNames);
4136
+ const keyField = collectionKeyField(callback, itemNames);
4137
+ const rowAction = collectionRowAction(callback);
4138
+ const itemType = inferItemType(dataSource, displayFields);
4139
+ const identityFields = inferIdentityFields(keyField, displayFields);
4140
+ collections.push({
4141
+ id: dataSource,
4142
+ component: enclosingName ?? "list",
4143
+ ...itemType ? { itemType } : {},
4144
+ dataSource,
4145
+ ...keyField ? { keyField } : {},
4146
+ ...displayFields.length > 0 ? { displayFields } : {},
4147
+ ...rowAction ? { rowAction, rowActions: [rowAction] } : {},
4148
+ ...identityFields.length > 0 ? { identityFields } : {}
4149
+ });
4150
+ }
4151
+ });
4152
+ return collections;
4153
+ }
4154
+ };
4155
+ function isRegisterScreenCallee(callee) {
4156
+ return t10.isIdentifier(callee) && callee.name === "registerScreen" || t10.isMemberExpression(callee) && t10.isIdentifier(callee.property) && callee.property.name === "registerScreen";
4157
+ }
4158
+ function literalToPlain(node) {
4159
+ if (t10.isStringLiteral(node) || t10.isNumericLiteral(node) || t10.isBooleanLiteral(node)) {
4160
+ return node.value;
4161
+ }
4162
+ if (t10.isNullLiteral(node)) return null;
4163
+ if (t10.isArrayExpression(node)) {
4164
+ return node.elements.filter((el) => el !== null && t10.isExpression(el)).map((el) => literalToPlain(el)).filter((value) => value !== void 0);
4165
+ }
4166
+ if (t10.isObjectExpression(node)) {
4167
+ const out = {};
4168
+ for (const prop of node.properties) {
4169
+ if (!t10.isObjectProperty(prop)) continue;
4170
+ const key = t10.isIdentifier(prop.key) ? prop.key.name : t10.isStringLiteral(prop.key) ? prop.key.value : void 0;
4171
+ if (!key || !t10.isExpression(prop.value)) continue;
4172
+ const value = literalToPlain(prop.value);
4173
+ if (value !== void 0) out[key] = value;
4174
+ }
4175
+ return out;
4176
+ }
4177
+ return void 0;
4178
+ }
4179
+ function jsxTextContent(element) {
4180
+ const parts = [];
4181
+ const walk = (children) => {
4182
+ for (const child of children) {
4183
+ if (t10.isJSXText(child)) {
4184
+ const trimmed = child.value.replace(/\s+/g, " ").trim();
4185
+ if (trimmed) parts.push(trimmed);
4186
+ } else if (t10.isJSXExpressionContainer(child) && t10.isStringLiteral(child.expression)) {
4187
+ parts.push(child.expression.value);
4188
+ } else if (t10.isJSXElement(child)) {
4189
+ walk(child.children);
4190
+ }
4191
+ }
4192
+ };
4193
+ walk(element.children);
4194
+ const text = parts.join(" ").trim();
4195
+ return text.length > 0 ? text : void 0;
4196
+ }
4197
+ function firstCalledFunctionName(node) {
4198
+ if (t10.isIdentifier(node)) return node.name;
4199
+ if (t10.isArrowFunctionExpression(node) || t10.isFunctionExpression(node)) {
4200
+ return firstCalledFunctionName(node.body);
4201
+ }
4202
+ if (t10.isBlockStatement(node)) {
4203
+ for (const statement of node.body) {
4204
+ const handler = firstCalledFunctionName(statement);
4205
+ if (handler) return handler;
4206
+ }
4207
+ return void 0;
4208
+ }
4209
+ if (t10.isExpressionStatement(node)) return firstCalledFunctionName(node.expression);
4210
+ if (t10.isReturnStatement(node)) {
4211
+ return node.argument ? firstCalledFunctionName(node.argument) : void 0;
4212
+ }
4213
+ if (t10.isAwaitExpression(node) || t10.isUnaryExpression(node)) {
4214
+ return firstCalledFunctionName(node.argument);
4215
+ }
4216
+ if (t10.isCallExpression(node)) {
4217
+ if (t10.isIdentifier(node.callee) && !/^(navigate|confirm|alert)$/.test(node.callee.name)) {
4218
+ return node.callee.name;
4219
+ }
4220
+ return void 0;
4221
+ }
4222
+ return void 0;
4223
+ }
4224
+ function containsWindowConfirm(body) {
4225
+ let found = false;
4226
+ (0, import_traverse9.default)(body, {
4227
+ noScope: true,
4228
+ CallExpression: (nodePath) => {
4229
+ const callee = nodePath.node.callee;
4230
+ if (t10.isIdentifier(callee) && callee.name === "confirm") found = true;
4231
+ if (t10.isMemberExpression(callee) && t10.isIdentifier(callee.object) && callee.object.name === "window" && t10.isIdentifier(callee.property) && callee.property.name === "confirm") {
4232
+ found = true;
4233
+ }
4234
+ }
4235
+ });
4236
+ return found;
4237
+ }
4238
+ function mergeLocator(current, next) {
4239
+ return { ...current ?? {}, ...next };
4240
+ }
4241
+ function routePathAttr(opening, attrName) {
4242
+ const literal = getStringAttr(opening, attrName);
4243
+ if (literal) return literal;
4244
+ const attr = opening.attributes.find(
4245
+ (candidate) => t10.isJSXAttribute(candidate) && t10.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
4246
+ );
4247
+ if (!attr?.value || !t10.isJSXExpressionContainer(attr.value)) return void 0;
4248
+ return staticRoutePath(attr.value.expression);
4249
+ }
4250
+ function slugify(label) {
4251
+ return label.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
4252
+ }
4253
+ function normalizeLabel(label) {
4254
+ return label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]/g, "");
4255
+ }
4256
+ function isWeakInferredFieldName(name) {
4257
+ return /^(text|value|input|query|search|selected|checked)$/i.test(name);
4258
+ }
4259
+ function collectionItemNames(callback) {
4260
+ const names = /* @__PURE__ */ new Set(["item"]);
4261
+ const firstParam = callback.params[0];
4262
+ if (t10.isIdentifier(firstParam)) names.add(firstParam.name);
4263
+ if (t10.isObjectPattern(firstParam)) {
4264
+ for (const prop of firstParam.properties) {
4265
+ if (t10.isObjectProperty(prop) && t10.isIdentifier(prop.key) && t10.isIdentifier(prop.value)) {
4266
+ names.add(prop.value.name);
4267
+ }
4268
+ }
4269
+ }
4270
+ return names;
4271
+ }
4272
+ function collectionDisplayFields(callback, itemNames) {
4273
+ const fields = /* @__PURE__ */ new Set();
4274
+ if (!callback.body) return [];
4275
+ (0, import_traverse9.default)(callback.body, {
4276
+ noScope: true,
4277
+ MemberExpression: (nodePath) => {
4278
+ const node = nodePath.node;
4279
+ if (t10.isIdentifier(node.object) && itemNames.has(node.object.name) && t10.isIdentifier(node.property)) {
4280
+ fields.add(node.property.name);
4281
+ }
4282
+ }
4283
+ });
4284
+ return Array.from(fields).sort();
4285
+ }
4286
+ function collectionKeyField(callback, itemNames) {
4287
+ let keyField;
4288
+ if (!callback.body) return void 0;
4289
+ (0, import_traverse9.default)(callback.body, {
4290
+ noScope: true,
4291
+ JSXAttribute: (nodePath) => {
4292
+ const attr = nodePath.node;
4293
+ if (!t10.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
4294
+ if (!attr.value || !t10.isJSXExpressionContainer(attr.value)) return;
4295
+ const expr = attr.value.expression;
4296
+ if (t10.isMemberExpression(expr) && t10.isIdentifier(expr.object) && itemNames.has(expr.object.name) && t10.isIdentifier(expr.property)) {
4297
+ keyField = keyField ?? expr.property.name;
4298
+ }
4299
+ }
4300
+ });
4301
+ return keyField;
4302
+ }
4303
+ function collectionRowAction(callback) {
4304
+ if (!callback.body) return void 0;
4305
+ const navCall = extractWebNavigationCalls(callback.body).find((call) => call.targetPath);
4306
+ if (!navCall?.targetPath) return void 0;
4307
+ return {
4308
+ type: "navigation",
4309
+ // Route path — the orchestrator resolves it to a screen name.
4310
+ targetScreen: navCall.targetPath,
4311
+ description: `Clicking a row opens ${navCall.targetPath}`
4312
+ };
4313
+ }
4314
+ function callbackReturnsTag(callback, tags) {
4315
+ let found = false;
4316
+ const inspect = (node) => {
4317
+ if (!node || found) return;
4318
+ if (t10.isJSXElement(node)) {
4319
+ const name = getJsxElementName(node.openingElement);
4320
+ if (name && tags.has(name)) found = true;
4321
+ return;
4322
+ }
4323
+ if (t10.isBlockStatement(node)) {
4324
+ for (const statement of node.body) {
4325
+ if (t10.isReturnStatement(statement)) inspect(statement.argument);
4326
+ }
4327
+ }
4328
+ if (t10.isParenthesizedExpression(node)) inspect(node.expression);
4329
+ if (t10.isConditionalExpression(node)) {
4330
+ inspect(node.consequent);
4331
+ inspect(node.alternate);
4332
+ }
4333
+ };
4334
+ inspect(callback.body);
4335
+ return found;
4336
+ }
4337
+ function inferItemType(dataSource, displayFields) {
4338
+ const singular = dataSource.replace(/^render/i, "").replace(/(List|Items|Data|Rows|Sections)$/i, "").replace(/s$/i, "");
4339
+ const candidate = singular.charAt(0).toUpperCase() + singular.slice(1);
4340
+ if (candidate.length > 1) return candidate;
4341
+ if (displayFields.length > 0) return "Item";
4342
+ return void 0;
4343
+ }
4344
+ function inferIdentityFields(keyField, displayFields) {
4345
+ const out = /* @__PURE__ */ new Set();
4346
+ if (keyField) out.add(keyField);
4347
+ for (const field of displayFields) {
4348
+ if (/^(id|uuid|key|name|title|plate|email|slug)$/i.test(field)) out.add(field);
4349
+ }
4350
+ return Array.from(out);
4351
+ }
4352
+
4353
+ // src/analyzers/web/WebNavigationAnalyzer.ts
4354
+ var import_fs7 = require("fs");
4355
+ var import_path6 = __toESM(require("path"));
4356
+ var import_fast_glob5 = __toESM(require("fast-glob"));
4357
+ var import_traverse10 = __toESM(require("@babel/traverse"));
4358
+ var t11 = __toESM(require("@babel/types"));
4359
+ var WEB_NAVIGATOR_TYPE = "route";
4360
+ var WebNavigationAnalyzer = class {
4361
+ config;
4362
+ navigationInclude;
4363
+ navigationExclude;
4364
+ constructor(config, options) {
4365
+ this.config = config;
4366
+ this.navigationInclude = options?.navigationInclude ?? [];
4367
+ this.navigationExclude = options?.navigationExclude ?? [];
4368
+ }
4369
+ async analyze() {
4370
+ const files = await this.findRouteFiles();
4371
+ const routes = [];
4372
+ for (const filePath of files) {
4373
+ try {
4374
+ const content = await import_fs7.promises.readFile(filePath, "utf-8");
4375
+ if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(content)) {
4376
+ continue;
4377
+ }
4378
+ const ast = parseSource(content, this.config.parserPlugins);
4379
+ routes.push(...this.extractJsxRoutes(ast));
4380
+ routes.push(...this.extractObjectRoutes(ast));
4381
+ } catch (error2) {
4382
+ console.warn(`[WebNavigationAnalyzer] Failed to parse ${filePath}:`, error2);
4383
+ }
4384
+ }
4385
+ const deduped = this.dedupeRoutes(routes);
4386
+ return { graph: this.buildGraph(deduped), routes: deduped };
4387
+ }
4388
+ /** Files likely to contain route configuration. */
4389
+ async findRouteFiles() {
4390
+ const patterns = [
4391
+ "**/*{router,routes,Router,Routes}*.{ts,tsx,js,jsx}",
4392
+ "**/App.{ts,tsx,js,jsx}",
4393
+ "**/app.{ts,tsx,js,jsx}",
4394
+ "**/main.{ts,tsx,js,jsx}",
4395
+ "**/index.{ts,tsx,js,jsx}",
4396
+ ...this.navigationInclude
4397
+ ];
4398
+ const ignore = [
4399
+ "**/node_modules/**",
4400
+ "**/dist/**",
4401
+ "**/build/**",
4402
+ ...this.config.exclude || [],
4403
+ ...this.navigationExclude
4404
+ ];
4405
+ const files = await (0, import_fast_glob5.default)(patterns, { cwd: this.config.rootDir, ignore });
4406
+ return files.map((file) => import_path6.default.join(this.config.rootDir, file));
4407
+ }
4408
+ // ── JSX <Route> style ────────────────────────────────────────────
4409
+ extractJsxRoutes(ast) {
4410
+ const routes = [];
4411
+ const visitRoute = (element, parentPath) => {
4412
+ const opening = element.openingElement;
4413
+ const name = getJsxElementName(opening);
4414
+ if (name !== "Route") {
4415
+ for (const child of element.children) {
4416
+ if (t11.isJSXElement(child)) visitRoute(child, parentPath);
4417
+ }
4418
+ return;
4419
+ }
4420
+ const segment = getStringAttr(opening, "path");
4421
+ const isIndex = hasJsxAttribute(opening, "index") && !segment;
4422
+ const fullPath = this.joinPaths(parentPath, segment, isIndex);
4423
+ const componentName = this.componentNameFromElementAttr(opening) ?? void 0;
4424
+ const isLeaf = !element.children.some(
4425
+ (child) => t11.isJSXElement(child) && getJsxElementName(child.openingElement) === "Route"
4426
+ );
4427
+ if ((segment || isIndex) && (componentName || isLeaf)) {
4428
+ routes.push(this.buildRoute(fullPath, componentName, isIndex, !isLeaf));
4429
+ }
4430
+ for (const child of element.children) {
4431
+ if (t11.isJSXElement(child)) visitRoute(child, fullPath);
4432
+ }
4433
+ };
4434
+ (0, import_traverse10.default)(ast, {
4435
+ JSXElement: (nodePath) => {
4436
+ const name = getJsxElementName(nodePath.node.openingElement);
4437
+ if (name !== "Routes" && name !== "Route") return;
4438
+ if (nodePath.findParent((p) => {
4439
+ if (!p.isJSXElement()) return false;
4440
+ const parentName = getJsxElementName(p.node.openingElement);
4441
+ return parentName === "Routes" || parentName === "Route";
4442
+ })) {
4443
+ return;
4444
+ }
4445
+ visitRoute(nodePath.node, "");
4446
+ }
4447
+ });
4448
+ return routes;
4449
+ }
4450
+ /** `element={<VehicleList/>}` or `Component={VehicleList}`. */
4451
+ componentNameFromElementAttr(opening) {
4452
+ for (const attr of opening.attributes) {
4453
+ if (!t11.isJSXAttribute(attr) || !t11.isJSXIdentifier(attr.name)) continue;
4454
+ if (attr.name.name === "element" && t11.isJSXExpressionContainer(attr.value)) {
4455
+ const expr = attr.value.expression;
4456
+ if (t11.isJSXElement(expr)) return getJsxElementName(expr.openingElement);
4457
+ }
4458
+ if (attr.name.name === "Component" && t11.isJSXExpressionContainer(attr.value)) {
4459
+ if (t11.isIdentifier(attr.value.expression)) return attr.value.expression.name;
4460
+ }
4461
+ }
4462
+ return null;
2989
4463
  }
2990
- buildForms(filePath, validationRules) {
2991
- if (this.inputElements.length === 0) return [];
2992
- const fileName = path3.basename(filePath, path3.extname(filePath));
2993
- const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
2994
- const fields = this.inputElements.map((input) => {
2995
- const fieldType = this.inferFieldType(input);
2996
- const required = this.isFieldRequired(input.varName, validationRules);
2997
- return {
2998
- name: input.appilotsId || input.varName || input.testID || "field",
2999
- label: input.label,
3000
- type: fieldType,
3001
- required,
3002
- placeholder: input.placeholder
3003
- };
3004
- });
3005
- const lastButton = this.submitButtons[this.submitButtons.length - 1];
3006
- const submitAction = lastButton?.handler;
3007
- return [
3008
- {
3009
- id: formId,
3010
- fields,
3011
- submitAction,
3012
- validationRules: Object.keys(validationRules).length > 0 ? validationRules : void 0
4464
+ // ── createBrowserRouter([...]) / useRoutes([...]) style ──────────
4465
+ extractObjectRoutes(ast) {
4466
+ const routes = [];
4467
+ const ROUTER_FACTORIES = /* @__PURE__ */ new Set([
4468
+ "createBrowserRouter",
4469
+ "createHashRouter",
4470
+ "createMemoryRouter",
4471
+ "useRoutes"
4472
+ ]);
4473
+ (0, import_traverse10.default)(ast, {
4474
+ CallExpression: (nodePath) => {
4475
+ const callee = nodePath.node.callee;
4476
+ if (!t11.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
4477
+ const first = nodePath.node.arguments[0];
4478
+ if (!t11.isArrayExpression(first)) return;
4479
+ this.visitRouteObjects(first, "", routes);
3013
4480
  }
3014
- ];
4481
+ });
4482
+ return routes;
3015
4483
  }
3016
- inferFieldType(input) {
3017
- const lowerVarName = input.varName.toLowerCase();
3018
- const lowerLabel = input.label?.toLowerCase() || "";
3019
- const lowerPlaceholder = input.placeholder?.toLowerCase() || "";
3020
- const combined = `${lowerVarName} ${lowerLabel} ${lowerPlaceholder}`;
3021
- if (input.keyboardType === "email-address" || input.keyboardType === "email") {
3022
- return "email";
4484
+ visitRouteObjects(arr, parentPath, out) {
4485
+ for (const element of arr.elements) {
4486
+ if (!t11.isObjectExpression(element)) continue;
4487
+ let segment;
4488
+ let isIndex = false;
4489
+ let componentName;
4490
+ let children;
4491
+ for (const prop of element.properties) {
4492
+ if (!t11.isObjectProperty(prop) || !t11.isIdentifier(prop.key)) continue;
4493
+ const key = prop.key.name;
4494
+ if (key === "path" && t11.isStringLiteral(prop.value)) segment = prop.value.value;
4495
+ if (key === "index" && t11.isBooleanLiteral(prop.value)) isIndex = prop.value.value;
4496
+ if (key === "element" && t11.isJSXElement(prop.value)) {
4497
+ componentName = getJsxElementName(prop.value.openingElement) ?? void 0;
4498
+ }
4499
+ if (key === "Component" && t11.isIdentifier(prop.value)) componentName = prop.value.name;
4500
+ if (key === "children" && t11.isArrayExpression(prop.value)) children = prop.value;
4501
+ }
4502
+ const fullPath = this.joinPaths(parentPath, segment, isIndex);
4503
+ if ((segment !== void 0 || isIndex) && (componentName || !children)) {
4504
+ out.push(this.buildRoute(fullPath, componentName, isIndex, Boolean(children)));
4505
+ }
4506
+ if (children) this.visitRouteObjects(children, fullPath, out);
4507
+ }
4508
+ }
4509
+ // ── Shared route building ────────────────────────────────────────
4510
+ joinPaths(parent, segment, isIndex) {
4511
+ if (isIndex || segment === void 0) return parent || "/";
4512
+ if (segment.startsWith("/")) return this.normalizePath(segment);
4513
+ return this.normalizePath(`${parent === "/" ? "" : parent}/${segment}`);
4514
+ }
4515
+ normalizePath(p) {
4516
+ const cleaned = `/${p}`.replace(/\/+/g, "/");
4517
+ return cleaned.length > 1 ? cleaned.replace(/\/$/, "") : cleaned;
4518
+ }
4519
+ buildRoute(fullPath, componentName, isIndex, isLayout) {
4520
+ const params = this.paramsFromPath(fullPath);
4521
+ return {
4522
+ path: fullPath,
4523
+ screenName: componentName ?? screenNameFromPath(fullPath),
4524
+ ...params.length > 0 ? { params } : {},
4525
+ ...isIndex ? { index: true } : {},
4526
+ ...isLayout ? { layout: true } : {}
4527
+ };
4528
+ }
4529
+ paramsFromPath(routePath) {
4530
+ const params = [];
4531
+ for (const segment of routePath.split("/")) {
4532
+ if (!segment.startsWith(":")) continue;
4533
+ const optional = segment.endsWith("?");
4534
+ const name = segment.slice(1, optional ? -1 : void 0);
4535
+ if (name) params.push({ name, type: "string", required: !optional });
3023
4536
  }
3024
- if (input.keyboardType === "phone-pad" || input.keyboardType === "numeric") {
3025
- return input.keyboardType === "numeric" ? "number" : "phone";
4537
+ return params;
4538
+ }
4539
+ /**
4540
+ * One entry per path. When several declarations resolve to the same
4541
+ * path, keep the one that best describes what the user lands on: a
4542
+ * page beats a layout wrapper (an `index` child and its parent layout
4543
+ * share a path), and a resolved component name beats a name derived
4544
+ * from the path.
4545
+ */
4546
+ dedupeRoutes(routes) {
4547
+ const byPath = /* @__PURE__ */ new Map();
4548
+ for (const route of routes) {
4549
+ const existing = byPath.get(route.path);
4550
+ if (!existing || routeScore(route) > routeScore(existing)) {
4551
+ byPath.set(route.path, route);
4552
+ }
3026
4553
  }
3027
- if (combined.includes("email")) return "email";
3028
- if (combined.includes("phone") || combined.includes("tel")) return "phone";
3029
- if (combined.includes("password")) return "text";
3030
- if (combined.includes("number") || combined.includes("numeric")) return "number";
3031
- if (combined.includes("date")) return "date";
3032
- if (combined.includes("toggle") || combined.includes("check")) return "toggle";
3033
- if (combined.includes("select") || combined.includes("choice")) return "select";
3034
- return "text";
4554
+ return Array.from(byPath.values());
3035
4555
  }
3036
- isFieldRequired(fieldName, rules) {
3037
- const rule = rules[fieldName];
3038
- return rule !== void 0 && rule.includes("required");
4556
+ buildGraph(routes) {
4557
+ const screens = {};
4558
+ const navigatorName = "router";
4559
+ const screenNames = routes.map((r) => r.screenName);
4560
+ for (const route of routes) {
4561
+ const others = screenNames.filter((name) => name !== route.screenName);
4562
+ screens[route.screenName] = {
4563
+ screenName: route.screenName,
4564
+ // Open-union value — web routes, not a RN stack/tab/drawer.
4565
+ navigatorType: WEB_NAVIGATOR_TYPE,
4566
+ parentNavigator: navigatorName,
4567
+ // Any route is one URL away from any other — both directions,
4568
+ // like the RN analyzer models tab navigators.
4569
+ reachableFrom: others,
4570
+ reachableTo: others,
4571
+ ...route.params ? { params: route.params } : {}
4572
+ };
4573
+ }
4574
+ const initialRoute = routes.find((r) => r.path === "/") ?? routes.find((r) => r.index) ?? routes[0];
4575
+ return {
4576
+ screens,
4577
+ initialScreen: initialRoute?.screenName ?? "",
4578
+ navigators: routes.length > 0 ? [{
4579
+ name: navigatorName,
4580
+ type: WEB_NAVIGATOR_TYPE,
4581
+ screens: screenNames
4582
+ }] : []
4583
+ };
3039
4584
  }
3040
4585
  };
4586
+ function screenNameFromPath(routePath) {
4587
+ if (routePath === "/" || routePath === "") return "Home";
4588
+ return routePath.split("/").filter(Boolean).map((segment) => segment.replace(/^:/, "").replace(/\?$/, "")).map(
4589
+ (segment) => segment.split(/[-_.]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("")
4590
+ ).join("");
4591
+ }
4592
+ function routeScore(route) {
4593
+ const isPage = route.layout ? 0 : 2;
4594
+ const hasRealName = route.screenName === screenNameFromPath(route.path) ? 0 : 1;
4595
+ return isPage + hasRealName;
4596
+ }
4597
+ function resolvePathToScreen(routes, target) {
4598
+ const normalized = `/${target}`.replace(/\/+/g, "/").replace(/\?.*$/, "").replace(/#.*$/, "");
4599
+ const cleaned = normalized.length > 1 ? normalized.replace(/\/$/, "") : normalized;
4600
+ const exact = routes.find((r) => r.path === cleaned);
4601
+ if (exact) return exact.screenName;
4602
+ const targetSegments = cleaned.split("/").filter(Boolean);
4603
+ for (const route of routes) {
4604
+ const routeSegments = route.path.split("/").filter(Boolean);
4605
+ if (routeSegments.length !== targetSegments.length) continue;
4606
+ const matches = routeSegments.every(
4607
+ (seg, i) => seg.startsWith(":") || seg === "*" || seg === targetSegments[i]
4608
+ );
4609
+ if (matches) return route.screenName;
4610
+ }
4611
+ return void 0;
4612
+ }
3041
4613
 
3042
- // src/analyzers/ReactNativePlatformAnalyzer.ts
3043
- var ReactNativePlatformAnalyzer = class {
3044
- platform = "react-native";
4614
+ // src/analyzers/web/ReactWebPlatformAnalyzer.ts
4615
+ var ReactWebPlatformAnalyzer = class {
4616
+ platform = "web";
3045
4617
  async analyze(config, options) {
3046
- const screenAnalyzer = new ScreenAnalyzer(config, {
3047
- strictScreens: options.strictScreens ?? true,
4618
+ const screenAnalyzer = new WebScreenAnalyzer(config, {
3048
4619
  screenPatterns: options.screenPatterns
3049
4620
  });
3050
- const navigationAnalyzer = new NavigationAnalyzer(config, {
4621
+ const navigationAnalyzer = new WebNavigationAnalyzer(config, {
3051
4622
  navigationInclude: options.navigationInclude,
3052
4623
  navigationExclude: options.navigationExclude
3053
4624
  });
3054
- const componentAnalyzer = new ComponentAnalyzer(config);
3055
- const formAnalyzer = new FormAnalyzer(config);
3056
- console.log("[ReactNativePlatformAnalyzer] Running analyzers...");
3057
- const [screens, navigation] = await Promise.all([
4625
+ console.log("[ReactWebPlatformAnalyzer] Running analyzers...");
4626
+ const [screenAnalysis, navigationResult] = await Promise.all([
3058
4627
  screenAnalyzer.analyze(),
3059
4628
  navigationAnalyzer.analyze()
3060
4629
  ]);
3061
- console.log(
3062
- `[ReactNativePlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens`
4630
+ const routeScreenNames = new Set(navigationResult.routes.map((route) => route.screenName));
4631
+ const strictScreens = options.strictScreens ?? true;
4632
+ let screensFilteredOut = 0;
4633
+ const included = [];
4634
+ for (const candidate of screenAnalysis.candidates) {
4635
+ if (!strictScreens || this.isScreen(candidate, routeScreenNames)) {
4636
+ included.push(candidate);
4637
+ } else {
4638
+ screensFilteredOut++;
4639
+ }
4640
+ }
4641
+ const screens = included.map(
4642
+ (candidate) => this.resolveRoutePaths(candidate.descriptor, navigationResult.routes)
3063
4643
  );
3064
- const screenFiles = await (0, import_fast_glob3.default)(config.include || ["**/*.tsx", "**/*.ts"], {
3065
- cwd: config.rootDir,
3066
- ignore: config.exclude || ["**/node_modules/**"]
3067
- });
3068
4644
  console.log(
3069
- `[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
4645
+ `[ReactWebPlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens, ${navigationResult.routes.length} routes`
3070
4646
  );
3071
- const enrichmentPromises = screenFiles.map(async (file) => {
3072
- const filePath = import_node_path.default.resolve(config.rootDir, file);
3073
- try {
3074
- const [components, forms] = await Promise.all([
3075
- componentAnalyzer.analyzeFile(filePath),
3076
- formAnalyzer.analyzeFile(filePath)
3077
- ]);
3078
- return { filePath, components, forms };
3079
- } catch (error2) {
3080
- console.warn(`[ReactNativePlatformAnalyzer] Failed to analyze ${file}:`, error2);
3081
- return { filePath, components: [], forms: [] };
3082
- }
3083
- });
3084
- const enrichmentResults = await Promise.all(enrichmentPromises);
3085
- const enrichmentMap = /* @__PURE__ */ new Map();
3086
- for (const result of enrichmentResults) {
3087
- enrichmentMap.set(result.filePath, {
3088
- components: result.components,
3089
- forms: result.forms
3090
- });
3091
- }
3092
- const enrichedScreens = screens.map((screen) => {
3093
- const enrichment = enrichmentMap.get(screen.filePath);
3094
- if (enrichment) {
3095
- const existingComponentNames = new Set(screen.components.map((c) => c.name));
3096
- const newComponents = enrichment.components.filter(
3097
- (c) => !existingComponentNames.has(c.name)
3098
- );
3099
- const mergedForms = screen.forms.map((form) => ({
3100
- ...form,
3101
- fields: [...form.fields]
3102
- }));
3103
- for (const newForm of enrichment.forms) {
3104
- const existingForm = mergedForms.find((form) => form.id === newForm.id) ?? this.findFormWithSharedFields(mergedForms, newForm);
3105
- if (existingForm) {
3106
- this.mergeForm(existingForm, newForm);
3107
- } else {
3108
- mergedForms.push({
3109
- ...newForm,
3110
- fields: [...newForm.fields],
3111
- submitAction: this.namedSubmitAction(newForm.submitAction)
3112
- });
3113
- }
3114
- }
3115
- return {
3116
- ...screen,
3117
- components: [...screen.components, ...newComponents],
3118
- forms: mergedForms
3119
- };
3120
- }
3121
- return screen;
3122
- });
3123
4647
  return {
3124
- screens: enrichedScreens,
3125
- navigation,
3126
- analyzedFiles: screenFiles.length,
3127
- ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
4648
+ screens,
4649
+ navigation: navigationResult.graph,
4650
+ analyzedFiles: screenAnalysis.analyzedFiles,
4651
+ ...screensFilteredOut > 0 ? { screensFilteredOut } : {}
3128
4652
  };
3129
4653
  }
3130
- mergeForm(target, source) {
3131
- for (const field of source.fields) {
3132
- const existingField = this.findEquivalentField(target.fields, field);
3133
- if (existingField) {
3134
- this.mergeField(existingField, field);
3135
- } else {
3136
- target.fields.push(field);
3137
- }
3138
- }
3139
- target.submitAction = target.submitAction ?? this.namedSubmitAction(source.submitAction);
3140
- target.validationRules = source.validationRules ? { ...source.validationRules, ...target.validationRules ?? {} } : target.validationRules;
4654
+ isScreen(candidate, routeScreenNames) {
4655
+ return candidate.hasRegisterScreen || candidate.matchesScreenPattern || routeScreenNames.has(candidate.descriptor.name);
3141
4656
  }
3142
- findEquivalentField(fields, incoming) {
3143
- return fields.find((field) => {
3144
- if (field.name && incoming.name && field.name === incoming.name) return true;
3145
- if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
3146
- if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
3147
- if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
3148
- if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
3149
- return true;
3150
- }
3151
- return false;
4657
+ /** Replace route-path references with screen names where the route table resolves them. */
4658
+ resolveRoutePaths(screen, routes) {
4659
+ const resolve2 = (target) => {
4660
+ if (!target || !target.startsWith("/")) return target;
4661
+ return resolvePathToScreen(routes, target) ?? target;
4662
+ };
4663
+ const navigationTargets = Array.from(
4664
+ new Set(
4665
+ screen.navigationTargets.map((target) => resolve2(target)).filter((target) => target !== screen.name)
4666
+ )
4667
+ ).sort();
4668
+ const actions = screen.actions.map((action) => {
4669
+ const resolved = resolve2(action.targetScreen);
4670
+ return resolved === action.targetScreen ? action : { ...action, targetScreen: resolved };
4671
+ });
4672
+ const collections = screen.collections?.map((collection) => {
4673
+ const resolveRow = (row) => {
4674
+ if (!row?.targetScreen) return row;
4675
+ const resolved = resolve2(row.targetScreen);
4676
+ if (resolved === row.targetScreen) return row;
4677
+ return {
4678
+ ...row,
4679
+ targetScreen: resolved,
4680
+ ...row.description ? { description: `Clicking a row opens ${resolved}` } : {}
4681
+ };
4682
+ };
4683
+ const rowAction = resolveRow(collection.rowAction);
4684
+ return {
4685
+ ...collection,
4686
+ ...rowAction ? { rowAction } : {},
4687
+ ...collection.rowActions ? { rowActions: collection.rowActions.map((row) => resolveRow(row)) } : {}
4688
+ };
3152
4689
  });
3153
- }
3154
- mergeField(target, source) {
3155
- if (this.isWeakInferredFieldName(target.name) && !this.isWeakInferredFieldName(source.name)) {
3156
- target.name = source.name;
3157
- }
3158
- target.label = target.label ?? source.label;
3159
- target.placeholder = target.placeholder ?? source.placeholder;
3160
- target.options = target.options ?? source.options;
3161
- target.locator = target.locator ?? source.locator;
3162
- target.sourceComponent = target.sourceComponent ?? source.sourceComponent;
3163
- target.valueBinding = target.valueBinding ?? source.valueBinding;
3164
- target.errorBinding = target.errorBinding ?? source.errorBinding;
3165
- target.required = target.required || source.required;
3166
- if (target.type === "text" && source.type !== "text") {
3167
- target.type = source.type;
3168
- }
3169
- }
3170
- namedSubmitAction(submitAction) {
3171
- return submitAction && submitAction !== "anonymous" ? submitAction : void 0;
3172
- }
3173
- isWeakInferredFieldName(name) {
3174
- return /^(text|value|input|query|search|selected|checked)$/i.test(name);
3175
- }
3176
- findFormWithSharedFields(forms, incoming) {
3177
- if (incoming.fields.length === 0) return void 0;
3178
- let best;
3179
- for (const form of forms) {
3180
- const overlap = incoming.fields.filter((field) => this.findEquivalentField(form.fields, field)).length;
3181
- if (overlap > 0 && (!best || overlap > best.overlap)) {
3182
- best = { form, overlap };
3183
- }
3184
- }
3185
- return best?.form;
3186
- }
3187
- };
3188
-
3189
- // src/analyzers/GenericPlatformAnalyzer.ts
3190
- var GenericPlatformAnalyzer = class {
3191
- constructor(platform) {
3192
- this.platform = platform;
3193
- }
3194
- platform;
3195
- async analyze(_config, _options) {
3196
4690
  return {
3197
- screens: [],
3198
- navigation: { screens: {}, initialScreen: "", navigators: [] },
3199
- analyzedFiles: 0
4691
+ ...screen,
4692
+ navigationTargets,
4693
+ actions,
4694
+ ...collections ? { collections } : {}
3200
4695
  };
3201
4696
  }
3202
4697
  };
3203
4698
 
3204
4699
  // src/manifest/loadManifest.ts
3205
- var import_promises2 = require("fs/promises");
4700
+ var import_promises3 = require("fs/promises");
3206
4701
  var import_node_path2 = __toESM(require("path"));
3207
4702
 
3208
4703
  // ../../node_modules/zod/v3/external.js
@@ -3410,8 +4905,8 @@ var ZodParsedType = util.arrayToEnum([
3410
4905
  "set"
3411
4906
  ]);
3412
4907
  var getParsedType = (data) => {
3413
- const t8 = typeof data;
3414
- switch (t8) {
4908
+ const t12 = typeof data;
4909
+ switch (t12) {
3415
4910
  case "undefined":
3416
4911
  return ZodParsedType.undefined;
3417
4912
  case "string":
@@ -3683,8 +5178,8 @@ function getErrorMap() {
3683
5178
 
3684
5179
  // ../../node_modules/zod/v3/helpers/parseUtil.js
3685
5180
  var makeIssue = (params) => {
3686
- const { data, path: path7, errorMaps, issueData } = params;
3687
- const fullPath = [...path7, ...issueData.path || []];
5181
+ const { data, path: path9, errorMaps, issueData } = params;
5182
+ const fullPath = [...path9, ...issueData.path || []];
3688
5183
  const fullIssue = {
3689
5184
  ...issueData,
3690
5185
  path: fullPath
@@ -3800,11 +5295,11 @@ var errorUtil;
3800
5295
 
3801
5296
  // ../../node_modules/zod/v3/types.js
3802
5297
  var ParseInputLazyPath = class {
3803
- constructor(parent, value, path7, key) {
5298
+ constructor(parent, value, path9, key) {
3804
5299
  this._cachedPath = [];
3805
5300
  this.parent = parent;
3806
5301
  this.data = value;
3807
- this._path = path7;
5302
+ this._path = path9;
3808
5303
  this._key = key;
3809
5304
  }
3810
5305
  get path() {
@@ -7246,7 +8741,7 @@ var coerce = {
7246
8741
  };
7247
8742
  var NEVER = INVALID;
7248
8743
 
7249
- // ../shared/dist/chunk-XLOYG3DH.mjs
8744
+ // ../shared/dist/chunk-MSOZJNXB.mjs
7250
8745
  var locatorSourceSchema = external_exports.enum([
7251
8746
  "appilotsId",
7252
8747
  "testID",
@@ -7431,7 +8926,19 @@ var navigationNodeSchema = external_exports.object({
7431
8926
  parentNavigator: external_exports.string().optional(),
7432
8927
  reachableFrom: external_exports.array(external_exports.string()).default([]),
7433
8928
  reachableTo: external_exports.array(external_exports.string()).default([]),
7434
- params: external_exports.array(paramDescriptorSchema).optional()
8929
+ params: external_exports.array(paramDescriptorSchema).optional(),
8930
+ /**
8931
+ * Web clients only (documented convention, additive — previously
8932
+ * round-tripped via `.passthrough()`): the screen's URL route
8933
+ * template, e.g. `/users/:id`. Param segments use `:name` (the
8934
+ * relay also accepts `[name]` / `{name}`). When present and the
8935
+ * request's `context.platform` is `'web'`, the relay resolves
8936
+ * `navigate` targets against these templates and injects the
8937
+ * concrete URL segments as the navigate payload's `path` — the web
8938
+ * equivalent of RN's nested-navigation path injection. See
8939
+ * docs/agent-contract.md ("Web clients" section).
8940
+ */
8941
+ path: external_exports.string().max(500).optional()
7435
8942
  }).passthrough();
7436
8943
  var navigatorDescriptorSchema = external_exports.object({
7437
8944
  name: external_exports.string(),
@@ -7478,6 +8985,41 @@ var loginSchema = external_exports.object({
7478
8985
  var registerSchema = loginSchema.extend({
7479
8986
  name: external_exports.string().min(2, "Name must be at least 2 characters").max(100)
7480
8987
  });
8988
+ var updateMeSchema = external_exports.object({
8989
+ name: external_exports.string().min(2, "Name must be at least 2 characters").max(100).optional(),
8990
+ avatarUrl: external_exports.string().url("Invalid URL").max(2048).nullable().optional()
8991
+ }).refine((v) => v.name !== void 0 || v.avatarUrl !== void 0, {
8992
+ message: "At least one field must be provided"
8993
+ });
8994
+ var changePasswordSchema = external_exports.object({
8995
+ currentPassword: external_exports.string().min(1, "Current password is required"),
8996
+ newPassword: external_exports.string().min(8, "Password must be at least 8 characters")
8997
+ });
8998
+ var totpCodeSchema = external_exports.string().transform((v) => v.replace(/\s/g, "")).pipe(external_exports.string().regex(/^\d{6}$/, "Code must be 6 digits"));
8999
+ var recoveryCodeSchema = external_exports.string().transform((v) => v.toUpperCase().replace(/[^A-Z0-9]/g, "")).pipe(external_exports.string().regex(/^[23456789BCDFGHJKMNPQRSTVWXYZ]{10}$/, "Invalid recovery code"));
9000
+ var mfaVerifySchema = external_exports.object({
9001
+ code: totpCodeSchema
9002
+ });
9003
+ var mfaChallengeSchema = external_exports.object({
9004
+ challengeToken: external_exports.string().min(1, "Challenge token is required"),
9005
+ code: totpCodeSchema.optional(),
9006
+ recoveryCode: recoveryCodeSchema.optional()
9007
+ }).refine((v) => Boolean(v.code) !== Boolean(v.recoveryCode), {
9008
+ message: "Provide either a TOTP code or a recovery code",
9009
+ path: ["code"]
9010
+ });
9011
+ var mfaDisableSchema = external_exports.object({
9012
+ password: external_exports.string().min(1, "Password is required"),
9013
+ code: totpCodeSchema.optional(),
9014
+ recoveryCode: recoveryCodeSchema.optional()
9015
+ }).refine((v) => Boolean(v.code) !== Boolean(v.recoveryCode), {
9016
+ message: "Provide either a TOTP code or a recovery code",
9017
+ path: ["code"]
9018
+ });
9019
+ var mfaRegenerateRecoveryCodesSchema = external_exports.object({
9020
+ password: external_exports.string().min(1, "Password is required"),
9021
+ code: totpCodeSchema
9022
+ });
7481
9023
  var createProjectSchema = external_exports.object({
7482
9024
  name: external_exports.string().min(1).max(100),
7483
9025
  description: external_exports.string().max(500).optional(),
@@ -8141,6 +9683,18 @@ var sandboxRunRequestSchema = external_exports.object({
8141
9683
  */
8142
9684
  isRecoveryHop: external_exports.boolean().optional()
8143
9685
  }).strict();
9686
+ var relayDiagnosticsSchema = external_exports.object({
9687
+ unverifiedTargets: external_exports.array(external_exports.string()),
9688
+ guardsFired: external_exports.array(
9689
+ external_exports.object({ guard: external_exports.string(), detail: external_exports.string().optional() })
9690
+ ),
9691
+ repairMode: external_exports.enum(["repair", "strict", "disabled"]),
9692
+ promptVersion: external_exports.string(),
9693
+ // Defaulted for producers that predate the field: before it existed
9694
+ // every response had reached the model, so `true` is the correct
9695
+ // backfill.
9696
+ modelInvoked: external_exports.boolean().default(true)
9697
+ });
8144
9698
  var sandboxTraceHopSchema = external_exports.object({
8145
9699
  hop: external_exports.number().int().nonnegative(),
8146
9700
  inputPreview: external_exports.string(),
@@ -8179,7 +9733,16 @@ var sandboxRunResponseSchema = external_exports.object({
8179
9733
  /** Hop-by-hop trace. Always at least one entry. */
8180
9734
  trace: external_exports.array(sandboxTraceHopSchema),
8181
9735
  /** True when the run was a no-op (e.g. project has no MCP yet). */
8182
- warning: external_exports.string().optional()
9736
+ warning: external_exports.string().optional(),
9737
+ /**
9738
+ * SPEC-046 — guard / hallucination telemetry for the run.
9739
+ *
9740
+ * ADDITIVE AND OPTIONAL, permanently: SDK and dashboard consumers
9741
+ * predate it, and older API deployments will not send it. Consumers
9742
+ * must treat an absent block as "no information", never as "no guards
9743
+ * fired".
9744
+ */
9745
+ diagnostics: relayDiagnosticsSchema.optional()
8183
9746
  });
8184
9747
  var saveSandboxTestSchema = external_exports.object({
8185
9748
  name: external_exports.string().min(1).max(120),
@@ -8401,7 +9964,7 @@ async function loadManifest(rootDir, manifestPath) {
8401
9964
  const resolvedPath = import_node_path2.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
8402
9965
  let raw;
8403
9966
  try {
8404
- raw = await (0, import_promises2.readFile)(resolvedPath, "utf-8");
9967
+ raw = await (0, import_promises3.readFile)(resolvedPath, "utf-8");
8405
9968
  } catch (error2) {
8406
9969
  if (error2?.code === "ENOENT") {
8407
9970
  return { manifest: null, resolvedPath };
@@ -8468,19 +10031,22 @@ var MCPGenerator = class _MCPGenerator {
8468
10031
  /**
8469
10032
  * Select the `PlatformAnalyzer` implementation for a `.appilotsrc`
8470
10033
  * `platform` value. `'react-native'` (or unset — the existing default)
8471
- * gets the real Babel/JSX pipeline; anything else gets the no-op
8472
- * generic analyzer, relying entirely on a declared manifest.
10034
+ * gets the RN Babel/JSX pipeline; `'web'` gets the React web (DOM +
10035
+ * React Router) pipeline; anything else gets the no-op generic
10036
+ * analyzer, relying entirely on a declared manifest. The manifest
10037
+ * still merges on top of every analyzer's output either way.
8473
10038
  */
8474
10039
  static createPlatformAnalyzer(platform) {
8475
10040
  const resolved = platform ?? "react-native";
8476
10041
  if (resolved === "react-native") return new ReactNativePlatformAnalyzer();
10042
+ if (resolved === "web") return new ReactWebPlatformAnalyzer();
8477
10043
  return new GenericPlatformAnalyzer(resolved);
8478
10044
  }
8479
10045
  /** Generate MCP documents from the project */
8480
10046
  async generate() {
8481
10047
  console.log("[MCPGenerator] Starting generation...");
8482
10048
  const outputDir = this.options.outputDir || ".appilots";
8483
- await (0, import_promises3.mkdir)(outputDir, { recursive: true });
10049
+ await (0, import_promises4.mkdir)(outputDir, { recursive: true });
8484
10050
  console.log(`[MCPGenerator] Output directory ensured: ${outputDir}`);
8485
10051
  console.log(`[MCPGenerator] Running "${this.platformAnalyzer.platform}" platform analyzer...`);
8486
10052
  const analyzed = await this.platformAnalyzer.analyze(this.analyzerConfig, {
@@ -8530,10 +10096,10 @@ var MCPGenerator = class _MCPGenerator {
8530
10096
  outputDir,
8531
10097
  `mcp-document.${this.options.format}`
8532
10098
  );
8533
- await (0, import_promises3.writeFile)(filePath, serialized, "utf-8");
10099
+ await (0, import_promises4.writeFile)(filePath, serialized, "utf-8");
8534
10100
  console.log(`[MCPGenerator] Document written to: ${filePath}`);
8535
10101
  const checksumFilePath = import_node_path3.default.resolve(outputDir, ".appilots-checksum");
8536
- await (0, import_promises3.writeFile)(checksumFilePath, checksum, "utf-8");
10102
+ await (0, import_promises4.writeFile)(checksumFilePath, checksum, "utf-8");
8537
10103
  console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
8538
10104
  console.log("[MCPGenerator] Generation complete!");
8539
10105
  return {
@@ -8554,7 +10120,7 @@ var MCPGenerator = class _MCPGenerator {
8554
10120
  static async readPreviousChecksum(outputDir) {
8555
10121
  const checksumFilePath = import_node_path3.default.resolve(outputDir, ".appilots-checksum");
8556
10122
  try {
8557
- const content = await (0, import_promises3.readFile)(checksumFilePath, "utf-8");
10123
+ const content = await (0, import_promises4.readFile)(checksumFilePath, "utf-8");
8558
10124
  return content.trim() || null;
8559
10125
  } catch {
8560
10126
  return null;
@@ -8572,7 +10138,7 @@ var MCPGenerator = class _MCPGenerator {
8572
10138
  async getProjectInfo() {
8573
10139
  try {
8574
10140
  const packageJsonPath = import_node_path3.default.resolve(this.analyzerConfig.rootDir, "package.json");
8575
- const packageJsonContent = await (0, import_promises3.readFile)(packageJsonPath, "utf-8");
10141
+ const packageJsonContent = await (0, import_promises4.readFile)(packageJsonPath, "utf-8");
8576
10142
  const packageJson = JSON.parse(packageJsonContent);
8577
10143
  return {
8578
10144
  name: packageJson.name || "Unknown Project",
@@ -8678,7 +10244,7 @@ function syncCommand() {
8678
10244
  let previousChecksum = "";
8679
10245
  try {
8680
10246
  const checksumPath = (0, import_node_path4.join)(outputDir, ".appilots-checksum");
8681
- previousChecksum = (await (0, import_promises4.readFile)(checksumPath, "utf-8")).trim();
10247
+ previousChecksum = (await (0, import_promises5.readFile)(checksumPath, "utf-8")).trim();
8682
10248
  } catch {
8683
10249
  }
8684
10250
  let spinner = createSpinner("Analyzing project...");
@@ -8799,7 +10365,12 @@ function watchCommand() {
8799
10365
  include: config.include,
8800
10366
  exclude: config.exclude,
8801
10367
  navigationInclude: config.navigationInclude,
8802
- navigationExclude: config.navigationExclude
10368
+ navigationExclude: config.navigationExclude,
10369
+ // Same analyzer selection `generate` and `sync` already do —
10370
+ // without these, watching a non-`react-native` project would
10371
+ // silently run the RN analyzers and ignore the manifest.
10372
+ platform: config.platform,
10373
+ manifestPath: config.manifestPath
8803
10374
  };
8804
10375
  const generator = new MCPGenerator(generatorConfig);
8805
10376
  const initialOutput = await generator.generate();
@@ -8978,12 +10549,12 @@ function statusCommand() {
8978
10549
 
8979
10550
  // src/cli/commands/eval.ts
8980
10551
  var import_commander6 = require("commander");
8981
- var import_fs9 = require("fs");
8982
- var import_path7 = require("path");
10552
+ var import_fs10 = require("fs");
10553
+ var import_path9 = require("path");
8983
10554
 
8984
10555
  // src/eval/scenario-loader.ts
8985
- var import_fs7 = require("fs");
8986
- var import_path5 = require("path");
10556
+ var import_fs8 = require("fs");
10557
+ var import_path7 = require("path");
8987
10558
  function validateScenarioShape(raw) {
8988
10559
  if (!raw || typeof raw !== "object") return "scenario must be a JSON object";
8989
10560
  const s = raw;
@@ -9003,15 +10574,15 @@ function loadScenarios(dir) {
9003
10574
  const errors = [];
9004
10575
  let files;
9005
10576
  try {
9006
- files = (0, import_fs7.readdirSync)(dir).filter((f) => f.endsWith(".json")).sort();
10577
+ files = (0, import_fs8.readdirSync)(dir).filter((f) => f.endsWith(".json")).sort();
9007
10578
  } catch {
9008
10579
  return { scenarios, errors };
9009
10580
  }
9010
10581
  for (const file of files) {
9011
- const full = (0, import_path5.join)(dir, file);
10582
+ const full = (0, import_path7.join)(dir, file);
9012
10583
  let raw;
9013
10584
  try {
9014
- raw = JSON.parse((0, import_fs7.readFileSync)(full, "utf-8"));
10585
+ raw = JSON.parse((0, import_fs8.readFileSync)(full, "utf-8"));
9015
10586
  } catch (err) {
9016
10587
  errors.push({ file, error: `invalid JSON: ${err instanceof Error ? err.message : String(err)}` });
9017
10588
  continue;
@@ -9167,13 +10738,13 @@ function matchActions(expected, actual, reply = "") {
9167
10738
  }
9168
10739
 
9169
10740
  // src/eval/baseline.ts
9170
- var import_fs8 = require("fs");
9171
- var import_path6 = require("path");
10741
+ var import_fs9 = require("fs");
10742
+ var import_path8 = require("path");
9172
10743
  var EMPTY_BASELINE = { generatedAt: null, gitSha: null, perScenario: {} };
9173
- function loadBaseline(path7) {
9174
- if (!(0, import_fs8.existsSync)(path7)) return { ...EMPTY_BASELINE, perScenario: {} };
10744
+ function loadBaseline(path9) {
10745
+ if (!(0, import_fs9.existsSync)(path9)) return { ...EMPTY_BASELINE, perScenario: {} };
9175
10746
  try {
9176
- const raw = JSON.parse((0, import_fs8.readFileSync)(path7, "utf-8"));
10747
+ const raw = JSON.parse((0, import_fs9.readFileSync)(path9, "utf-8"));
9177
10748
  return {
9178
10749
  generatedAt: raw.generatedAt ?? null,
9179
10750
  gitSha: raw.gitSha ?? null,
@@ -9183,9 +10754,9 @@ function loadBaseline(path7) {
9183
10754
  return { ...EMPTY_BASELINE, perScenario: {} };
9184
10755
  }
9185
10756
  }
9186
- function saveBaseline(path7, baseline) {
9187
- (0, import_fs8.mkdirSync)((0, import_path6.dirname)(path7), { recursive: true });
9188
- (0, import_fs8.writeFileSync)(path7, `${JSON.stringify(baseline, null, 2)}
10757
+ function saveBaseline(path9, baseline) {
10758
+ (0, import_fs9.mkdirSync)((0, import_path8.dirname)(path9), { recursive: true });
10759
+ (0, import_fs9.writeFileSync)(path9, `${JSON.stringify(baseline, null, 2)}
9189
10760
  `, "utf-8");
9190
10761
  }
9191
10762
  function buildBaseline(outcomes, meta) {
@@ -9283,14 +10854,14 @@ function evalCommand() {
9283
10854
  banner();
9284
10855
  const loaded = loadConfig();
9285
10856
  const outputDir = loaded?.outputDir || ".appilots";
9286
- const scenariosDir = options.dir || loaded?.eval?.scenariosDir || (0, import_path7.join)(outputDir, "scenarios");
10857
+ const scenariosDir = options.dir || loaded?.eval?.scenariosDir || (0, import_path9.join)(outputDir, "scenarios");
9287
10858
  if (options.init) {
9288
- (0, import_fs9.mkdirSync)(scenariosDir, { recursive: true });
9289
- const examplePath = (0, import_path7.join)(scenariosDir, "example.json");
9290
- if ((0, import_fs9.existsSync)(examplePath) && !options.force) {
10859
+ (0, import_fs10.mkdirSync)(scenariosDir, { recursive: true });
10860
+ const examplePath = (0, import_path9.join)(scenariosDir, "example.json");
10861
+ if ((0, import_fs10.existsSync)(examplePath) && !options.force) {
9291
10862
  warn(`${examplePath} already exists \u2014 pass --force to overwrite`);
9292
10863
  } else {
9293
- (0, import_fs9.writeFileSync)(examplePath, `${JSON.stringify(EXAMPLE_SCENARIO, null, 2)}
10864
+ (0, import_fs10.writeFileSync)(examplePath, `${JSON.stringify(EXAMPLE_SCENARIO, null, 2)}
9294
10865
  `, "utf-8");
9295
10866
  success(`Wrote example scenario to ${examplePath}`);
9296
10867
  }
@@ -9316,7 +10887,7 @@ function evalCommand() {
9316
10887
  ...options.projectId ? { projectId: options.projectId } : {},
9317
10888
  ...options.server ? { serverUrl: options.server } : {}
9318
10889
  };
9319
- const baselinePath = options.baseline || loaded?.eval?.baselinePath || (0, import_path7.join)(outputDir, "eval-baseline.json");
10890
+ const baselinePath = options.baseline || loaded?.eval?.baselinePath || (0, import_path9.join)(outputDir, "eval-baseline.json");
9320
10891
  const minPassRate = parseRate(
9321
10892
  options.minPassRate,
9322
10893
  loaded?.eval?.minPassRate ?? DEFAULT_MIN_PASS_RATE,