opal-vite 0.1.0 → 0.2.1

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: b141645cbef98169c750427f39d902e6f8377430fb41ec282ccfdaf9ff6bc5a5
4
- data.tar.gz: ec0717edcaf168380c5bd92419d84d2e14f12d41430a373d45c7f095109d2b82
3
+ metadata.gz: a747844bb38869490030ea9a7649cdd6f5b665cf2a60da55f3cc12f479aa556b
4
+ data.tar.gz: 8e0de9d6b0b124f5ee0cb83cfd242199785c6a7ecdfb1f2e3ba0f7253bb5c663
5
5
  SHA512:
6
- metadata.gz: e27461464983db947b29e81c64da1ef4d5872c45709e66e078c315dc9133c15ec453b88d596788a5321a235e604da1c7c7f1eefb87240574566da709d933cbbb
7
- data.tar.gz: 8d70ec17da3cb04a529e55640aec9e8ed6efc6bc1acdf4aad0ac93949b83593820a18d624c86db11df764b3c5481cc2228828617c71e8694005dc4e0abead6f2
6
+ metadata.gz: d12af9028e4ca9537a4af9043a74fc3dd3cf043a0ed928ad199a5bf36625e2d3a0d392d7e45ac8f49832edb5c409664334ddb60571f794511156a4d50332644c
7
+ data.tar.gz: '083ab7d7c102a315c9253491790014b6a5af41996bf81190d4a3f2c56bce9e503a3d204a0013866d2f9a956d311b9136bc2f5b86a317350d9f9585fe9ffc0299'
@@ -9,6 +9,7 @@ module Opal
9
9
  def initialize(options = {})
10
10
  @options = options
11
11
  @config = options[:config] || Opal::Vite.config
12
+ @include_concerns = options.fetch(:include_concerns, true)
12
13
  end
13
14
 
14
15
  # Compile Ruby source code to JavaScript
@@ -27,6 +28,9 @@ module Opal
27
28
  parent_dir = File.dirname(file_dir)
28
29
  builder.append_paths(parent_dir)
29
30
 
31
+ # Add gem paths from $LOAD_PATH so Opal can find gems
32
+ add_gem_paths(builder)
33
+
30
34
  builder.build_str(source, file_path)
31
35
 
32
36
  result = {
@@ -72,6 +76,62 @@ module Opal
72
76
 
73
77
  private
74
78
 
79
+ def add_gem_paths(builder)
80
+ # Add gem directories from $LOAD_PATH to Opal's load paths
81
+ # This allows Opal to find and compile gems like inesita
82
+
83
+ # Add opal-vite's built-in concerns if enabled
84
+ if @include_concerns
85
+ opal_vite_opal_path = Opal::Vite.opal_lib_path
86
+ if File.directory?(opal_vite_opal_path)
87
+ builder.append_paths(opal_vite_opal_path) unless builder.path_reader.paths.include?(opal_vite_opal_path)
88
+ end
89
+ end
90
+
91
+ # First collect all gem paths
92
+ gem_lib_paths = []
93
+ gem_opal_paths = []
94
+
95
+ $LOAD_PATH.each do |path|
96
+ # Only process gem directories
97
+ if path.include?('/gems/') && File.directory?(path)
98
+ gem_lib_paths << path
99
+
100
+ # Check if gem has an opal directory (for gems like inesita)
101
+ # Gems with opal-specific code often have an 'opal' directory sibling to 'lib'
102
+ gem_root = File.dirname(path)
103
+ opal_path = File.join(gem_root, 'opal')
104
+ if File.directory?(opal_path)
105
+ gem_opal_paths << opal_path
106
+ end
107
+ end
108
+ end
109
+
110
+ # Add opal directories FIRST so they take priority over lib directories
111
+ # This ensures that 'require "inesita"' finds opal/inesita.rb before lib/inesita.rb
112
+ gem_opal_paths.uniq.each do |path|
113
+ builder.append_paths(path) unless builder.path_reader.paths.include?(path)
114
+ end
115
+
116
+ # Then add regular lib directories, but ONLY for Opal-compatible gems
117
+ # to avoid pulling in Rails/server-side dependencies
118
+ gem_lib_paths.uniq.each do |path|
119
+ # Only add paths from gems that:
120
+ # 1. Have an 'opal' directory (already processed above)
121
+ # 2. OR have 'opal' in their gem name
122
+ gem_root = File.dirname(path)
123
+ gem_name = File.basename(gem_root)
124
+
125
+ # Check if this gem has opal support
126
+ is_opal_gem = gem_name.start_with?('opal') ||
127
+ gem_opal_paths.any? { |opal_path| opal_path.start_with?(gem_root) }
128
+
129
+ if is_opal_gem
130
+ builder.append_paths(path) unless builder.path_reader.paths.include?(path)
131
+ end
132
+ end
133
+ end
134
+
75
135
  def compiler_options
76
136
  @config.to_compiler_options
77
137
  end
@@ -1,5 +1,5 @@
1
1
  module Opal
2
2
  module Vite
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.1"
4
4
  end
5
5
  end
data/lib/opal-vite.rb CHANGED
@@ -21,9 +21,17 @@ module Opal
21
21
  yield(config) if block_given?
22
22
  end
23
23
 
24
+ # Returns the path to the opal/ directory in this gem
25
+ # Contains built-in concerns like StimulusHelpers
26
+ def opal_lib_path
27
+ File.expand_path('../../opal', __dir__)
28
+ end
29
+
24
30
  # CLI entry point for compilation
25
- def compile_for_vite(file_path)
26
- compiler = Compiler.new
31
+ # @param file_path [String] The path to the Ruby file to compile
32
+ # @param include_concerns [Boolean] Whether to include built-in concerns
33
+ def compile_for_vite(file_path, include_concerns: true)
34
+ compiler = Compiler.new(include_concerns: include_concerns)
27
35
  result = compiler.compile_file(file_path)
28
36
 
29
37
  # Output JSON to stdout for the Vite plugin to consume
@@ -0,0 +1,89 @@
1
+ # backtick_javascript: true
2
+
3
+ module OpalVite
4
+ module Concerns
5
+ # DomHelpers concern - provides common DOM manipulation methods
6
+ module DomHelpers
7
+ # Create a custom event and dispatch it on a target
8
+ def dispatch_custom_event(event_name, detail = {}, target = nil)
9
+ target ||= window
10
+ `
11
+ const event = new CustomEvent(#{event_name}, {
12
+ detail: #{detail.to_n}
13
+ });
14
+ #{target.to_n}.dispatchEvent(event);
15
+ `
16
+ end
17
+
18
+ # Create a standard event
19
+ def create_event(event_type, options = { bubbles: true })
20
+ `new Event(#{event_type}, #{options.to_n})`
21
+ end
22
+
23
+ # Query selector shorthand on element
24
+ def query(selector)
25
+ element.query_selector(selector)
26
+ end
27
+
28
+ # Query selector all shorthand on element
29
+ def query_all(selector)
30
+ element.query_selector_all(selector)
31
+ end
32
+
33
+ # Add CSS class to element
34
+ def add_class(el, class_name)
35
+ el.class_list.add(class_name)
36
+ end
37
+
38
+ # Remove CSS class from element
39
+ def remove_class(el, class_name)
40
+ el.class_list.remove(class_name)
41
+ end
42
+
43
+ # Toggle CSS class on element
44
+ def toggle_class(el, class_name)
45
+ el.class_list.toggle(class_name)
46
+ end
47
+
48
+ # Check if element has CSS class
49
+ def has_class?(el, class_name)
50
+ el.class_list.contains(class_name)
51
+ end
52
+
53
+ # Set timeout helper
54
+ def set_timeout(delay_ms, &block)
55
+ window.set_timeout(block, delay_ms)
56
+ end
57
+
58
+ # Check if element exists (not null)
59
+ def element_exists?(el)
60
+ !el.nil? && el.to_n
61
+ end
62
+
63
+ # Set style property on element
64
+ def set_style(el, property, value)
65
+ return unless element_exists?(el)
66
+ `#{el.to_n}.style[#{property}] = #{value}`
67
+ end
68
+
69
+ # Get style property from element
70
+ def get_style(el, property)
71
+ return nil unless element_exists?(el)
72
+ `#{el.to_n}.style[#{property}]`
73
+ end
74
+
75
+ # Show element (set display to block)
76
+ def show_element(el)
77
+ set_style(el, 'display', 'block')
78
+ end
79
+
80
+ # Hide element (set display to none)
81
+ def hide_element(el)
82
+ set_style(el, 'display', 'none')
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ # Alias for backward compatibility
89
+ DomHelpers = OpalVite::Concerns::DomHelpers
@@ -0,0 +1,414 @@
1
+ # backtick_javascript: true
2
+
3
+ module OpalVite
4
+ module Concerns
5
+ # JS::ProxyEx - Extended JS proxy utilities for Ruby-like JavaScript interop
6
+ #
7
+ # This module provides Ruby-friendly wrappers around JavaScript objects and APIs
8
+ # that opal_stimulus's JS::Proxy doesn't fully cover yet.
9
+ #
10
+ # Usage:
11
+ # include OpalVite::Concerns::JsProxyEx
12
+ #
13
+ # # Global objects
14
+ # local_storage.get_item('key')
15
+ # json.parse(data)
16
+ #
17
+ # # Object creation
18
+ # new_event('click', bubbles: true)
19
+ # new_url('https://example.com')
20
+ # new_regexp('[a-z]+')
21
+ #
22
+ # # Event listeners with proper block handling
23
+ # window.add_event_listener('click') { |event| ... }
24
+ #
25
+ module JsProxyEx
26
+ # ============================================
27
+ # Global JavaScript Objects
28
+ # ============================================
29
+
30
+ def local_storage
31
+ @local_storage ||= JsObject.new(`localStorage`)
32
+ end
33
+
34
+ def session_storage
35
+ @session_storage ||= JsObject.new(`sessionStorage`)
36
+ end
37
+
38
+ def js_json
39
+ @js_json ||= JsonWrapper.new
40
+ end
41
+
42
+ def js_date
43
+ DateWrapper
44
+ end
45
+
46
+ def js_console
47
+ @js_console ||= JsObject.new(`console`)
48
+ end
49
+
50
+ # ============================================
51
+ # Object Creation Helpers
52
+ # ============================================
53
+
54
+ def new_event(type, options = {})
55
+ JsObject.new(`new Event(#{type}, #{options.to_n})`)
56
+ end
57
+
58
+ def new_custom_event(type, detail = {})
59
+ JsObject.new(`new CustomEvent(#{type}, { detail: #{detail.to_n} })`)
60
+ end
61
+
62
+ def new_url(url_string)
63
+ JsObject.new(`new URL(#{url_string})`)
64
+ end
65
+
66
+ def new_regexp(pattern, flags = '')
67
+ RegExpWrapper.new(pattern, flags)
68
+ end
69
+
70
+ def new_date(value = nil)
71
+ if value
72
+ JsObject.new(`new Date(#{value})`)
73
+ else
74
+ JsObject.new(`new Date()`)
75
+ end
76
+ end
77
+
78
+ def date_now
79
+ `Date.now()`
80
+ end
81
+
82
+ # ============================================
83
+ # Wrap existing JS::Proxy objects
84
+ # ============================================
85
+
86
+ # Wrap a JS::Proxy or native object in JsObject for enhanced access
87
+ def wrap_js(obj)
88
+ return nil if obj.nil?
89
+ native = obj.respond_to?(:to_n) ? obj.to_n : obj
90
+ JsObject.new(native)
91
+ end
92
+
93
+ # ============================================
94
+ # Array/Object utilities
95
+ # ============================================
96
+
97
+ # Convert JS array to Ruby array with JsObject wrapping
98
+ def js_array_to_ruby(js_array)
99
+ result = []
100
+ length = `#{js_array}.length`
101
+ length.times do |i|
102
+ item = `#{js_array}[#{i}]`
103
+ result << JsObject.new(item)
104
+ end
105
+ result
106
+ end
107
+
108
+ # Create empty JS array
109
+ def new_js_array
110
+ JsObject.new(`[]`)
111
+ end
112
+
113
+ # ============================================
114
+ # JsObject - Wrapper class with method_missing
115
+ # ============================================
116
+
117
+ class JsObject
118
+ def initialize(native)
119
+ @native = native
120
+ end
121
+
122
+ def to_n
123
+ @native
124
+ end
125
+
126
+ # Access properties with [] - returns wrapped JsObject
127
+ def [](key)
128
+ key_str = key.to_s
129
+ # Convert snake_case to camelCase for property access
130
+ camel_key = snake_to_camel(key_str)
131
+ result = `#{@native}[#{camel_key}]`
132
+ wrap_result(result)
133
+ end
134
+
135
+ # Set properties with []=
136
+ def []=(key, value)
137
+ key_str = key.to_s
138
+ camel_key = snake_to_camel(key_str)
139
+ native_value = value.respond_to?(:to_n) ? value.to_n : value
140
+ `#{@native}[#{camel_key}] = #{native_value}`
141
+ end
142
+
143
+ # Method missing for snake_case -> camelCase conversion
144
+ def method_missing(name, *args, &block)
145
+ name_str = name.to_s
146
+
147
+ # Handle setters (e.g., text_content=)
148
+ if name_str.end_with?('=')
149
+ prop_name = name_str[0..-2]
150
+ camel_name = snake_to_camel(prop_name)
151
+ value = args[0]
152
+ native_value = value.respond_to?(:to_n) ? value.to_n : value
153
+ `#{@native}[#{camel_name}] = #{native_value}`
154
+ return value
155
+ end
156
+
157
+ # Handle predicates (e.g., has_attribute?)
158
+ if name_str.end_with?('?')
159
+ prop_name = name_str[0..-2]
160
+ camel_name = snake_to_camel(prop_name)
161
+ return !!`#{@native}[#{camel_name}]`
162
+ end
163
+
164
+ camel_name = snake_to_camel(name_str)
165
+
166
+ # Check if it's a function
167
+ is_function = `typeof #{@native}[#{camel_name}] === 'function'`
168
+
169
+ if is_function
170
+ # Handle event listeners specially
171
+ if camel_name == 'addEventListener' && block
172
+ native_callback = ->(event) { block.call(JsObject.new(event)) }
173
+ `#{@native}.addEventListener(#{args[0]}, #{native_callback})`
174
+ return nil
175
+ end
176
+
177
+ # Convert args to native
178
+ native_args = args.map { |a| a.respond_to?(:to_n) ? a.to_n : a }
179
+
180
+ # If block provided, wrap it
181
+ if block
182
+ native_callback = ->(*cb_args) {
183
+ wrapped_args = cb_args.map { |a| wrap_result(a) }
184
+ block.call(*wrapped_args)
185
+ }
186
+ native_args << native_callback
187
+ end
188
+
189
+ result = case native_args.length
190
+ when 0 then `#{@native}[#{camel_name}]()`
191
+ when 1 then `#{@native}[#{camel_name}](#{native_args[0]})`
192
+ when 2 then `#{@native}[#{camel_name}](#{native_args[0]}, #{native_args[1]})`
193
+ when 3 then `#{@native}[#{camel_name}](#{native_args[0]}, #{native_args[1]}, #{native_args[2]})`
194
+ else
195
+ # For more args, use apply
196
+ `#{@native}[#{camel_name}].apply(#{@native}, #{native_args})`
197
+ end
198
+
199
+ wrap_result(result)
200
+ else
201
+ # Property access
202
+ result = `#{@native}[#{camel_name}]`
203
+ wrap_result(result)
204
+ end
205
+ end
206
+
207
+ def respond_to_missing?(name, include_private = false)
208
+ true
209
+ end
210
+
211
+ # Enumerable support - iterate over array-like objects
212
+ def each(&block)
213
+ return enum_for(:each) unless block
214
+ length = `#{@native}.length`
215
+ return self unless length
216
+ length.times do |i|
217
+ item = `#{@native}[#{i}]`
218
+ block.call(wrap_result(item))
219
+ end
220
+ self
221
+ end
222
+
223
+ def length
224
+ `#{@native}.length`
225
+ end
226
+
227
+ def size
228
+ length
229
+ end
230
+
231
+ # Check for null/undefined
232
+ def nil?
233
+ `#{@native} == null`
234
+ end
235
+
236
+ def exists?
237
+ !nil?
238
+ end
239
+
240
+ # String conversion
241
+ def to_s
242
+ `String(#{@native})`
243
+ end
244
+
245
+ def inspect
246
+ "#<JsObject: #{to_s}>"
247
+ end
248
+
249
+ private
250
+
251
+ def snake_to_camel(str)
252
+ # Handle special cases first
253
+ return str if str == str.upcase # ALL_CAPS stays as is
254
+
255
+ parts = str.split('_')
256
+ return str if parts.length == 1
257
+
258
+ # First part stays lowercase, rest get capitalized
259
+ parts[0] + parts[1..-1].map(&:capitalize).join
260
+ end
261
+
262
+ def wrap_result(result)
263
+ return nil if `#{result} === null || #{result} === undefined`
264
+ return result if `typeof #{result} === 'number'`
265
+ return result if `typeof #{result} === 'string'`
266
+ return result if `typeof #{result} === 'boolean'`
267
+ JsObject.new(result)
268
+ end
269
+ end
270
+
271
+ # ============================================
272
+ # JsonWrapper - Ruby-like JSON API
273
+ # ============================================
274
+
275
+ class JsonWrapper
276
+ def parse(json_string)
277
+ result = `JSON.parse(#{json_string})`
278
+ JsObject.new(result)
279
+ end
280
+
281
+ def stringify(data)
282
+ native_data = data.respond_to?(:to_n) ? data.to_n : data
283
+ `JSON.stringify(#{native_data})`
284
+ end
285
+ end
286
+
287
+ # ============================================
288
+ # RegExpWrapper - Ruby-like RegExp API
289
+ # ============================================
290
+
291
+ class RegExpWrapper
292
+ def initialize(pattern, flags = '')
293
+ @native = `new RegExp(#{pattern}, #{flags})`
294
+ end
295
+
296
+ def to_n
297
+ @native
298
+ end
299
+
300
+ def test(string)
301
+ `#{@native}.test(#{string})`
302
+ end
303
+
304
+ def match(string)
305
+ result = `#{string}.match(#{@native})`
306
+ return nil if `#{result} === null`
307
+ JsObject.new(result)
308
+ end
309
+
310
+ def =~(string)
311
+ test(string)
312
+ end
313
+ end
314
+
315
+ # ============================================
316
+ # DateWrapper - Static methods for Date
317
+ # ============================================
318
+
319
+ module DateWrapper
320
+ def self.now
321
+ `Date.now()`
322
+ end
323
+
324
+ def self.new(value = nil)
325
+ if value
326
+ JsObject.new(`new Date(#{value})`)
327
+ else
328
+ JsObject.new(`new Date()`)
329
+ end
330
+ end
331
+
332
+ def self.parse(date_string)
333
+ `Date.parse(#{date_string})`
334
+ end
335
+ end
336
+
337
+ # ============================================
338
+ # Stimulus Target Helpers
339
+ # For accessing Stimulus targets with snake_case
340
+ # ============================================
341
+
342
+ # Check if target exists: has_target?(:submit_btn)
343
+ def has_target?(target_name)
344
+ camel_name = snake_to_camel(target_name.to_s)
345
+ has_method = "has#{camel_name[0].upcase}#{camel_name[1..-1]}Target"
346
+ `this[#{has_method}]`
347
+ end
348
+
349
+ # Get target element: target(:submit_btn)
350
+ def target(target_name)
351
+ camel_name = snake_to_camel(target_name.to_s)
352
+ target_method = "#{camel_name}Target"
353
+ result = `this[#{target_method}]`
354
+ JsObject.new(result)
355
+ end
356
+
357
+ # Get all targets: targets(:field)
358
+ def targets(target_name)
359
+ camel_name = snake_to_camel(target_name.to_s)
360
+ targets_method = "#{camel_name}Targets"
361
+ result = `this[#{targets_method}]`
362
+ js_array_to_ruby(result)
363
+ end
364
+
365
+ # Update target text content: set_target_text(:status, "Hello")
366
+ def set_target_text(target_name, text)
367
+ camel_name = snake_to_camel(target_name.to_s)
368
+ has_method = "has#{camel_name[0].upcase}#{camel_name[1..-1]}Target"
369
+ target_method = "#{camel_name}Target"
370
+ `
371
+ if (this[#{has_method}]) {
372
+ this[#{target_method}].textContent = #{text};
373
+ }
374
+ `
375
+ end
376
+
377
+ # Update target class: set_target_class(:status, "form-status error")
378
+ def set_target_class(target_name, class_name)
379
+ camel_name = snake_to_camel(target_name.to_s)
380
+ has_method = "has#{camel_name[0].upcase}#{camel_name[1..-1]}Target"
381
+ target_method = "#{camel_name}Target"
382
+ `
383
+ if (this[#{has_method}]) {
384
+ this[#{target_method}].className = #{class_name};
385
+ }
386
+ `
387
+ end
388
+
389
+ # Set target disabled state: set_target_disabled(:submit_btn, true)
390
+ def set_target_disabled(target_name, disabled)
391
+ camel_name = snake_to_camel(target_name.to_s)
392
+ has_method = "has#{camel_name[0].upcase}#{camel_name[1..-1]}Target"
393
+ target_method = "#{camel_name}Target"
394
+ `
395
+ if (this[#{has_method}]) {
396
+ this[#{target_method}].disabled = #{disabled};
397
+ }
398
+ `
399
+ end
400
+
401
+ private
402
+
403
+ def snake_to_camel(str)
404
+ return str if str == str.upcase # ALL_CAPS stays as is
405
+ parts = str.to_s.split('_')
406
+ return str if parts.length == 1
407
+ parts[0] + parts[1..-1].map(&:capitalize).join
408
+ end
409
+ end
410
+ end
411
+ end
412
+
413
+ # Alias for backward compatibility
414
+ JsProxyEx = OpalVite::Concerns::JsProxyEx