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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +149 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/PLAN.md +236 -0
- data/README.md +328 -0
- data/Rakefile +12 -0
- data/exe/jsx_rosetta +6 -0
- data/lib/jsx_rosetta/ast/inflector.rb +23 -0
- data/lib/jsx_rosetta/ast/node.rb +151 -0
- data/lib/jsx_rosetta/ast/types.rb +224 -0
- data/lib/jsx_rosetta/ast/visitor.rb +47 -0
- data/lib/jsx_rosetta/ast.rb +15 -0
- data/lib/jsx_rosetta/backend/base.rb +21 -0
- data/lib/jsx_rosetta/backend/rails_view.rb +41 -0
- data/lib/jsx_rosetta/backend/routes_script.rb +191 -0
- data/lib/jsx_rosetta/backend/view_component/expression_translator.rb +120 -0
- data/lib/jsx_rosetta/backend/view_component.rb +638 -0
- data/lib/jsx_rosetta/backend.rb +12 -0
- data/lib/jsx_rosetta/cli.rb +182 -0
- data/lib/jsx_rosetta/ir/lowering.rb +727 -0
- data/lib/jsx_rosetta/ir/types.rb +276 -0
- data/lib/jsx_rosetta/ir.rb +16 -0
- data/lib/jsx_rosetta/node_bridge.rb +56 -0
- data/lib/jsx_rosetta/parse_error.rb +19 -0
- data/lib/jsx_rosetta/parser.rb +30 -0
- data/lib/jsx_rosetta/routes.rb +72 -0
- data/lib/jsx_rosetta/version.rb +5 -0
- data/lib/jsx_rosetta.rb +41 -0
- data/node/.gitignore +1 -0
- data/node/package-lock.json +64 -0
- data/node/package.json +16 -0
- data/node/parse.js +77 -0
- metadata +84 -0
|
@@ -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
|