opal-vite 0.3.14 → 0.3.16

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: 70f625bc74c24b95f12fb5fbfddd5f5aee028c1f1953affb73d9bfab639deedf
4
- data.tar.gz: 416c5805372e4ff28a55005ec0d0fc0cb23e2d2d76cfe81db04e1da5131ac4d5
3
+ metadata.gz: 3c20b89cec58a20be779bfbfd9bbffdf4c2dc337fa654e82791d346130a8f8f1
4
+ data.tar.gz: a51c0fe478c873b9aacd5df3080d06b3173cafa5b7b08225be113cb24471b90d
5
5
  SHA512:
6
- metadata.gz: 97ecab260b4b8284f5a07a5bdbb9ede917e5708193348d40c77b2d953be852b3f6b97a5a7405082ce2aec62c3abae0561c9a04960f599d622af8ea1ce801edc0
7
- data.tar.gz: d51d3a7602afbbc9e3d5cc3898f31f95a9f0c731635074e3d9fe88ae731aadf676f6a9d8e39f5ece06cedca44998fac708983311068fcb38c8c5f2f2a11dc679
6
+ metadata.gz: e270c06cdd81265b491f9f51b55493438138029d3cd106e482870542dd4479a355cae65042b017c18c2943311f1cd83b45137bd0332bcc685c182fe82556a6ad
7
+ data.tar.gz: 874b556449b4603cdf752af695f4cfa60b42d9b659d0d17b1e786748d14f345b5e967c343f7d3aa36913944391dd026320ad3f8ca9b7ce524731ddcabc80c333
@@ -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
@@ -107,6 +126,10 @@ module Opal
107
126
  end
108
127
  end
109
128
 
129
+ # opal-vite's own opal/ directory holds the built-in concerns; it is
130
+ # on $LOAD_PATH like any gem, so drop it here when they are disabled.
131
+ gem_opal_paths.delete(File.expand_path(Opal::Vite.opal_lib_path)) unless @include_concerns
132
+
110
133
  # Add opal directories FIRST so they take priority over lib directories
111
134
  # This ensures that 'require "inesita"' finds opal/inesita.rb before lib/inesita.rb
112
135
  gem_opal_paths.uniq.each do |path|
@@ -224,6 +224,22 @@ module Opal
224
224
  js_poll_until(js_condition, timeout: timeout, error_message: "Condition not met: #{js_condition}")
225
225
  end
226
226
 
227
+ # Wait until every Stimulus controller declared on the page is connected
228
+ # (expects the Application on window.Stimulus). Checking for elements or
229
+ # targets is not enough: they are in the static HTML before Opal has
230
+ # registered the controllers, so tests could act before connect() ran.
231
+ # @param timeout [Integer] Maximum wait time in seconds
232
+ def wait_for_stimulus_connected(timeout: DEFAULT_TIMEOUT)
233
+ js_poll_until(<<~JS.strip, timeout: timeout, error_message: "Stimulus controllers not connected within #{timeout}s")
234
+ (typeof Stimulus !== 'undefined') &&
235
+ Array.from(document.querySelectorAll('[data-controller]')).every(function(el) {
236
+ return el.getAttribute('data-controller').split(/\\s+/).filter(Boolean).every(function(id) {
237
+ return !!Stimulus.getControllerForElementAndIdentifier(el, id);
238
+ });
239
+ })
240
+ JS
241
+ end
242
+
227
243
  # Wait for DOM to be stable (no pending mutations)
228
244
  # Uses MutationObserver setup in JS with Ruby-based polling
229
245
  # @param timeout [Integer] Maximum wait time in seconds
@@ -1,5 +1,5 @@
1
1
  module Opal
2
2
  module Vite
3
- VERSION = "0.3.14"
3
+ VERSION = "0.3.16"
4
4
  end
5
5
  end
data/lib/opal-vite.rb CHANGED
@@ -24,7 +24,8 @@ module Opal
24
24
  # Returns the path to the opal/ directory in this gem
25
25
  # Contains built-in concerns like StimulusHelpers
26
26
  def opal_lib_path
27
- File.expand_path('../../opal', __dir__)
27
+ # This file is lib/opal-vite.rb, so opal/ is one level up
28
+ File.expand_path('../opal', __dir__)
28
29
  end
29
30
 
30
31
  # CLI entry point for compilation
@@ -32,12 +33,14 @@ module Opal
32
33
  # @param include_concerns [Boolean] Whether to include built-in concerns
33
34
  # @param source_map [Boolean] Whether to generate source maps
34
35
  # @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: [])
36
+ # @param external_runtime [Boolean] Leave Opal's corelib out of the output
37
+ # (the caller loads it separately, e.g. via the `/@opal-runtime` module)
38
+ def compile_for_vite(file_path, include_concerns: true, source_map: true, stubs: [], external_runtime: false)
36
39
  # Temporarily override source map setting if specified
37
40
  original_source_map = config.source_map_enabled
38
41
  config.source_map_enabled = source_map
39
42
 
40
- compiler = Compiler.new(include_concerns: include_concerns, stubs: stubs)
43
+ compiler = Compiler.new(include_concerns: include_concerns, stubs: stubs, external_runtime: external_runtime)
41
44
  result = compiler.compile_file(file_path)
42
45
 
43
46
  # 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)
@@ -217,7 +217,10 @@ module OpalVite
217
217
  # button({ onClick: set_count.with { |c| c + 1 } }, '+')
218
218
  def with(&block)
219
219
  setter_fn = @setter_fn
220
- `function() {
220
+ # Opal emits a multi-line x-string as a statement (no implicit
221
+ # return), and a bare anonymous function statement is a
222
+ # JavaScript syntax error, so return explicitly.
223
+ `return function() {
221
224
  #{setter_fn}(function(current) {
222
225
  return #{block.call(`current`)};
223
226
  });
@@ -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
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.14
4
+ version: 0.3.16
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