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.
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ts
4
+ module Morph
5
+ VERSION = "0.1.1"
6
+ end
7
+ 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
data/sig/ts/morph.rbs ADDED
@@ -0,0 +1,6 @@
1
+ module Ts
2
+ module Morph
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end