jsx_rosetta 0.1.0

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,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "open3"
6
+
7
+ require_relative "node_bridge"
8
+
9
+ module JsxRosetta
10
+ # Command-line interface for the gem.
11
+ #
12
+ # Subcommands:
13
+ # install npm-install the Node sidecar dependencies.
14
+ # translate FILE [-o DIR] JSX/TSX → ViewComponent files written to DIR
15
+ # (default: current directory). TSX is detected
16
+ # via the .tsx extension or --tsx.
17
+ # parse FILE Print the parsed Babel AST as pretty JSON.
18
+ # version Print the gem version.
19
+ # help Show usage.
20
+ class CLI
21
+ EXIT_OK = 0
22
+ EXIT_USAGE = 64
23
+ EXIT_FAILURE = 1
24
+
25
+ def initialize(argv = ARGV.dup, stdout: $stdout, stderr: $stderr)
26
+ @argv = argv
27
+ @stdout = stdout
28
+ @stderr = stderr
29
+ end
30
+
31
+ def run
32
+ command = @argv.shift
33
+ case command
34
+ when "install" then run_install
35
+ when "translate" then run_translate
36
+ when "routes" then run_routes
37
+ when "parse" then run_parse
38
+ when "version", "-v", "--version" then run_version
39
+ when nil, "help", "-h", "--help" then print_help(EXIT_OK)
40
+ else
41
+ @stderr.puts "jsx_rosetta: unknown command: #{command.inspect}"
42
+ print_help(EXIT_USAGE)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def run_install
49
+ sidecar_dir = NodeBridge::SIDECAR_DIR
50
+ @stdout.puts "Installing Node sidecar dependencies in #{sidecar_dir}"
51
+ output, status = Open3.capture2e("npm", "install", chdir: sidecar_dir)
52
+ @stdout.print(output)
53
+ status.success? ? EXIT_OK : EXIT_FAILURE
54
+ rescue Errno::ENOENT
55
+ @stderr.puts "jsx_rosetta install: could not find `npm` on PATH. Install Node.js (>= 18) first."
56
+ EXIT_FAILURE
57
+ end
58
+
59
+ def run_translate
60
+ options, positional = parse_translate_options
61
+ input_path = positional.first
62
+ return missing_argument("translate FILE", "translate") unless input_path
63
+
64
+ out_dir = options[:out] || "."
65
+ typescript = options[:tsx] || input_path.end_with?(".tsx")
66
+ backend = options[:as] == "view" ? :rails_view : :view_component
67
+
68
+ source = File.read(input_path)
69
+ files = JsxRosetta.translate(
70
+ source,
71
+ backend: backend,
72
+ typescript: typescript,
73
+ source_filename: input_path
74
+ )
75
+
76
+ FileUtils.mkdir_p(out_dir)
77
+ files.each do |file|
78
+ target = File.join(out_dir, file.path)
79
+ FileUtils.mkdir_p(File.dirname(target))
80
+ File.write(target, file.contents)
81
+ @stdout.puts "wrote #{target}"
82
+ end
83
+ EXIT_OK
84
+ rescue ParseError, IR::Lowering::LoweringError => e
85
+ @stderr.puts "jsx_rosetta translate: #{e.message}"
86
+ EXIT_FAILURE
87
+ end
88
+
89
+ def run_routes
90
+ options, positional = parse_translate_options
91
+ input_path = positional.first
92
+ return missing_argument("routes FILE [-o OUT.rb]", "routes") unless input_path
93
+
94
+ typescript = options[:tsx] || input_path.end_with?(".tsx")
95
+ source = File.read(input_path)
96
+ ast = JsxRosetta.parse(source, typescript: typescript, source_filename: input_path)
97
+ route_tree = Routes.lower(ast)
98
+ script = Backend::RoutesScript.new(source_path: input_path).emit(route_tree)
99
+
100
+ if options[:out]
101
+ File.write(options[:out], script)
102
+ @stdout.puts "wrote #{options[:out]}"
103
+ else
104
+ @stdout.print(script)
105
+ end
106
+ EXIT_OK
107
+ rescue ParseError => e
108
+ @stderr.puts "jsx_rosetta routes: #{e.message}"
109
+ EXIT_FAILURE
110
+ end
111
+
112
+ def run_parse
113
+ options, positional = parse_translate_options
114
+ input_path = positional.first
115
+ return missing_argument("parse FILE", "parse") unless input_path
116
+
117
+ typescript = options[:tsx] || input_path.end_with?(".tsx")
118
+
119
+ source = File.read(input_path)
120
+ ast = JsxRosetta.parse(source, typescript: typescript, source_filename: input_path)
121
+
122
+ @stdout.puts JSON.pretty_generate(ast.raw)
123
+ EXIT_OK
124
+ rescue ParseError => e
125
+ @stderr.puts "jsx_rosetta parse: #{e.message}"
126
+ EXIT_FAILURE
127
+ end
128
+
129
+ def run_version
130
+ @stdout.puts JsxRosetta::VERSION
131
+ EXIT_OK
132
+ end
133
+
134
+ def parse_translate_options
135
+ options = {}
136
+ positional = []
137
+
138
+ until @argv.empty?
139
+ arg = @argv.shift
140
+ case arg
141
+ when "-o", "--out" then options[:out] = @argv.shift
142
+ when "--tsx", "--typescript" then options[:tsx] = true
143
+ when "--as" then options[:as] = @argv.shift
144
+ when /\A--as=(.+)\z/ then options[:as] = ::Regexp.last_match(1)
145
+ else positional << arg
146
+ end
147
+ end
148
+
149
+ [options, positional]
150
+ end
151
+
152
+ def missing_argument(usage, command)
153
+ @stderr.puts "jsx_rosetta #{command}: missing required argument."
154
+ @stderr.puts " usage: jsx_rosetta #{usage}"
155
+ EXIT_USAGE
156
+ end
157
+
158
+ def print_help(exit_code)
159
+ @stdout.puts <<~USAGE
160
+ Usage: jsx_rosetta <command> [args]
161
+
162
+ Commands:
163
+ install Install the gem's Node sidecar dependencies (runs `npm install`).
164
+ translate FILE [-o DIR] Translate JSX/TSX into ViewComponent files in DIR (default: ".").
165
+ Pass --tsx to force TypeScript parsing if the input is .jsx.
166
+ Pass --as=view to emit a Rails view template (`<snake>.html.erb`)
167
+ instead of a ViewComponent class + sidecar template — appropriate
168
+ for pages tied to a route.
169
+ routes FILE [-o OUT.rb] Parse <Route path=... element={<X/>} /> patterns from FILE
170
+ and emit a reviewable Ruby script that calls `rails generate
171
+ controller` and prints suggested config/routes.rb additions.
172
+ parse FILE Parse the input and print the Babel AST as JSON.
173
+ version Print the gem version.
174
+ help Show this help.
175
+
176
+ Environment:
177
+ JSX_ROSETTA_NODE Absolute path to a node executable (default: PATH lookup).
178
+ USAGE
179
+ exit_code
180
+ end
181
+ end
182
+ end