opal-vite 0.2.2 → 0.2.5

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: d02e4907cfae995a3c6496d788bf45bee0815c506be5685bc43caa417572f864
4
- data.tar.gz: da33d83bca5ccc6eab82467d639c209911283a55bda3d9ce89d522639f1a2347
3
+ metadata.gz: 589d2115317b97d0c2c1452001b128f4ac3269d8e05c390e60e87d0446c8ebf0
4
+ data.tar.gz: 2811c208a9e64dfd800d2f1d4540a052f348b82b03f255ff6cae820895af8098
5
5
  SHA512:
6
- metadata.gz: bd7612a8e8b417ede04c9dcc8f71a7238e170f0d4d909e8fb44015211aa3ef59253e62cea003e5050d0d75052ea54da3f2c77fbf7211147987a566c97677b9cf
7
- data.tar.gz: 0f325e40dd3785586ca1c39912602b4553fe2b8842d3d22504378c1887d9026b41ab85f70562c17025905879bacc44cf4295a7d3abf799a75137b459634dbdf6
6
+ metadata.gz: a85ead98a10e206d858a508ef808f9410af6bf6597c6f167b93560da2cb706cbffb252b90ac5144eb9003ef45291bcdbdd84f8dab7c56425d08933be952bbbe8
7
+ data.tar.gz: 117923f00c860592866c31fba50371b84a8c47007c822ce240fa377c510119b1f640443cfa338b78298e5fe59ec1c7d44137340c71503634a71f9b26d9813783
@@ -1,5 +1,6 @@
1
1
  require 'opal'
2
2
  require 'json'
3
+ require 'pathname'
3
4
 
4
5
  module Opal
5
6
  module Vite
@@ -38,12 +39,10 @@ module Opal
38
39
  dependencies: extract_dependencies(builder)
39
40
  }
40
41
 
41
- # Try to extract source map if available
42
+ # Extract source map if enabled
42
43
  if @config.source_map_enabled
43
44
  begin
44
- # Opal::Builder should have source map information
45
- # Try to get it from the processed assets
46
- source_map = extract_source_map(builder)
45
+ source_map = extract_source_map(builder, file_path)
47
46
  result[:map] = source_map if source_map
48
47
  rescue => e
49
48
  # Source map extraction failed, log but don't fail compilation
@@ -141,26 +140,122 @@ module Opal
141
140
  builder.processed.map { |asset| asset.filename }.compact
142
141
  end
143
142
 
144
- def extract_source_map(builder)
145
- # Get source map from the builder's processed assets
146
- # Opal stores source maps in the compiled assets
147
- return nil unless builder.processed.any?
143
+ def extract_source_map(builder, file_path)
144
+ # Use builder's combined source_map which includes all compiled files
145
+ # This allows debugging of all required files (controllers, services, etc.)
146
+ return nil unless builder.respond_to?(:source_map) && builder.source_map
148
147
 
149
- # Get the main compiled asset (usually the last one)
150
- main_asset = builder.processed.last
151
- return nil unless main_asset
148
+ source_map = builder.source_map
149
+ map_hash = source_map.to_h
150
+ return nil unless map_hash
152
151
 
153
- # Check if the asset has source map data
154
- if main_asset.respond_to?(:source_map) && main_asset.source_map
155
- return main_asset.source_map.to_json
152
+ # If it's an index format with sections, merge all sections
153
+ if map_hash['sections']
154
+ map_hash = merge_all_sections(map_hash, file_path)
156
155
  end
157
156
 
158
- # Try to get from the builder directly
159
- if builder.respond_to?(:source_map) && builder.source_map
160
- return builder.source_map.to_json
157
+ return nil unless map_hash
158
+
159
+ # Normalize source paths for browser debugging
160
+ if map_hash['sources']
161
+ map_hash['sources'] = map_hash['sources'].map do |source|
162
+ normalize_source_path(source, file_path)
163
+ end
164
+ end
165
+
166
+ map_hash.to_json
167
+ end
168
+
169
+ def merge_all_sections(index_map, file_path)
170
+ sections = index_map['sections']
171
+ return nil if sections.nil? || sections.empty?
172
+
173
+ # For single section, just return that section's map
174
+ if sections.length == 1
175
+ return sections.first['map']
176
+ end
177
+
178
+ # Merge all sections into a single standard source map
179
+ # This allows debugging of all files (application.rb + all required files)
180
+ merged = {
181
+ 'version' => 3,
182
+ 'file' => File.basename(file_path),
183
+ 'sources' => [],
184
+ 'sourcesContent' => [],
185
+ 'names' => [],
186
+ 'mappings' => ''
187
+ }
188
+
189
+ current_line = 0
190
+
191
+ sections.each_with_index do |section, idx|
192
+ section_map = section['map']
193
+ next unless section_map
194
+
195
+ offset = section['offset'] || { 'line' => 0, 'column' => 0 }
196
+ section_start_line = offset['line'] || 0
197
+
198
+ # Add empty lines to reach the section's starting line
199
+ lines_to_add = section_start_line - current_line
200
+ if lines_to_add > 0
201
+ merged['mappings'] += ';' * lines_to_add
202
+ current_line = section_start_line
203
+ end
204
+
205
+ # Track source index offset for this section
206
+ source_offset = merged['sources'].length
207
+ name_offset = merged['names'].length
208
+
209
+ # Add sources and sourcesContent from this section
210
+ if section_map['sources']
211
+ section_map['sources'].each_with_index do |source, i|
212
+ merged['sources'] << source
213
+ if section_map['sourcesContent'] && section_map['sourcesContent'][i]
214
+ merged['sourcesContent'] << section_map['sourcesContent'][i]
215
+ else
216
+ merged['sourcesContent'] << nil
217
+ end
218
+ end
219
+ end
220
+
221
+ # Add names from this section
222
+ if section_map['names']
223
+ merged['names'].concat(section_map['names'])
224
+ end
225
+
226
+ # Add mappings from this section
227
+ if section_map['mappings'] && !section_map['mappings'].empty?
228
+ # If we need to adjust source/name indices, we'd need to decode/re-encode VLQ
229
+ # For now, append as-is (works when each section has its own source indices starting at 0)
230
+ # This is a simplification - proper implementation would adjust indices
231
+ if idx > 0 && !merged['mappings'].empty? && !merged['mappings'].end_with?(';')
232
+ merged['mappings'] += ';'
233
+ end
234
+ merged['mappings'] += section_map['mappings']
235
+
236
+ # Count lines in this section's mappings
237
+ lines_in_section = section_map['mappings'].count(';') + 1
238
+ current_line += lines_in_section
239
+ end
161
240
  end
162
241
 
163
- nil
242
+ merged
243
+ end
244
+
245
+ def normalize_source_path(source, file_path)
246
+ # Convert absolute paths to relative for better browser debugging
247
+ return source if source.nil? || source.empty?
248
+
249
+ # If source is already relative or a URL, keep it
250
+ return source unless source.start_with?('/')
251
+
252
+ # Try to make path relative to the original file
253
+ begin
254
+ Pathname.new(source).relative_path_from(Pathname.new(File.dirname(file_path))).to_s
255
+ rescue ArgumentError
256
+ # On different drives/mounts, keep absolute path
257
+ source
258
+ end
164
259
  end
165
260
  end
166
261
  end
@@ -0,0 +1,399 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opal
4
+ module Vite
5
+ module Testing
6
+ # StableHelpers - JavaScript-based element interaction methods for Capybara
7
+ # All polling and retry logic runs in JavaScript for reliability
8
+ # Reduces Ruby ↔ JavaScript communication overhead
9
+ #
10
+ # Usage:
11
+ # # In spec_helper.rb
12
+ # require 'opal/vite/testing/stable_helpers'
13
+ #
14
+ # RSpec.configure do |config|
15
+ # config.include Opal::Vite::Testing::StableHelpers, type: :feature
16
+ # end
17
+ #
18
+ module StableHelpers
19
+ # Default configuration
20
+ DEFAULT_TIMEOUT = 10
21
+ DEFAULT_INTERVAL = 50 # ms (JavaScript side)
22
+
23
+ # Wait for element to be present and stable in DOM using JavaScript polling
24
+ # @param selector [String] CSS selector
25
+ # @param timeout [Integer] Maximum wait time in seconds
26
+ # @return [Capybara::Node::Element] The found element
27
+ def stable_find(selector, timeout: DEFAULT_TIMEOUT, visible: true)
28
+ js_wait_for_element(selector, timeout: timeout, visible: visible)
29
+ find(selector, visible: visible, wait: 1)
30
+ end
31
+
32
+ # Find all elements after ensuring DOM stability
33
+ # @param selector [String] CSS selector
34
+ # @param timeout [Integer] Maximum wait time in seconds
35
+ # @return [Array<Capybara::Node::Element>] Found elements
36
+ def stable_all(selector, timeout: DEFAULT_TIMEOUT)
37
+ wait_for_dom_stable(timeout: timeout)
38
+ all(selector, wait: 1)
39
+ end
40
+
41
+ # Click element with retry logic
42
+ # Uses Capybara native click with JS fallback for reliability
43
+ # @param selector [String] CSS selector
44
+ # @param timeout [Integer] Maximum wait time in seconds
45
+ def stable_click(selector, timeout: DEFAULT_TIMEOUT)
46
+ escaped_selector = escape_js(selector)
47
+ start_time = Time.now
48
+
49
+ loop do
50
+ begin
51
+ # Use Capybara's find with wait, then click
52
+ element = find(selector, wait: 2, visible: true)
53
+ element.click
54
+ # Small delay to allow event processing
55
+ sleep(0.05)
56
+ return true
57
+ rescue Capybara::ElementNotFound, Ferrum::NodeNotFoundError, Ferrum::TimeoutError,
58
+ Capybara::Cuprite::MouseEventFailed => e
59
+ # If native click fails, try JavaScript click
60
+ js_clicked = page.evaluate_script(<<~JS)
61
+ (function() {
62
+ var el = document.querySelector('#{escaped_selector}');
63
+ if (el && el.offsetParent !== null) {
64
+ el.click();
65
+ return true;
66
+ }
67
+ return false;
68
+ })()
69
+ JS
70
+ return true if js_clicked
71
+
72
+ elapsed = Time.now - start_time
73
+ raise e if elapsed > timeout
74
+ sleep(DEFAULT_INTERVAL / 1000.0)
75
+ end
76
+ end
77
+ end
78
+
79
+ # Set value on input with stability check
80
+ # @param selector [String] CSS selector
81
+ # @param value [String] Value to set
82
+ # @param timeout [Integer] Maximum wait time in seconds
83
+ def stable_set(selector, value, timeout: DEFAULT_TIMEOUT)
84
+ escaped_value = escape_js(value)
85
+ js_retry_action(selector, 'set', value: escaped_value, timeout: timeout)
86
+ end
87
+
88
+ # Send keys to element with stability check
89
+ # @param selector [String] CSS selector
90
+ # @param keys [Array] Keys to send
91
+ def stable_send_keys(selector, *keys, timeout: DEFAULT_TIMEOUT)
92
+ element = stable_find(selector, timeout: timeout)
93
+ element.native.send_keys(*keys)
94
+ end
95
+
96
+ # Combined set and send_keys for form inputs (common pattern)
97
+ # Uses JavaScript for value setting, Capybara native for key events
98
+ # @param selector [String] CSS selector
99
+ # @param value [String] Value to type
100
+ # @param submit_key [Symbol] Key to send after value (e.g., :enter)
101
+ def stable_input(selector, value, submit_key: nil, timeout: DEFAULT_TIMEOUT)
102
+ escaped_selector = escape_js(selector)
103
+ escaped_value = escape_js(value)
104
+ start_time = Time.now
105
+
106
+ loop do
107
+ # Set value via JavaScript for reliability
108
+ result = page.evaluate_script(<<~JS)
109
+ (function() {
110
+ var el = document.querySelector('#{escaped_selector}');
111
+ if (!el || el.offsetParent === null) {
112
+ return { success: false, reason: 'not_found' };
113
+ }
114
+
115
+ try {
116
+ el.focus();
117
+ el.value = '#{escaped_value}';
118
+ el.dispatchEvent(new Event('input', { bubbles: true }));
119
+ el.dispatchEvent(new Event('change', { bubbles: true }));
120
+
121
+ if (el.value !== '#{escaped_value}') {
122
+ return { success: false, reason: 'value_not_set' };
123
+ }
124
+
125
+ return { success: true };
126
+ } catch (e) {
127
+ return { success: false, reason: e.message };
128
+ }
129
+ })()
130
+ JS
131
+
132
+ if result && result['success']
133
+ # Use Capybara's native send_keys for Enter key (more reliable with Stimulus)
134
+ if submit_key
135
+ # Small delay to ensure Stimulus has processed the value change
136
+ sleep(0.05)
137
+ element = find(selector)
138
+ element.native.send_keys(submit_key)
139
+ end
140
+ return true
141
+ end
142
+
143
+ elapsed = Time.now - start_time
144
+ if elapsed > timeout
145
+ reason = result ? result['reason'] : 'unknown'
146
+ raise Capybara::ElementNotFound, "stable_input failed on '#{selector}': #{reason}"
147
+ end
148
+
149
+ sleep(DEFAULT_INTERVAL / 1000.0)
150
+ end
151
+ end
152
+
153
+ # Wait for element count to match expected (all polling in JavaScript)
154
+ # @param selector [String] CSS selector
155
+ # @param count [Integer] Expected count
156
+ # @param timeout [Integer] Maximum wait time in seconds
157
+ def wait_for_count(selector, count, timeout: DEFAULT_TIMEOUT)
158
+ escaped_selector = escape_js(selector)
159
+ js_poll_until(
160
+ "document.querySelectorAll('#{escaped_selector}').length === #{count}",
161
+ timeout: timeout,
162
+ error_message: "Expected #{count} elements for '#{selector}'"
163
+ )
164
+ end
165
+
166
+ # Wait for element to contain text
167
+ # @param selector [String] CSS selector
168
+ # @param text [String] Text to find
169
+ # @param timeout [Integer] Maximum wait time in seconds
170
+ def wait_for_text(selector, text, timeout: DEFAULT_TIMEOUT)
171
+ escaped_selector = escape_js(selector)
172
+ escaped_text = escape_js(text)
173
+ js_poll_until(
174
+ "(function() { var el = document.querySelector('#{escaped_selector}'); return el && el.textContent.includes('#{escaped_text}'); })()",
175
+ timeout: timeout,
176
+ error_message: "Text '#{text}' not found in '#{selector}'"
177
+ )
178
+ end
179
+
180
+ # Wait for element to have specific class
181
+ # @param selector [String] CSS selector
182
+ # @param class_name [String] CSS class name
183
+ # @param timeout [Integer] Maximum wait time in seconds
184
+ def wait_for_class(selector, class_name, timeout: DEFAULT_TIMEOUT)
185
+ escaped_selector = escape_js(selector)
186
+ escaped_class = escape_js(class_name)
187
+ js_poll_until(
188
+ "(function() { var el = document.querySelector('#{escaped_selector}'); return el && el.classList.contains('#{escaped_class}'); })()",
189
+ timeout: timeout,
190
+ error_message: "Class '#{class_name}' not found on '#{selector}'"
191
+ )
192
+ end
193
+
194
+ # Wait for element to NOT have specific class
195
+ # @param selector [String] CSS selector
196
+ # @param class_name [String] CSS class name
197
+ # @param timeout [Integer] Maximum wait time in seconds
198
+ def wait_for_no_class(selector, class_name, timeout: DEFAULT_TIMEOUT)
199
+ escaped_selector = escape_js(selector)
200
+ escaped_class = escape_js(class_name)
201
+ js_poll_until(
202
+ "(function() { var el = document.querySelector('#{escaped_selector}'); return el && !el.classList.contains('#{escaped_class}'); })()",
203
+ timeout: timeout,
204
+ error_message: "Class '#{class_name}' still present on '#{selector}'"
205
+ )
206
+ end
207
+
208
+ # Wait for checkbox to be checked
209
+ # @param selector [String] CSS selector
210
+ # @param timeout [Integer] Maximum wait time in seconds
211
+ def wait_for_checked(selector, timeout: DEFAULT_TIMEOUT)
212
+ escaped_selector = escape_js(selector)
213
+ js_poll_until(
214
+ "(function() { var el = document.querySelector('#{escaped_selector}'); return el && el.checked === true; })()",
215
+ timeout: timeout,
216
+ error_message: "Checkbox '#{selector}' not checked"
217
+ )
218
+ end
219
+
220
+ # Public API for custom JavaScript conditions
221
+ # @param js_condition [String] JavaScript expression that returns true when ready
222
+ # @param timeout [Integer] Maximum wait time in seconds
223
+ def js_wait_for(js_condition, timeout: DEFAULT_TIMEOUT)
224
+ js_poll_until(js_condition, timeout: timeout, error_message: "Condition not met: #{js_condition}")
225
+ end
226
+
227
+ # Wait for DOM to be stable (no pending mutations)
228
+ # Uses MutationObserver setup in JS with Ruby-based polling
229
+ # @param timeout [Integer] Maximum wait time in seconds
230
+ # @param stability_time [Integer] Time in ms with no changes to consider stable
231
+ def wait_for_dom_stable(timeout: DEFAULT_TIMEOUT, stability_time: 100)
232
+ # Setup MutationObserver in JavaScript
233
+ page.execute_script(<<~JS)
234
+ window.__domStabilityState = { stable: false, lastChange: Date.now() };
235
+ if (window.__domObserver) window.__domObserver.disconnect();
236
+
237
+ window.__domObserver = new MutationObserver(function() {
238
+ window.__domStabilityState.stable = false;
239
+ window.__domStabilityState.lastChange = Date.now();
240
+ });
241
+
242
+ window.__domObserver.observe(document.body, {
243
+ childList: true,
244
+ subtree: true,
245
+ attributes: true
246
+ });
247
+ JS
248
+
249
+ start_time = Time.now
250
+
251
+ # Poll for stability using Ruby loop
252
+ loop do
253
+ result = page.evaluate_script(<<~JS)
254
+ (function() {
255
+ var state = window.__domStabilityState;
256
+ if (!state) return { stable: true };
257
+ var elapsed = Date.now() - state.lastChange;
258
+ return { stable: elapsed >= #{stability_time}, elapsed: elapsed };
259
+ })()
260
+ JS
261
+
262
+ if result && result['stable']
263
+ # Cleanup observer
264
+ page.execute_script(<<~JS)
265
+ if (window.__domObserver) {
266
+ window.__domObserver.disconnect();
267
+ delete window.__domObserver;
268
+ }
269
+ delete window.__domStabilityState;
270
+ JS
271
+ return true
272
+ end
273
+
274
+ elapsed = Time.now - start_time
275
+ if elapsed > timeout
276
+ # Cleanup observer on timeout
277
+ page.execute_script('if (window.__domObserver) window.__domObserver.disconnect();')
278
+ raise Capybara::ElementNotFound, 'DOM did not stabilize within timeout'
279
+ end
280
+
281
+ sleep(DEFAULT_INTERVAL / 1000.0)
282
+ end
283
+ end
284
+
285
+ private
286
+
287
+ # Wait for element to be present and visible using JavaScript polling
288
+ def js_wait_for_element(selector, timeout:, visible:)
289
+ escaped_selector = escape_js(selector)
290
+ visibility_check = visible ? ' && el.offsetParent !== null' : ''
291
+
292
+ js_poll_until(
293
+ "(function() { var el = document.querySelector('#{escaped_selector}'); return el !== null#{visibility_check}; })()",
294
+ timeout: timeout,
295
+ error_message: "Element not found: #{selector}"
296
+ )
297
+ end
298
+
299
+ # Wait for input to have specific value
300
+ def wait_for_value(selector, value, timeout:)
301
+ escaped_selector = escape_js(selector)
302
+ escaped_value = escape_js(value)
303
+ js_poll_until(
304
+ "(function() { var el = document.querySelector('#{escaped_selector}'); return el && el.value === '#{escaped_value}'; })()",
305
+ timeout: timeout,
306
+ error_message: "Value '#{value}' not set on '#{selector}'"
307
+ )
308
+ end
309
+
310
+ # Core JavaScript polling - all retry logic runs in browser
311
+ # @param js_condition [String] JavaScript expression that returns true when ready
312
+ # @param timeout [Integer] Maximum wait time in seconds
313
+ # @param error_message [String] Error message on timeout
314
+ def js_poll_until(js_condition, timeout:, error_message: nil)
315
+ error_msg = error_message || "Condition not met: #{js_condition}"
316
+
317
+ # Use synchronous polling with Ruby timeout to avoid async script timeout issues
318
+ start_time = Time.now
319
+
320
+ loop do
321
+ result = page.evaluate_script(<<~JS)
322
+ (function() {
323
+ try {
324
+ return #{js_condition} ? true : false;
325
+ } catch (e) {
326
+ return false;
327
+ }
328
+ })()
329
+ JS
330
+
331
+ return true if result
332
+
333
+ elapsed = Time.now - start_time
334
+ raise Capybara::ElementNotFound, error_msg if elapsed > timeout
335
+
336
+ sleep(DEFAULT_INTERVAL / 1000.0)
337
+ end
338
+ end
339
+
340
+ # JavaScript-based action with Ruby retry loop (click, set, etc.)
341
+ # @param selector [String] CSS selector
342
+ # @param action [String] Action to perform ('click', 'set')
343
+ # @param value [String] Value for 'set' action
344
+ # @param timeout [Integer] Maximum wait time in seconds
345
+ def js_retry_action(selector, action, value: nil, timeout: DEFAULT_TIMEOUT)
346
+ escaped_selector = escape_js(selector)
347
+ escaped_value = value ? escape_js(value) : ''
348
+ start_time = Time.now
349
+
350
+ loop do
351
+ result = page.evaluate_script(<<~JS)
352
+ (function() {
353
+ var el = document.querySelector('#{escaped_selector}');
354
+ if (!el || el.offsetParent === null) {
355
+ return { success: false, reason: 'not_found' };
356
+ }
357
+
358
+ try {
359
+ if ('#{action}' === 'click') {
360
+ el.click();
361
+ return { success: true };
362
+ } else if ('#{action}' === 'set') {
363
+ el.focus();
364
+ el.value = '#{escaped_value}';
365
+ el.dispatchEvent(new Event('input', { bubbles: true }));
366
+ el.dispatchEvent(new Event('change', { bubbles: true }));
367
+ return { success: true };
368
+ } else {
369
+ return { success: false, reason: 'unknown_action' };
370
+ }
371
+ } catch (e) {
372
+ return { success: false, reason: e.message };
373
+ }
374
+ })()
375
+ JS
376
+
377
+ return true if result && result['success']
378
+
379
+ elapsed = Time.now - start_time
380
+ if elapsed > timeout
381
+ reason = result ? result['reason'] : 'unknown'
382
+ raise Capybara::ElementNotFound, "Action '#{action}' failed on '#{selector}': #{reason}"
383
+ end
384
+
385
+ sleep(DEFAULT_INTERVAL / 1000.0)
386
+ end
387
+ end
388
+
389
+ # Escape string for JavaScript
390
+ def escape_js(str)
391
+ str.to_s.gsub('\\', '\\\\\\\\').gsub("'", "\\\\'").gsub("\n", '\\n')
392
+ end
393
+ end
394
+ end
395
+ end
396
+ end
397
+
398
+ # Alias for backward compatibility
399
+ StableHelpers = Opal::Vite::Testing::StableHelpers
@@ -1,5 +1,5 @@
1
1
  module Opal
2
2
  module Vite
3
- VERSION = "0.2.2"
3
+ VERSION = "0.2.5"
4
4
  end
5
5
  end
data/lib/opal-vite.rb CHANGED
@@ -30,12 +30,20 @@ module Opal
30
30
  # CLI entry point for compilation
31
31
  # @param file_path [String] The path to the Ruby file to compile
32
32
  # @param include_concerns [Boolean] Whether to include built-in concerns
33
- def compile_for_vite(file_path, include_concerns: true)
33
+ # @param source_map [Boolean] Whether to generate source maps
34
+ def compile_for_vite(file_path, include_concerns: true, source_map: true)
35
+ # Temporarily override source map setting if specified
36
+ original_source_map = config.source_map_enabled
37
+ config.source_map_enabled = source_map
38
+
34
39
  compiler = Compiler.new(include_concerns: include_concerns)
35
40
  result = compiler.compile_file(file_path)
36
41
 
37
42
  # Output JSON to stdout for the Vite plugin to consume
38
43
  puts JSON.generate(result)
44
+
45
+ # Restore original setting
46
+ config.source_map_enabled = original_source_map
39
47
  rescue Compiler::CompilationError => e
40
48
  STDERR.puts e.message
41
49
  exit 1
@@ -1274,12 +1274,23 @@ module OpalVite
1274
1274
 
1275
1275
  private
1276
1276
 
1277
- # Convert snake_case to camelCase
1277
+ # Convert snake_case to camelCase, preserving existing camelCase
1278
1278
  # @param name [Symbol, String] The name to convert
1279
1279
  # @param capitalize_first [Boolean] Whether to capitalize first letter
1280
1280
  # @return [String] camelCase string
1281
1281
  def camelize(name, capitalize_first = true)
1282
1282
  str = name.to_s
1283
+
1284
+ # If no underscores, assume already camelCase - just adjust first letter
1285
+ unless str.include?('_')
1286
+ if capitalize_first
1287
+ return str[0].upcase + str[1..-1].to_s
1288
+ else
1289
+ return str[0].downcase + str[1..-1].to_s
1290
+ end
1291
+ end
1292
+
1293
+ # Convert snake_case to camelCase
1283
1294
  parts = str.split('_')
1284
1295
  if capitalize_first
1285
1296
  parts.map(&:capitalize).join
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.2.2
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - stofu1234
@@ -109,6 +109,7 @@ files:
109
109
  - lib/opal/vite/compiler.rb
110
110
  - lib/opal/vite/config.rb
111
111
  - lib/opal/vite/source_map.rb
112
+ - lib/opal/vite/testing/stable_helpers.rb
112
113
  - lib/opal/vite/version.rb
113
114
  - opal/opal_vite.rb
114
115
  - opal/opal_vite/concerns.rb