ruby2js 3.6.1 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -7
  3. data/lib/ruby2js.rb +32 -9
  4. data/lib/ruby2js/converter.rb +8 -2
  5. data/lib/ruby2js/converter/assign.rb +159 -0
  6. data/lib/ruby2js/converter/begin.rb +7 -2
  7. data/lib/ruby2js/converter/case.rb +7 -2
  8. data/lib/ruby2js/converter/class.rb +77 -21
  9. data/lib/ruby2js/converter/class2.rb +39 -11
  10. data/lib/ruby2js/converter/def.rb +6 -2
  11. data/lib/ruby2js/converter/dstr.rb +8 -3
  12. data/lib/ruby2js/converter/hash.rb +9 -5
  13. data/lib/ruby2js/converter/hide.rb +13 -0
  14. data/lib/ruby2js/converter/if.rb +10 -2
  15. data/lib/ruby2js/converter/import.rb +18 -3
  16. data/lib/ruby2js/converter/kwbegin.rb +9 -2
  17. data/lib/ruby2js/converter/literal.rb +2 -2
  18. data/lib/ruby2js/converter/module.rb +37 -5
  19. data/lib/ruby2js/converter/opasgn.rb +8 -0
  20. data/lib/ruby2js/converter/send.rb +41 -2
  21. data/lib/ruby2js/converter/vasgn.rb +5 -0
  22. data/lib/ruby2js/demo.rb +53 -0
  23. data/lib/ruby2js/es2022.rb +5 -0
  24. data/lib/ruby2js/es2022/strict.rb +3 -0
  25. data/lib/ruby2js/filter.rb +9 -1
  26. data/lib/ruby2js/filter/active_functions.rb +1 -0
  27. data/lib/ruby2js/filter/cjs.rb +2 -0
  28. data/lib/ruby2js/filter/esm.rb +44 -10
  29. data/lib/ruby2js/filter/functions.rb +84 -95
  30. data/lib/ruby2js/filter/{wunderbar.rb → jsx.rb} +29 -7
  31. data/lib/ruby2js/filter/react.rb +191 -56
  32. data/lib/ruby2js/filter/require.rb +100 -5
  33. data/lib/ruby2js/filter/return.rb +13 -1
  34. data/lib/ruby2js/filter/stimulus.rb +185 -0
  35. data/lib/ruby2js/jsx.rb +291 -0
  36. data/lib/ruby2js/namespace.rb +75 -0
  37. data/lib/ruby2js/version.rb +3 -3
  38. metadata +12 -4
@@ -1,17 +1,19 @@
1
1
  require 'ruby2js'
2
2
 
3
+ # Convert Wunderbar syntax to JSX
4
+
3
5
  module Ruby2JS
4
6
  module Filter
5
- module Wunderbar
7
+ module JSX
6
8
  include SEXP
7
9
 
8
10
  def on_send(node)
9
- target, method, *attrs = node.children
11
+ target, method, *args = node.children
10
12
 
11
13
  if target == s(:const, nil, :Wunderbar)
12
14
  if [:debug, :info, :warn, :error, :fatal].include? method
13
15
  method = :error if method == :fatal
14
- return node.updated(nil, [s(:const, nil, :console), method, *attrs])
16
+ return node.updated(nil, [s(:const, nil, :console), method, *args])
15
17
  end
16
18
  end
17
19
 
@@ -27,7 +29,17 @@ module Ruby2JS
27
29
  end
28
30
 
29
31
  if target == nil and method.to_s.start_with? "_"
30
- S(:xnode, *method.to_s[1..-1], *stack, *process_all(attrs))
32
+ S(:xnode, method.to_s[1..-1], *stack, *process_all(args))
33
+
34
+ elsif method == :createElement and target == s(:const, nil, :React)
35
+ if args.first.type == :str and \
36
+ (args.length == 1 or %i(nil hash).include? args[1].type)
37
+ attrs = (args[1]&.type != :nil && args[1]) || s(:hash)
38
+ S(:xnode, args[0].children.first, attrs, *process_all(args[2..-1]))
39
+ else
40
+ super
41
+ end
42
+
31
43
  else
32
44
  super
33
45
  end
@@ -42,8 +54,18 @@ module Ruby2JS
42
54
 
43
55
  if target == nil and method.to_s.start_with? "_"
44
56
  if args.children.empty?
45
- # append block as a standalone proc
46
- process send.updated(nil, [*send.children, *process_all(block)])
57
+ if method == :_
58
+ # Fragment
59
+ if send.children.length == 2
60
+ process send.updated(:xnode, ['', *process_all(block)])
61
+ else
62
+ process s(:xnode, 'React.Fragment', *send.children[2..-1],
63
+ *process_all(block))
64
+ end
65
+ else
66
+ # append block as a standalone proc
67
+ process send.updated(nil, [*send.children, *process_all(block)])
68
+ end
47
69
  else
48
70
  # iterate over Enumerable arguments if there are args present
49
71
  send = send.children
@@ -58,6 +80,6 @@ module Ruby2JS
58
80
  end
59
81
  end
60
82
 
61
- DEFAULTS.push Wunderbar
83
+ DEFAULTS.push JSX
62
84
  end
63
85
  end
@@ -17,30 +17,39 @@
17
17
  # * ~"x" becomes document.querySelector("x")
18
18
  #
19
19
  require 'ruby2js'
20
+ require 'ruby2js/jsx'
20
21
 
21
22
  module Ruby2JS
22
23
  module Filter
23
24
  module React
24
25
  include SEXP
26
+ extend SEXP
27
+
28
+ REACT_IMPORTS = {
29
+ React: s(:import, ['React'], s(:attr, nil, :React)),
30
+ ReactDOM: s(:import, ['ReactDOM'], s(:attr, nil, :ReactDOM))
31
+ }
25
32
 
26
33
  # the following command can be used to generate ReactAttrs:
27
34
  #
28
35
  # ruby -r ruby2js/filter/react -e "Ruby2JS::Filter::React.genAttrs"
29
36
  #
30
37
  def self.genAttrs
31
- require 'nokogumbo'
32
- page = 'https://facebook.github.io/react/docs/tags-and-attributes.html'
33
- doc = Nokogiri::HTML5.get(page)
34
-
35
- # delete contents of page prior to the list of supported attributes
36
- attrs = doc.at('a[name=supported-attributes]')
37
- attrs = attrs.parent while attrs and not attrs.name.start_with? 'h'
38
- attrs.previous_sibling.remove while attrs and attrs.previous_sibling
39
-
40
- # extract attribute names with uppercase chars from code and format
41
- attrs = doc.search('code').map(&:text).join(' ')
42
- attrs = attrs.split(/\s+/).grep(/[A-Z]/).sort.uniq.join(' ')
43
- puts "ReactAttrs = %w(#{attrs})".gsub(/(.{1,72})(\s+|\Z)/, "\\1\n")
38
+ unless RUBY_ENGINE == 'opal'
39
+ require 'nokogumbo'
40
+ page = 'https://facebook.github.io/react/docs/tags-and-attributes.html'
41
+ doc = Nokogiri::HTML5.get(page)
42
+
43
+ # delete contents of page prior to the list of supported attributes
44
+ attrs = doc.at('a[name=supported-attributes]')
45
+ attrs = attrs.parent while attrs and not attrs.name.start_with? 'h'
46
+ attrs.previous_sibling.remove while attrs and attrs.previous_sibling
47
+
48
+ # extract attribute names with uppercase chars from code and format
49
+ attrs = doc.search('code').map(&:text).join(' ')
50
+ attrs = attrs.split(/\s+/).grep(/[A-Z]/).sort.uniq.join(' ')
51
+ puts "ReactAttrs = %w(#{attrs})".gsub(/(.{1,72})(\s+|\Z)/, "\\1\n")
52
+ end
44
53
  end
45
54
 
46
55
  # list of react attributes that require special processing
@@ -60,8 +69,13 @@ module Ruby2JS
60
69
  xlinkActuate xlinkArcrole xlinkHref xlinkRole xlinkShow xlinkTitle
61
70
  xlinkType xmlBase xmlLang xmlSpace)
62
71
 
72
+ ReactLifecycle = %w(render componentDidMount shouldComponentUpdate
73
+ getShapshotBeforeUpdate componentDidUpdate componentWillUnmount
74
+ componentDidCatch)
75
+
63
76
  ReactAttrMap = Hash[ReactAttrs.map {|name| [name.downcase, name]}]
64
77
  ReactAttrMap['for'] = 'htmlFor'
78
+ ReactFragment = :'_React.Fragment'
65
79
 
66
80
  def initialize(*args)
67
81
  @react = nil
@@ -71,6 +85,7 @@ module Ruby2JS
71
85
  @react_props = []
72
86
  @react_methods = []
73
87
  @react_filter_functions = false
88
+ @react_imports = false
74
89
  @jsx = false
75
90
  super
76
91
  end
@@ -88,8 +103,17 @@ module Ruby2JS
88
103
  end
89
104
 
90
105
  if \
91
- defined? Ruby2JS::Filter::Wunderbar and
92
- filters.include? Ruby2JS::Filter::Wunderbar
106
+ (defined? Ruby2JS::Filter::ESM and
107
+ filters.include? Ruby2JS::Filter::ESM) or
108
+ (defined? Ruby2JS::Filter::CJS and
109
+ filters.include? Ruby2JS::Filter::CJS)
110
+ then
111
+ @react_imports = true
112
+ end
113
+
114
+ if \
115
+ defined? Ruby2JS::Filter::JSX and
116
+ filters.include? Ruby2JS::Filter::JSX
93
117
  then
94
118
  @jsx = true
95
119
  end
@@ -106,8 +130,11 @@ module Ruby2JS
106
130
  return super unless cname.children.first == nil
107
131
  return super unless inheritance == s(:const, nil, :React) or
108
132
  inheritance == s(:const, nil, :Vue) or
133
+ inheritance == s(:const, s(:const, nil, :React), :Component) or
109
134
  inheritance == s(:send, s(:const, nil, :React), :Component)
110
135
 
136
+ prepend_list << REACT_IMPORTS[:React] if @react_imports
137
+
111
138
  # traverse down to actual list of class statements
112
139
  if body.length == 1
113
140
  if not body.first
@@ -156,24 +183,46 @@ module Ruby2JS
156
183
  end
157
184
  end
158
185
 
159
- # collect instance methods (including getters and setters)
186
+ # collect instance methods (including getters and setters)
160
187
  @react_props = []
161
188
  @react_methods = []
162
- body.each do |statement|
163
- if statement.type == :def
164
- method = statement.children.first
165
- unless method == :initialize
166
- if method.to_s.end_with? '='
167
- method = method.to_s[0..-2].to_sym
168
- @react_props << method unless @react_props.include? method
169
- elsif statement.is_method?
170
- @react_methods << method unless @react_methods.include? method
171
- else
172
- @react_props << method unless @react_props.include? method
173
- end
174
- end
175
- end
176
- end
189
+ body.each do |statement|
190
+ if statement.type == :def
191
+ method = statement.children.first
192
+ unless method == :initialize
193
+ if method.to_s.end_with? '='
194
+ method = method.to_s[0..-2].to_sym
195
+ @react_props << method unless @react_props.include? method
196
+ elsif statement.is_method?
197
+ @react_methods << method unless @react_methods.include? method
198
+ else
199
+ @react_props << method unless @react_props.include? method
200
+ end
201
+ end
202
+ end
203
+ end
204
+
205
+ # determine which instance methods need binding
206
+ needs_binding = []
207
+ scan_events = lambda do |list|
208
+ list.each do |node|
209
+ next unless Parser::AST::Node === node
210
+ node = process node if node.type == :xstr
211
+ if node.type == :hash
212
+ node.children.each do |pair|
213
+ value = pair.children.last
214
+ if value.type == :send and \
215
+ @react_methods.include? value.children[1] and \
216
+ [nil, s(:self), s(:send, nil, :this)].include? value.children[0]
217
+
218
+ needs_binding << value.children[1]
219
+ end
220
+ end
221
+ end
222
+ scan_events[node.children]
223
+ end
224
+ end
225
+ scan_events[body]
177
226
 
178
227
  # append statics (if any)
179
228
  unless statics.empty?
@@ -181,9 +230,9 @@ module Ruby2JS
181
230
  end
182
231
 
183
232
  # create a default getInitialState method if there is no such method
184
- # and there are references to instance variables.
233
+ # and there are either references to instance variables or there are
234
+ # methods that need to be bound.
185
235
  if \
186
- not es2015 and
187
236
  not body.any? do |child|
188
237
  child.type == :def and
189
238
  [:getInitialState, :initialize].include? child.children.first
@@ -191,9 +240,11 @@ module Ruby2JS
191
240
  then
192
241
  @reactIvars = {pre: [], post: [], asgn: [], ref: [], cond: []}
193
242
  react_walk(node)
194
- unless @reactIvars.values.flatten.empty?
243
+ if not es2015 and not @reactIvars.values.flatten.empty?
195
244
  body = [s(:def, :getInitialState, s(:args),
196
245
  s(:return, s(:hash))), *body]
246
+ elsif not needs_binding.empty? or not @reactIvars.values.flatten.empty?
247
+ body = [s(:def, :initialize, s(:args)), *body]
197
248
  end
198
249
  end
199
250
 
@@ -221,9 +272,22 @@ module Ruby2JS
221
272
  end
222
273
  end
223
274
 
275
+ # add props argument if there is a reference to a prop
276
+ if args.children.length == 0
277
+ has_cvar = lambda {|list|
278
+ list.any? {|node|
279
+ next unless Parser::AST::Node === node
280
+ return true if node.type == :cvar
281
+ has_cvar.call(node.children)
282
+ }
283
+ }
284
+ args = s(:args, s(:arg, 'prop$')) if has_cvar[block]
285
+ end
286
+
224
287
  # peel off the initial set of instance variable assignment stmts
225
288
  assigns = []
226
289
  block = block.dup
290
+ block.shift if block.first == s(:zsuper)
227
291
  while not block.empty? and block.first.type == :ivasgn
228
292
  node = block.shift
229
293
  vars = [node.children.first]
@@ -240,9 +304,15 @@ module Ruby2JS
240
304
  state = s(:hash, *assigns.map {|anode| s(:pair, s(:str,
241
305
  anode.children.first.to_s[1..-1]), anode.children.last)})
242
306
 
307
+ # bind methods as needed
308
+ needs_binding.each do |method|
309
+ block.push(s(:send, s(:self), "#{method}=",
310
+ s(:send, s(:attr, s(:self), method), :bind, s(:self))))
311
+ end
312
+
243
313
  # modify block to build and/or return state
244
314
  if mname == :initialize
245
- block.unshift(s(:send, s(:self), :state=, state))
315
+ block.unshift(s(:zsuper), s(:send, s(:self), :state=, state))
246
316
  elsif block.empty?
247
317
  block = [s(:return, state)]
248
318
  else
@@ -250,7 +320,7 @@ module Ruby2JS
250
320
  block.push(s(:return, s(:attr, s(:self), :state)))
251
321
  end
252
322
 
253
- elsif mname == :render
323
+ elsif mname == :render and not react_wunderbar_free(block, true)
254
324
  if \
255
325
  block.length != 1 or not block.last or
256
326
  not [:send, :block].include? block.last.type
@@ -271,9 +341,9 @@ module Ruby2JS
271
341
  block = [*prolog, s(:return,
272
342
  s(:xnode, '', *process_all(block)))]
273
343
  else
274
- # wrap multi-line blocks with a 'span' element
344
+ # wrap multi-line blocks with a React Fragment
275
345
  block = [s(:return,
276
- s(:block, s(:send, nil, :_span), s(:args), *block))]
346
+ s(:block, s(:send, nil, ReactFragment), s(:args), *block))]
277
347
  end
278
348
  end
279
349
 
@@ -300,7 +370,10 @@ module Ruby2JS
300
370
  end
301
371
 
302
372
  if es2015
303
- pairs << s(:def, mname, args, process(s(type, *block)))
373
+ pairs << child.updated(
374
+ ReactLifecycle.include?(mname.to_s) ? :defm : :def,
375
+ [mname, args, process(s(type, *block))]
376
+ )
304
377
  else
305
378
  pairs << s(:pair, s(:sym, mname), child.updated(:block,
306
379
  [s(:send, nil, :proc), args, process(s(type, *block))]))
@@ -369,8 +442,12 @@ module Ruby2JS
369
442
  # enable React filtering within React class method calls or
370
443
  # React component calls
371
444
  if \
372
- node.children.first == s(:const, nil, :React)
445
+ node.children.first == s(:const, nil, :React) or
446
+ node.children.first == s(:const, nil, :ReactDOM)
373
447
  then
448
+ if @react_imports
449
+ prepend_list << REACT_IMPORTS[node.children.first.children.last]
450
+ end
374
451
 
375
452
  begin
376
453
  react, @react = @react, true
@@ -429,6 +506,10 @@ module Ruby2JS
429
506
  # :block arguments are inserted by on_block logic below
430
507
  block = child
431
508
 
509
+ elsif child.type == :splat
510
+ # arrays need not be expanded
511
+ text = child.children.first
512
+
432
513
  else
433
514
  # everything else added as text
434
515
  text = child
@@ -586,6 +667,9 @@ module Ruby2JS
586
667
  next true if arg.children[1] == :createElement and
587
668
  arg.children[0] == s(:const, nil, :Vue)
588
669
 
670
+ # JSX
671
+ next true if arg.type == :xstr
672
+
589
673
  # wunderbar style call
590
674
  arg = arg.children.first if arg.type == :block
591
675
  while arg.type == :send and arg.children.first != nil
@@ -598,7 +682,19 @@ module Ruby2JS
598
682
  if simple
599
683
  # in the normal case, process each argument
600
684
  reactApply, @reactApply = @reactApply, false
601
- params += args.map {|arg| process(arg)}
685
+ args.each do |arg|
686
+ arg = process(arg)
687
+ if arg.type == :send and
688
+ arg.children[0] == s(:const, nil, :React) and
689
+ arg.children[1] == :createElement and
690
+ arg.children[2] == s(:const, nil, "React.Fragment") and
691
+ arg.children[3] == s(:nil)
692
+ then
693
+ params += arg.children[4..-1]
694
+ else
695
+ params << arg
696
+ end
697
+ end
602
698
  else
603
699
  reactApply, @reactApply = @reactApply, true
604
700
 
@@ -860,7 +956,15 @@ module Ruby2JS
860
956
  end
861
957
 
862
958
  # wunderbar style calls
863
- if !@jsx and child.children[0] == nil and child.children[1] =~ /^_\w/
959
+ if child.children[0] == nil and child.children[1] == :_ and \
960
+ node.children[1].children.empty? and !@jsx
961
+
962
+ block = s(:block, s(:send, nil, :proc), s(:args),
963
+ *node.children[2..-1])
964
+ return on_send node.children.first.updated(:send,
965
+ [nil, ReactFragment, block])
966
+
967
+ elsif !@jsx and child.children[0] == nil and child.children[1] =~ /^_\w/
864
968
  if node.children[1].children.empty?
865
969
  # append block as a standalone proc
866
970
  block = s(:block, s(:send, nil, :proc), s(:args),
@@ -871,9 +975,18 @@ module Ruby2JS
871
975
  # iterate over Enumerable arguments if there are args present
872
976
  send = node.children.first.children
873
977
  return super if send.length < 3
874
- return process s(:block, s(:send, *send[0..1], *send[3..-1]),
875
- s(:args), s(:block, s(:send, send[2], :forEach),
876
- *node.children[1..-1]))
978
+ if node.children.length == 3 and
979
+ node.children.last.respond_to? :type and
980
+ node.children.last.type == :send
981
+
982
+ return process s(:send, *send[0..1], *send[3..-1],
983
+ s(:splat, s(:block, s(:send, send[2], :map),
984
+ node.children[1], s(:return, node.children[2]))))
985
+ else
986
+ return process s(:block, s(:send, *send[0..1], *send[3..-1]),
987
+ s(:args), s(:block, s(:send, send[2], :forEach),
988
+ *node.children[1..-1]))
989
+ end
877
990
  end
878
991
  end
879
992
 
@@ -885,6 +998,13 @@ module Ruby2JS
885
998
  end
886
999
  end
887
1000
 
1001
+ def on_lvasgn(node)
1002
+ return super unless @reactClass
1003
+ return super unless @react_props.include? node.children.first
1004
+ node.updated(:send, [s(:self), "#{node.children.first}=",
1005
+ node.children.last])
1006
+ end
1007
+
888
1008
  # convert global variables to refs
889
1009
  def on_gvar(node)
890
1010
  return super unless @reactClass
@@ -993,7 +1113,7 @@ module Ruby2JS
993
1113
  end
994
1114
 
995
1115
  # is this a "wunderbar" style call or createElement?
996
- def react_element?(node)
1116
+ def react_element?(node, wunderbar_only=false)
997
1117
  return false unless node
998
1118
 
999
1119
  forEach = [:forEach]
@@ -1001,15 +1121,17 @@ module Ruby2JS
1001
1121
 
1002
1122
  return true if node.type == :block and
1003
1123
  forEach.include? node.children.first.children.last and
1004
- react_element?(node.children.last)
1124
+ react_element?(node.children.last, wunderbar_only)
1005
1125
 
1006
- # explicit call to React.createElement
1007
- return true if node.children[1] == :createElement and
1008
- node.children[0] == s(:const, nil, :React)
1126
+ unless wunderbar_only
1127
+ # explicit call to React.createElement
1128
+ return true if node.children[1] == :createElement and
1129
+ node.children[0] == s(:const, nil, :React)
1009
1130
 
1010
- # explicit call to Vue.createElement
1011
- return true if node.children[1] == :createElement and
1012
- node.children[0] == s(:const, nil, :Vue)
1131
+ # explicit call to Vue.createElement
1132
+ return true if node.children[1] == :createElement and
1133
+ node.children[0] == s(:const, nil, :Vue)
1134
+ end
1013
1135
 
1014
1136
  # wunderbar style call
1015
1137
  node = node.children.first if node.type == :block
@@ -1021,13 +1143,14 @@ module Ruby2JS
1021
1143
 
1022
1144
  # ensure that there are no "wunderbar" or "createElement" calls in
1023
1145
  # a set of statements.
1024
- def react_wunderbar_free(nodes)
1146
+ def react_wunderbar_free(nodes, wunderbar_only=false)
1025
1147
  nodes.each do |node|
1026
1148
  if Parser::AST::Node === node
1027
- return false if react_element?(node)
1149
+ return false if node.type == :xstr
1150
+ return false if react_element?(node, wunderbar_only)
1028
1151
 
1029
1152
  # recurse
1030
- return false unless react_wunderbar_free(node.children)
1153
+ return false unless react_wunderbar_free(node.children, wunderbar_only)
1031
1154
  end
1032
1155
  end
1033
1156
 
@@ -1181,6 +1304,18 @@ module Ruby2JS
1181
1304
 
1182
1305
  block
1183
1306
  end
1307
+
1308
+ def on_xstr(node)
1309
+ loc = node.loc
1310
+ return super unless loc
1311
+ source = loc.begin.source_buffer.source
1312
+ source = source[loc.begin.end_pos...loc.end.begin_pos].strip
1313
+ return super unless @reactClass or source.start_with? '<'
1314
+ source = Ruby2JS.jsx2_rb(source)
1315
+ ast = Ruby2JS.parse(source).first
1316
+ ast = s(:block, s(:send, nil, :_), s(:args), ast) if ast.type == :begin
1317
+ process ast
1318
+ end
1184
1319
  end
1185
1320
 
1186
1321
  DEFAULTS.push React