@appilots/cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1812 -241
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +200 -12
- package/dist/index.d.ts +200 -12
- package/dist/index.js +1594 -43
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1583 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/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
|
|
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
|
|
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
|
|
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 &&
|
|
1974
|
-
if (actionId &&
|
|
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((
|
|
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((
|
|
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: (
|
|
2697
|
-
const component = this.extractComponentFromJSXElement(
|
|
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: (
|
|
2832
|
-
this.extractStateVariables(
|
|
2831
|
+
CallExpression: (path9) => {
|
|
2832
|
+
this.extractStateVariables(path9.node);
|
|
2833
2833
|
}
|
|
2834
2834
|
});
|
|
2835
2835
|
(0, import_traverse7.default)(ast, {
|
|
2836
|
-
JSXElement: (
|
|
2837
|
-
this.extractFormElements(
|
|
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: (
|
|
2938
|
-
const 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
|
-
|
|
2991
|
-
|
|
2992
|
-
const
|
|
2993
|
-
const
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
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
|
-
|
|
3017
|
-
const
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
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
|
-
|
|
3025
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3037
|
-
const
|
|
3038
|
-
|
|
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/
|
|
3043
|
-
var
|
|
3044
|
-
platform = "
|
|
4614
|
+
// src/analyzers/web/ReactWebPlatformAnalyzer.ts
|
|
4615
|
+
var ReactWebPlatformAnalyzer = class {
|
|
4616
|
+
platform = "web";
|
|
3045
4617
|
async analyze(config, options) {
|
|
3046
|
-
const screenAnalyzer = new
|
|
3047
|
-
strictScreens: options.strictScreens ?? true,
|
|
4618
|
+
const screenAnalyzer = new WebScreenAnalyzer(config, {
|
|
3048
4619
|
screenPatterns: options.screenPatterns
|
|
3049
4620
|
});
|
|
3050
|
-
const navigationAnalyzer = new
|
|
4621
|
+
const navigationAnalyzer = new WebNavigationAnalyzer(config, {
|
|
3051
4622
|
navigationInclude: options.navigationInclude,
|
|
3052
4623
|
navigationExclude: options.navigationExclude
|
|
3053
4624
|
});
|
|
3054
|
-
|
|
3055
|
-
const
|
|
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
|
-
|
|
3062
|
-
|
|
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
|
-
`[
|
|
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
|
|
3125
|
-
navigation,
|
|
3126
|
-
analyzedFiles:
|
|
3127
|
-
...
|
|
4648
|
+
screens,
|
|
4649
|
+
navigation: navigationResult.graph,
|
|
4650
|
+
analyzedFiles: screenAnalysis.analyzedFiles,
|
|
4651
|
+
...screensFilteredOut > 0 ? { screensFilteredOut } : {}
|
|
3128
4652
|
};
|
|
3129
4653
|
}
|
|
3130
|
-
|
|
3131
|
-
|
|
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
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
if (
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
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
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
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
|
|
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
|
|
3414
|
-
switch (
|
|
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:
|
|
3687
|
-
const fullPath = [...
|
|
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,
|
|
5298
|
+
constructor(parent, value, path9, key) {
|
|
3804
5299
|
this._cachedPath = [];
|
|
3805
5300
|
this.parent = parent;
|
|
3806
5301
|
this.data = value;
|
|
3807
|
-
this._path =
|
|
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-
|
|
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,
|
|
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
|
|
8472
|
-
*
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
8982
|
-
var
|
|
10552
|
+
var import_fs10 = require("fs");
|
|
10553
|
+
var import_path9 = require("path");
|
|
8983
10554
|
|
|
8984
10555
|
// src/eval/scenario-loader.ts
|
|
8985
|
-
var
|
|
8986
|
-
var
|
|
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,
|
|
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,
|
|
10582
|
+
const full = (0, import_path7.join)(dir, file);
|
|
9012
10583
|
let raw;
|
|
9013
10584
|
try {
|
|
9014
|
-
raw = JSON.parse((0,
|
|
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
|
|
9171
|
-
var
|
|
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(
|
|
9174
|
-
if (!(0,
|
|
10744
|
+
function loadBaseline(path9) {
|
|
10745
|
+
if (!(0, import_fs9.existsSync)(path9)) return { ...EMPTY_BASELINE, perScenario: {} };
|
|
9175
10746
|
try {
|
|
9176
|
-
const raw = JSON.parse((0,
|
|
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(
|
|
9187
|
-
(0,
|
|
9188
|
-
(0,
|
|
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,
|
|
10857
|
+
const scenariosDir = options.dir || loaded?.eval?.scenariosDir || (0, import_path9.join)(outputDir, "scenarios");
|
|
9287
10858
|
if (options.init) {
|
|
9288
|
-
(0,
|
|
9289
|
-
const examplePath = (0,
|
|
9290
|
-
if ((0,
|
|
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,
|
|
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,
|
|
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,
|