@dev-blinq/cucumber_client 1.0.1721-dev → 1.0.1723-dev

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.
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parse } from "@babel/parser";
6
+ import traverseImport from "@babel/traverse";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ const ROOT = process.cwd();
12
+ const ALLOWED_HARDCODE_RATIO = 0.2;
13
+ const IGNORED_RATIO = 1.0;
14
+ const PLACEHOLDER_REGEX = /{[^}]+}/;
15
+ const IGNORE_DIRS = new Set(["node_modules", ".git", ".idea"]);
16
+
17
+ const results = [];
18
+
19
+ function hasPlaceholder(str) {
20
+ return PLACEHOLDER_REGEX.test(str);
21
+ }
22
+
23
+ function stringFromNode(node) {
24
+ if (!node) return undefined;
25
+ if (node.type === "StringLiteral") return node.value;
26
+ if (node.type === "TemplateLiteral") {
27
+ const raw = node.quasis.map((q) => q.value.cooked ?? q.value.raw).join("${}");
28
+ return raw;
29
+ }
30
+ return undefined;
31
+ }
32
+
33
+ function locatorIsParameterized(objNode) {
34
+ if (objNode.type !== "ObjectExpression") return false;
35
+ return objNode.properties.some((prop) => {
36
+ if (prop.type !== "ObjectProperty") return false;
37
+ const valueString = stringFromNode(prop.value);
38
+ return typeof valueString === "string" && hasPlaceholder(valueString);
39
+ });
40
+ }
41
+
42
+ function analyzeMjsFile(filePath, code) {
43
+ const ast = parse(code, {
44
+ sourceType: "module",
45
+ plugins: ["jsx", "typescript", "classProperties", "optionalChaining", "nullishCoalescingOperator", "topLevelAwait"],
46
+ });
47
+
48
+ const traverse = typeof traverseImport === "function" ? traverseImport : traverseImport.default;
49
+
50
+ traverse(ast, {
51
+ ObjectExpression(pathObj) {
52
+ const props = pathObj.node.properties;
53
+ const locatorProp = props.find(
54
+ (p) =>
55
+ p.type === "ObjectProperty" &&
56
+ ((p.key.type === "Identifier" && p.key.name === "locators") ||
57
+ (p.key.type === "StringLiteral" && p.key.value === "locators"))
58
+ );
59
+ if (!locatorProp || locatorProp.value.type !== "ArrayExpression") return;
60
+
61
+ const locators = locatorProp.value.elements.filter(Boolean);
62
+ if (locators.length === 0) return;
63
+
64
+ const total = locators.length;
65
+ const hardcoded = locators.filter((loc) => !locatorIsParameterized(loc)).length;
66
+ if (hardcoded / total === IGNORED_RATIO) return; // Skip if all locators are hardcoded (likely a non-locator object)
67
+ if (hardcoded / total <= ALLOWED_HARDCODE_RATIO) return;
68
+
69
+ const nameProp = props.find(
70
+ (p) =>
71
+ p.type === "ObjectProperty" &&
72
+ ((p.key.type === "Identifier" && (p.key.name === "element_name" || p.key.name === "element_key")) ||
73
+ (p.key.type === "StringLiteral" && (p.key.value === "element_name" || p.key.value === "element_key")))
74
+ );
75
+ const elementNameValue = nameProp ? stringFromNode(nameProp.value) : undefined;
76
+ const loc = pathObj.node.loc?.start;
77
+ results.push({
78
+ file: filePath,
79
+ line: loc?.line ?? 0,
80
+ element: elementNameValue ?? "<unknown>",
81
+ hardcoded,
82
+ total,
83
+ });
84
+ },
85
+ });
86
+ }
87
+
88
+ function locatorIsParameterizedJson(locator) {
89
+ if (locator && typeof locator === "object") {
90
+ return Object.values(locator).some((val) => typeof val === "string" && hasPlaceholder(val));
91
+ }
92
+ return false;
93
+ }
94
+
95
+ function findLineNumber(content, searchValue) {
96
+ const idx = content.indexOf(searchValue);
97
+ if (idx === -1) return 0;
98
+ return content.slice(0, idx).split("\n").length;
99
+ }
100
+
101
+ function analyzeJsonFile(filePath, content) {
102
+ let data;
103
+ try {
104
+ data = JSON.parse(content);
105
+ } catch (err) {
106
+ console.warn(`Skipping ${filePath}: invalid JSON`);
107
+ return;
108
+ }
109
+
110
+ const visit = (node) => {
111
+ if (!node || typeof node !== "object") return;
112
+ if (Array.isArray(node)) {
113
+ node.forEach(visit);
114
+ return;
115
+ }
116
+
117
+ if (Array.isArray(node.locators) && node.locators.length > 0) {
118
+ const total = node.locators.length;
119
+ const hardcoded = node.locators.filter((loc) => !locatorIsParameterizedJson(loc)).length;
120
+
121
+ if (hardcoded / total > ALLOWED_HARDCODE_RATIO) {
122
+ if (hardcoded / total === IGNORED_RATIO) return; // Skip if all locators are hardcoded (likely a non-locator object)
123
+ const name = node.element_name || node.element_key || "<unknown>";
124
+ const line = findLineNumber(content, name);
125
+ results.push({ file: filePath, line, element: name, hardcoded, total });
126
+ }
127
+ }
128
+
129
+ Object.values(node).forEach(visit);
130
+ };
131
+
132
+ visit(data);
133
+ }
134
+
135
+ async function walk(dir) {
136
+ const entries = await fs.readdir(dir, { withFileTypes: true });
137
+ for (const entry of entries) {
138
+ if (IGNORE_DIRS.has(entry.name)) continue;
139
+ const resolved = path.join(dir, entry.name);
140
+ if (entry.isDirectory()) {
141
+ await walk(resolved);
142
+ } else if (entry.isFile() && (entry.name.endsWith(".mjs") || entry.name.endsWith(".json"))) {
143
+ const content = await fs.readFile(resolved, "utf8");
144
+ try {
145
+ if (entry.name.endsWith(".mjs")) {
146
+ analyzeMjsFile(resolved, content);
147
+ } else {
148
+ analyzeJsonFile(resolved, content);
149
+ }
150
+ } catch (err) {
151
+ console.error(`Failed to analyze ${resolved}:`, err.message);
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ async function main() {
158
+ await walk(ROOT);
159
+ if (results.length === 0) {
160
+ console.log("No elements exceeded 20% hardcoded locator threshold.");
161
+ return;
162
+ }
163
+
164
+ for (const r of results) {
165
+ console.log(`${r.file}:${r.line} — ${r.element} (${r.hardcoded}/${r.total} hardcoded)`);
166
+ }
167
+ process.exitCode = 1;
168
+ }
169
+
170
+ main().catch((err) => {
171
+ console.error(err);
172
+ process.exit(1);
173
+ });
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parse } from "@babel/parser";
6
+ import traverseImport from "@babel/traverse";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ const ROOT = process.cwd();
12
+ const IGNORE_DIRS = new Set(["node_modules", ".git", ".idea"]);
13
+
14
+ const results = [];
15
+
16
+ const traverse = typeof traverseImport === "function" ? traverseImport : traverseImport.default;
17
+
18
+ function propName(prop) {
19
+ if (prop.type !== "ObjectProperty") return undefined;
20
+ const key = prop.key;
21
+ if (key.type === "Identifier") return key.name;
22
+ if (key.type === "StringLiteral") return key.value;
23
+ if (key.type === "NumericLiteral") return String(key.value);
24
+ return undefined;
25
+ }
26
+
27
+ function isNumericKey(name) {
28
+ return typeof name === "string" && /^\d+$/.test(name);
29
+ }
30
+
31
+ function getElementLabel(props) {
32
+ const labelProp = props.find(
33
+ (p) => p.type === "ObjectProperty" && ["element_name", "element_key"].includes(propName(p))
34
+ );
35
+ if (!labelProp) return undefined;
36
+
37
+ const value = labelProp.value;
38
+ if (value.type === "StringLiteral") return value.value;
39
+ if (value.type === "TemplateLiteral") {
40
+ return value.quasis.map((q) => q.value.cooked ?? q.value.raw).join("${}");
41
+ }
42
+ return undefined;
43
+ }
44
+
45
+ function reportIssue({ file, line, element, numericCount }) {
46
+ results.push({ file, line, element: element ?? "<unknown>", numericCount });
47
+ }
48
+
49
+ function analyzeAst(filePath, code) {
50
+ const ast = parse(code, {
51
+ sourceType: "module",
52
+ plugins: ["jsx", "typescript", "classProperties", "optionalChaining", "nullishCoalescingOperator", "topLevelAwait"],
53
+ });
54
+
55
+ traverse(ast, {
56
+ ObjectExpression(pathObj) {
57
+ const props = pathObj.node.properties.filter((p) => p.type === "ObjectProperty");
58
+ if (props.length === 0) return;
59
+
60
+ const names = props.map(propName).filter(Boolean);
61
+ const hasElementMeta = names.some((n) => n === "element_name" || n === "element_key");
62
+ if (!hasElementMeta) return;
63
+
64
+ const hasLocators = names.includes("locators");
65
+ const numericProps = names.filter(isNumericKey);
66
+
67
+ if (!hasLocators && numericProps.length > 0) {
68
+ const element = getElementLabel(props);
69
+ const line = pathObj.node.loc?.start.line ?? 0;
70
+ reportIssue({ file: filePath, line, element, numericCount: numericProps.length });
71
+ }
72
+ },
73
+ });
74
+ }
75
+
76
+ function findLineNumber(content, searchValue) {
77
+ if (!searchValue) return 0;
78
+ const idx = content.indexOf(searchValue);
79
+ if (idx === -1) return 0;
80
+ return content.slice(0, idx).split("\n").length;
81
+ }
82
+
83
+ function analyzeJson(filePath, content) {
84
+ let data;
85
+ try {
86
+ data = JSON.parse(content);
87
+ } catch (err) {
88
+ console.warn(`Skipping ${filePath}: invalid JSON`);
89
+ return;
90
+ }
91
+
92
+ const visit = (node) => {
93
+ if (!node || typeof node !== "object") return;
94
+ if (Array.isArray(node)) {
95
+ node.forEach(visit);
96
+ return;
97
+ }
98
+
99
+ const keys = Object.keys(node);
100
+ const hasElementMeta = "element_name" in node || "element_key" in node;
101
+ const hasLocators = "locators" in node;
102
+ const numericKeys = keys.filter(isNumericKey);
103
+
104
+ if (hasElementMeta && !hasLocators && numericKeys.length > 0) {
105
+ const element = node.element_name || node.element_key || "<unknown>";
106
+ const line = findLineNumber(content, element);
107
+ reportIssue({ file: filePath, line, element, numericCount: numericKeys.length });
108
+ }
109
+
110
+ Object.values(node).forEach(visit);
111
+ };
112
+
113
+ visit(data);
114
+ }
115
+
116
+ async function walk(dir) {
117
+ const entries = await fs.readdir(dir, { withFileTypes: true });
118
+ for (const entry of entries) {
119
+ if (IGNORE_DIRS.has(entry.name)) continue;
120
+ const resolved = path.join(dir, entry.name);
121
+ if (entry.isDirectory()) {
122
+ await walk(resolved);
123
+ continue;
124
+ }
125
+
126
+ if (!entry.isFile()) continue;
127
+
128
+ if (entry.name.endsWith(".mjs") || entry.name.endsWith(".js")) {
129
+ const code = await fs.readFile(resolved, "utf8");
130
+ try {
131
+ analyzeAst(resolved, code);
132
+ } catch (err) {
133
+ console.error(`Failed to analyze ${resolved}:`, err.message);
134
+ }
135
+ } else if (entry.name.endsWith(".json")) {
136
+ const content = await fs.readFile(resolved, "utf8");
137
+ try {
138
+ analyzeJson(resolved, content);
139
+ } catch (err) {
140
+ console.error(`Failed to analyze ${resolved}:`, err.message);
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ async function main() {
147
+ await walk(ROOT);
148
+
149
+ if (results.length === 0) {
150
+ console.log("No misstructured elements found (numeric keys without locators).");
151
+ return;
152
+ }
153
+
154
+ for (const r of results) {
155
+ console.log(`${r.file}:${r.line} — ${r.element} (${r.numericCount} numeric keys, missing locators)`);
156
+ }
157
+
158
+ process.exitCode = 1;
159
+ }
160
+
161
+ main().catch((err) => {
162
+ console.error(err);
163
+ process.exit(1);
164
+ });
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Codemod to drop hardcoded locators flagged by find_harcoded_locators.mjs.
4
+ * - Default: clones workspace to a temp dir, git init + baseline commit, applies fixes, prints diffs.
5
+ * - --inplace: modifies files in the current workspace.
6
+ *
7
+ * Behavior:
8
+ * For each reported file, in every object with a `locators` array,
9
+ * remove locator entries that contain no placeholders ({...}).
10
+ * If all locators are hardcoded, the array is left unchanged to avoid emptying it.
11
+ * JSON files are handled similarly.
12
+ *
13
+ * Usage:
14
+ * node find_harcoded_locators.mjs | node fix-hardcoded-locators.mjs
15
+ * node find_harcoded_locators.mjs | node fix-hardcoded-locators.mjs --inplace
16
+ */
17
+
18
+ import fs from "node:fs";
19
+ import { promises as fsPromises } from "node:fs";
20
+ import path from "node:path";
21
+ import os from "node:os";
22
+ import { parse } from "@babel/parser";
23
+ import traverseImport from "@babel/traverse";
24
+ import { spawnSync } from "node:child_process";
25
+
26
+ const traverse = typeof traverseImport === "function" ? traverseImport : traverseImport.default;
27
+ const PLACEHOLDER_REGEX = /{[^}]+}/;
28
+ const ROOT = process.cwd();
29
+
30
+ const INPUT = await readStdin();
31
+ if (!INPUT.trim()) {
32
+ console.error("No input provided. Pipe find_harcoded_locators.mjs into this script.");
33
+ process.exit(1);
34
+ }
35
+
36
+ const filesToFix = parseInputLines(INPUT);
37
+ if (filesToFix.size === 0) {
38
+ console.log("No actionable lines detected.");
39
+ process.exit(0);
40
+ }
41
+
42
+ const INPLACE = process.argv.includes("--inplace");
43
+ const { cloneRoot, mapper } = INPLACE ? { cloneRoot: ROOT, mapper: (p) => p } : await cloneWorkspace(ROOT);
44
+ if (!INPLACE) {
45
+ console.log(`Cloned workspace to: ${cloneRoot}`);
46
+ await initGitRepo(cloneRoot);
47
+ } else {
48
+ console.log("Running in-place (original files will be modified).");
49
+ }
50
+
51
+ const changes = [];
52
+
53
+ for (const originalPath of filesToFix) {
54
+ const clonedPath = mapper(originalPath);
55
+ const ext = path.extname(clonedPath).toLowerCase();
56
+ const original = await fsPromises.readFile(clonedPath, "utf8");
57
+ let fixed = original;
58
+
59
+ if (ext === ".json") {
60
+ fixed = fixJson(original);
61
+ } else if (ext === ".mjs" || ext === ".js") {
62
+ fixed = fixJs(original);
63
+ }
64
+
65
+ if (fixed === original) continue;
66
+
67
+ const diff = makeDiff(originalPath, clonedPath, fixed);
68
+ await fsPromises.writeFile(clonedPath, fixed, "utf8");
69
+ changes.push({ filePath: originalPath, diff });
70
+ }
71
+
72
+ if (changes.length === 0) {
73
+ console.log("No changes needed.");
74
+ process.exit(0);
75
+ }
76
+
77
+ for (const { filePath, diff } of changes) {
78
+ console.log(`\n=== ${filePath} (cloned) ===`);
79
+ console.log(diff.trimEnd());
80
+ }
81
+
82
+ process.exit(1);
83
+
84
+ // ---------- helpers ----------
85
+
86
+ function parseInputLines(text) {
87
+ const set = new Set();
88
+ for (const line of text.split(/\r?\n/)) {
89
+ const m = line.match(/^(.*?):\d+ — /);
90
+ if (m) set.add(m[1]);
91
+ }
92
+ return set;
93
+ }
94
+
95
+ function hasPlaceholder(str) {
96
+ return PLACEHOLDER_REGEX.test(str);
97
+ }
98
+
99
+ function locatorHasPlaceholderNode(node) {
100
+ if (!node || node.type !== "ObjectExpression") return false;
101
+ return node.properties.some((prop) => {
102
+ if (prop.type !== "ObjectProperty") return false;
103
+ const val = prop.value;
104
+ if (val.type === "StringLiteral") return hasPlaceholder(val.value);
105
+ if (val.type === "TemplateLiteral") {
106
+ const raw = val.quasis.map((q) => q.value.cooked ?? q.value.raw).join("${}");
107
+ return hasPlaceholder(raw);
108
+ }
109
+ return false;
110
+ });
111
+ }
112
+
113
+ function propName(prop) {
114
+ if (prop.type !== "ObjectProperty") return undefined;
115
+ const k = prop.key;
116
+ if (k.type === "Identifier") return k.name;
117
+ if (k.type === "StringLiteral") return k.value;
118
+ return undefined;
119
+ }
120
+
121
+ function leadingWhitespace(text, index) {
122
+ const lineStart = text.lastIndexOf("\n", index - 1) + 1;
123
+ const m = text.slice(lineStart, index).match(/^[ \t]*/);
124
+ return m ? m[0] : "";
125
+ }
126
+
127
+ function fixJs(code) {
128
+ let touched = false;
129
+ const replacements = [];
130
+
131
+ const ast = parse(code, {
132
+ sourceType: "module",
133
+ plugins: ["jsx", "typescript", "classProperties", "optionalChaining", "nullishCoalescingOperator", "topLevelAwait"],
134
+ ranges: true,
135
+ });
136
+
137
+ traverse(ast, {
138
+ ObjectExpression(pathObj) {
139
+ const props = pathObj.node.properties.filter((p) => p.type === "ObjectProperty");
140
+ if (!props.length) return;
141
+ const locProp = props.find((p) => propName(p) === "locators" && p.value.type === "ArrayExpression");
142
+ if (!locProp) return;
143
+ const elements = locProp.value.elements.filter(Boolean);
144
+ if (elements.length === 0) return;
145
+
146
+ const keep = elements.filter(locatorHasPlaceholderNode);
147
+ if (keep.length === elements.length) return;
148
+ if (keep.length === 0) return; // avoid emptying; leave as-is
149
+
150
+ if (typeof locProp.value.start !== "number" || typeof locProp.value.end !== "number") return;
151
+
152
+ const valueStart = locProp.value.start;
153
+ const valueEnd = locProp.value.end;
154
+ const indent = leadingWhitespace(code, valueStart) + " ";
155
+ const keepTexts = keep.map((el) => code.slice(el.start, el.end));
156
+ const newArray =
157
+ "[\n" + keepTexts.map((t) => indent + t.trim()).join(",\n") + "\n" + leadingWhitespace(code, valueStart) + "]";
158
+
159
+ replacements.push({ start: valueStart, end: valueEnd, text: newArray });
160
+ touched = true;
161
+ },
162
+ });
163
+
164
+ if (!touched) return code;
165
+
166
+ let out = code;
167
+ replacements
168
+ .sort((a, b) => b.start - a.start)
169
+ .forEach(({ start, end, text }) => {
170
+ out = out.slice(0, start) + text + out.slice(end);
171
+ });
172
+ return out;
173
+ }
174
+
175
+ function locatorHasPlaceholderJson(locator) {
176
+ if (locator && typeof locator === "object") {
177
+ return Object.values(locator).some((val) => typeof val === "string" && hasPlaceholder(val));
178
+ }
179
+ return false;
180
+ }
181
+
182
+ function fixJson(content) {
183
+ let data;
184
+ try {
185
+ data = JSON.parse(content);
186
+ } catch {
187
+ return content;
188
+ }
189
+ let touched = false;
190
+
191
+ const visit = (node) => {
192
+ if (!node || typeof node !== "object") return;
193
+ if (Array.isArray(node)) return node.forEach(visit);
194
+
195
+ if (Array.isArray(node.locators) && node.locators.length > 0) {
196
+ const keep = node.locators.filter(locatorHasPlaceholderJson);
197
+ if (keep.length > 0 && keep.length < node.locators.length) {
198
+ node.locators = keep;
199
+ touched = true;
200
+ }
201
+ }
202
+ Object.values(node).forEach(visit);
203
+ };
204
+
205
+ visit(data);
206
+ return touched ? JSON.stringify(data, null, 2) + "\n" : content;
207
+ }
208
+
209
+ async function cloneWorkspace(root) {
210
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "codemod-clone-"));
211
+ const cloneRoot = path.join(tmpDir, path.basename(root));
212
+
213
+ if (fsPromises.cp) {
214
+ await fsPromises.cp(root, cloneRoot, { recursive: true, force: true });
215
+ } else {
216
+ const res = spawnSync("cp", ["-R", ".", cloneRoot], { cwd: root, stdio: "inherit" });
217
+ if (res.status !== 0) throw new Error("Failed to clone workspace");
218
+ }
219
+
220
+ const mapper = (orig) => path.join(cloneRoot, path.relative(root, orig));
221
+ return { cloneRoot, mapper };
222
+ }
223
+
224
+ async function initGitRepo(cloneRoot) {
225
+ if (fs.existsSync(path.join(cloneRoot, ".git"))) return;
226
+ const run = (args) => spawnSync("git", args, { cwd: cloneRoot, stdio: "inherit" });
227
+ run(["init"]);
228
+ run(["add", "."]);
229
+ run(["commit", "-m", "chore: baseline before codemod"]);
230
+ }
231
+
232
+ function makeDiff(originalPath, clonedPath, fixed) {
233
+ const tmpOutDir = fs.mkdtempSync(path.join(os.tmpdir(), "codemod-out-"));
234
+ const fixedPath = path.join(tmpOutDir, path.basename(clonedPath));
235
+ fs.writeFileSync(fixedPath, fixed, "utf8");
236
+ const diff = spawnSync("diff", ["-u", originalPath, fixedPath], { encoding: "utf8" });
237
+ return diff.stdout || diff.stderr || "";
238
+ }
239
+
240
+ async function readStdin() {
241
+ return await new Promise((resolve) => {
242
+ let d = "";
243
+ process.stdin.setEncoding("utf8");
244
+ process.stdin.on("data", (c) => (d += c));
245
+ process.stdin.on("end", () => resolve(d));
246
+ });
247
+ }
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Low-noise codemod: fixes objects with numeric keys but no `locators` by splicing text in place.
4
+ * - Clones the workspace; originals untouched.
5
+ * - For each flagged object, inserts a `locators` array built from numeric keys and removes those keys.
6
+ * - Preserves surrounding formatting; only the object literal text is changed.
7
+ *
8
+ * Usage:
9
+ * node find-misstructured-elements.mjs | node fix-misstructured-elements.mjs
10
+ */
11
+
12
+ import fs from "node:fs";
13
+ import { promises as fsPromises } from "node:fs";
14
+ import path from "node:path";
15
+ import os from "node:os";
16
+ import { parse } from "@babel/parser";
17
+ import traverseImport from "@babel/traverse";
18
+ import { spawnSync } from "node:child_process";
19
+
20
+ const traverse = typeof traverseImport === "function" ? traverseImport : traverseImport.default;
21
+ const ROOT = process.cwd();
22
+
23
+ const INPUT = await readStdin();
24
+ if (!INPUT.trim()) {
25
+ console.error("No input provided. Pipe find-misstructured-elements.mjs into this script.");
26
+ process.exit(1);
27
+ }
28
+
29
+ const filesToFix = parseInputLines(INPUT);
30
+ if (filesToFix.size === 0) {
31
+ console.log("No actionable lines detected.");
32
+ process.exit(0);
33
+ }
34
+
35
+ const INPLACE = process.argv.includes("--inplace");
36
+
37
+ const { cloneRoot, mapper } = INPLACE ? { cloneRoot: ROOT, mapper: (p) => p } : await cloneWorkspace(ROOT);
38
+ if (!INPLACE) {
39
+ console.log(`Cloned workspace to: ${cloneRoot}`);
40
+ await initGitRepo(cloneRoot);
41
+ } else {
42
+ console.log("Running in-place (original files will be modified).");
43
+ }
44
+
45
+ const changes = [];
46
+
47
+ for (const originalPath of filesToFix) {
48
+ const clonedPath = mapper(originalPath);
49
+ const ext = path.extname(clonedPath).toLowerCase();
50
+ const original = await fsPromises.readFile(clonedPath, "utf8");
51
+ let fixed = original;
52
+
53
+ if (ext === ".json") {
54
+ fixed = fixJson(original);
55
+ } else if (ext === ".mjs" || ext === ".js") {
56
+ fixed = fixJsPreserveFormat(original);
57
+ }
58
+
59
+ if (fixed === original) continue;
60
+
61
+ const diff = makeDiff(originalPath, clonedPath, fixed);
62
+ await fsPromises.writeFile(clonedPath, fixed, "utf8");
63
+ changes.push({ filePath: originalPath, diff });
64
+ }
65
+
66
+ if (changes.length === 0) {
67
+ console.log("No changes needed.");
68
+ process.exit(0);
69
+ }
70
+
71
+ for (const { filePath, diff } of changes) {
72
+ console.log(`\n=== ${filePath} (cloned) ===`);
73
+ console.log(diff.trimEnd());
74
+ }
75
+
76
+ process.exit(1);
77
+
78
+ // ---------- helpers ----------
79
+
80
+ function parseInputLines(text) {
81
+ const set = new Set();
82
+ for (const line of text.split(/\r?\n/)) {
83
+ const m = line.match(/^(.*?):\d+ — /);
84
+ if (m) set.add(m[1]);
85
+ }
86
+ return set;
87
+ }
88
+
89
+ function isNumericKey(key) {
90
+ return typeof key === "string" && /^\d+$/.test(key);
91
+ }
92
+
93
+ function propName(prop) {
94
+ if (prop.type !== "ObjectProperty") return undefined;
95
+ const k = prop.key;
96
+ if (k.type === "Identifier") return k.name;
97
+ if (k.type === "StringLiteral") return k.value;
98
+ if (k.type === "NumericLiteral") return String(k.value);
99
+ return undefined;
100
+ }
101
+
102
+ function stringifyValue(node, source) {
103
+ // Slice raw text to preserve formatting
104
+ if (typeof node.start === "number" && typeof node.end === "number") {
105
+ return source.slice(node.start, node.end);
106
+ }
107
+ return JSON.stringify(null);
108
+ }
109
+
110
+ function buildLocatorsArray(numericProps, source, indent) {
111
+ const elements = numericProps
112
+ .sort((a, b) => Number(propName(a)) - Number(propName(b)))
113
+ .map((p) => indent + " " + stringifyValue(p.value, source));
114
+ if (elements.length === 0) return "locators: []";
115
+ return [indent + "locators: [", elements.join(",\n"), indent + "]"].join("\n");
116
+ }
117
+
118
+ function fixJsPreserveFormat(code) {
119
+ let touched = false;
120
+ const replacements = [];
121
+
122
+ const ast = parse(code, {
123
+ sourceType: "module",
124
+ plugins: ["jsx", "typescript", "classProperties", "optionalChaining", "nullishCoalescingOperator", "topLevelAwait"],
125
+ ranges: true,
126
+ });
127
+
128
+ traverse(ast, {
129
+ ObjectExpression(pathObj) {
130
+ const props = pathObj.node.properties.filter((p) => p.type === "ObjectProperty");
131
+ if (!props.length) return;
132
+
133
+ const names = props.map(propName).filter(Boolean);
134
+ const hasElementMeta = names.some((n) => n === "element_name" || n === "element_key");
135
+ if (!hasElementMeta) return;
136
+
137
+ const hasLocators = names.includes("locators");
138
+ const numericProps = props.filter((p) => isNumericKey(propName(p)));
139
+ if (hasLocators || numericProps.length === 0) return;
140
+
141
+ if (typeof pathObj.node.start !== "number" || typeof pathObj.node.end !== "number") return;
142
+
143
+ const objectText = code.slice(pathObj.node.start, pathObj.node.end);
144
+ const leading = leadingWhitespace(code, pathObj.node.start);
145
+
146
+ const otherProps = props.filter((p) => !numericProps.includes(p));
147
+ const otherTexts = otherProps.map((p) => code.slice(p.start, p.end));
148
+
149
+ const locatorsText = buildLocatorsArray(numericProps, code, leading + " ");
150
+ const pieces = [locatorsText, ...otherTexts];
151
+ const baseIndent = leading + " ";
152
+ const normalized = pieces.map((p) => (p.startsWith(baseIndent) ? p : baseIndent + p));
153
+ const newObjectText = "{\n" + normalized.join(",\n") + "\n" + leading + "}";
154
+
155
+ replacements.push({ start: pathObj.node.start, end: pathObj.node.end, text: newObjectText });
156
+ touched = true;
157
+ },
158
+ });
159
+
160
+ if (!touched) return code;
161
+
162
+ let out = code;
163
+ replacements
164
+ .sort((a, b) => b.start - a.start)
165
+ .forEach(({ start, end, text }) => {
166
+ out = out.slice(0, start) + text + out.slice(end);
167
+ });
168
+ return out;
169
+ }
170
+
171
+ function fixJson(content) {
172
+ let data;
173
+ try {
174
+ data = JSON.parse(content);
175
+ } catch {
176
+ return content;
177
+ }
178
+ let touched = false;
179
+
180
+ const visit = (node) => {
181
+ if (!node || typeof node !== "object") return;
182
+ if (Array.isArray(node)) return node.forEach(visit);
183
+
184
+ const keys = Object.keys(node);
185
+ const hasMeta = "element_name" in node || "element_key" in node;
186
+ const hasLoc = "locators" in node;
187
+ const nums = keys.filter(isNumericKey);
188
+
189
+ if (hasMeta && !hasLoc && nums.length) {
190
+ node.locators = nums.sort((a, b) => Number(a) - Number(b)).map((k) => node[k]);
191
+ nums.forEach((k) => delete node[k]);
192
+ touched = true;
193
+ }
194
+ Object.values(node).forEach(visit);
195
+ };
196
+
197
+ visit(data);
198
+ return touched ? JSON.stringify(data, null, 2) + "\n" : content;
199
+ }
200
+
201
+ async function cloneWorkspace(root) {
202
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "codemod-clone-"));
203
+ const cloneRoot = path.join(tmpDir, path.basename(root));
204
+
205
+ if (fsPromises.cp) {
206
+ await fsPromises.cp(root, cloneRoot, { recursive: true, force: true });
207
+ } else {
208
+ const res = spawnSync("cp", ["-R", ".", cloneRoot], { cwd: root, stdio: "inherit" });
209
+ if (res.status !== 0) throw new Error("Failed to clone workspace");
210
+ }
211
+
212
+ const mapper = (orig) => path.join(cloneRoot, path.relative(root, orig));
213
+ return { cloneRoot, mapper };
214
+ }
215
+
216
+ async function initGitRepo(cloneRoot) {
217
+ // Skip if already a repo
218
+ if (fs.existsSync(path.join(cloneRoot, ".git"))) return;
219
+ const run = (args) => spawnSync("git", args, { cwd: cloneRoot, stdio: "inherit" });
220
+ run(["init"]);
221
+ run(["add", "."]);
222
+ run(["commit", "-m", "chore: baseline before codemod"]);
223
+ }
224
+
225
+ function makeDiff(originalPath, clonedPath, fixed) {
226
+ const tmpOutDir = fs.mkdtempSync(path.join(os.tmpdir(), "codemod-out-"));
227
+ const fixedPath = path.join(tmpOutDir, path.basename(clonedPath));
228
+ fs.writeFileSync(fixedPath, fixed, "utf8");
229
+ const diff = spawnSync("diff", ["-u", originalPath, fixedPath], { encoding: "utf8" });
230
+ return diff.stdout || diff.stderr || "";
231
+ }
232
+
233
+ async function readStdin() {
234
+ return await new Promise((resolve) => {
235
+ let d = "";
236
+ process.stdin.setEncoding("utf8");
237
+ process.stdin.on("data", (c) => (d += c));
238
+ process.stdin.on("end", () => resolve(d));
239
+ });
240
+ }
241
+
242
+ function leadingWhitespace(text, index) {
243
+ const lineStart = text.lastIndexOf("\n", index - 1) + 1;
244
+ const m = text.slice(lineStart, index).match(/^[ \t]*/);
245
+ return m ? m[0] : "";
246
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev-blinq/cucumber_client",
3
- "version": "1.0.1721-dev",
3
+ "version": "1.0.1723-dev",
4
4
  "description": " ",
5
5
  "main": "bin/index.js",
6
6
  "types": "bin/index.d.ts",
@@ -39,7 +39,7 @@
39
39
  "@babel/traverse": "^7.27.1",
40
40
  "@babel/types": "^7.27.1",
41
41
  "@cucumber/tag-expressions": "^6.1.1",
42
- "@dev-blinq/cucumber-js": "1.0.220-dev",
42
+ "@dev-blinq/cucumber-js": "1.0.221-dev",
43
43
  "@faker-js/faker": "^8.4.1",
44
44
  "automation_model": "1.0.928-dev",
45
45
  "axios": "^1.7.4",