ruby2js 3.6.1 → 4.0.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.
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