opal-vite 0.3.13 → 0.3.15

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67f79084fe35586cb8dc0f98103778f6fe8a87a14876dc94d974c5ab434be56d
4
- data.tar.gz: 4f4f7c07407bd93599f9230168992cd16af6f61a708fab7db12a3229300a1f55
3
+ metadata.gz: 3ff5610634e2852d6053397650789a4873d1dfa2faf891c634503849531a0a0c
4
+ data.tar.gz: f8169727ae99fb3faa4f9bd75e48786b667b1ad2c4cd261a985b411c60bbea6c
5
5
  SHA512:
6
- metadata.gz: 2ef2c6d46990c04cc7b7d23267d255a384d4d6b263f7a1d83d47f2a7c82969796edd7d6e1ebcfa6cf209fd1eb53ff83cde108aced69e387b27e7ff9274a08a38
7
- data.tar.gz: 11497fc562b1fc9a1ca9836cbe218988e771576a3bb2258915ceb7440c91aabfeecea843cea34a9f861f2c30498084574ccf94c10a311301c2bdb7130025761c
6
+ metadata.gz: 485d95f570220009668240c82cf363dbf092b743601045b78820c1b0628f041d4b1bda86393eaac16156261cd794190d062392f2303af0338b89646658f3c62c
7
+ data.tar.gz: e2970ff1eaaa5828ccc128f564aab1a1ce8b628300d480731aae8f6eaee6df094c2253940914b39049ebc5424efd188c316bfed4af0bba3a90c1fdc42e8d7d44
@@ -12,6 +12,10 @@ module Opal
12
12
  @config = options[:config] || Opal::Vite.config
13
13
  @include_concerns = options.fetch(:include_concerns, true)
14
14
  @stubs = options.fetch(:stubs, [])
15
+ # When true, Opal's corelib is treated as already loaded and left out
16
+ # of the output. The Vite plugin then imports the shared runtime
17
+ # (`/@opal-runtime`) instead, so corelib is only loaded once.
18
+ @external_runtime = options.fetch(:external_runtime, false)
15
19
  end
16
20
 
17
21
  # Compile Ruby source code to JavaScript
@@ -20,6 +24,7 @@ module Opal
20
24
  begin
21
25
  # Use Opal::Builder and add the file's directory to load paths
22
26
  builder = Opal::Builder.new(stubs: @stubs)
27
+ builder.prerequired = self.class.runtime_requires if @external_runtime
23
28
 
24
29
  # Add the directory containing the file to load paths
25
30
  # This allows require statements to work relative to the file
@@ -68,10 +73,24 @@ module Opal
68
73
  end
69
74
 
70
75
  # Get the Opal runtime code
76
+ #
77
+ # The runtime's entry file (opal.rb) is not registered as a requirable
78
+ # module, so mark it as loaded explicitly. Otherwise `require 'opal'` in
79
+ # a bundle compiled with external_runtime raises LoadError at runtime.
71
80
  def self.runtime_code
72
81
  builder = Opal::Builder.new
73
82
  builder.build('opal')
74
- builder.to_s
83
+ "#{builder.to_s}\nOpal.loaded([\"opal\"]);\n"
84
+ end
85
+
86
+ # Logical require paths that are part of runtime_code (opal itself and
87
+ # the corelib files it pulls in).
88
+ def self.runtime_requires
89
+ @runtime_requires ||= begin
90
+ builder = Opal::Builder.new
91
+ builder.build('opal')
92
+ ['opal', *builder.already_processed.to_a].freeze
93
+ end
75
94
  end
76
95
 
77
96
  private
@@ -1,5 +1,5 @@
1
1
  module Opal
2
2
  module Vite
3
- VERSION = "0.3.13"
3
+ VERSION = "0.3.15"
4
4
  end
5
5
  end
data/lib/opal-vite.rb CHANGED
@@ -32,12 +32,14 @@ module Opal
32
32
  # @param include_concerns [Boolean] Whether to include built-in concerns
33
33
  # @param source_map [Boolean] Whether to generate source maps
34
34
  # @param stubs [Array<String>] List of modules to stub (return empty implementations)
35
- def compile_for_vite(file_path, include_concerns: true, source_map: true, stubs: [])
35
+ # @param external_runtime [Boolean] Leave Opal's corelib out of the output
36
+ # (the caller loads it separately, e.g. via the `/@opal-runtime` module)
37
+ def compile_for_vite(file_path, include_concerns: true, source_map: true, stubs: [], external_runtime: false)
36
38
  # Temporarily override source map setting if specified
37
39
  original_source_map = config.source_map_enabled
38
40
  config.source_map_enabled = source_map
39
41
 
40
- compiler = Compiler.new(include_concerns: include_concerns, stubs: stubs)
42
+ compiler = Compiler.new(include_concerns: include_concerns, stubs: stubs, external_runtime: external_runtime)
41
43
  result = compiler.compile_file(file_path)
42
44
 
43
45
  # Output JSON to stdout for the Vite plugin to consume
@@ -0,0 +1,168 @@
1
+ # backtick_javascript: true
2
+ #
3
+ # Fixes for opal_stimulus (0.2.x) accessors that look up the wrong
4
+ # JavaScript property for multi-word names.
5
+ #
6
+ # opal_stimulus builds names such as "hasSoundButtonTarget" with
7
+ # String#capitalize, which lowercases the rest of the word ("hasSoundbuttonTarget"),
8
+ # and passes snake_case value names straight to Stimulus. As a result:
9
+ #
10
+ # - has_*_target / has_*_outlet / has_*_class always return undefined for
11
+ # multi-word names
12
+ # - multi-word values read and write the wrong property, use the wrong
13
+ # data attribute, and never fire *_value_changed
14
+ # - dashed outlet names (e.g. "user-status") read the wrong property and never
15
+ # fire *_outlet_connected / *_outlet_disconnected
16
+ #
17
+ # Usage (after opal_stimulus, before defining controllers):
18
+ #
19
+ # require 'opal_stimulus/stimulus_controller'
20
+ # require 'opal_vite/compat/opal_stimulus'
21
+ #
22
+ # Values declared as `self.values = { latest_paid: :number }` then map to
23
+ # Stimulus' latestPaidValue / data-<controller>-latest-paid-value, as they
24
+ # would in a JavaScript controller.
25
+ require 'opal_stimulus/stimulus_controller'
26
+
27
+ module OpalVite
28
+ module Compat
29
+ module OpalStimulus
30
+ # "soundButton" -> "SoundButton" (String#capitalize gives "Soundbutton")
31
+ def self.upcase_first(name)
32
+ str = name.to_s
33
+ str.empty? ? str : str[0].upcase + str[1..-1]
34
+ end
35
+
36
+ # Stimulus' camelize: "latest_paid" / "user-status" -> "latestPaid" / "userStatus"
37
+ def self.camelize(name)
38
+ name.to_s.gsub(/[_-]([a-z0-9])/) { $1.upcase }
39
+ end
40
+
41
+ # Stimulus' namespaceCamelize for outlet names ("admin--user-status" -> "adminUserStatus")
42
+ def self.namespace_camelize(name)
43
+ camelize(name.to_s.gsub('--', '-').gsub('__', '_'))
44
+ end
45
+
46
+ module ClassMethods
47
+ def targets=(targets = [])
48
+ super
49
+
50
+ targets.each do |target|
51
+ has_prop = "has#{OpalStimulus.upcase_first(target)}Target"
52
+ define_method("has_#{to_ruby_name(target)}_target") do
53
+ `this[#{has_prop}]`
54
+ end
55
+ end
56
+ end
57
+
58
+ def outlets=(outlets = [])
59
+ super
60
+
61
+ outlets.each do |outlet|
62
+ ruby_name = to_ruby_name(outlet).tr('-', '_')
63
+ js_name = OpalStimulus.namespace_camelize(outlet)
64
+ outlet_prop = "#{js_name}Outlet"
65
+ outlets_prop = "#{js_name}Outlets"
66
+ has_prop = "has#{OpalStimulus.upcase_first(js_name)}Outlet"
67
+
68
+ define_method("#{ruby_name}_outlet") do
69
+ `this[#{outlet_prop}]`
70
+ end
71
+
72
+ define_method("#{ruby_name}_outlets") do
73
+ `this[#{outlets_prop}]`
74
+ end
75
+
76
+ define_method("has_#{ruby_name}_outlet") do
77
+ `this[#{has_prop}]`
78
+ end
79
+
80
+ # Stimulus calls the camelized callbacks; forward them to Ruby.
81
+ ["connected", "disconnected"].each do |event|
82
+ snake_case_callback = "#{ruby_name}_outlet_#{event}"
83
+ camel_case_callback = "#{js_name}Outlet#{OpalStimulus.upcase_first(event)}"
84
+ %x{
85
+ #{stimulus_controller}.prototype[#{camel_case_callback}] = function(outlet, element) {
86
+ if (this['$respond_to?'] && this['$respond_to?'](#{snake_case_callback})) {
87
+ return this['$' + #{snake_case_callback}](outlet, element);
88
+ }
89
+ }
90
+ }
91
+ end
92
+ end
93
+ end
94
+
95
+ # Stimulus uses the class key as-is for xxxClass / xxxClasses, so only
96
+ # the has_ accessor needs fixing.
97
+ def classes=(class_names = [])
98
+ super
99
+
100
+ class_names.each do |class_name|
101
+ has_prop = "has#{OpalStimulus.upcase_first(class_name)}Class"
102
+ define_method("has_#{to_ruby_name(class_name)}_class") do
103
+ `this[#{has_prop}]`
104
+ end
105
+ end
106
+ end
107
+
108
+ # Replaces (does not wrap) the original: the original registers
109
+ # snake_case keys with Stimulus, which changes the data attribute
110
+ # name, so it cannot be fixed after the fact.
111
+ def values=(values_hash = {})
112
+ js_values = {}
113
+
114
+ values_hash.each do |name, type|
115
+ ruby_name = to_ruby_name(name)
116
+ js_name = OpalStimulus.camelize(ruby_name)
117
+
118
+ js_type = case type
119
+ when :string then `String`
120
+ when :number then `Number`
121
+ when :boolean then `Boolean`
122
+ when :array then `Array`
123
+ when :object then `Object`
124
+ else
125
+ raise ArgumentError,
126
+ "Unsupported value type: #{type}, please use :string, :number, :boolean, :array, or :object"
127
+ end
128
+
129
+ js_values[js_name] = js_type
130
+
131
+ value_prop = "#{js_name}Value"
132
+ has_prop = "has#{OpalStimulus.upcase_first(js_name)}Value"
133
+
134
+ define_method("#{ruby_name}_value") do
135
+ Native(`this[#{value_prop}]`)
136
+ end
137
+
138
+ define_method("#{ruby_name}_value=") do |value|
139
+ Native(`this[#{value_prop}] = #{value}`)
140
+ end
141
+
142
+ define_method("has_#{ruby_name}") do
143
+ `this[#{has_prop}]`
144
+ end
145
+
146
+ snake_case_changed = "#{ruby_name}_value_changed"
147
+ camel_case_changed = "#{js_name}ValueChanged"
148
+ %x{
149
+ #{stimulus_controller}.prototype[#{camel_case_changed}] = function(value, previousValue) {
150
+ if (#{type == :object}) {
151
+ value = JSON.stringify(value)
152
+ previousValue = JSON.stringify(previousValue)
153
+ }
154
+ if (this['$respond_to?'] && this['$respond_to?'](#{snake_case_changed})) {
155
+ return this['$' + #{snake_case_changed}](value, previousValue);
156
+ }
157
+ }
158
+ }
159
+ end
160
+
161
+ `#{stimulus_controller}.values = #{js_values.to_n}`
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ StimulusController.singleton_class.prepend(OpalVite::Compat::OpalStimulus::ClassMethods)
@@ -318,7 +318,7 @@ module OpalVite
318
318
  json_str = cable_data(data, key)
319
319
  return nil if json_str.nil?
320
320
  `JSON.parse(#{json_str})`
321
- rescue
321
+ rescue Exception
322
322
  nil
323
323
  end
324
324
 
@@ -29,7 +29,7 @@ module OpalVite
29
29
  # @return [String] Base64 encoded string
30
30
  def base64_encode(str)
31
31
  `btoa(#{str})`
32
- rescue
32
+ rescue Exception
33
33
  nil
34
34
  end
35
35
 
@@ -38,7 +38,7 @@ module OpalVite
38
38
  # @return [String] Decoded string
39
39
  def base64_decode(str)
40
40
  `atob(#{str})`
41
- rescue
41
+ rescue Exception
42
42
  nil
43
43
  end
44
44
 
@@ -81,7 +81,7 @@ module OpalVite
81
81
  def base64_encode_unicode(str)
82
82
  # Convert to UTF-8 bytes, then encode
83
83
  `btoa(unescape(encodeURIComponent(#{str})))`
84
- rescue
84
+ rescue Exception
85
85
  nil
86
86
  end
87
87
 
@@ -90,7 +90,7 @@ module OpalVite
90
90
  # @return [String] Decoded Unicode string
91
91
  def base64_decode_unicode(str)
92
92
  `decodeURIComponent(escape(atob(#{str})))`
93
- rescue
93
+ rescue Exception
94
94
  nil
95
95
  end
96
96
 
@@ -108,7 +108,7 @@ module OpalVite
108
108
  }
109
109
  return btoa(binary);
110
110
  `
111
- rescue
111
+ rescue Exception
112
112
  nil
113
113
  end
114
114
 
@@ -124,7 +124,7 @@ module OpalVite
124
124
  }
125
125
  return bytes;
126
126
  `
127
- rescue
127
+ rescue Exception
128
128
  nil
129
129
  end
130
130
 
@@ -202,7 +202,7 @@ module OpalVite
202
202
  return nil unless payload_json
203
203
 
204
204
  `JSON.parse(#{payload_json})`
205
- rescue
205
+ rescue Exception
206
206
  nil
207
207
  end
208
208
 
@@ -264,7 +264,9 @@ module OpalVite
264
264
 
265
265
  len = str.length
266
266
  padding = str.end_with?('==') ? 2 : (str.end_with?('=') ? 1 : 0)
267
- (len * 3 / 4) - padding
267
+ # Use integer division: in Opal `len * 3 / 4` is JS float division, so
268
+ # unpadded/URL-safe input (len % 4 == 2 or 3) would yield e.g. 16.5.
269
+ ((len * 3) / 4).floor - padding
268
270
  end
269
271
  end
270
272
  end
@@ -275,7 +275,10 @@ module OpalVite
275
275
 
276
276
  # Stringify to JSON
277
277
  def to_json(object)
278
- `JSON.stringify(#{object})`
278
+ # Convert Ruby Hash/Array to a native JS value first; passing a raw
279
+ # Opal Hash to JSON.stringify serializes to "{}" (silent data loss).
280
+ native = object.respond_to?(:to_n) ? object.to_n : object
281
+ `JSON.stringify(#{native})`
279
282
  end
280
283
 
281
284
  # ===================
@@ -857,11 +857,12 @@ module OpalVite
857
857
  end
858
858
 
859
859
  # Call a method on a JavaScript object
860
- # @param obj [Native] JavaScript object
860
+ # @param obj [Object] JavaScript object (raw, Native or JS::Proxy)
861
861
  # @param method [Symbol, String] Method name
862
862
  # @param args [Array] Arguments to pass
863
863
  # @return [Native] Method return value
864
864
  def js_call_on(obj, method, *args)
865
+ obj = to_native_element(obj)
865
866
  if args.empty?
866
867
  `#{obj}[#{method.to_s}]()`
867
868
  else
@@ -871,21 +872,40 @@ module OpalVite
871
872
  end
872
873
 
873
874
  # Get a property from a JavaScript object
874
- # @param obj [Native] JavaScript object
875
+ #
876
+ # Unlike calling the property as a method on a JS::Proxy (e.g. the
877
+ # elements opal_stimulus' `*_target` methods return), a missing property
878
+ # does not raise NoMethodError; you get JavaScript's undefined.
879
+ #
880
+ # @param obj [Object] JavaScript object (raw, Native or JS::Proxy)
875
881
  # @param prop [Symbol, String] Property name
876
- # @return [Native] Property value
882
+ # @return [Object] Raw property value (may be undefined)
877
883
  def js_get(obj, prop)
884
+ obj = to_native_element(obj)
878
885
  `#{obj}[#{prop.to_s}]`
879
886
  end
880
887
 
881
888
  # Set a property on a JavaScript object
882
- # @param obj [Native] JavaScript object
889
+ # @param obj [Object] JavaScript object (raw, Native or JS::Proxy)
883
890
  # @param prop [Symbol, String] Property name
884
891
  # @param value [Object] Value to set
885
892
  def js_set(obj, prop, value)
893
+ obj = to_native_element(obj)
886
894
  `#{obj}[#{prop.to_s}] = #{value}`
887
895
  end
888
896
 
897
+ # Read a data-* attribute through element.dataset
898
+ # @param element [Object] DOM element (raw, Native or JS::Proxy)
899
+ # @param key [Symbol, String] Attribute name without "data-", in
900
+ # snake_case or camelCase (:confirm_message and :confirmMessage both
901
+ # read data-confirm-message)
902
+ # @return [String, nil] The attribute value, or nil when it is absent
903
+ def dataset_value(element, key)
904
+ el = to_native_element(element)
905
+ name = camelize(key, false)
906
+ `(#{el}.dataset[#{name}] == null) ? nil : #{el}.dataset[#{name}]`
907
+ end
908
+
889
909
  # ===== JSON Methods =====
890
910
 
891
911
  # Parse JSON string to JavaScript object
@@ -11,7 +11,7 @@ module OpalVite
11
11
 
12
12
  begin
13
13
  `JSON.parse(stored)`
14
- rescue
14
+ rescue Exception
15
15
  nil
16
16
  end
17
17
  end
@@ -356,7 +356,13 @@ module OpalVite
356
356
  # source = turbo_stream_from("/notifications/stream")
357
357
  # # Turbo will automatically process incoming streams
358
358
  def turbo_stream_from(url)
359
- `window.Turbo.connectStreamSource(new EventSource(#{url}))`
359
+ # Keep a reference to the EventSource and return it: connectStreamSource
360
+ # returns undefined, so returning its result would make the documented
361
+ # `turbo_stream_disconnect(source)` call `undefined.close()` and leak
362
+ # the SSE connection.
363
+ source = `new EventSource(#{url})`
364
+ `window.Turbo.connectStreamSource(#{source})`
365
+ source
360
366
  end
361
367
 
362
368
  # Disconnect a Turbo Stream SSE source
@@ -27,7 +27,7 @@ module OpalVite
27
27
  # @return [Native] JavaScript URL object
28
28
  def parse_url(url_string)
29
29
  `new URL(#{url_string})`
30
- rescue
30
+ rescue Exception
31
31
  nil
32
32
  end
33
33
 
@@ -37,7 +37,7 @@ module OpalVite
37
37
  # @return [Native] JavaScript URL object
38
38
  def parse_url_with_base(url_string, base)
39
39
  `new URL(#{url_string}, #{base})`
40
- rescue
40
+ rescue Exception
41
41
  nil
42
42
  end
43
43
 
@@ -225,7 +225,7 @@ module OpalVite
225
225
  # @return [String] Decoded string
226
226
  def decode_uri_component(str)
227
227
  `decodeURIComponent(#{str})`
228
- rescue
228
+ rescue Exception
229
229
  str
230
230
  end
231
231
 
@@ -241,7 +241,7 @@ module OpalVite
241
241
  # @return [String] Decoded URI
242
242
  def decode_uri(str)
243
243
  `decodeURI(#{str})`
244
- rescue
244
+ rescue Exception
245
245
  str
246
246
  end
247
247
 
@@ -315,7 +315,10 @@ module OpalVite
315
315
 
316
316
  # Stringify to JSON
317
317
  def to_json_string(object)
318
- `JSON.stringify(#{object})`
318
+ # Convert Ruby Hash/Array to a native JS value first; passing a raw
319
+ # Opal Hash to JSON.stringify serializes to "{}" (silent data loss).
320
+ native = object.respond_to?(:to_n) ? object.to_n : object
321
+ `JSON.stringify(#{native})`
319
322
  end
320
323
 
321
324
  # ===================
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opal-vite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.13
4
+ version: 0.3.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - stofu1234
@@ -112,6 +112,7 @@ files:
112
112
  - lib/opal/vite/testing/stable_helpers.rb
113
113
  - lib/opal/vite/version.rb
114
114
  - opal/opal_vite.rb
115
+ - opal/opal_vite/compat/opal_stimulus.rb
115
116
  - opal/opal_vite/concerns.rb
116
117
  - opal/opal_vite/concerns/dom_helpers.rb
117
118
  - opal/opal_vite/concerns/js_proxy_ex.rb