ts-morph 0.1.1
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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +33 -0
- data/LICENSE.txt +21 -0
- data/PUBLISHING.md +184 -0
- data/README.md +287 -0
- data/Rakefile +10 -0
- data/SUMMARY.md +114 -0
- data/examples/basic_transformation.rb +63 -0
- data/examples/rails_integration.rb +174 -0
- data/ext/ts_morph/extconf.rb +649 -0
- data/lib/ts/morph/error.rb +17 -0
- data/lib/ts/morph/node_service.rb +351 -0
- data/lib/ts/morph/transformations.rb +98 -0
- data/lib/ts/morph/transformer.rb +82 -0
- data/lib/ts/morph/version.rb +7 -0
- data/lib/ts/morph.rb +50 -0
- data/sig/ts/morph.rbs +6 -0
- metadata +150 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'mkmf'
|
|
6
|
+
require 'json'
|
|
7
|
+
|
|
8
|
+
# Define Node service code generator first
|
|
9
|
+
def node_service_code
|
|
10
|
+
<<~JAVASCRIPT
|
|
11
|
+
const { Project, SyntaxKind } = require("ts-morph");
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
|
|
14
|
+
class TsMorphService {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.project = new Project({
|
|
17
|
+
useInMemoryFileSystem: true,
|
|
18
|
+
compilerOptions: {
|
|
19
|
+
jsx: "react",
|
|
20
|
+
target: "ES2020",
|
|
21
|
+
module: "ESNext"
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
transform(code, transformations) {
|
|
27
|
+
const sourceFile = this.project.createSourceFile("temp.tsx", code, { overwrite: true });
|
|
28
|
+
|
|
29
|
+
transformations.forEach(transform => {
|
|
30
|
+
switch (transform.type) {
|
|
31
|
+
case 'addDefaultProps':
|
|
32
|
+
this.addDefaultProps(sourceFile, transform.component, transform.props);
|
|
33
|
+
break;
|
|
34
|
+
case 'wrapComponents':
|
|
35
|
+
this.wrapComponents(sourceFile, transform.target, transform.wrapper);
|
|
36
|
+
break;
|
|
37
|
+
case 'renameComponents':
|
|
38
|
+
this.renameComponents(sourceFile, transform.oldName, transform.newName);
|
|
39
|
+
break;
|
|
40
|
+
case 'addImports':
|
|
41
|
+
this.addImports(sourceFile, transform.imports);
|
|
42
|
+
break;
|
|
43
|
+
case 'injectStyles':
|
|
44
|
+
this.injectStyles(sourceFile, transform.styles);
|
|
45
|
+
break;
|
|
46
|
+
case 'removeComponent':
|
|
47
|
+
this.removeComponent(sourceFile, transform.component);
|
|
48
|
+
break;
|
|
49
|
+
case 'extractComponent':
|
|
50
|
+
this.extractComponent(sourceFile, transform.component);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return sourceFile.getFullText();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
addDefaultProps(sourceFile, componentName, defaultProps) {
|
|
59
|
+
// Add props to JSX elements
|
|
60
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
|
|
61
|
+
const openingElement = element.getOpeningElement();
|
|
62
|
+
if (openingElement.getTagNameNode().getText() === componentName) {
|
|
63
|
+
Object.entries(defaultProps).forEach(([propName, propValue]) => {
|
|
64
|
+
const existingProp = openingElement.getAttribute(propName);
|
|
65
|
+
if (!existingProp) {
|
|
66
|
+
openingElement.addAttribute({
|
|
67
|
+
name: propName,
|
|
68
|
+
initializer: typeof propValue === 'string' ? `"${propValue}"` : `{${JSON.stringify(propValue)}}`
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Add props to self-closing elements
|
|
76
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
|
|
77
|
+
if (element.getTagNameNode().getText() === componentName) {
|
|
78
|
+
Object.entries(defaultProps).forEach(([propName, propValue]) => {
|
|
79
|
+
const existingProp = element.getAttribute(propName);
|
|
80
|
+
if (!existingProp) {
|
|
81
|
+
element.addAttribute({
|
|
82
|
+
name: propName,
|
|
83
|
+
initializer: typeof propValue === 'string' ? `"${propValue}"` : `{${JSON.stringify(propValue)}}`
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
wrapComponents(sourceFile, targetComponent, wrapperComponent) {
|
|
92
|
+
const jsxElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement)
|
|
93
|
+
.filter(el => el.getOpeningElement().getTagNameNode().getText() === targetComponent);
|
|
94
|
+
|
|
95
|
+
jsxElements.forEach(element => {
|
|
96
|
+
const elementText = element.getFullText();
|
|
97
|
+
const wrappedCode = `<${wrapperComponent}>${elementText}</${wrapperComponent}>`;
|
|
98
|
+
element.replaceWithText(wrappedCode);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
renameComponents(sourceFile, oldName, newName) {
|
|
103
|
+
// Rename JSX elements
|
|
104
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
|
|
105
|
+
const openingElement = element.getOpeningElement();
|
|
106
|
+
const closingElement = element.getClosingElement();
|
|
107
|
+
|
|
108
|
+
if (openingElement.getTagNameNode().getText() === oldName) {
|
|
109
|
+
openingElement.getTagNameNode().replaceWithText(newName);
|
|
110
|
+
if (closingElement) {
|
|
111
|
+
closingElement.getTagNameNode().replaceWithText(newName);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Rename self-closing elements
|
|
117
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
|
|
118
|
+
if (element.getTagNameNode().getText() === oldName) {
|
|
119
|
+
element.getTagNameNode().replaceWithText(newName);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Rename imports
|
|
124
|
+
sourceFile.getImportDeclarations().forEach(importDecl => {
|
|
125
|
+
importDecl.getNamedImports().forEach(namedImport => {
|
|
126
|
+
if (namedImport.getName() === oldName) {
|
|
127
|
+
namedImport.renameAlias(newName);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
addImports(sourceFile, imports) {
|
|
134
|
+
imports.forEach(imp => {
|
|
135
|
+
const existingImport = sourceFile.getImportDeclaration(
|
|
136
|
+
decl => decl.getModuleSpecifierValue() === imp.from
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (existingImport) {
|
|
140
|
+
imp.named?.forEach(name => {
|
|
141
|
+
existingImport.addNamedImport(name);
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
sourceFile.addImportDeclaration({
|
|
145
|
+
moduleSpecifier: imp.from,
|
|
146
|
+
namedImports: imp.named,
|
|
147
|
+
defaultImport: imp.default
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
injectStyles(sourceFile, styles) {
|
|
154
|
+
Object.entries(styles).forEach(([componentName, styleRules]) => {
|
|
155
|
+
// Handle JSX elements
|
|
156
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
|
|
157
|
+
const openingElement = element.getOpeningElement();
|
|
158
|
+
if (openingElement.getTagNameNode().getText() === componentName) {
|
|
159
|
+
const styleAttr = openingElement.getAttribute("style");
|
|
160
|
+
if (styleAttr) {
|
|
161
|
+
const existingValue = styleAttr.getInitializer()?.getText() || "{}";
|
|
162
|
+
const mergedStyles = `{...${existingValue.slice(1, -1)}, ...${JSON.stringify(styleRules)}}`;
|
|
163
|
+
styleAttr.setInitializer(mergedStyles);
|
|
164
|
+
} else {
|
|
165
|
+
openingElement.addAttribute({
|
|
166
|
+
name: "style",
|
|
167
|
+
initializer: `{${JSON.stringify(styleRules)}}`
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Handle self-closing elements
|
|
174
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
|
|
175
|
+
if (element.getTagNameNode().getText() === componentName) {
|
|
176
|
+
const styleAttr = element.getAttribute("style");
|
|
177
|
+
if (styleAttr) {
|
|
178
|
+
const existingValue = styleAttr.getInitializer()?.getText() || "{}";
|
|
179
|
+
const mergedStyles = `{...${existingValue.slice(1, -1)}, ...${JSON.stringify(styleRules)}}`;
|
|
180
|
+
styleAttr.setInitializer(mergedStyles);
|
|
181
|
+
} else {
|
|
182
|
+
element.addAttribute({
|
|
183
|
+
name: "style",
|
|
184
|
+
initializer: `{${JSON.stringify(styleRules)}}`
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
removeComponent(sourceFile, componentName) {
|
|
193
|
+
// Remove JSX elements
|
|
194
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
|
|
195
|
+
if (element.getOpeningElement().getTagNameNode().getText() === componentName) {
|
|
196
|
+
element.remove();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Remove self-closing elements
|
|
201
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
|
|
202
|
+
if (element.getTagNameNode().getText() === componentName) {
|
|
203
|
+
element.remove();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
extractComponent(sourceFile, componentName) {
|
|
209
|
+
const components = [];
|
|
210
|
+
|
|
211
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
|
|
212
|
+
if (element.getOpeningElement().getTagNameNode().getText() === componentName) {
|
|
213
|
+
components.push(element.getFullText());
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
|
|
218
|
+
if (element.getTagNameNode().getText() === componentName) {
|
|
219
|
+
components.push(element.getFullText());
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return components;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
parse(code) {
|
|
227
|
+
const sourceFile = this.project.createSourceFile("temp.tsx", code, { overwrite: true });
|
|
228
|
+
return this.buildAst(sourceFile);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
buildAst(node) {
|
|
232
|
+
const ast = {
|
|
233
|
+
kind: SyntaxKind[node.getKind()],
|
|
234
|
+
text: node.getText(),
|
|
235
|
+
children: []
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
node.getChildren().forEach(child => {
|
|
239
|
+
ast.children.push(this.buildAst(child));
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return ast;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
format(code) {
|
|
246
|
+
const sourceFile = this.project.createSourceFile("temp.tsx", code, { overwrite: true });
|
|
247
|
+
sourceFile.formatText();
|
|
248
|
+
return sourceFile.getFullText();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Main execution
|
|
253
|
+
if (require.main === module) {
|
|
254
|
+
const args = process.argv.slice(2);
|
|
255
|
+
const operation = args[0];
|
|
256
|
+
const inputFile = args[1];
|
|
257
|
+
const outputFile = args[2];
|
|
258
|
+
|
|
259
|
+
const service = new TsMorphService();
|
|
260
|
+
const code = fs.readFileSync(inputFile, 'utf8');
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
let result;
|
|
264
|
+
|
|
265
|
+
switch (operation) {
|
|
266
|
+
case 'transform':
|
|
267
|
+
const transformationsFile = args[3];
|
|
268
|
+
const transformations = JSON.parse(fs.readFileSync(transformationsFile, 'utf8'));
|
|
269
|
+
result = service.transform(code, transformations);
|
|
270
|
+
break;
|
|
271
|
+
case 'parse':
|
|
272
|
+
result = JSON.stringify(service.parse(code), null, 2);
|
|
273
|
+
break;
|
|
274
|
+
case 'format':
|
|
275
|
+
result = service.format(code);
|
|
276
|
+
break;
|
|
277
|
+
default:
|
|
278
|
+
throw new Error(`Unknown operation: ${operation}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fs.writeFileSync(outputFile, result);
|
|
282
|
+
console.log("Success");
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error("Error:", error.message);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = TsMorphService;
|
|
290
|
+
JAVASCRIPT
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# This runs during gem installation
|
|
294
|
+
|
|
295
|
+
def install_node_dependencies
|
|
296
|
+
puts "Installing Node.js dependencies for ts-morph..."
|
|
297
|
+
|
|
298
|
+
# Get the gem root directory
|
|
299
|
+
# Install into the gem's lib/ts/morph/node directory so it's packaged with the gem
|
|
300
|
+
# Write to the same directory used by Ts::Morph::NodeService::NODE_DIR
|
|
301
|
+
node_dir = File.expand_path('../../lib/ts/morph/node', __dir__)
|
|
302
|
+
|
|
303
|
+
# Create node directory
|
|
304
|
+
FileUtils.mkdir_p(node_dir)
|
|
305
|
+
|
|
306
|
+
# Create package.json
|
|
307
|
+
package_json_path = File.join(node_dir, 'package.json')
|
|
308
|
+
unless File.exist?(package_json_path)
|
|
309
|
+
package_content = {
|
|
310
|
+
name: "ts-morph-ruby-service",
|
|
311
|
+
version: "1.0.0",
|
|
312
|
+
private: true,
|
|
313
|
+
dependencies: {
|
|
314
|
+
"ts-morph": "^23.0.0",
|
|
315
|
+
"@babel/parser": "^7.24.0",
|
|
316
|
+
"@babel/traverse": "^7.24.0",
|
|
317
|
+
"@babel/generator": "^7.24.0",
|
|
318
|
+
"@babel/types": "^7.24.0",
|
|
319
|
+
"typescript": "^5.0.0"
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
File.write(package_json_path, JSON.pretty_generate(package_content))
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Install npm dependencies
|
|
327
|
+
Dir.chdir(node_dir) do
|
|
328
|
+
unless system("npm install")
|
|
329
|
+
puts "WARNING: Failed to install Node.js dependencies. You may need to run 'npm install' manually in #{node_dir}"
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Create the service.js file
|
|
334
|
+
service_script_path = File.join(node_dir, 'service.js')
|
|
335
|
+
unless File.exist?(service_script_path)
|
|
336
|
+
File.write(service_script_path, node_service_code)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
puts "Node.js dependencies installed successfully!"
|
|
340
|
+
rescue => e
|
|
341
|
+
puts "WARNING: Error installing Node.js dependencies: #{e.message}"
|
|
342
|
+
puts "You may need to install them manually by running: Ts::Morph.install_dependencies"
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Check if Node.js and npm are available
|
|
346
|
+
if system("which node > /dev/null 2>&1") && system("which npm > /dev/null 2>&1")
|
|
347
|
+
install_node_dependencies
|
|
348
|
+
else
|
|
349
|
+
puts "WARNING: Node.js and npm are required but not found in PATH"
|
|
350
|
+
puts "Please install Node.js and npm, then run: Ts::Morph.install_dependencies"
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def node_service_code
|
|
354
|
+
<<~JAVASCRIPT
|
|
355
|
+
const { Project, SyntaxKind } = require("ts-morph");
|
|
356
|
+
const fs = require("fs");
|
|
357
|
+
|
|
358
|
+
class TsMorphService {
|
|
359
|
+
constructor() {
|
|
360
|
+
this.project = new Project({
|
|
361
|
+
useInMemoryFileSystem: true,
|
|
362
|
+
compilerOptions: {
|
|
363
|
+
jsx: "react",
|
|
364
|
+
target: "ES2020",
|
|
365
|
+
module: "ESNext"
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
transform(code, transformations) {
|
|
371
|
+
const sourceFile = this.project.createSourceFile("temp.tsx", code, { overwrite: true });
|
|
372
|
+
|
|
373
|
+
transformations.forEach(transform => {
|
|
374
|
+
switch (transform.type) {
|
|
375
|
+
case 'addDefaultProps':
|
|
376
|
+
this.addDefaultProps(sourceFile, transform.component, transform.props);
|
|
377
|
+
break;
|
|
378
|
+
case 'wrapComponents':
|
|
379
|
+
this.wrapComponents(sourceFile, transform.target, transform.wrapper);
|
|
380
|
+
break;
|
|
381
|
+
case 'renameComponents':
|
|
382
|
+
this.renameComponents(sourceFile, transform.oldName, transform.newName);
|
|
383
|
+
break;
|
|
384
|
+
case 'addImports':
|
|
385
|
+
this.addImports(sourceFile, transform.imports);
|
|
386
|
+
break;
|
|
387
|
+
case 'injectStyles':
|
|
388
|
+
this.injectStyles(sourceFile, transform.styles);
|
|
389
|
+
break;
|
|
390
|
+
case 'removeComponent':
|
|
391
|
+
this.removeComponent(sourceFile, transform.component);
|
|
392
|
+
break;
|
|
393
|
+
case 'extractComponent':
|
|
394
|
+
this.extractComponent(sourceFile, transform.component);
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
return sourceFile.getFullText();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
addDefaultProps(sourceFile, componentName, defaultProps) {
|
|
403
|
+
// Add props to JSX elements
|
|
404
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
|
|
405
|
+
const openingElement = element.getOpeningElement();
|
|
406
|
+
if (openingElement.getTagNameNode().getText() === componentName) {
|
|
407
|
+
Object.entries(defaultProps).forEach(([propName, propValue]) => {
|
|
408
|
+
const existingProp = openingElement.getAttribute(propName);
|
|
409
|
+
if (!existingProp) {
|
|
410
|
+
openingElement.addAttribute({
|
|
411
|
+
name: propName,
|
|
412
|
+
initializer: typeof propValue === 'string' ? `"${propValue}"` : `{${JSON.stringify(propValue)}}`
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Add props to self-closing elements
|
|
420
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
|
|
421
|
+
if (element.getTagNameNode().getText() === componentName) {
|
|
422
|
+
Object.entries(defaultProps).forEach(([propName, propValue]) => {
|
|
423
|
+
const existingProp = element.getAttribute(propName);
|
|
424
|
+
if (!existingProp) {
|
|
425
|
+
element.addAttribute({
|
|
426
|
+
name: propName,
|
|
427
|
+
initializer: typeof propValue === 'string' ? `"${propValue}"` : `{${JSON.stringify(propValue)}}`
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
wrapComponents(sourceFile, targetComponent, wrapperComponent) {
|
|
436
|
+
const jsxElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement)
|
|
437
|
+
.filter(el => el.getOpeningElement().getTagNameNode().getText() === targetComponent);
|
|
438
|
+
|
|
439
|
+
jsxElements.forEach(element => {
|
|
440
|
+
const elementText = element.getFullText();
|
|
441
|
+
const wrappedCode = `<${wrapperComponent}>${elementText}</${wrapperComponent}>`;
|
|
442
|
+
element.replaceWithText(wrappedCode);
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
renameComponents(sourceFile, oldName, newName) {
|
|
447
|
+
// Rename JSX elements
|
|
448
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
|
|
449
|
+
const openingElement = element.getOpeningElement();
|
|
450
|
+
const closingElement = element.getClosingElement();
|
|
451
|
+
|
|
452
|
+
if (openingElement.getTagNameNode().getText() === oldName) {
|
|
453
|
+
openingElement.getTagNameNode().replaceWithText(newName);
|
|
454
|
+
if (closingElement) {
|
|
455
|
+
closingElement.getTagNameNode().replaceWithText(newName);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Rename self-closing elements
|
|
461
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
|
|
462
|
+
if (element.getTagNameNode().getText() === oldName) {
|
|
463
|
+
element.getTagNameNode().replaceWithText(newName);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Rename imports
|
|
468
|
+
sourceFile.getImportDeclarations().forEach(importDecl => {
|
|
469
|
+
importDecl.getNamedImports().forEach(namedImport => {
|
|
470
|
+
if (namedImport.getName() === oldName) {
|
|
471
|
+
namedImport.renameAlias(newName);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
addImports(sourceFile, imports) {
|
|
478
|
+
imports.forEach(imp => {
|
|
479
|
+
const existingImport = sourceFile.getImportDeclaration(
|
|
480
|
+
decl => decl.getModuleSpecifierValue() === imp.from
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
if (existingImport) {
|
|
484
|
+
imp.named?.forEach(name => {
|
|
485
|
+
existingImport.addNamedImport(name);
|
|
486
|
+
});
|
|
487
|
+
} else {
|
|
488
|
+
sourceFile.addImportDeclaration({
|
|
489
|
+
moduleSpecifier: imp.from,
|
|
490
|
+
namedImports: imp.named,
|
|
491
|
+
defaultImport: imp.default
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
injectStyles(sourceFile, styles) {
|
|
498
|
+
Object.entries(styles).forEach(([componentName, styleRules]) => {
|
|
499
|
+
// Handle JSX elements
|
|
500
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
|
|
501
|
+
const openingElement = element.getOpeningElement();
|
|
502
|
+
if (openingElement.getTagNameNode().getText() === componentName) {
|
|
503
|
+
const styleAttr = openingElement.getAttribute("style");
|
|
504
|
+
if (styleAttr) {
|
|
505
|
+
const existingValue = styleAttr.getInitializer()?.getText() || "{}";
|
|
506
|
+
const mergedStyles = `{...${existingValue.slice(1, -1)}, ...${JSON.stringify(styleRules)}}`;
|
|
507
|
+
styleAttr.setInitializer(mergedStyles);
|
|
508
|
+
} else {
|
|
509
|
+
openingElement.addAttribute({
|
|
510
|
+
name: "style",
|
|
511
|
+
initializer: `{${JSON.stringify(styleRules)}}`
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Handle self-closing elements
|
|
518
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
|
|
519
|
+
if (element.getTagNameNode().getText() === componentName) {
|
|
520
|
+
const styleAttr = element.getAttribute("style");
|
|
521
|
+
if (styleAttr) {
|
|
522
|
+
const existingValue = styleAttr.getInitializer()?.getText() || "{}";
|
|
523
|
+
const mergedStyles = `{...${existingValue.slice(1, -1)}, ...${JSON.stringify(styleRules)}}`;
|
|
524
|
+
styleAttr.setInitializer(mergedStyles);
|
|
525
|
+
} else {
|
|
526
|
+
element.addAttribute({
|
|
527
|
+
name: "style",
|
|
528
|
+
initializer: `{${JSON.stringify(styleRules)}}`
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
removeComponent(sourceFile, componentName) {
|
|
537
|
+
// Remove JSX elements
|
|
538
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
|
|
539
|
+
if (element.getOpeningElement().getTagNameNode().getText() === componentName) {
|
|
540
|
+
element.remove();
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Remove self-closing elements
|
|
545
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
|
|
546
|
+
if (element.getTagNameNode().getText() === componentName) {
|
|
547
|
+
element.remove();
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
extractComponent(sourceFile, componentName) {
|
|
553
|
+
const components = [];
|
|
554
|
+
|
|
555
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
|
|
556
|
+
if (element.getOpeningElement().getTagNameNode().getText() === componentName) {
|
|
557
|
+
components.push(element.getFullText());
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
|
|
562
|
+
if (element.getTagNameNode().getText() === componentName) {
|
|
563
|
+
components.push(element.getFullText());
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
return components;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
parse(code) {
|
|
571
|
+
const sourceFile = this.project.createSourceFile("temp.tsx", code, { overwrite: true });
|
|
572
|
+
return this.buildAst(sourceFile);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
buildAst(node) {
|
|
576
|
+
const ast = {
|
|
577
|
+
kind: SyntaxKind[node.getKind()],
|
|
578
|
+
text: node.getText(),
|
|
579
|
+
children: []
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
node.getChildren().forEach(child => {
|
|
583
|
+
ast.children.push(this.buildAst(child));
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
return ast;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
format(code) {
|
|
590
|
+
const sourceFile = this.project.createSourceFile("temp.tsx", code, { overwrite: true });
|
|
591
|
+
sourceFile.formatText();
|
|
592
|
+
return sourceFile.getFullText();
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Main execution
|
|
597
|
+
if (require.main === module) {
|
|
598
|
+
const args = process.argv.slice(2);
|
|
599
|
+
const operation = args[0];
|
|
600
|
+
const inputFile = args[1];
|
|
601
|
+
const outputFile = args[2];
|
|
602
|
+
|
|
603
|
+
const service = new TsMorphService();
|
|
604
|
+
const code = fs.readFileSync(inputFile, 'utf8');
|
|
605
|
+
|
|
606
|
+
try {
|
|
607
|
+
let result;
|
|
608
|
+
|
|
609
|
+
switch (operation) {
|
|
610
|
+
case 'transform':
|
|
611
|
+
const transformationsFile = args[3];
|
|
612
|
+
const transformations = JSON.parse(fs.readFileSync(transformationsFile, 'utf8'));
|
|
613
|
+
result = service.transform(code, transformations);
|
|
614
|
+
break;
|
|
615
|
+
case 'parse':
|
|
616
|
+
result = JSON.stringify(service.parse(code), null, 2);
|
|
617
|
+
break;
|
|
618
|
+
case 'format':
|
|
619
|
+
result = service.format(code);
|
|
620
|
+
break;
|
|
621
|
+
default:
|
|
622
|
+
throw new Error(`Unknown operation: ${operation}`);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
fs.writeFileSync(outputFile, result);
|
|
626
|
+
console.log("Success");
|
|
627
|
+
} catch (error) {
|
|
628
|
+
console.error("Error:", error.message);
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
module.exports = TsMorphService;
|
|
634
|
+
JAVASCRIPT
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
# Create a dummy Makefile to satisfy the extension build process
|
|
638
|
+
File.write("Makefile", <<~MAKEFILE)
|
|
639
|
+
# Dummy Makefile for ts-morph gem
|
|
640
|
+
.PHONY: all install clean
|
|
641
|
+
|
|
642
|
+
all:
|
|
643
|
+
|
|
644
|
+
install:
|
|
645
|
+
|
|
646
|
+
clean:
|
|
647
|
+
MAKEFILE
|
|
648
|
+
|
|
649
|
+
exit 0
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ts
|
|
4
|
+
module Morph
|
|
5
|
+
# Base error class for ts-morph operations
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Raised when transformation fails
|
|
9
|
+
class TransformationError < Error; end
|
|
10
|
+
|
|
11
|
+
# Raised when Node.js service is not available
|
|
12
|
+
class ServiceUnavailableError < Error; end
|
|
13
|
+
|
|
14
|
+
# Raised when parsing fails
|
|
15
|
+
class ParseError < Error; end
|
|
16
|
+
end
|
|
17
|
+
end
|