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,351 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Ts
|
|
6
|
+
module Morph
|
|
7
|
+
# Manages the Node.js service for ts-morph operations
|
|
8
|
+
class NodeService
|
|
9
|
+
NODE_DIR = File.expand_path("./node", __dir__)
|
|
10
|
+
PACKAGE_JSON = File.join(NODE_DIR, "package.json")
|
|
11
|
+
SERVICE_SCRIPT = File.join(NODE_DIR, "service.js")
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def available?
|
|
15
|
+
File.exist?(SERVICE_SCRIPT) && dependencies_installed?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def install_dependencies
|
|
19
|
+
ensure_node_directory
|
|
20
|
+
create_package_json unless File.exist?(PACKAGE_JSON)
|
|
21
|
+
|
|
22
|
+
Dir.chdir(NODE_DIR) do
|
|
23
|
+
system("npm install", exception: true)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
create_service_script unless File.exist?(SERVICE_SCRIPT)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def script_path
|
|
30
|
+
SERVICE_SCRIPT
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def dependencies_installed?
|
|
36
|
+
File.exist?(File.join(NODE_DIR, "node_modules", "ts-morph"))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def ensure_node_directory
|
|
40
|
+
FileUtils.mkdir_p(NODE_DIR)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def create_package_json
|
|
44
|
+
package_content = {
|
|
45
|
+
name: "ts-morph-ruby-service",
|
|
46
|
+
version: "1.0.0",
|
|
47
|
+
private: true,
|
|
48
|
+
dependencies: {
|
|
49
|
+
"ts-morph": "^23.0.0",
|
|
50
|
+
"@babel/parser": "^7.24.0",
|
|
51
|
+
"@babel/traverse": "^7.24.0",
|
|
52
|
+
"@babel/generator": "^7.24.0",
|
|
53
|
+
"@babel/types": "^7.24.0",
|
|
54
|
+
"typescript": "^5.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
File.write(PACKAGE_JSON, JSON.pretty_generate(package_content))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def create_service_script
|
|
62
|
+
File.write(SERVICE_SCRIPT, node_service_code)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def node_service_code
|
|
66
|
+
<<~JAVASCRIPT
|
|
67
|
+
const { Project, SyntaxKind } = require("ts-morph");
|
|
68
|
+
const fs = require("fs");
|
|
69
|
+
|
|
70
|
+
class TsMorphService {
|
|
71
|
+
constructor() {
|
|
72
|
+
this.project = new Project({
|
|
73
|
+
useInMemoryFileSystem: true,
|
|
74
|
+
compilerOptions: {
|
|
75
|
+
jsx: "react",
|
|
76
|
+
target: "ES2020",
|
|
77
|
+
module: "ESNext"
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
transform(code, transformations) {
|
|
83
|
+
const sourceFile = this.project.createSourceFile("temp.tsx", code, { overwrite: true });
|
|
84
|
+
|
|
85
|
+
transformations.forEach(transform => {
|
|
86
|
+
switch (transform.type) {
|
|
87
|
+
case 'addDefaultProps':
|
|
88
|
+
this.addDefaultProps(sourceFile, transform.component, transform.props);
|
|
89
|
+
break;
|
|
90
|
+
case 'wrapComponents':
|
|
91
|
+
this.wrapComponents(sourceFile, transform.target, transform.wrapper);
|
|
92
|
+
break;
|
|
93
|
+
case 'renameComponents':
|
|
94
|
+
this.renameComponents(sourceFile, transform.oldName, transform.newName);
|
|
95
|
+
break;
|
|
96
|
+
case 'addImports':
|
|
97
|
+
this.addImports(sourceFile, transform.imports);
|
|
98
|
+
break;
|
|
99
|
+
case 'injectStyles':
|
|
100
|
+
this.injectStyles(sourceFile, transform.styles);
|
|
101
|
+
break;
|
|
102
|
+
case 'removeComponent':
|
|
103
|
+
this.removeComponent(sourceFile, transform.component);
|
|
104
|
+
break;
|
|
105
|
+
case 'extractComponent':
|
|
106
|
+
this.extractComponent(sourceFile, transform.component);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return sourceFile.getFullText();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
addDefaultProps(sourceFile, componentName, defaultProps) {
|
|
115
|
+
// Add props to JSX elements
|
|
116
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
|
|
117
|
+
const openingElement = element.getOpeningElement();
|
|
118
|
+
if (openingElement.getTagNameNode().getText() === componentName) {
|
|
119
|
+
Object.entries(defaultProps).forEach(([propName, propValue]) => {
|
|
120
|
+
const existingProp = openingElement.getAttribute(propName);
|
|
121
|
+
if (!existingProp) {
|
|
122
|
+
openingElement.addAttribute({
|
|
123
|
+
name: propName,
|
|
124
|
+
initializer: typeof propValue === 'string' ? `"${propValue}"` : `{${JSON.stringify(propValue)}}`
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Add props to self-closing elements
|
|
132
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
|
|
133
|
+
if (element.getTagNameNode().getText() === componentName) {
|
|
134
|
+
Object.entries(defaultProps).forEach(([propName, propValue]) => {
|
|
135
|
+
const existingProp = element.getAttribute(propName);
|
|
136
|
+
if (!existingProp) {
|
|
137
|
+
element.addAttribute({
|
|
138
|
+
name: propName,
|
|
139
|
+
initializer: typeof propValue === 'string' ? `"${propValue}"` : `{${JSON.stringify(propValue)}}`
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
wrapComponents(sourceFile, targetComponent, wrapperComponent) {
|
|
148
|
+
const jsxElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement)
|
|
149
|
+
.filter(el => el.getOpeningElement().getTagNameNode().getText() === targetComponent);
|
|
150
|
+
|
|
151
|
+
jsxElements.forEach(element => {
|
|
152
|
+
const elementText = element.getFullText();
|
|
153
|
+
const wrappedCode = `<${wrapperComponent}>${elementText}</${wrapperComponent}>`;
|
|
154
|
+
element.replaceWithText(wrappedCode);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
renameComponents(sourceFile, oldName, newName) {
|
|
159
|
+
// Rename JSX elements
|
|
160
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
|
|
161
|
+
const openingElement = element.getOpeningElement();
|
|
162
|
+
const closingElement = element.getClosingElement();
|
|
163
|
+
|
|
164
|
+
if (openingElement.getTagNameNode().getText() === oldName) {
|
|
165
|
+
openingElement.getTagNameNode().replaceWithText(newName);
|
|
166
|
+
if (closingElement) {
|
|
167
|
+
closingElement.getTagNameNode().replaceWithText(newName);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Rename self-closing elements
|
|
173
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
|
|
174
|
+
if (element.getTagNameNode().getText() === oldName) {
|
|
175
|
+
element.getTagNameNode().replaceWithText(newName);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Rename imports
|
|
180
|
+
sourceFile.getImportDeclarations().forEach(importDecl => {
|
|
181
|
+
importDecl.getNamedImports().forEach(namedImport => {
|
|
182
|
+
if (namedImport.getName() === oldName) {
|
|
183
|
+
namedImport.renameAlias(newName);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
addImports(sourceFile, imports) {
|
|
190
|
+
imports.forEach(imp => {
|
|
191
|
+
const existingImport = sourceFile.getImportDeclaration(
|
|
192
|
+
decl => decl.getModuleSpecifierValue() === imp.from
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (existingImport) {
|
|
196
|
+
imp.named?.forEach(name => {
|
|
197
|
+
existingImport.addNamedImport(name);
|
|
198
|
+
});
|
|
199
|
+
} else {
|
|
200
|
+
sourceFile.addImportDeclaration({
|
|
201
|
+
moduleSpecifier: imp.from,
|
|
202
|
+
namedImports: imp.named,
|
|
203
|
+
defaultImport: imp.default
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
injectStyles(sourceFile, styles) {
|
|
210
|
+
Object.entries(styles).forEach(([componentName, styleRules]) => {
|
|
211
|
+
// Handle JSX elements
|
|
212
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
|
|
213
|
+
const openingElement = element.getOpeningElement();
|
|
214
|
+
if (openingElement.getTagNameNode().getText() === componentName) {
|
|
215
|
+
const styleAttr = openingElement.getAttribute("style");
|
|
216
|
+
if (styleAttr) {
|
|
217
|
+
const existingValue = styleAttr.getInitializer()?.getText() || "{}";
|
|
218
|
+
const mergedStyles = `{...${existingValue.slice(1, -1)}, ...${JSON.stringify(styleRules)}}`;
|
|
219
|
+
styleAttr.setInitializer(mergedStyles);
|
|
220
|
+
} else {
|
|
221
|
+
openingElement.addAttribute({
|
|
222
|
+
name: "style",
|
|
223
|
+
initializer: `{${JSON.stringify(styleRules)}}`
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Handle self-closing elements
|
|
230
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
|
|
231
|
+
if (element.getTagNameNode().getText() === componentName) {
|
|
232
|
+
const styleAttr = element.getAttribute("style");
|
|
233
|
+
if (styleAttr) {
|
|
234
|
+
const existingValue = styleAttr.getInitializer()?.getText() || "{}";
|
|
235
|
+
const mergedStyles = `{...${existingValue.slice(1, -1)}, ...${JSON.stringify(styleRules)}}`;
|
|
236
|
+
styleAttr.setInitializer(mergedStyles);
|
|
237
|
+
} else {
|
|
238
|
+
element.addAttribute({
|
|
239
|
+
name: "style",
|
|
240
|
+
initializer: `{${JSON.stringify(styleRules)}}`
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
removeComponent(sourceFile, componentName) {
|
|
249
|
+
// Remove JSX elements
|
|
250
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
|
|
251
|
+
if (element.getOpeningElement().getTagNameNode().getText() === componentName) {
|
|
252
|
+
element.remove();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Remove self-closing elements
|
|
257
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
|
|
258
|
+
if (element.getTagNameNode().getText() === componentName) {
|
|
259
|
+
element.remove();
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
extractComponent(sourceFile, componentName) {
|
|
265
|
+
const components = [];
|
|
266
|
+
|
|
267
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
|
|
268
|
+
if (element.getOpeningElement().getTagNameNode().getText() === componentName) {
|
|
269
|
+
components.push(element.getFullText());
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
|
|
274
|
+
if (element.getTagNameNode().getText() === componentName) {
|
|
275
|
+
components.push(element.getFullText());
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
return components;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
parse(code) {
|
|
283
|
+
const sourceFile = this.project.createSourceFile("temp.tsx", code, { overwrite: true });
|
|
284
|
+
return this.buildAst(sourceFile);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
buildAst(node) {
|
|
288
|
+
const ast = {
|
|
289
|
+
kind: SyntaxKind[node.getKind()],
|
|
290
|
+
text: node.getText(),
|
|
291
|
+
children: []
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
node.getChildren().forEach(child => {
|
|
295
|
+
ast.children.push(this.buildAst(child));
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
return ast;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
format(code) {
|
|
302
|
+
const sourceFile = this.project.createSourceFile("temp.tsx", code, { overwrite: true });
|
|
303
|
+
sourceFile.formatText();
|
|
304
|
+
return sourceFile.getFullText();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Main execution
|
|
309
|
+
if (require.main === module) {
|
|
310
|
+
const args = process.argv.slice(2);
|
|
311
|
+
const operation = args[0];
|
|
312
|
+
const inputFile = args[1];
|
|
313
|
+
const outputFile = args[2];
|
|
314
|
+
|
|
315
|
+
const service = new TsMorphService();
|
|
316
|
+
const code = fs.readFileSync(inputFile, 'utf8');
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
let result;
|
|
320
|
+
|
|
321
|
+
switch (operation) {
|
|
322
|
+
case 'transform':
|
|
323
|
+
const transformationsFile = args[3];
|
|
324
|
+
const transformations = JSON.parse(fs.readFileSync(transformationsFile, 'utf8'));
|
|
325
|
+
result = service.transform(code, transformations);
|
|
326
|
+
break;
|
|
327
|
+
case 'parse':
|
|
328
|
+
result = JSON.stringify(service.parse(code), null, 2);
|
|
329
|
+
break;
|
|
330
|
+
case 'format':
|
|
331
|
+
result = service.format(code);
|
|
332
|
+
break;
|
|
333
|
+
default:
|
|
334
|
+
throw new Error(`Unknown operation: ${operation}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
fs.writeFileSync(outputFile, result);
|
|
338
|
+
console.log("Success");
|
|
339
|
+
} catch (error) {
|
|
340
|
+
console.error("Error:", error.message);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = TsMorphService;
|
|
346
|
+
JAVASCRIPT
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ts
|
|
4
|
+
module Morph
|
|
5
|
+
# Helper methods for building transformations
|
|
6
|
+
module Transformations
|
|
7
|
+
class << self
|
|
8
|
+
# Add default props to a component
|
|
9
|
+
# @param component [String] Component name
|
|
10
|
+
# @param props [Hash] Default props to add
|
|
11
|
+
# @return [Hash] Transformation specification
|
|
12
|
+
def add_default_props(component, props)
|
|
13
|
+
{
|
|
14
|
+
type: "addDefaultProps",
|
|
15
|
+
component: component,
|
|
16
|
+
props: props
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Wrap components with another component
|
|
21
|
+
# @param target [String] Component to wrap
|
|
22
|
+
# @param wrapper [String] Wrapper component
|
|
23
|
+
# @return [Hash] Transformation specification
|
|
24
|
+
def wrap_components(target, wrapper)
|
|
25
|
+
{
|
|
26
|
+
type: "wrapComponents",
|
|
27
|
+
target: target,
|
|
28
|
+
wrapper: wrapper
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Rename components
|
|
33
|
+
# @param old_name [String] Current component name
|
|
34
|
+
# @param new_name [String] New component name
|
|
35
|
+
# @return [Hash] Transformation specification
|
|
36
|
+
def rename_components(old_name, new_name)
|
|
37
|
+
{
|
|
38
|
+
type: "renameComponents",
|
|
39
|
+
oldName: old_name,
|
|
40
|
+
newName: new_name
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Add imports
|
|
45
|
+
# @param imports [Array<Hash>] Import specifications
|
|
46
|
+
# @return [Hash] Transformation specification
|
|
47
|
+
def add_imports(imports)
|
|
48
|
+
{
|
|
49
|
+
type: "addImports",
|
|
50
|
+
imports: imports
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Inject styles into components
|
|
55
|
+
# @param styles [Hash] Component => style rules mapping
|
|
56
|
+
# @return [Hash] Transformation specification
|
|
57
|
+
def inject_styles(styles)
|
|
58
|
+
{
|
|
59
|
+
type: "injectStyles",
|
|
60
|
+
styles: styles
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Remove a component
|
|
65
|
+
# @param component [String] Component name to remove
|
|
66
|
+
# @return [Hash] Transformation specification
|
|
67
|
+
def remove_component(component)
|
|
68
|
+
{
|
|
69
|
+
type: "removeComponent",
|
|
70
|
+
component: component
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Extract a component
|
|
75
|
+
# @param component [String] Component name to extract
|
|
76
|
+
# @return [Hash] Transformation specification
|
|
77
|
+
def extract_component(component)
|
|
78
|
+
{
|
|
79
|
+
type: "extractComponent",
|
|
80
|
+
component: component
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Build import specification
|
|
85
|
+
# @param from [String] Module to import from
|
|
86
|
+
# @param named [Array<String>] Named imports
|
|
87
|
+
# @param default [String] Default import
|
|
88
|
+
# @return [Hash] Import specification
|
|
89
|
+
def import_spec(from:, named: nil, default: nil)
|
|
90
|
+
spec = { from: from }
|
|
91
|
+
spec[:named] = named if named
|
|
92
|
+
spec[:default] = default if default
|
|
93
|
+
spec
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ts
|
|
4
|
+
module Morph
|
|
5
|
+
# Main transformer class that interfaces with the Node.js service
|
|
6
|
+
class Transformer
|
|
7
|
+
def initialize
|
|
8
|
+
ensure_service_available!
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def transform(code, transformations)
|
|
12
|
+
transformations_file = write_temp_file(JSON.generate(transformations), ".json")
|
|
13
|
+
begin
|
|
14
|
+
execute_node_operation("transform", code) do |input_file, output_file|
|
|
15
|
+
[input_file, output_file, transformations_file.path]
|
|
16
|
+
end
|
|
17
|
+
ensure
|
|
18
|
+
transformations_file&.close
|
|
19
|
+
transformations_file&.unlink
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def parse(code)
|
|
24
|
+
result = execute_node_operation("parse", code) do |input_file, output_file|
|
|
25
|
+
[input_file, output_file]
|
|
26
|
+
end
|
|
27
|
+
JSON.parse(result)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def format(code)
|
|
31
|
+
execute_node_operation("format", code) do |input_file, output_file|
|
|
32
|
+
[input_file, output_file]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def ensure_service_available!
|
|
39
|
+
return if NodeService.available?
|
|
40
|
+
|
|
41
|
+
raise ServiceUnavailableError, <<~MSG
|
|
42
|
+
ts-morph Node.js service is not available.
|
|
43
|
+
Please run: Ts::Morph.install_dependencies
|
|
44
|
+
MSG
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def execute_node_operation(operation, code)
|
|
48
|
+
input_file = write_temp_file(code, ".tsx")
|
|
49
|
+
output_file = create_temp_file(".tsx")
|
|
50
|
+
|
|
51
|
+
begin
|
|
52
|
+
args = yield(input_file.path, output_file.path)
|
|
53
|
+
|
|
54
|
+
cmd = ["node", NodeService.script_path, operation] + args
|
|
55
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
56
|
+
|
|
57
|
+
unless status.success?
|
|
58
|
+
raise TransformationError, "#{operation} failed: #{stderr}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
File.read(output_file.path)
|
|
62
|
+
ensure
|
|
63
|
+
[input_file, output_file].each do |file|
|
|
64
|
+
file&.close
|
|
65
|
+
file&.unlink
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def write_temp_file(content, extension)
|
|
71
|
+
file = Tempfile.new(["ts-morph", extension])
|
|
72
|
+
file.write(content)
|
|
73
|
+
file.flush
|
|
74
|
+
file
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def create_temp_file(extension)
|
|
78
|
+
Tempfile.new(["ts-morph-output", extension])
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/ts/morph.rb
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "tempfile"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require_relative "morph/version"
|
|
8
|
+
require_relative "morph/error"
|
|
9
|
+
require_relative "morph/transformer"
|
|
10
|
+
require_relative "morph/node_service"
|
|
11
|
+
require_relative "morph/transformations"
|
|
12
|
+
|
|
13
|
+
module Ts
|
|
14
|
+
module Morph
|
|
15
|
+
class << self
|
|
16
|
+
# Transform TypeScript/TSX code with specified transformations
|
|
17
|
+
# @param code [String] The TypeScript/TSX code to transform
|
|
18
|
+
# @param transformations [Array<Hash>] Array of transformation specifications
|
|
19
|
+
# @return [String] The transformed code
|
|
20
|
+
def transform(code, transformations = [])
|
|
21
|
+
Transformer.new.transform(code, transformations)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Parse TypeScript/TSX code into an AST
|
|
25
|
+
# @param code [String] The TypeScript/TSX code to parse
|
|
26
|
+
# @return [Hash] The AST representation
|
|
27
|
+
def parse(code)
|
|
28
|
+
Transformer.new.parse(code)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Format TypeScript/TSX code
|
|
32
|
+
# @param code [String] The TypeScript/TSX code to format
|
|
33
|
+
# @return [String] The formatted code
|
|
34
|
+
def format(code)
|
|
35
|
+
Transformer.new.format(code)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Check if the Node.js service is available
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
def service_available?
|
|
41
|
+
NodeService.available?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Install the required Node.js dependencies
|
|
45
|
+
def install_dependencies
|
|
46
|
+
NodeService.install_dependencies
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|