@tailor-platform/sdk-codemod 0.3.0-next.0 → 0.3.0-next.2
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/CHANGELOG.md +86 -0
- package/dist/codemods/v2/apply-to-deploy/scripts/transform.js +22 -4
- package/dist/codemods/v2/auth-invoker-unwrap/scripts/transform.js +99 -11
- package/dist/codemods/v2/cli-rename/scripts/transform.js +373 -14
- package/dist/codemods/v2/execute-script-arg/scripts/transform.js +60 -0
- package/dist/codemods/v2/principal-unify/scripts/transform.js +1131 -43
- package/dist/codemods/v2/tailordb-namespace/scripts/transform.js +5 -4
- package/dist/index.js +521 -37
- package/package.json +5 -5
|
@@ -1,15 +1,377 @@
|
|
|
1
1
|
import * as path from "pathe";
|
|
2
2
|
//#region codemods/v2/cli-rename/scripts/transform.ts
|
|
3
3
|
const COMMAND_RENAMES = [["crash-report", "crashreport"]];
|
|
4
|
-
const
|
|
4
|
+
const OPTION_RENAMES = [["--machineuser", "--machine-user"]];
|
|
5
|
+
const ARG_VALUE = `(?:[^\\s'"\`;&|]+|'[^']*'|"(?:(?:\\\\.)|[^"\\\\])*")`;
|
|
6
|
+
const GLOBAL_ARG_PATTERN = `(?:(?:\\s+(?:--verbose|--json|-j))|(?:\\s+(?:--env-file|--env-file-if-exists|-e)(?:=${ARG_VALUE}|\\s+${ARG_VALUE})))*`;
|
|
7
|
+
const TAILOR_BINARY = `(?<![\\w-])tailor-sdk(?:@[^\\s'"\`]+)?(?![\\w-])`;
|
|
8
|
+
const COMMAND_PATTERN = new RegExp(`${TAILOR_BINARY}(${GLOBAL_ARG_PATTERN}\\s+)(${COMMAND_RENAMES.map(([from]) => from).join("|")})\\b`, "g");
|
|
9
|
+
const TAILOR_BINARY_PATTERN = new RegExp(TAILOR_BINARY, "g");
|
|
5
10
|
const COMMAND_MAP = new Map(COMMAND_RENAMES);
|
|
6
|
-
function
|
|
7
|
-
return value
|
|
11
|
+
function isOptionBoundaryChar(value) {
|
|
12
|
+
return value === void 0 || !/[\w-]/.test(value);
|
|
8
13
|
}
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
14
|
+
function findInlineCodeSpanEnd(source, start) {
|
|
15
|
+
const lineStart = source.lastIndexOf("\n", start - 1) + 1;
|
|
16
|
+
if ([...source.slice(lineStart, start).matchAll(/`/g)].length % 2 === 0) return void 0;
|
|
17
|
+
const codeSpanEnd = source.indexOf("`", start);
|
|
18
|
+
return codeSpanEnd === -1 ? void 0 : codeSpanEnd;
|
|
19
|
+
}
|
|
20
|
+
function findEnclosingLineQuoteEnd(source, start) {
|
|
21
|
+
const lineStart = source.lastIndexOf("\n", start - 1) + 1;
|
|
22
|
+
const lineEnd = source.indexOf("\n", start);
|
|
23
|
+
const limit = lineEnd === -1 ? source.length : lineEnd;
|
|
24
|
+
let quote = null;
|
|
25
|
+
for (let index = lineStart; index < start; index += 1) {
|
|
26
|
+
const ch = source[index];
|
|
27
|
+
if (quote !== null) {
|
|
28
|
+
if (ch === "\\" && quote === "\"" && index + 1 < start) {
|
|
29
|
+
index += 1;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (ch === quote) quote = null;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (ch === "'" || ch === "\"") quote = ch;
|
|
36
|
+
}
|
|
37
|
+
if (quote === null) return void 0;
|
|
38
|
+
for (let index = start; index < limit; index += 1) {
|
|
39
|
+
const ch = source[index];
|
|
40
|
+
if (ch === "\\" && quote === "\"" && index + 1 < limit) {
|
|
41
|
+
index += 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (ch === quote) return index;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function lineIndent(line) {
|
|
48
|
+
return line.match(/^ */)?.[0].length ?? 0;
|
|
49
|
+
}
|
|
50
|
+
function isFoldedScalarHeader(line) {
|
|
51
|
+
return /^\s*(?:-\s*)?[^#\n]*:\s*>[+-]?(?:\s*(?:#.*)?)?$/.test(line);
|
|
52
|
+
}
|
|
53
|
+
function findFoldedYamlRanges(source) {
|
|
54
|
+
const ranges = [];
|
|
55
|
+
const lines = source.match(/^.*(?:\n|$)/gm) ?? [];
|
|
56
|
+
let offset = 0;
|
|
57
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
58
|
+
const line = lines[index];
|
|
59
|
+
const body = line.replace(/\r?\n$/, "");
|
|
60
|
+
offset += line.length;
|
|
61
|
+
if (!isFoldedScalarHeader(body)) continue;
|
|
62
|
+
const baseIndent = lineIndent(body);
|
|
63
|
+
let rangeStart;
|
|
64
|
+
let rangeEnd;
|
|
65
|
+
let cursor = offset;
|
|
66
|
+
for (let nextIndex = index + 1; nextIndex < lines.length; nextIndex += 1) {
|
|
67
|
+
const nextLine = lines[nextIndex];
|
|
68
|
+
const nextBody = nextLine.replace(/\r?\n$/, "");
|
|
69
|
+
const trimmed = nextBody.trim();
|
|
70
|
+
const indent = lineIndent(nextBody);
|
|
71
|
+
if (trimmed !== "" && indent <= baseIndent) break;
|
|
72
|
+
if (trimmed === "") {
|
|
73
|
+
if (rangeStart !== void 0 && rangeEnd !== void 0) {
|
|
74
|
+
ranges.push({
|
|
75
|
+
start: rangeStart,
|
|
76
|
+
end: rangeEnd
|
|
77
|
+
});
|
|
78
|
+
rangeStart = void 0;
|
|
79
|
+
rangeEnd = void 0;
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
rangeStart ??= cursor;
|
|
83
|
+
rangeEnd = cursor + nextLine.length;
|
|
84
|
+
}
|
|
85
|
+
cursor += nextLine.length;
|
|
86
|
+
}
|
|
87
|
+
if (rangeStart !== void 0 && rangeEnd !== void 0) ranges.push({
|
|
88
|
+
start: rangeStart,
|
|
89
|
+
end: rangeEnd
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return ranges;
|
|
93
|
+
}
|
|
94
|
+
function findMarkdownFencedCodeRanges(source) {
|
|
95
|
+
const ranges = [];
|
|
96
|
+
const lines = source.match(/^.*(?:\n|$)/gm) ?? [];
|
|
97
|
+
let offset = 0;
|
|
98
|
+
let open;
|
|
99
|
+
for (const line of lines) {
|
|
100
|
+
const body = line.replace(/\r?\n$/, "");
|
|
101
|
+
if (open) {
|
|
102
|
+
const marker = body.match(/^ {0,3}(`{3,}|~{3,})\s*$/)?.[1];
|
|
103
|
+
if (marker && marker[0] === open.char && marker.length >= open.length) {
|
|
104
|
+
ranges.push({
|
|
105
|
+
start: open.start,
|
|
106
|
+
end: offset
|
|
107
|
+
});
|
|
108
|
+
open = void 0;
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
const marker = body.match(/^ {0,3}(`{3,}|~{3,}).*$/)?.[1];
|
|
112
|
+
if (marker) open = {
|
|
113
|
+
char: marker[0],
|
|
114
|
+
length: marker.length,
|
|
115
|
+
start: offset + line.length
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
offset += line.length;
|
|
119
|
+
}
|
|
120
|
+
if (open) ranges.push({
|
|
121
|
+
start: open.start,
|
|
122
|
+
end: source.length
|
|
123
|
+
});
|
|
124
|
+
return ranges;
|
|
125
|
+
}
|
|
126
|
+
function isCommandSeparator(source, index) {
|
|
127
|
+
const ch = source[index];
|
|
128
|
+
const prev = source[index - 1];
|
|
129
|
+
if (prev === "\\") return false;
|
|
130
|
+
if (ch === "&") {
|
|
131
|
+
const next = source[index + 1];
|
|
132
|
+
if (prev === ">" || prev === "<" || next === ">") return false;
|
|
133
|
+
}
|
|
134
|
+
return ch === ";" || ch === "&" || ch === "|";
|
|
135
|
+
}
|
|
136
|
+
function startsCommandSubstitution(source, index) {
|
|
137
|
+
return source[index] === "$" && source[index + 1] === "(" && source[index - 1] !== "\\";
|
|
138
|
+
}
|
|
139
|
+
function findCommandSubstitutionEnd(source, start) {
|
|
140
|
+
let depth = 1;
|
|
141
|
+
let index = start + 2;
|
|
142
|
+
let quote = null;
|
|
143
|
+
while (index < source.length) {
|
|
144
|
+
const ch = source[index];
|
|
145
|
+
if (quote !== null) {
|
|
146
|
+
if (quote.escaped) {
|
|
147
|
+
if (ch === "\\" && source[index + 1] === quote.char) {
|
|
148
|
+
quote = null;
|
|
149
|
+
index += 2;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
index += 1;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (quote.char === "\"" && startsCommandSubstitution(source, index)) {
|
|
156
|
+
depth += 1;
|
|
157
|
+
index += 2;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (ch === "\\" && quote.char === "\"" && index + 1 < source.length) {
|
|
161
|
+
index += 2;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (ch === quote.char) quote = null;
|
|
165
|
+
index += 1;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (ch === "\\" && source[index + 1] === "\"") {
|
|
169
|
+
quote = {
|
|
170
|
+
char: "\"",
|
|
171
|
+
escaped: true
|
|
172
|
+
};
|
|
173
|
+
index += 2;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (ch === "'" || ch === "\"") {
|
|
177
|
+
quote = {
|
|
178
|
+
char: ch,
|
|
179
|
+
escaped: false
|
|
180
|
+
};
|
|
181
|
+
index += 1;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (startsCommandSubstitution(source, index)) {
|
|
185
|
+
depth += 1;
|
|
186
|
+
index += 2;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (ch === ")") {
|
|
190
|
+
depth -= 1;
|
|
191
|
+
if (depth === 0) return index;
|
|
192
|
+
}
|
|
193
|
+
index += 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function findContainingRange(ranges, index) {
|
|
197
|
+
return ranges?.find((range) => range.start <= index && index < range.end);
|
|
198
|
+
}
|
|
199
|
+
function findTailorCommandEnd(source, start, foldedYamlRanges, markdownFencedCodeRanges) {
|
|
200
|
+
const inlineCodeSpanEnd = findInlineCodeSpanEnd(source, start);
|
|
201
|
+
const enclosingLineQuoteEnd = findEnclosingLineQuoteEnd(source, start);
|
|
202
|
+
const limit = Math.min(inlineCodeSpanEnd ?? source.length, enclosingLineQuoteEnd ?? source.length);
|
|
203
|
+
const foldedYamlRange = findContainingRange(foldedYamlRanges, start);
|
|
204
|
+
const markdownFencedCodeRange = findContainingRange(markdownFencedCodeRanges, start);
|
|
205
|
+
const delimitedRange = foldedYamlRange ?? markdownFencedCodeRange;
|
|
206
|
+
const commandLimit = delimitedRange ? Math.min(limit, delimitedRange.end) : limit;
|
|
207
|
+
let quote = null;
|
|
208
|
+
let end = start;
|
|
209
|
+
while (end < commandLimit) {
|
|
210
|
+
const ch = source[end];
|
|
211
|
+
if (quote !== null) {
|
|
212
|
+
if (quote.escaped) {
|
|
213
|
+
if (ch === "\\" && source[end + 1] === quote.char) {
|
|
214
|
+
quote = null;
|
|
215
|
+
end += 2;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
end += 1;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (quote.char === "\"" && startsCommandSubstitution(source, end)) {
|
|
222
|
+
const substitutionEnd = findCommandSubstitutionEnd(source, end);
|
|
223
|
+
if (substitutionEnd !== void 0) {
|
|
224
|
+
end = substitutionEnd + 1;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (ch === "\\" && quote.char === "\"" && end + 1 < commandLimit) {
|
|
229
|
+
end += 2;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (ch === quote.char) quote = null;
|
|
233
|
+
end += 1;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (ch === "\\" && source[end + 1] === "\"") {
|
|
237
|
+
quote = {
|
|
238
|
+
char: "\"",
|
|
239
|
+
escaped: true
|
|
240
|
+
};
|
|
241
|
+
end += 2;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (ch === "'" || ch === "\"") {
|
|
245
|
+
quote = {
|
|
246
|
+
char: ch,
|
|
247
|
+
escaped: false
|
|
248
|
+
};
|
|
249
|
+
end += 1;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (startsCommandSubstitution(source, end)) {
|
|
253
|
+
const substitutionEnd = findCommandSubstitutionEnd(source, end);
|
|
254
|
+
if (substitutionEnd !== void 0) {
|
|
255
|
+
end = substitutionEnd + 1;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const prev = source[end - 1];
|
|
260
|
+
if (ch === ")") break;
|
|
261
|
+
if (isCommandSeparator(source, end)) break;
|
|
262
|
+
if (ch === "\n" && prev !== "\\" && !foldedYamlRange) break;
|
|
263
|
+
end += 1;
|
|
264
|
+
}
|
|
265
|
+
return end;
|
|
266
|
+
}
|
|
267
|
+
function isDelimitedCommandContext(source, start, foldedYamlRanges, markdownFencedCodeRanges) {
|
|
268
|
+
return findInlineCodeSpanEnd(source, start) !== void 0 || findEnclosingLineQuoteEnd(source, start) !== void 0 || findContainingRange(foldedYamlRanges, start) !== void 0 || findContainingRange(markdownFencedCodeRanges, start) !== void 0;
|
|
269
|
+
}
|
|
270
|
+
function findOptionRename(command, index) {
|
|
271
|
+
return OPTION_RENAMES.find(([from]) => command.startsWith(from, index) && isOptionBoundaryChar(command[index - 1]) && isOptionBoundaryChar(command[index + from.length]));
|
|
272
|
+
}
|
|
273
|
+
function replaceOptionsInCommand(command) {
|
|
274
|
+
let updated = "";
|
|
275
|
+
let index = 0;
|
|
276
|
+
let quote = null;
|
|
277
|
+
while (index < command.length) {
|
|
278
|
+
const ch = command[index];
|
|
279
|
+
if (quote !== null) {
|
|
280
|
+
if (quote.char === "\"" && startsCommandSubstitution(command, index)) {
|
|
281
|
+
const substitutionEnd = findCommandSubstitutionEnd(command, index);
|
|
282
|
+
if (substitutionEnd !== void 0) {
|
|
283
|
+
updated += "$(";
|
|
284
|
+
updated += replaceAll(command.slice(index + 2, substitutionEnd));
|
|
285
|
+
updated += ")";
|
|
286
|
+
index = substitutionEnd + 1;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
updated += ch;
|
|
291
|
+
if (quote.escaped) {
|
|
292
|
+
if (ch === "\\" && command[index + 1] === quote.char) {
|
|
293
|
+
index += 1;
|
|
294
|
+
updated += command[index];
|
|
295
|
+
quote = null;
|
|
296
|
+
}
|
|
297
|
+
} else if (ch === "\\" && quote.char === "\"" && index + 1 < command.length) {
|
|
298
|
+
index += 1;
|
|
299
|
+
updated += command[index];
|
|
300
|
+
} else if (ch === quote.char) quote = null;
|
|
301
|
+
index += 1;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (ch === "\\" && command[index + 1] === "\"") {
|
|
305
|
+
quote = {
|
|
306
|
+
char: "\"",
|
|
307
|
+
escaped: true
|
|
308
|
+
};
|
|
309
|
+
updated += ch;
|
|
310
|
+
index += 1;
|
|
311
|
+
updated += command[index];
|
|
312
|
+
index += 1;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (ch === "'" || ch === "\"") {
|
|
316
|
+
quote = {
|
|
317
|
+
char: ch,
|
|
318
|
+
escaped: false
|
|
319
|
+
};
|
|
320
|
+
updated += ch;
|
|
321
|
+
index += 1;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (startsCommandSubstitution(command, index)) {
|
|
325
|
+
const substitutionEnd = findCommandSubstitutionEnd(command, index);
|
|
326
|
+
if (substitutionEnd !== void 0) {
|
|
327
|
+
updated += "$(";
|
|
328
|
+
updated += replaceAll(command.slice(index + 2, substitutionEnd));
|
|
329
|
+
updated += ")";
|
|
330
|
+
index = substitutionEnd + 1;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const rename = findOptionRename(command, index);
|
|
335
|
+
if (rename) {
|
|
336
|
+
updated += rename[1];
|
|
337
|
+
index += rename[0].length;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
updated += ch;
|
|
341
|
+
index += 1;
|
|
342
|
+
}
|
|
343
|
+
return updated;
|
|
344
|
+
}
|
|
345
|
+
function replaceOptionsInTailorCommands(source, foldedYamlRanges, requireDelimitedContext = false, markdownFencedCodeRanges) {
|
|
346
|
+
let updated = "";
|
|
347
|
+
let cursor = 0;
|
|
348
|
+
TAILOR_BINARY_PATTERN.lastIndex = 0;
|
|
349
|
+
for (;;) {
|
|
350
|
+
const match = TAILOR_BINARY_PATTERN.exec(source);
|
|
351
|
+
if (!match) break;
|
|
352
|
+
const start = match.index;
|
|
353
|
+
if (start < cursor) continue;
|
|
354
|
+
if (requireDelimitedContext && !isDelimitedCommandContext(source, start, foldedYamlRanges, markdownFencedCodeRanges)) {
|
|
355
|
+
TAILOR_BINARY_PATTERN.lastIndex = start + match[0].length;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const end = findTailorCommandEnd(source, start, foldedYamlRanges, markdownFencedCodeRanges);
|
|
359
|
+
updated += source.slice(cursor, start);
|
|
360
|
+
updated += replaceOptionsInCommand(source.slice(start, end));
|
|
361
|
+
cursor = end;
|
|
362
|
+
TAILOR_BINARY_PATTERN.lastIndex = end;
|
|
363
|
+
}
|
|
364
|
+
return updated + source.slice(cursor);
|
|
365
|
+
}
|
|
366
|
+
function replaceAll(value, parseFoldedYaml = false, requireDelimitedContext = false, parseMarkdownFencedCode = false) {
|
|
367
|
+
const updated = value.replace(COMMAND_PATTERN, (match, _prefix, cmd) => `${match.slice(0, -cmd.length)}${COMMAND_MAP.get(cmd) ?? cmd}`);
|
|
368
|
+
return replaceOptionsInTailorCommands(updated, parseFoldedYaml ? findFoldedYamlRanges(updated) : void 0, requireDelimitedContext, parseMarkdownFencedCode ? findMarkdownFencedCodeRanges(updated) : void 0);
|
|
369
|
+
}
|
|
370
|
+
function transformText(source, filePath) {
|
|
371
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
372
|
+
const isYaml = ext === ".yml" || ext === ".yaml";
|
|
373
|
+
const isMarkdown = ext === ".md";
|
|
374
|
+
const updated = replaceAll(source, isYaml, isMarkdown, isMarkdown);
|
|
13
375
|
return updated === source ? null : updated;
|
|
14
376
|
}
|
|
15
377
|
function transformPackageJson(source) {
|
|
@@ -35,20 +397,17 @@ function transformPackageJson(source) {
|
|
|
35
397
|
}
|
|
36
398
|
/**
|
|
37
399
|
* Apply v2 CLI naming conventions: multi-word commands collapse into a single
|
|
38
|
-
* word (`crash-report` → `crashreport`)
|
|
39
|
-
* (`
|
|
400
|
+
* word (`crash-report` → `crashreport`), and legacy option spellings are
|
|
401
|
+
* rewritten to kebab-case (`--machineuser` → `--machine-user`). Optional
|
|
402
|
+
* `@version` pins on the binary (`tailor-sdk@latest`) are preserved.
|
|
40
403
|
*
|
|
41
|
-
* Long options (`--executionId`, `--executorName`, `--jobId`) and the
|
|
42
|
-
* positional argument keys with the same names are intentionally not rewritten:
|
|
43
|
-
* those tokens are positional in the SDK CLI and never appear as long flags in
|
|
44
|
-
* user scripts, so a transform here would have no real-world target.
|
|
45
404
|
* @param source - File contents
|
|
46
405
|
* @param filePath - Absolute path to the file (used to dispatch package.json vs text)
|
|
47
406
|
* @returns Transformed source or null when nothing matched.
|
|
48
407
|
*/
|
|
49
408
|
function transform(source, filePath) {
|
|
50
409
|
if (path.extname(filePath).toLowerCase() === ".json") return transformPackageJson(source);
|
|
51
|
-
return transformText(source);
|
|
410
|
+
return transformText(source, filePath);
|
|
52
411
|
}
|
|
53
412
|
//#endregion
|
|
54
413
|
export { transform as default };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Lang, parse } from "@ast-grep/napi";
|
|
2
|
+
//#region codemods/v2/execute-script-arg/scripts/transform.ts
|
|
3
|
+
const NEEDLE = "executeScript";
|
|
4
|
+
function quickFilter(source) {
|
|
5
|
+
return source.includes(NEEDLE) && source.includes("JSON.stringify");
|
|
6
|
+
}
|
|
7
|
+
function pairKeyText(pair) {
|
|
8
|
+
const key = pair.children()[0];
|
|
9
|
+
if (!key) return null;
|
|
10
|
+
return key.text().replace(/^['"]|['"]$/g, "");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* True when `stringifyCall` is the value of a top-level `arg:` property in the
|
|
14
|
+
* object literal passed directly to `executeScript(...)`. The chain checked is
|
|
15
|
+
* `JSON.stringify(...)` → pair (`arg:`) → object → arguments → `executeScript`
|
|
16
|
+
* call, so a nested `arg:` (e.g. `executeScript({ opts: { arg: ... } })`) or an
|
|
17
|
+
* unrelated `JSON.stringify` is left untouched.
|
|
18
|
+
*/
|
|
19
|
+
function isExecuteScriptArg(stringifyCall) {
|
|
20
|
+
const pair = stringifyCall.parent();
|
|
21
|
+
if (!pair || pair.kind() !== "pair") return false;
|
|
22
|
+
if (pairKeyText(pair) !== "arg") return false;
|
|
23
|
+
const obj = pair.parent();
|
|
24
|
+
if (!obj || obj.kind() !== "object") return false;
|
|
25
|
+
const args = obj.parent();
|
|
26
|
+
if (!args || args.kind() !== "arguments") return false;
|
|
27
|
+
const call = args.parent();
|
|
28
|
+
if (!call || call.kind() !== "call_expression") return false;
|
|
29
|
+
const callee = call.children()[0];
|
|
30
|
+
return !!callee && callee.text() === NEEDLE;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Rewrite `executeScript({ ..., arg: JSON.stringify(X), ... })` to
|
|
34
|
+
* `executeScript({ ..., arg: X, ... })`.
|
|
35
|
+
*
|
|
36
|
+
* In v2 the `executeScript` `arg` option takes a JSON-serializable value and
|
|
37
|
+
* serializes it internally, so a pre-stringified argument double-encodes. Only
|
|
38
|
+
* the literal `arg: JSON.stringify(<single expr>)` form is rewritten; indirect
|
|
39
|
+
* forms (a stringified value held in a variable, `JSON.stringify(x, null, 2)`,
|
|
40
|
+
* etc.) are left for manual migration.
|
|
41
|
+
* @param source - File contents
|
|
42
|
+
* @param _filePath - Absolute path to the file (kept for the runner signature)
|
|
43
|
+
* @returns Transformed source or null when nothing matched.
|
|
44
|
+
*/
|
|
45
|
+
function transform(source, _filePath) {
|
|
46
|
+
if (!quickFilter(source)) return null;
|
|
47
|
+
const root = parse(source.includes("</") || source.includes("/>") ? Lang.Tsx : Lang.TypeScript, source).root();
|
|
48
|
+
const edits = [];
|
|
49
|
+
for (const match of root.findAll({ rule: { pattern: "JSON.stringify($X)" } })) {
|
|
50
|
+
if (!isExecuteScriptArg(match)) continue;
|
|
51
|
+
const inner = match.getMatch("X");
|
|
52
|
+
if (!inner) continue;
|
|
53
|
+
edits.push(match.replace(inner.text()));
|
|
54
|
+
}
|
|
55
|
+
if (edits.length === 0) return null;
|
|
56
|
+
const result = root.commitEdits(edits);
|
|
57
|
+
return result === source ? null : result;
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { transform as default };
|