@hapico/cli 0.0.16 → 0.0.17
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/bin/index.js +1 -1
- package/bin/utils/codemod/utils/path.mockup.js +84 -0
- package/bun.lock +6 -0
- package/dist/index.js +1 -1
- package/dist/utils/codemod/utils/path.mockup.js +84 -0
- package/index.ts +1 -1
- package/package.json +3 -1
- package/utils/codemod/index.tsx +1588 -0
- package/utils/codemod/utils/path.mockup.ts +97 -0
|
@@ -0,0 +1,1588 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import * as Babel from "@babel/standalone";
|
|
3
|
+
import * as uuid from "uuid";
|
|
4
|
+
import * as prettier from "prettier/standalone";
|
|
5
|
+
import parserBabel from "prettier/plugins/babel";
|
|
6
|
+
import estree from "prettier/plugins/estree";
|
|
7
|
+
import { includes, map } from "lodash";
|
|
8
|
+
|
|
9
|
+
const generator = Babel.packages.generator.default;
|
|
10
|
+
const traverse = Babel.packages.traverse.default;
|
|
11
|
+
const parser = Babel.packages.parser;
|
|
12
|
+
const parse = Babel.packages.parser.parse;
|
|
13
|
+
|
|
14
|
+
export interface IFile {
|
|
15
|
+
path: string;
|
|
16
|
+
content: string;
|
|
17
|
+
es5?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ImportStatement {
|
|
21
|
+
default: string;
|
|
22
|
+
named: Array<string>;
|
|
23
|
+
from: string;
|
|
24
|
+
code: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type LiteralChangeRequest = {
|
|
28
|
+
id: string;
|
|
29
|
+
replacement: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type WrapComponentRequest = {
|
|
33
|
+
id: string;
|
|
34
|
+
wrapper: string; // Name of the wrapper component
|
|
35
|
+
attributes: Record<string, string>; // Attributes to pass to the wrapper component
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const removeExtension = (filename: string) => {
|
|
39
|
+
return filename.replace(/\.[^/.]+$/, "");
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* This tool is used to modify the code of a component.
|
|
44
|
+
* JSX React code can be modified using this tool.
|
|
45
|
+
*/
|
|
46
|
+
export const ID_ATTRIBUTE = "_id";
|
|
47
|
+
class CodeMod {
|
|
48
|
+
public code: string;
|
|
49
|
+
private presets: Array<any> = [
|
|
50
|
+
// Keep the code as is
|
|
51
|
+
["typescript", { allExtensions: true, isTSX: true }], // Handle TypeScript syntax
|
|
52
|
+
];
|
|
53
|
+
constructor(code: string) {
|
|
54
|
+
this.code = code;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getCode() {
|
|
58
|
+
return this.code;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
isValid() {
|
|
62
|
+
try {
|
|
63
|
+
Babel.transform(this.code, {
|
|
64
|
+
presets: this.presets,
|
|
65
|
+
filename: "file.tsx", // Ensure it's treated as TSX file
|
|
66
|
+
});
|
|
67
|
+
return true;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error("IS_VALID_ERROR", error);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Return es5 code
|
|
76
|
+
* @returns
|
|
77
|
+
*/
|
|
78
|
+
compile() {
|
|
79
|
+
try {
|
|
80
|
+
// Transform the TypeScript React.js code into ES5
|
|
81
|
+
const result = Babel.transform(this.code, {
|
|
82
|
+
presets: [
|
|
83
|
+
["env", { targets: { esmodules: false } }], // Compiles to ES5
|
|
84
|
+
"typescript", // Handles TypeScript syntax
|
|
85
|
+
"react", // Handles JSX syntax in TSX files
|
|
86
|
+
],
|
|
87
|
+
filename: "file.tsx", // Ensure the file is treated as TSX
|
|
88
|
+
});
|
|
89
|
+
return result.code;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
// console.error("COMPILE_ERROR", error);
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Encode
|
|
98
|
+
* Mean every JSX tag will be injected with ID_ATTRIBUTE={uuidv4()}
|
|
99
|
+
* @param params
|
|
100
|
+
*/
|
|
101
|
+
encode() {
|
|
102
|
+
const ast = this.ast();
|
|
103
|
+
// traverse the code and add ID_ATTRIBUTE={uuidv4()} to all JSX elements
|
|
104
|
+
traverse(ast, {
|
|
105
|
+
JSXOpeningElement(path: any) {
|
|
106
|
+
if (!path.node.attributes) {
|
|
107
|
+
path.node.attributes = [];
|
|
108
|
+
}
|
|
109
|
+
if (
|
|
110
|
+
path.node.attributes.find(
|
|
111
|
+
(attr: any) => attr.name?.name === ID_ATTRIBUTE
|
|
112
|
+
)
|
|
113
|
+
) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
path.node.attributes.push({
|
|
117
|
+
type: "JSXAttribute",
|
|
118
|
+
name: {
|
|
119
|
+
type: "JSXIdentifier",
|
|
120
|
+
name: ID_ATTRIBUTE,
|
|
121
|
+
},
|
|
122
|
+
value: {
|
|
123
|
+
type: "JSXExpressionContainer",
|
|
124
|
+
expression: {
|
|
125
|
+
type: "StringLiteral",
|
|
126
|
+
value: uuid.v4(),
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
// convert the AST back to code
|
|
133
|
+
this.astToCode(ast);
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* AST
|
|
139
|
+
* @returns
|
|
140
|
+
*/
|
|
141
|
+
ast() {
|
|
142
|
+
const ast = parser.parse(this.code, {
|
|
143
|
+
sourceType: "module",
|
|
144
|
+
plugins: ["jsx", "typescript"],
|
|
145
|
+
});
|
|
146
|
+
return ast as any;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* AST to Code
|
|
151
|
+
* @param ast
|
|
152
|
+
* @returns
|
|
153
|
+
*/
|
|
154
|
+
astToCode(ast: any) {
|
|
155
|
+
const { code } = generator(ast, {}, this.code);
|
|
156
|
+
this.code = code;
|
|
157
|
+
return this;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Decode
|
|
162
|
+
* Remove all _____id attributes from JSX elements
|
|
163
|
+
* @param params
|
|
164
|
+
*/
|
|
165
|
+
decode(options?: {
|
|
166
|
+
exceptIds: Array<string>;
|
|
167
|
+
}) {
|
|
168
|
+
const { exceptIds = [] } = options || { exceptIds: [] };
|
|
169
|
+
const ast = this.ast();
|
|
170
|
+
// traverse the code and remove ID_ATTRIBUTE from all JSX elements
|
|
171
|
+
traverse(ast, {
|
|
172
|
+
JSXOpeningElement(path: any) {
|
|
173
|
+
if (!path.node.attributes) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
path.node.attributes = path.node.attributes.filter(
|
|
177
|
+
(attr: any) => attr.name?.name !== ID_ATTRIBUTE || exceptIds.includes(attr.value?.expression?.value)
|
|
178
|
+
);
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
// convert the AST back to code
|
|
182
|
+
this.astToCode(ast);
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Prettify the code
|
|
188
|
+
*/
|
|
189
|
+
async prettify() {
|
|
190
|
+
try {
|
|
191
|
+
this.code = await prettier.format(this.code, {
|
|
192
|
+
parser: "babel",
|
|
193
|
+
plugins: [parserBabel, estree],
|
|
194
|
+
});
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error("PRETTIFY_ERROR", this.code);
|
|
197
|
+
console.error(error);
|
|
198
|
+
}
|
|
199
|
+
return this;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
public async output() {
|
|
203
|
+
return this.code;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Modify JSX Text Content
|
|
208
|
+
* @param request
|
|
209
|
+
* @returns
|
|
210
|
+
*/
|
|
211
|
+
modifyJSXTextContent(request: LiteralChangeRequest) {
|
|
212
|
+
this.code =
|
|
213
|
+
Babel.transform(this.code, {
|
|
214
|
+
presets: [
|
|
215
|
+
// "react", // Handle JSX syntax as is (no React.createElement transformation),
|
|
216
|
+
["typescript", { allExtensions: true, isTSX: true }], // Handle TypeScript syntax
|
|
217
|
+
],
|
|
218
|
+
filename: "file.tsx", // Ensure it's treated as TSX file
|
|
219
|
+
plugins: [
|
|
220
|
+
() => ({
|
|
221
|
+
visitor: {
|
|
222
|
+
JSXElement(path: any) {
|
|
223
|
+
const openingElement = path.node.openingElement;
|
|
224
|
+
const openingAttribute = openingElement.attributes.find(
|
|
225
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
226
|
+
);
|
|
227
|
+
if (openingAttribute?.value?.expression?.value === request.id) {
|
|
228
|
+
path.node.children = [
|
|
229
|
+
{
|
|
230
|
+
type: "JSXText",
|
|
231
|
+
value: request.replacement,
|
|
232
|
+
},
|
|
233
|
+
];
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
}),
|
|
238
|
+
],
|
|
239
|
+
})?.code ?? "";
|
|
240
|
+
|
|
241
|
+
return this;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Check if the code has a React import
|
|
246
|
+
* @returns {boolean}
|
|
247
|
+
*/
|
|
248
|
+
hasReactImport() {
|
|
249
|
+
const ast = this.ast();
|
|
250
|
+
let hasReact = false;
|
|
251
|
+
traverse(ast, {
|
|
252
|
+
ImportDeclaration(path: any) {
|
|
253
|
+
if (path.node.source.value === "react") {
|
|
254
|
+
hasReact = true;
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
return hasReact;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* wrap component with another component
|
|
263
|
+
*/
|
|
264
|
+
wrapJSXComponent(request: WrapComponentRequest) {
|
|
265
|
+
const { id, wrapper, attributes } = request;
|
|
266
|
+
let isDone = false;
|
|
267
|
+
this.code =
|
|
268
|
+
Babel.transform(this.code, {
|
|
269
|
+
presets: this.presets,
|
|
270
|
+
filename: "file.tsx", // Ensure it's treated as TSX file
|
|
271
|
+
plugins: [
|
|
272
|
+
() => ({
|
|
273
|
+
visitor: {
|
|
274
|
+
JSXElement(path: any) {
|
|
275
|
+
if (isDone) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
console.log("PATH", path.node);
|
|
279
|
+
const openingElement = path.node.openingElement;
|
|
280
|
+
const closingElement = path.node.closingElement;
|
|
281
|
+
|
|
282
|
+
const openingAttribute = openingElement.attributes.find(
|
|
283
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
284
|
+
);
|
|
285
|
+
if (
|
|
286
|
+
openingAttribute?.value?.expression?.value === id ||
|
|
287
|
+
openingAttribute?.value?.value === id
|
|
288
|
+
) {
|
|
289
|
+
const wrapperAttributes = map(attributes, (value, key) => ({
|
|
290
|
+
type: "JSXAttribute",
|
|
291
|
+
name: {
|
|
292
|
+
type: "JSXIdentifier",
|
|
293
|
+
name: key,
|
|
294
|
+
},
|
|
295
|
+
value: {
|
|
296
|
+
type: "StringLiteral",
|
|
297
|
+
value,
|
|
298
|
+
},
|
|
299
|
+
}));
|
|
300
|
+
|
|
301
|
+
// Insert wrapper around the current node
|
|
302
|
+
path.replaceWith({
|
|
303
|
+
type: "JSXElement",
|
|
304
|
+
openingElement: {
|
|
305
|
+
type: "JSXOpeningElement",
|
|
306
|
+
name: {
|
|
307
|
+
type: "JSXIdentifier",
|
|
308
|
+
name: wrapper,
|
|
309
|
+
},
|
|
310
|
+
attributes: wrapperAttributes,
|
|
311
|
+
selfClosing: false,
|
|
312
|
+
},
|
|
313
|
+
closingElement: {
|
|
314
|
+
type: "JSXClosingElement",
|
|
315
|
+
name: {
|
|
316
|
+
type: "JSXIdentifier",
|
|
317
|
+
name: wrapper,
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
children: [
|
|
321
|
+
{
|
|
322
|
+
type: "JSXElement",
|
|
323
|
+
openingElement,
|
|
324
|
+
closingElement,
|
|
325
|
+
children: path.node.children || [],
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
});
|
|
329
|
+
isDone = true;
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
}),
|
|
334
|
+
],
|
|
335
|
+
})?.code ?? "";
|
|
336
|
+
return this;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
getAttributes(id: string) {
|
|
340
|
+
const attributes: any = {};
|
|
341
|
+
Babel.transform(this.code, {
|
|
342
|
+
presets: this.presets,
|
|
343
|
+
filename: "file.tsx", // Ensure it's treated as TSX file
|
|
344
|
+
plugins: [
|
|
345
|
+
() => ({
|
|
346
|
+
visitor: {
|
|
347
|
+
JSXElement(path: any) {
|
|
348
|
+
const openingElement = path.node.openingElement;
|
|
349
|
+
if (!openingElement?.attributes) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const openingAttribute = openingElement.attributes.find(
|
|
353
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
354
|
+
);
|
|
355
|
+
if (
|
|
356
|
+
openingAttribute?.value?.expression?.value === id ||
|
|
357
|
+
openingAttribute?.value?.value === id
|
|
358
|
+
) {
|
|
359
|
+
openingElement.attributes.forEach((attr: any) => {
|
|
360
|
+
if (attr?.name?.name !== ID_ATTRIBUTE) {
|
|
361
|
+
if (attr?.value?.type === "JSXExpressionContainer") {
|
|
362
|
+
if (attr?.value?.expression?.type === "StringLiteral") {
|
|
363
|
+
attributes[attr?.name?.name] =
|
|
364
|
+
attr.value.expression.value;
|
|
365
|
+
} else if (
|
|
366
|
+
attr?.value?.expression?.type === "NumericLiteral"
|
|
367
|
+
) {
|
|
368
|
+
attributes[attr?.name?.name] =
|
|
369
|
+
attr.value.expression.value;
|
|
370
|
+
}
|
|
371
|
+
} else if (
|
|
372
|
+
attr?.value?.type === "StringLiteral" ||
|
|
373
|
+
attr?.value?.type === "NumericLiteral"
|
|
374
|
+
) {
|
|
375
|
+
attributes[attr?.name?.name] = attr.value.value;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
}),
|
|
383
|
+
],
|
|
384
|
+
});
|
|
385
|
+
return attributes;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
removeJSXElement(id: string) {
|
|
389
|
+
this.code =
|
|
390
|
+
Babel.transform(this.code, {
|
|
391
|
+
presets: this.presets,
|
|
392
|
+
filename: "file.tsx", // Ensure it's treated as TSX file
|
|
393
|
+
plugins: [
|
|
394
|
+
() => ({
|
|
395
|
+
visitor: {
|
|
396
|
+
JSXElement(path: any) {
|
|
397
|
+
const openingElement = path.node.openingElement;
|
|
398
|
+
const openingAttribute = openingElement.attributes.find(
|
|
399
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
400
|
+
);
|
|
401
|
+
if (
|
|
402
|
+
openingAttribute?.value?.expression?.value === id ||
|
|
403
|
+
openingAttribute?.value?.value === id
|
|
404
|
+
) {
|
|
405
|
+
path.remove();
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
}),
|
|
410
|
+
],
|
|
411
|
+
})?.code || "";
|
|
412
|
+
return this;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
moveJSXElement(id: string, direction: "up" | "down") {
|
|
416
|
+
this.code =
|
|
417
|
+
Babel.transform(this.code, {
|
|
418
|
+
presets: this.presets,
|
|
419
|
+
filename: "file.tsx", // Ensure it's treated as TSX file
|
|
420
|
+
plugins: [
|
|
421
|
+
() => ({
|
|
422
|
+
visitor: {
|
|
423
|
+
JSXElement(path: any) {
|
|
424
|
+
// move up means check if the current JSX element is in children array
|
|
425
|
+
// if it is, then move it up
|
|
426
|
+
const openingElement = path.node.openingElement;
|
|
427
|
+
const openingAttribute = openingElement.attributes.find(
|
|
428
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
429
|
+
);
|
|
430
|
+
if (
|
|
431
|
+
openingAttribute?.value?.expression?.value === id ||
|
|
432
|
+
openingAttribute?.value?.value === id
|
|
433
|
+
) {
|
|
434
|
+
const parentPath = path.parentPath;
|
|
435
|
+
if (parentPath?.node?.type === "JSXElement") {
|
|
436
|
+
const parentChildren = parentPath.node.children;
|
|
437
|
+
if (Array.isArray(parentChildren)) {
|
|
438
|
+
const currentIndex = parentChildren.findIndex(
|
|
439
|
+
(child: any) => child === path.node
|
|
440
|
+
);
|
|
441
|
+
if (direction === "down") {
|
|
442
|
+
if (currentIndex > 0) {
|
|
443
|
+
// find the previous JSX element
|
|
444
|
+
let index = currentIndex - 1;
|
|
445
|
+
while (
|
|
446
|
+
index >= 0 &&
|
|
447
|
+
parentChildren[index].type !== "JSXElement"
|
|
448
|
+
) {
|
|
449
|
+
index--;
|
|
450
|
+
}
|
|
451
|
+
if (index >= 0) {
|
|
452
|
+
const previousElement = parentChildren[index];
|
|
453
|
+
parentChildren[index] = path.node;
|
|
454
|
+
parentChildren[currentIndex] = previousElement;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
} else if (direction === "up") {
|
|
458
|
+
console.log(
|
|
459
|
+
"DOWN",
|
|
460
|
+
currentIndex,
|
|
461
|
+
parentChildren.length
|
|
462
|
+
);
|
|
463
|
+
if (currentIndex < parentChildren.length - 1) {
|
|
464
|
+
// find the next JSX element
|
|
465
|
+
let index = currentIndex + 1;
|
|
466
|
+
while (
|
|
467
|
+
index < parentChildren.length &&
|
|
468
|
+
parentChildren[index].type !== "JSXElement"
|
|
469
|
+
) {
|
|
470
|
+
index++;
|
|
471
|
+
}
|
|
472
|
+
if (index < parentChildren.length) {
|
|
473
|
+
const nextElement = parentChildren[index];
|
|
474
|
+
parentChildren[index] = path.node;
|
|
475
|
+
parentChildren[currentIndex] = nextElement;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
}),
|
|
485
|
+
],
|
|
486
|
+
})?.code || "";
|
|
487
|
+
return this;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
getClassName(id: string) {
|
|
491
|
+
let className = "";
|
|
492
|
+
const ast = this.ast();
|
|
493
|
+
traverse(ast, {
|
|
494
|
+
JSXElement(path: any) {
|
|
495
|
+
const openingElement = path.node.openingElement;
|
|
496
|
+
const openingAttribute = openingElement.attributes.find(
|
|
497
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
498
|
+
);
|
|
499
|
+
if (
|
|
500
|
+
openingAttribute?.value?.expression?.value === id ||
|
|
501
|
+
openingAttribute?.value?.value === id
|
|
502
|
+
) {
|
|
503
|
+
const classNameAttribute = openingElement.attributes.find(
|
|
504
|
+
(attr: any) => attr?.name?.name === "className"
|
|
505
|
+
);
|
|
506
|
+
className =
|
|
507
|
+
classNameAttribute?.value?.value ??
|
|
508
|
+
classNameAttribute?.value?.expression.value;
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
return className;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
updateClassName(id: string, className: string) {
|
|
517
|
+
this.code =
|
|
518
|
+
Babel.transform(this.code, {
|
|
519
|
+
presets: this.presets,
|
|
520
|
+
filename: "file.tsx", // Ensure it's treated as TSX file
|
|
521
|
+
plugins: [
|
|
522
|
+
() => ({
|
|
523
|
+
visitor: {
|
|
524
|
+
JSXElement(path: any) {
|
|
525
|
+
const openingElement = path.node.openingElement;
|
|
526
|
+
console.log("UPDATE_CLASS_NAME", id, openingElement.attributes);
|
|
527
|
+
const openingAttribute = openingElement.attributes.find(
|
|
528
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
if (
|
|
532
|
+
openingAttribute?.value?.expression?.value === id ||
|
|
533
|
+
openingAttribute?.value?.value === id
|
|
534
|
+
) {
|
|
535
|
+
const classNameAttribute = openingElement.attributes.find(
|
|
536
|
+
(attr: any) => attr?.name?.name === "className"
|
|
537
|
+
);
|
|
538
|
+
if (classNameAttribute && classNameAttribute?.value) {
|
|
539
|
+
classNameAttribute.value = {
|
|
540
|
+
type: "StringLiteral",
|
|
541
|
+
value: className,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
}),
|
|
548
|
+
],
|
|
549
|
+
})?.code || "";
|
|
550
|
+
return this;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
removeImport(source: string) {
|
|
554
|
+
const newCode = Babel.transform(this.code + "", {
|
|
555
|
+
presets: this.presets,
|
|
556
|
+
filename: "file.tsx", // Ensure it's treated as TSX file
|
|
557
|
+
plugins: [
|
|
558
|
+
() => ({
|
|
559
|
+
visitor: {
|
|
560
|
+
ImportDeclaration(path: any) {
|
|
561
|
+
if (path.node?.source?.value === source) {
|
|
562
|
+
path.remove();
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
}),
|
|
567
|
+
],
|
|
568
|
+
})?.code;
|
|
569
|
+
this.code = newCode || "";
|
|
570
|
+
return this;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Check if the code has import statement from a specific source
|
|
575
|
+
* @param from
|
|
576
|
+
* @returns
|
|
577
|
+
*/
|
|
578
|
+
isImported(from: string) {
|
|
579
|
+
const ast = this.ast();
|
|
580
|
+
let isImported = false;
|
|
581
|
+
traverse(ast, {
|
|
582
|
+
ImportDeclaration(path: any) {
|
|
583
|
+
if (path.node.source.value === from) {
|
|
584
|
+
isImported = true;
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
return isImported;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Add import statement to the code
|
|
593
|
+
* @param importStatement
|
|
594
|
+
* @returns
|
|
595
|
+
*/
|
|
596
|
+
/**
|
|
597
|
+
* Add import statement to the code
|
|
598
|
+
* @param importStatement
|
|
599
|
+
* @returns
|
|
600
|
+
*/
|
|
601
|
+
addImport(params: { default?: string; named?: string[]; from: string }) {
|
|
602
|
+
if (!params.from) {
|
|
603
|
+
throw new Error("Import source cannot be empty");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const { default: defaultImport, named = [], from } = params;
|
|
607
|
+
const ast = this.ast();
|
|
608
|
+
|
|
609
|
+
// Check if import already exists
|
|
610
|
+
let importExists = false;
|
|
611
|
+
traverse(ast, {
|
|
612
|
+
ImportDeclaration(path: any) {
|
|
613
|
+
if (path.node.source.value === from) {
|
|
614
|
+
importExists = true;
|
|
615
|
+
// Add new specifiers to existing import
|
|
616
|
+
if (defaultImport) {
|
|
617
|
+
const hasDefault = path.node.specifiers.some(
|
|
618
|
+
(spec: any) => spec.type === "ImportDefaultSpecifier"
|
|
619
|
+
);
|
|
620
|
+
if (!hasDefault) {
|
|
621
|
+
path.node.specifiers.unshift({
|
|
622
|
+
type: "ImportDefaultSpecifier",
|
|
623
|
+
local: {
|
|
624
|
+
type: "Identifier",
|
|
625
|
+
name: defaultImport,
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (named.length > 0) {
|
|
631
|
+
named.forEach((name) => {
|
|
632
|
+
const hasNamed = path.node.specifiers.some(
|
|
633
|
+
(spec: any) =>
|
|
634
|
+
spec.local.name === name && spec.type === "ImportSpecifier"
|
|
635
|
+
);
|
|
636
|
+
if (!hasNamed) {
|
|
637
|
+
path.node.specifiers.push({
|
|
638
|
+
type: "ImportSpecifier",
|
|
639
|
+
local: {
|
|
640
|
+
type: "Identifier",
|
|
641
|
+
name,
|
|
642
|
+
},
|
|
643
|
+
imported: {
|
|
644
|
+
type: "Identifier",
|
|
645
|
+
name,
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// If import doesn't exist, create new import declaration
|
|
656
|
+
if (!importExists) {
|
|
657
|
+
const specifiers: any[] = [];
|
|
658
|
+
|
|
659
|
+
if (defaultImport) {
|
|
660
|
+
specifiers.push({
|
|
661
|
+
type: "ImportDefaultSpecifier",
|
|
662
|
+
local: {
|
|
663
|
+
type: "Identifier",
|
|
664
|
+
name: defaultImport,
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (named.length > 0) {
|
|
670
|
+
named.forEach((name) => {
|
|
671
|
+
specifiers.push({
|
|
672
|
+
type: "ImportSpecifier",
|
|
673
|
+
local: {
|
|
674
|
+
type: "Identifier",
|
|
675
|
+
name,
|
|
676
|
+
},
|
|
677
|
+
imported: {
|
|
678
|
+
type: "Identifier",
|
|
679
|
+
name,
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
ast.program.body.unshift({
|
|
686
|
+
type: "ImportDeclaration",
|
|
687
|
+
specifiers,
|
|
688
|
+
source: {
|
|
689
|
+
type: "StringLiteral",
|
|
690
|
+
value: from,
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
this.astToCode(ast);
|
|
696
|
+
return this;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Add import
|
|
702
|
+
* @param importStatements
|
|
703
|
+
* @returns
|
|
704
|
+
*/
|
|
705
|
+
addImportStatements(importStatements: Array<string>) {
|
|
706
|
+
const ast = this.ast();
|
|
707
|
+
importStatements.forEach((importStatement) => {
|
|
708
|
+
const importAst = parse(importStatement, {
|
|
709
|
+
sourceType: "module",
|
|
710
|
+
plugins: ["jsx", "typescript"],
|
|
711
|
+
});
|
|
712
|
+
ast.program.body.push(...importAst.program.body);
|
|
713
|
+
});
|
|
714
|
+
this.astToCode(ast);
|
|
715
|
+
return this;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* List all imports
|
|
721
|
+
*/
|
|
722
|
+
listImports(): Array<ImportStatement> {
|
|
723
|
+
const imports: Array<ImportStatement> = [];
|
|
724
|
+
Babel.transform(this.code, {
|
|
725
|
+
presets: this.presets,
|
|
726
|
+
filename: "file.tsx", // Ensure it's treated as TSX file
|
|
727
|
+
plugins: [
|
|
728
|
+
() => ({
|
|
729
|
+
visitor: {
|
|
730
|
+
ImportDeclaration(path: any) {
|
|
731
|
+
const defaultImport = path.node.specifiers.find(
|
|
732
|
+
(specifier: any) => specifier.type === "ImportDefaultSpecifier"
|
|
733
|
+
);
|
|
734
|
+
const namedImports = path.node.specifiers.filter(
|
|
735
|
+
(specifier: any) => specifier.type === "ImportSpecifier"
|
|
736
|
+
);
|
|
737
|
+
imports.push({
|
|
738
|
+
default: defaultImport?.local.name,
|
|
739
|
+
named: namedImports.map(
|
|
740
|
+
(specifier: any) => specifier.local.name
|
|
741
|
+
),
|
|
742
|
+
from: path.node.source.value,
|
|
743
|
+
code: path.toString(),
|
|
744
|
+
});
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
}),
|
|
748
|
+
],
|
|
749
|
+
});
|
|
750
|
+
return imports;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Now only support @/.. import path
|
|
755
|
+
* @param importPath
|
|
756
|
+
*/
|
|
757
|
+
getFileContent(
|
|
758
|
+
importPath: string,
|
|
759
|
+
files: IFile[]
|
|
760
|
+
// currentIndexPath: string
|
|
761
|
+
) {
|
|
762
|
+
if (importPath.startsWith("@/")) {
|
|
763
|
+
const importFile = files.find((file) =>
|
|
764
|
+
file.path.includes(importPath.replace("@/", "") + ".")
|
|
765
|
+
);
|
|
766
|
+
if (importFile) {
|
|
767
|
+
return importFile.content;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return "";
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* List all ids where the JSX element has attribute ID_ATTRIBUTE with value
|
|
775
|
+
*/
|
|
776
|
+
listAllIds = (files: IFile[]) => {
|
|
777
|
+
const ids: string[] = [];
|
|
778
|
+
Babel.transform(this.code, {
|
|
779
|
+
presets: this.presets,
|
|
780
|
+
filename: "file.tsx", // Ensure it's treated as TSX file
|
|
781
|
+
plugins: [
|
|
782
|
+
() => ({
|
|
783
|
+
visitor: {
|
|
784
|
+
JSXElement(path: any) {
|
|
785
|
+
const openingElement = path.node.openingElement;
|
|
786
|
+
const openingAttribute = openingElement.attributes.find(
|
|
787
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
788
|
+
);
|
|
789
|
+
if (openingAttribute?.value?.expression?.value) {
|
|
790
|
+
ids.push(openingAttribute?.value?.expression?.value);
|
|
791
|
+
}
|
|
792
|
+
},
|
|
793
|
+
},
|
|
794
|
+
}),
|
|
795
|
+
],
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// list all imports
|
|
799
|
+
const imports = this.listImports();
|
|
800
|
+
const importFiles = map(imports, (item) => {
|
|
801
|
+
return this.getFileContent(item.from, files);
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
// list all ids
|
|
805
|
+
map(importFiles, (file) => {
|
|
806
|
+
const codeMode = new CodeMod(file);
|
|
807
|
+
const idsInFile = codeMode.listAllIds(files);
|
|
808
|
+
ids.push(...idsInFile);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
return ids;
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Get the props of a JSX element
|
|
816
|
+
* @param id
|
|
817
|
+
* @returns
|
|
818
|
+
*/
|
|
819
|
+
getItemProps(id: string) {
|
|
820
|
+
const attributes = this.getAttributes(id);
|
|
821
|
+
const className = this.getClassName(id);
|
|
822
|
+
return {
|
|
823
|
+
id,
|
|
824
|
+
className,
|
|
825
|
+
...attributes,
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Update the props of a JSX element
|
|
831
|
+
* Update keeps the existing props and only updates the new props
|
|
832
|
+
* @param id
|
|
833
|
+
* @param props
|
|
834
|
+
* @returns
|
|
835
|
+
*/
|
|
836
|
+
updateItemProps(id: string, props: any) {
|
|
837
|
+
const { ...attributes } = props;
|
|
838
|
+
this.code =
|
|
839
|
+
Babel.transform(this.code, {
|
|
840
|
+
presets: this.presets,
|
|
841
|
+
filename: "file.tsx", // Ensure it's treated as TSX file
|
|
842
|
+
plugins: [
|
|
843
|
+
() => ({
|
|
844
|
+
visitor: {
|
|
845
|
+
JSXElement(path: any) {
|
|
846
|
+
const openingElement = path.node.openingElement;
|
|
847
|
+
const openingAttribute = openingElement.attributes.find(
|
|
848
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
849
|
+
);
|
|
850
|
+
if (
|
|
851
|
+
openingAttribute?.value?.expression?.value === id ||
|
|
852
|
+
openingAttribute?.value?.value === id
|
|
853
|
+
) {
|
|
854
|
+
// remove already existing attributes
|
|
855
|
+
for (const key in attributes) {
|
|
856
|
+
openingElement.attributes =
|
|
857
|
+
openingElement.attributes.filter(
|
|
858
|
+
(attr: any) => attr?.name?.name !== key
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
for (const key in attributes) {
|
|
862
|
+
openingElement.attributes.push({
|
|
863
|
+
type: "JSXAttribute",
|
|
864
|
+
name: {
|
|
865
|
+
type: "JSXIdentifier",
|
|
866
|
+
name: key,
|
|
867
|
+
},
|
|
868
|
+
value: {
|
|
869
|
+
type: "StringLiteral",
|
|
870
|
+
value: attributes[key],
|
|
871
|
+
},
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
},
|
|
877
|
+
}),
|
|
878
|
+
],
|
|
879
|
+
})?.code || "";
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
updateItemText(id: string, html: string) {
|
|
883
|
+
this.code =
|
|
884
|
+
Babel.transform(this.code, {
|
|
885
|
+
presets: this.presets,
|
|
886
|
+
filename: "file.tsx", // Ensure it's treated as TSX file
|
|
887
|
+
plugins: [
|
|
888
|
+
() => ({
|
|
889
|
+
visitor: {
|
|
890
|
+
JSXElement(path: any) {
|
|
891
|
+
const openingElement = path.node.openingElement;
|
|
892
|
+
const openingAttribute = openingElement.attributes.find(
|
|
893
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
894
|
+
);
|
|
895
|
+
if (
|
|
896
|
+
openingAttribute?.value?.expression?.value === id ||
|
|
897
|
+
openingAttribute?.value?.value === id
|
|
898
|
+
) {
|
|
899
|
+
path.node.children = [
|
|
900
|
+
{
|
|
901
|
+
type: "JSXText",
|
|
902
|
+
value: html,
|
|
903
|
+
},
|
|
904
|
+
];
|
|
905
|
+
}
|
|
906
|
+
},
|
|
907
|
+
},
|
|
908
|
+
}),
|
|
909
|
+
],
|
|
910
|
+
})?.code || "";
|
|
911
|
+
|
|
912
|
+
console.log("UPDATE_ITEM_TEXT", id, html, this.code);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Check if code has logic like
|
|
917
|
+
* Identifier, BlockStatement,..
|
|
918
|
+
* @returns
|
|
919
|
+
*/
|
|
920
|
+
checkHasLogicCode() {
|
|
921
|
+
let hasLogicCode = false;
|
|
922
|
+
Babel.transform(this.code, {
|
|
923
|
+
presets: this.presets,
|
|
924
|
+
filename: "file.tsx", // Ensure it's treated as TSX file
|
|
925
|
+
plugins: [
|
|
926
|
+
() => ({
|
|
927
|
+
visitor: {
|
|
928
|
+
Identifier() {
|
|
929
|
+
hasLogicCode = true;
|
|
930
|
+
},
|
|
931
|
+
BlockStatement() {
|
|
932
|
+
hasLogicCode = true;
|
|
933
|
+
},
|
|
934
|
+
},
|
|
935
|
+
}),
|
|
936
|
+
],
|
|
937
|
+
});
|
|
938
|
+
return hasLogicCode;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
checkChildrenIsContentEditable(id: string) {
|
|
942
|
+
let isContentEditable = false;
|
|
943
|
+
// isContentEditable it means code of children has only text
|
|
944
|
+
// p, h1, h2, h3, h4, h5, h6, span, div, a, button, label, li, td, th, textarea
|
|
945
|
+
|
|
946
|
+
const ALOWED_TAGS = [
|
|
947
|
+
"p",
|
|
948
|
+
"h1",
|
|
949
|
+
"h2",
|
|
950
|
+
"h3",
|
|
951
|
+
"h4",
|
|
952
|
+
"h5",
|
|
953
|
+
"h6",
|
|
954
|
+
"span",
|
|
955
|
+
"div",
|
|
956
|
+
"a",
|
|
957
|
+
"button",
|
|
958
|
+
"label",
|
|
959
|
+
"li",
|
|
960
|
+
"td",
|
|
961
|
+
"th",
|
|
962
|
+
"textarea",
|
|
963
|
+
];
|
|
964
|
+
|
|
965
|
+
Babel.transform(this.code, {
|
|
966
|
+
presets: this.presets,
|
|
967
|
+
filename: "file.tsx", // Ensure it's treated as TSX file
|
|
968
|
+
plugins: [
|
|
969
|
+
() => ({
|
|
970
|
+
visitor: {
|
|
971
|
+
JSXElement(path: any) {
|
|
972
|
+
const openingElement = path.node.openingElement;
|
|
973
|
+
const openingAttribute = openingElement.attributes.find(
|
|
974
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
975
|
+
);
|
|
976
|
+
if (
|
|
977
|
+
openingAttribute?.value?.expression?.value === id ||
|
|
978
|
+
openingAttribute?.value?.value === id
|
|
979
|
+
) {
|
|
980
|
+
const tagName = openingElement.name.name;
|
|
981
|
+
if (includes(ALOWED_TAGS, tagName)) {
|
|
982
|
+
isContentEditable = true;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
},
|
|
987
|
+
}),
|
|
988
|
+
],
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
// And it must have not include any logic code
|
|
992
|
+
if (isContentEditable) {
|
|
993
|
+
const childrenCode = this.getChildrenContent(id);
|
|
994
|
+
const hasLogicCode = new CodeMod(childrenCode).checkHasLogicCode();
|
|
995
|
+
if (hasLogicCode) {
|
|
996
|
+
isContentEditable = false;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return isContentEditable;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Get all code of children of a JSX element
|
|
1005
|
+
* @param elementId
|
|
1006
|
+
*/
|
|
1007
|
+
getChildrenContent(elementId: string) {
|
|
1008
|
+
let code = "";
|
|
1009
|
+
Babel.transform(this.code, {
|
|
1010
|
+
presets: this.presets,
|
|
1011
|
+
filename: "file.tsx", // Ensure it's treated as TSX file
|
|
1012
|
+
plugins: [
|
|
1013
|
+
() => ({
|
|
1014
|
+
visitor: {
|
|
1015
|
+
JSXElement(path: any) {
|
|
1016
|
+
const openingElement = path.node.openingElement;
|
|
1017
|
+
const openingAttribute = openingElement.attributes.find(
|
|
1018
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
1019
|
+
);
|
|
1020
|
+
if (
|
|
1021
|
+
openingAttribute?.value?.expression?.value === elementId ||
|
|
1022
|
+
openingAttribute?.value?.value === elementId
|
|
1023
|
+
) {
|
|
1024
|
+
code = path.toString();
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
},
|
|
1028
|
+
}),
|
|
1029
|
+
],
|
|
1030
|
+
});
|
|
1031
|
+
return code;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Remove a JSX element from the code
|
|
1036
|
+
*/
|
|
1037
|
+
removeItem(id: string) {
|
|
1038
|
+
this.code =
|
|
1039
|
+
Babel.transform(this.code, {
|
|
1040
|
+
presets: this.presets,
|
|
1041
|
+
filename: "file.tsx", // Ensure it's treated as TSX file
|
|
1042
|
+
plugins: [
|
|
1043
|
+
() => ({
|
|
1044
|
+
visitor: {
|
|
1045
|
+
JSXElement(path: any) {
|
|
1046
|
+
const openingElement = path.node.openingElement;
|
|
1047
|
+
const openingAttribute = openingElement.attributes.find(
|
|
1048
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
1049
|
+
);
|
|
1050
|
+
if (
|
|
1051
|
+
openingAttribute?.value?.expression?.value === id ||
|
|
1052
|
+
openingAttribute?.value?.value === id
|
|
1053
|
+
) {
|
|
1054
|
+
path.remove();
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
},
|
|
1058
|
+
}),
|
|
1059
|
+
],
|
|
1060
|
+
})?.code || "";
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Remove all imports
|
|
1065
|
+
* @param oldPath
|
|
1066
|
+
* @param newPath
|
|
1067
|
+
*/
|
|
1068
|
+
renameImportSource(oldPath: string, newPath: string) {
|
|
1069
|
+
this.code =
|
|
1070
|
+
Babel.transform(this.code, {
|
|
1071
|
+
presets: this.presets,
|
|
1072
|
+
filename: "file.tsx", // Ensure it's treated as TSX file
|
|
1073
|
+
plugins: [
|
|
1074
|
+
() => ({
|
|
1075
|
+
visitor: {
|
|
1076
|
+
ImportDeclaration(path: any) {
|
|
1077
|
+
if (
|
|
1078
|
+
removeExtension(path.node.source.value) ===
|
|
1079
|
+
removeExtension(oldPath)
|
|
1080
|
+
) {
|
|
1081
|
+
path.node.source.value = removeExtension(newPath);
|
|
1082
|
+
}
|
|
1083
|
+
},
|
|
1084
|
+
},
|
|
1085
|
+
}),
|
|
1086
|
+
],
|
|
1087
|
+
})?.code || "";
|
|
1088
|
+
console.log("RENAME_IMPORT_SOURCE_AFTER", this.code);
|
|
1089
|
+
return this;
|
|
1090
|
+
}
|
|
1091
|
+
renameDefaultImport(oldName: string, newName: string) {
|
|
1092
|
+
this.code =
|
|
1093
|
+
Babel.transform(this.code, {
|
|
1094
|
+
presets: this.presets,
|
|
1095
|
+
filename: "file.tsx", // Ensure it's treated as TSX file
|
|
1096
|
+
plugins: [
|
|
1097
|
+
() => ({
|
|
1098
|
+
visitor: {
|
|
1099
|
+
ImportDefaultSpecifier(path: any) {
|
|
1100
|
+
if (path.node.local.name === oldName) {
|
|
1101
|
+
path.node.local.name = newName;
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
},
|
|
1105
|
+
}),
|
|
1106
|
+
],
|
|
1107
|
+
})?.code || "";
|
|
1108
|
+
return this;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* Only Custom Component
|
|
1113
|
+
* @returns
|
|
1114
|
+
*/
|
|
1115
|
+
refactor_RenameAllComponentNames(key: string) {
|
|
1116
|
+
const firstLetterIsUpperCase = (str: string) => {
|
|
1117
|
+
return str[0] === str[0].toUpperCase();
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
1121
|
+
this.code =
|
|
1122
|
+
Babel.transform(this.code, {
|
|
1123
|
+
presets: this.presets,
|
|
1124
|
+
filename: "file.tsx",
|
|
1125
|
+
plugins: [
|
|
1126
|
+
() => ({
|
|
1127
|
+
visitor: {
|
|
1128
|
+
JSXElement(path: any) {
|
|
1129
|
+
const openingElement = path.node.openingElement;
|
|
1130
|
+
const closingElement = path.node.closingElement;
|
|
1131
|
+
const componentName = openingElement.name.name;
|
|
1132
|
+
console.log("COMPONENT_NAME___", componentName);
|
|
1133
|
+
if (openingElement && firstLetterIsUpperCase(componentName)) {
|
|
1134
|
+
console.log(
|
|
1135
|
+
"FOUND_COMPONENT",
|
|
1136
|
+
openingElement,
|
|
1137
|
+
closingElement
|
|
1138
|
+
);
|
|
1139
|
+
openingElement.name.name = `${componentName}${key}`;
|
|
1140
|
+
if (closingElement)
|
|
1141
|
+
closingElement.name.name = `${componentName}${key}`;
|
|
1142
|
+
}
|
|
1143
|
+
},
|
|
1144
|
+
ImportDeclaration(path: any) {
|
|
1145
|
+
// Refactor all imports
|
|
1146
|
+
// Example: import A from "./A" => import NewA from "./A"
|
|
1147
|
+
// import { A } from "./A" => import { A as NewA } from "./A"
|
|
1148
|
+
console.log("IMPORT_DECLARATION", path.node);
|
|
1149
|
+
const specifiers = path.node.specifiers;
|
|
1150
|
+
|
|
1151
|
+
if (specifiers && specifiers.length > 0) {
|
|
1152
|
+
for (let i = 0; i < specifiers.length; i++) {
|
|
1153
|
+
const specifier = specifiers[i];
|
|
1154
|
+
if (specifier.type === "ImportDefaultSpecifier") {
|
|
1155
|
+
const localName = specifier.local.name;
|
|
1156
|
+
if (firstLetterIsUpperCase(localName)) {
|
|
1157
|
+
specifier.local.name = `${localName}${key}`;
|
|
1158
|
+
}
|
|
1159
|
+
} else if (specifier.type === "ImportSpecifier") {
|
|
1160
|
+
const localName = specifier.local.name;
|
|
1161
|
+
if (firstLetterIsUpperCase(localName)) {
|
|
1162
|
+
specifier.local.name = `${localName}${key}`;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
},
|
|
1168
|
+
},
|
|
1169
|
+
}),
|
|
1170
|
+
],
|
|
1171
|
+
})?.code || "";
|
|
1172
|
+
return this;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/**
|
|
1176
|
+
* Refactor all component names
|
|
1177
|
+
* @param attribute
|
|
1178
|
+
*/
|
|
1179
|
+
refreshIDAttributes() {
|
|
1180
|
+
this.code =
|
|
1181
|
+
Babel.transform(this.code, {
|
|
1182
|
+
presets: this.presets,
|
|
1183
|
+
filename: "file.tsx",
|
|
1184
|
+
plugins: [
|
|
1185
|
+
() => ({
|
|
1186
|
+
visitor: {
|
|
1187
|
+
JSXElement(path: any) {
|
|
1188
|
+
const openingElement = path.node.openingElement;
|
|
1189
|
+
if (openingElement) {
|
|
1190
|
+
// update ID_ATTRIBUTE to new value
|
|
1191
|
+
const idAttribute = openingElement.attributes.find(
|
|
1192
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
1193
|
+
);
|
|
1194
|
+
if (idAttribute) {
|
|
1195
|
+
idAttribute.value = {
|
|
1196
|
+
type: "JSXExpressionContainer",
|
|
1197
|
+
expression: {
|
|
1198
|
+
type: "StringLiteral",
|
|
1199
|
+
value: uuid.v4(),
|
|
1200
|
+
},
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
},
|
|
1205
|
+
},
|
|
1206
|
+
}),
|
|
1207
|
+
],
|
|
1208
|
+
})?.code || "";
|
|
1209
|
+
return this;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Get the tag name of a JSX element
|
|
1214
|
+
* @param id
|
|
1215
|
+
* @returns
|
|
1216
|
+
*/
|
|
1217
|
+
getTagName(id: string) {
|
|
1218
|
+
let tagName = "";
|
|
1219
|
+
Babel.transform(this.code, {
|
|
1220
|
+
presets: this.presets,
|
|
1221
|
+
filename: "file.tsx",
|
|
1222
|
+
plugins: [
|
|
1223
|
+
() => ({
|
|
1224
|
+
visitor: {
|
|
1225
|
+
JSXElement(path: any) {
|
|
1226
|
+
const openingElement = path.node.openingElement;
|
|
1227
|
+
const openingAttribute = openingElement.attributes.find(
|
|
1228
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
1229
|
+
);
|
|
1230
|
+
if (
|
|
1231
|
+
openingAttribute?.value?.expression?.value === id ||
|
|
1232
|
+
openingAttribute?.value?.value === id
|
|
1233
|
+
) {
|
|
1234
|
+
tagName = openingElement.name.name;
|
|
1235
|
+
}
|
|
1236
|
+
},
|
|
1237
|
+
},
|
|
1238
|
+
}),
|
|
1239
|
+
],
|
|
1240
|
+
});
|
|
1241
|
+
return tagName;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
getRelatedFiles(elementId: string, files: IFile[]) {
|
|
1245
|
+
const allRelatedFiles: IFile[] = [];
|
|
1246
|
+
// find file that contains the elementId
|
|
1247
|
+
const currentFile = files.find((file) => includes(file.content, elementId));
|
|
1248
|
+
const listRelatedFiles = (
|
|
1249
|
+
elementId: string | undefined,
|
|
1250
|
+
file?: IFile
|
|
1251
|
+
): Array<string> => {
|
|
1252
|
+
if (!file) {
|
|
1253
|
+
return [];
|
|
1254
|
+
}
|
|
1255
|
+
const codeMode = new CodeMod(file.content);
|
|
1256
|
+
const allImports = codeMode.listImports();
|
|
1257
|
+
// list all Component between ID_ATTRIBUTE={elementId}
|
|
1258
|
+
const codeBlock = elementId
|
|
1259
|
+
? codeMode.extractComponentCode(elementId)
|
|
1260
|
+
: file.content;
|
|
1261
|
+
// List all components in the code block
|
|
1262
|
+
// const codeMod = new CodeMod(codeBlock);
|
|
1263
|
+
const importString: Array<string> = [];
|
|
1264
|
+
|
|
1265
|
+
allImports.forEach((item) => {
|
|
1266
|
+
if (elementId === undefined) {
|
|
1267
|
+
// import all
|
|
1268
|
+
importString.push(item.from);
|
|
1269
|
+
}
|
|
1270
|
+
// if import is in codeBlock
|
|
1271
|
+
if (
|
|
1272
|
+
includes(codeBlock, `<${item.default}`) ||
|
|
1273
|
+
includes(codeBlock, `<${item.named[0]}`)
|
|
1274
|
+
) {
|
|
1275
|
+
importString.push(item.from);
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
if (importString.length > 0) {
|
|
1279
|
+
importString.forEach((importPath) => {
|
|
1280
|
+
const file = files.find((file) => {
|
|
1281
|
+
return "@/" + removeExtension(file.path) === importPath;
|
|
1282
|
+
});
|
|
1283
|
+
if (file) {
|
|
1284
|
+
importString.push(...listRelatedFiles(undefined, file));
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
return importString;
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
const allRelatedPaths = listRelatedFiles(elementId, currentFile);
|
|
1292
|
+
for (const path of allRelatedPaths) {
|
|
1293
|
+
const file = files.find((file) => {
|
|
1294
|
+
return "@/" + removeExtension(file.path) === path;
|
|
1295
|
+
});
|
|
1296
|
+
if (file) {
|
|
1297
|
+
allRelatedFiles.push(file);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
return {
|
|
1302
|
+
currentFile,
|
|
1303
|
+
files: allRelatedFiles,
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Extract the code of a JSX element
|
|
1309
|
+
* @param elementId
|
|
1310
|
+
* @returns
|
|
1311
|
+
*/
|
|
1312
|
+
extractComponentCode(elementId: string) {
|
|
1313
|
+
let code = "";
|
|
1314
|
+
Babel.transform(this.code, {
|
|
1315
|
+
presets: this.presets,
|
|
1316
|
+
filename: "file.tsx",
|
|
1317
|
+
plugins: [
|
|
1318
|
+
() => ({
|
|
1319
|
+
visitor: {
|
|
1320
|
+
JSXElement(path: any) {
|
|
1321
|
+
const openingElement = path.node.openingElement;
|
|
1322
|
+
const openingAttribute = openingElement.attributes.find(
|
|
1323
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
1324
|
+
);
|
|
1325
|
+
if (
|
|
1326
|
+
openingAttribute?.value?.expression?.value === elementId ||
|
|
1327
|
+
openingAttribute?.value?.value === elementId
|
|
1328
|
+
) {
|
|
1329
|
+
code = path.toString();
|
|
1330
|
+
}
|
|
1331
|
+
},
|
|
1332
|
+
},
|
|
1333
|
+
}),
|
|
1334
|
+
],
|
|
1335
|
+
});
|
|
1336
|
+
return code;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* List all ids where the JSX element has attribute ID_ATTRIBUTE with value
|
|
1341
|
+
* @returns
|
|
1342
|
+
*/
|
|
1343
|
+
listIdAttributes() {
|
|
1344
|
+
const ids: string[] = [];
|
|
1345
|
+
Babel.transform(this.code, {
|
|
1346
|
+
presets: this.presets,
|
|
1347
|
+
filename: "file.tsx",
|
|
1348
|
+
plugins: [
|
|
1349
|
+
() => ({
|
|
1350
|
+
visitor: {
|
|
1351
|
+
JSXElement(path: any) {
|
|
1352
|
+
const openingElement = path.node.openingElement;
|
|
1353
|
+
const openingAttribute = openingElement.attributes.find(
|
|
1354
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
1355
|
+
);
|
|
1356
|
+
if (openingAttribute?.value?.expression?.value) {
|
|
1357
|
+
ids.push(openingAttribute?.value?.expression?.value);
|
|
1358
|
+
}
|
|
1359
|
+
},
|
|
1360
|
+
},
|
|
1361
|
+
}),
|
|
1362
|
+
],
|
|
1363
|
+
});
|
|
1364
|
+
return ids;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
replaceLiteral(from: string, to: string) {
|
|
1368
|
+
this.code =
|
|
1369
|
+
Babel.transform(this.code, {
|
|
1370
|
+
presets: this.presets,
|
|
1371
|
+
filename: "file.tsx",
|
|
1372
|
+
plugins: [
|
|
1373
|
+
() => ({
|
|
1374
|
+
visitor: {
|
|
1375
|
+
StringLiteral(path: any) {
|
|
1376
|
+
// update the value of the string literal
|
|
1377
|
+
if (path.node.value.includes(from)) {
|
|
1378
|
+
console.log(
|
|
1379
|
+
"===> REPLACE_LITERAL",
|
|
1380
|
+
path.node.value,
|
|
1381
|
+
path.node.value.replace(from, to)
|
|
1382
|
+
);
|
|
1383
|
+
path.node.value = path.node.value.replace(from, to);
|
|
1384
|
+
}
|
|
1385
|
+
},
|
|
1386
|
+
},
|
|
1387
|
+
}),
|
|
1388
|
+
],
|
|
1389
|
+
})?.code || "";
|
|
1390
|
+
console.log("REPLACE_LITERAL", from, to, this.code);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
/**
|
|
1394
|
+
* List all identifiers in the code
|
|
1395
|
+
* @returns
|
|
1396
|
+
*/
|
|
1397
|
+
listAllIndentifiers() {
|
|
1398
|
+
const identifiers: string[] = [];
|
|
1399
|
+
Babel.transform(this.code, {
|
|
1400
|
+
presets: this.presets,
|
|
1401
|
+
filename: "file.tsx",
|
|
1402
|
+
plugins: [
|
|
1403
|
+
() => ({
|
|
1404
|
+
visitor: {
|
|
1405
|
+
Identifier(path: any) {
|
|
1406
|
+
identifiers.push(path.node.name);
|
|
1407
|
+
},
|
|
1408
|
+
},
|
|
1409
|
+
}),
|
|
1410
|
+
],
|
|
1411
|
+
});
|
|
1412
|
+
return identifiers;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* List all component names in the code
|
|
1417
|
+
* @returns
|
|
1418
|
+
*/
|
|
1419
|
+
listAllComponentNames() {
|
|
1420
|
+
const ComponentNames: string[] = [];
|
|
1421
|
+
Babel.transform(this.code, {
|
|
1422
|
+
presets: this.presets,
|
|
1423
|
+
filename: "file.tsx",
|
|
1424
|
+
plugins: [
|
|
1425
|
+
() => ({
|
|
1426
|
+
visitor: {
|
|
1427
|
+
JSXElement(path: any) {
|
|
1428
|
+
const openingElement = path.node.openingElement;
|
|
1429
|
+
const componentName = openingElement.name.name;
|
|
1430
|
+
if (
|
|
1431
|
+
componentName &&
|
|
1432
|
+
componentName[0] === componentName[0].toUpperCase()
|
|
1433
|
+
) {
|
|
1434
|
+
ComponentNames.push(componentName);
|
|
1435
|
+
}
|
|
1436
|
+
},
|
|
1437
|
+
},
|
|
1438
|
+
}),
|
|
1439
|
+
],
|
|
1440
|
+
});
|
|
1441
|
+
return ComponentNames;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
listAllComponentIds(componentName: string) {
|
|
1445
|
+
const ComponentIds: string[] = [];
|
|
1446
|
+
Babel.transform(this.code, {
|
|
1447
|
+
presets: this.presets,
|
|
1448
|
+
filename: "file.tsx",
|
|
1449
|
+
plugins: [
|
|
1450
|
+
() => ({
|
|
1451
|
+
visitor: {
|
|
1452
|
+
JSXElement(path: any) {
|
|
1453
|
+
const openingElement = path.node.openingElement;
|
|
1454
|
+
const _componentName = openingElement.name.name;
|
|
1455
|
+
if (componentName === _componentName) {
|
|
1456
|
+
const openingAttribute = openingElement.attributes.find(
|
|
1457
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
1458
|
+
);
|
|
1459
|
+
if (openingAttribute?.value?.expression?.value) {
|
|
1460
|
+
ComponentIds.push(openingAttribute?.value?.expression?.value);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
},
|
|
1464
|
+
},
|
|
1465
|
+
}),
|
|
1466
|
+
],
|
|
1467
|
+
});
|
|
1468
|
+
return ComponentIds;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
createUUID() {
|
|
1472
|
+
return uuid.v4();
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
addChild(payload: { id: string; content?: string }) {
|
|
1476
|
+
const { id } = payload;
|
|
1477
|
+
const ast = this.ast();
|
|
1478
|
+
traverse(ast, {
|
|
1479
|
+
JSXElement(path: any) {
|
|
1480
|
+
const openingElement = path.node.openingElement;
|
|
1481
|
+
const openingAttribute = openingElement.attributes.find(
|
|
1482
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
1483
|
+
);
|
|
1484
|
+
if (
|
|
1485
|
+
openingAttribute?.value?.expression?.value === id ||
|
|
1486
|
+
openingAttribute?.value?.value === id
|
|
1487
|
+
) {
|
|
1488
|
+
// convert content to JSX
|
|
1489
|
+
const content = payload.content
|
|
1490
|
+
? parse(payload.content, {
|
|
1491
|
+
sourceType: "module",
|
|
1492
|
+
plugins: ["jsx", "typescript"],
|
|
1493
|
+
}).program.body[0]
|
|
1494
|
+
: null;
|
|
1495
|
+
|
|
1496
|
+
if (content) {
|
|
1497
|
+
path.node.children.push((content as any)?.expression);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
},
|
|
1501
|
+
});
|
|
1502
|
+
this.astToCode(ast);
|
|
1503
|
+
return this;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
updateElementContent(id: string, content: string) {
|
|
1507
|
+
const ast = this.ast();
|
|
1508
|
+
traverse(ast, {
|
|
1509
|
+
JSXElement(path: any) {
|
|
1510
|
+
const openingElement = path.node.openingElement;
|
|
1511
|
+
const openingAttribute = openingElement.attributes.find(
|
|
1512
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
1513
|
+
);
|
|
1514
|
+
if (
|
|
1515
|
+
openingAttribute?.value?.expression?.value === id ||
|
|
1516
|
+
openingAttribute?.value?.value === id
|
|
1517
|
+
) {
|
|
1518
|
+
// update the children of the element
|
|
1519
|
+
path.node.children = [
|
|
1520
|
+
{
|
|
1521
|
+
type: "JSXText",
|
|
1522
|
+
value: content,
|
|
1523
|
+
},
|
|
1524
|
+
];
|
|
1525
|
+
}
|
|
1526
|
+
},
|
|
1527
|
+
});
|
|
1528
|
+
this.astToCode(ast);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
removeElement(id: string) {
|
|
1532
|
+
console.log("REMOVE_ELEMENT", id);
|
|
1533
|
+
const ast = this.ast();
|
|
1534
|
+
traverse(ast, {
|
|
1535
|
+
JSXElement(path: any) {
|
|
1536
|
+
const openingElement = path.node.openingElement;
|
|
1537
|
+
const openingAttribute = openingElement.attributes.find(
|
|
1538
|
+
(attr: any) => attr?.name?.name === ID_ATTRIBUTE
|
|
1539
|
+
);
|
|
1540
|
+
if (
|
|
1541
|
+
openingAttribute?.value?.expression?.value === id ||
|
|
1542
|
+
openingAttribute?.value?.value === id
|
|
1543
|
+
) {
|
|
1544
|
+
path.remove();
|
|
1545
|
+
}
|
|
1546
|
+
},
|
|
1547
|
+
});
|
|
1548
|
+
this.astToCode(ast);
|
|
1549
|
+
return this;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
/**
|
|
1553
|
+
* Append ui code into return of default export function
|
|
1554
|
+
* @param code
|
|
1555
|
+
*/
|
|
1556
|
+
appendDefaultReturn(code: string) {
|
|
1557
|
+
const ast = this.ast();
|
|
1558
|
+
let isHandled = false;
|
|
1559
|
+
traverse(ast, {
|
|
1560
|
+
ExportDefaultDeclaration(path: any) {
|
|
1561
|
+
if (isHandled) {
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
isHandled = true;
|
|
1565
|
+
console.log("EXPORT_DEFAULT_DECLARATION", path.node);
|
|
1566
|
+
if (path.node.declaration.type === "FunctionDeclaration") {
|
|
1567
|
+
// Find the return statement
|
|
1568
|
+
const body = path.node.declaration.body.body;
|
|
1569
|
+
const returnStatement = body.find(
|
|
1570
|
+
(stmt: any) => stmt.type === "ReturnStatement"
|
|
1571
|
+
);
|
|
1572
|
+
if (returnStatement) {
|
|
1573
|
+
// Append the code to the return statement
|
|
1574
|
+
const newCode = parse(code, {
|
|
1575
|
+
sourceType: "module",
|
|
1576
|
+
plugins: ["jsx", "typescript"],
|
|
1577
|
+
}).program.body[0];
|
|
1578
|
+
const expression = (newCode as any).expression || newCode;
|
|
1579
|
+
returnStatement.argument.children.push(expression);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
},
|
|
1583
|
+
});
|
|
1584
|
+
this.astToCode(ast);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
export default CodeMod;
|