islandjs-rails 0.4.0 → 0.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -1
- data/README.md +10 -9
- data/lib/islandjs_rails/configuration.rb +14 -0
- data/lib/islandjs_rails/core.rb +7 -31
- data/lib/islandjs_rails/core_methods.rb +74 -10
- data/lib/islandjs_rails/rails_helpers.rb +70 -21
- data/lib/islandjs_rails/tasks.rb +2 -2
- data/lib/islandjs_rails/version.rb +1 -1
- data/lib/islandjs_rails.rb +2 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 161513f810a18384f6f80fd33826774b01027e0a3167e7cdfea5af2903419ccc
|
|
4
|
+
data.tar.gz: 756c73ddf0f8347c4ca86c484fff6c62cb01600304a04abc84f54b693555062b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ca585f21d6501f6fca88792b125fdd9f70ed839867509445a59eca25f521919a6918554168f834d3deb36d9e9e7eb39c05d36c32222e8d354f7442ea4ca394e
|
|
7
|
+
data.tar.gz: 6f6eacf4afa87d6f130a71c88724deaa0e17d9d8e06cde38cb883b2a1bc35253604b5163e8d40580fbe0c40e8811fd723c424b3f769c96a495962a38ff3aedda
|
data/CHANGELOG.md
CHANGED
|
@@ -5,10 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.6.0] - 2025-10-24
|
|
9
|
+
|
|
10
|
+
- **React 19 Support Added**
|
|
11
|
+
- **Enhanced version parsing** in `cdn_package_name` with nil/empty string handling and exception safety
|
|
12
|
+
- **SSL certificate issues** with improved fallback mechanism for CDN downloads
|
|
13
|
+
|
|
14
|
+
## [0.5.0] - 2025-09-29
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **CSP support for script tags**: All IslandJS-generated `<script>` tags now automatically include a CSP nonce when one is present in the Rails request.
|
|
18
|
+
- **Flexible script attributes**: Helpers (`react_component`, `vue_component`, etc.) now support passing standard script attributes (`nonce`, `defer`, `async`, `crossorigin`, `integrity`).
|
|
19
|
+
|
|
8
20
|
## [0.4.0] - 2025-08-10
|
|
9
21
|
|
|
10
22
|
### Added
|
|
11
|
-
- Add ENV flag to control the dev UMD bundle info footer. The floating footer is
|
|
23
|
+
- Add ENV flag to control the dev UMD bundle info footer. The floating footer is disabled by default and only shows in development when `ISLANDJS_RAILS_SHOW_UMD_DEBUG` is truthy.
|
|
12
24
|
|
|
13
25
|
## [0.3.0] - 2025-08-09
|
|
14
26
|
|
data/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
**Launch quickly:** *upgrade with vite only if necessary (MYAGNI).*
|
|
4
4
|
|
|
5
5
|
[](https://github.com/praxis-emergent/islandjs-rails/actions/workflows/github-actions-demo.yml)
|
|
6
|
-
[](coverage/index.html)
|
|
7
|
+
[](spec/)
|
|
8
8
|
[](#rails-8-ready)
|
|
9
9
|
|
|
10
10
|
IslandJS Rails supports the development of React islands in Rails apps by synchronizing `package.json` dependencies with UMD libraries served in `public/islands/vendor`.
|
|
@@ -31,8 +31,8 @@ rails islandjs:init
|
|
|
31
31
|
|
|
32
32
|
### Install React
|
|
33
33
|
```bash
|
|
34
|
-
rails "islandjs:install[react,
|
|
35
|
-
rails "islandjs:install[react-dom,
|
|
34
|
+
rails "islandjs:install[react,19.2.0]"
|
|
35
|
+
rails "islandjs:install[react-dom,19.2.0]"
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
### Run Yarn In Development
|
|
@@ -115,7 +115,7 @@ Modern Rails developers face a painful choice:
|
|
|
115
115
|
### The IslandJS Rails Solution
|
|
116
116
|
```bash
|
|
117
117
|
# Instead of complex vite/webpack configuration:
|
|
118
|
-
rails "islandjs:install[react,
|
|
118
|
+
rails "islandjs:install[react,19.2.0]"
|
|
119
119
|
rails "islandjs:install[react-beautiful-dnd]"
|
|
120
120
|
rails "islandjs:install[quill]"
|
|
121
121
|
rails "islandjs:install[recharts]"
|
|
@@ -132,7 +132,7 @@ const quill = new window.Quill("#editor", {
|
|
|
132
132
|
});
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
-
**Important Note:** IslandJS Rails works with packages that ship UMD builds. Many popular packages have UMD builds, but some modern packages do not
|
|
135
|
+
**Important Note:** IslandJS Rails works with packages that ship UMD builds. Many popular packages have UMD builds, but some modern packages do not. Starting with **React 19**, official UMD builds were removed, but IslandJS Rails now automatically fetches React 19+ UMD builds from the community-maintained [`umd-react`](https://github.com/lofcz/umd-react) package. No manual setup needed.
|
|
136
136
|
|
|
137
137
|
If you absolutely need a package that doesn't ship UMD builds, you have a few options:
|
|
138
138
|
|
|
@@ -170,12 +170,12 @@ rails islandjs:init
|
|
|
170
170
|
|
|
171
171
|
# Install packages (adds to package.json + saves to vendor directory)
|
|
172
172
|
rails "islandjs:install[react]"
|
|
173
|
-
rails "islandjs:install[react,
|
|
173
|
+
rails "islandjs:install[react,19.2.0]" # With specific version
|
|
174
174
|
rails "islandjs:install[lodash]"
|
|
175
175
|
|
|
176
176
|
# Update packages (updates package.json + refreshes vendor files)
|
|
177
177
|
rails "islandjs:update[react]"
|
|
178
|
-
rails "islandjs:update[react,
|
|
178
|
+
rails "islandjs:update[react,19.2.0]" # To specific version
|
|
179
179
|
|
|
180
180
|
# Remove packages (removes from package.json + deletes vendor files)
|
|
181
181
|
rails "islandjs:remove[react]"
|
|
@@ -354,7 +354,7 @@ rails "islandjs:install[@solana/web3]" # ❌ Wrong
|
|
|
354
354
|
| Command | What it does | Example |
|
|
355
355
|
|---------|--------------|---------|
|
|
356
356
|
| `install` | Adds package via yarn + downloads UMD + saves to vendor | `rails islandjs:install[react]` |
|
|
357
|
-
| `update` | Updates package version + refreshes UMD | `rails islandjs:update[react,
|
|
357
|
+
| `update` | Updates package version + refreshes UMD | `rails islandjs:update[react,19.2.0]` |
|
|
358
358
|
| `remove` | Removes package via yarn + deletes vendor files | `rails islandjs:remove[react]` |
|
|
359
359
|
| `clean` | Removes ALL vendor files (destructive!) | `rails islandjs:clean` |
|
|
360
360
|
|
|
@@ -420,6 +420,7 @@ Renders a React component with Turbo-compatible lifecycle and optional placehold
|
|
|
420
420
|
- `class`: CSS class for container
|
|
421
421
|
- `placeholder_class`: CSS class for placeholder content
|
|
422
422
|
- `placeholder_style`: Inline styles for placeholder content
|
|
423
|
+
- Script attributes: `nonce` (auto-detected for CSP), `defer`, `async`, `crossorigin`, `integrity`
|
|
423
424
|
|
|
424
425
|
## Placeholder Support
|
|
425
426
|
|
|
@@ -27,6 +27,20 @@ module IslandjsRails
|
|
|
27
27
|
'@babel/preset-env' => 'babel-preset-env',
|
|
28
28
|
'@babel/preset-react' => 'babel-preset-react'
|
|
29
29
|
}.freeze
|
|
30
|
+
|
|
31
|
+
# Maps packages to alternate CDN names (e.g., React 19+ → umd-react).
|
|
32
|
+
def cdn_package_name(package_name, version)
|
|
33
|
+
# Handle nil or empty version strings
|
|
34
|
+
return package_name if version.nil? || version.empty?
|
|
35
|
+
|
|
36
|
+
# Check if React 19+ to use community-maintained umd-react package
|
|
37
|
+
if ['react', 'react-dom'].include?(package_name)
|
|
38
|
+
major_version = version.split('.').first.to_i rescue 0
|
|
39
|
+
return 'umd-react' if major_version >= 19
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
package_name
|
|
43
|
+
end
|
|
30
44
|
|
|
31
45
|
# Vendor file helper methods
|
|
32
46
|
def vendor_manifest_path
|
data/lib/islandjs_rails/core.rb
CHANGED
|
@@ -63,8 +63,8 @@ module IslandjsRails
|
|
|
63
63
|
|
|
64
64
|
puts "\n🎉 IslandjsRails initialized successfully!"
|
|
65
65
|
puts "\n📋 Next steps:"
|
|
66
|
-
puts "1. Install libraries: rails \"islandjs:install[react,
|
|
67
|
-
puts " rails \"islandjs:install[react-dom,
|
|
66
|
+
puts "1. Install libraries: rails \"islandjs:install[react,19.2.0]\""
|
|
67
|
+
puts " rails \"islandjs:install[react-dom,19.2.0]\" "
|
|
68
68
|
puts "2. Start dev: yarn watch"
|
|
69
69
|
puts "3. Use components: <%= react_component('HelloWorld') %>"
|
|
70
70
|
puts "4. Build for prod: yarn build"
|
|
@@ -281,6 +281,9 @@ module IslandjsRails
|
|
|
281
281
|
version ||= version_for(package_name)
|
|
282
282
|
return nil unless version
|
|
283
283
|
|
|
284
|
+
# Use cdn_package_name to handle React 19+ mapping to umd-react
|
|
285
|
+
cdn_package = configuration.cdn_package_name(package_name, version)
|
|
286
|
+
|
|
284
287
|
# Use original package name for URL, but get clean name for {name} substitution
|
|
285
288
|
clean_name = (Configuration::SCOPED_PACKAGE_MAPPINGS[package_name] || package_name).split('/').last
|
|
286
289
|
|
|
@@ -292,8 +295,7 @@ module IslandjsRails
|
|
|
292
295
|
else
|
|
293
296
|
pattern # Use pattern as-is for fixed filenames like IIFE
|
|
294
297
|
end
|
|
295
|
-
url = "#{cdn_base}/#{
|
|
296
|
-
|
|
298
|
+
url = "#{cdn_base}/#{cdn_package}@#{version}/#{path}"
|
|
297
299
|
|
|
298
300
|
if url_accessible?(url)
|
|
299
301
|
puts "✓ Found island: #{url}"
|
|
@@ -306,20 +308,6 @@ module IslandjsRails
|
|
|
306
308
|
nil
|
|
307
309
|
end
|
|
308
310
|
|
|
309
|
-
def download_umd_content(url)
|
|
310
|
-
require 'net/http'
|
|
311
|
-
require 'uri'
|
|
312
|
-
|
|
313
|
-
uri = URI(url)
|
|
314
|
-
response = Net::HTTP.get_response(uri)
|
|
315
|
-
|
|
316
|
-
unless response.code == '200'
|
|
317
|
-
raise IslandjsRails::Error, "Failed to download UMD from #{url}: #{response.code}"
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
response.body
|
|
321
|
-
end
|
|
322
|
-
|
|
323
311
|
def find_working_umd_url(package_name, version)
|
|
324
312
|
puts " 🔍 Searching for UMD build..."
|
|
325
313
|
|
|
@@ -334,7 +322,7 @@ module IslandjsRails
|
|
|
334
322
|
else
|
|
335
323
|
pattern # Use pattern as-is for fixed filenames like IIFE
|
|
336
324
|
end
|
|
337
|
-
url = "#{cdn_base}/#{package_name}@#{version}/#{path}"
|
|
325
|
+
url = "#{cdn_base}/#{configuration.cdn_package_name(package_name, version)}@#{version}/#{path}"
|
|
338
326
|
|
|
339
327
|
|
|
340
328
|
if url_accessible?(url)
|
|
@@ -353,18 +341,6 @@ module IslandjsRails
|
|
|
353
341
|
end
|
|
354
342
|
|
|
355
343
|
private
|
|
356
|
-
|
|
357
|
-
# Check if a URL is accessible (returns 200 status)
|
|
358
|
-
def url_accessible?(url)
|
|
359
|
-
require 'net/http'
|
|
360
|
-
require 'uri'
|
|
361
|
-
|
|
362
|
-
uri = URI(url)
|
|
363
|
-
response = Net::HTTP.get_response(uri)
|
|
364
|
-
response.code == '200'
|
|
365
|
-
rescue => e
|
|
366
|
-
false
|
|
367
|
-
end
|
|
368
344
|
|
|
369
345
|
# Check if a package has a partial file
|
|
370
346
|
def has_partial?(package_name)
|
|
@@ -37,8 +37,16 @@ module IslandjsRails
|
|
|
37
37
|
return
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# Skip prompt in non-interactive mode
|
|
41
|
+
unless STDIN.tty?
|
|
42
|
+
puts "\n💡 To render your HelloWorld component:"
|
|
43
|
+
puts " In any view: <%= react_component('HelloWorld') %>"
|
|
44
|
+
puts " Don't forget to: yarn build && rails server"
|
|
45
|
+
return
|
|
46
|
+
end
|
|
47
|
+
|
|
40
48
|
print "\n❓ Would you like to create a demo route at /islandjs/react to showcase your HelloWorld component? (y/n): "
|
|
41
|
-
answer = STDIN.gets
|
|
49
|
+
answer = STDIN.gets&.chomp&.downcase
|
|
42
50
|
|
|
43
51
|
if answer == 'y' || answer == 'yes'
|
|
44
52
|
create_demo_route!
|
|
@@ -393,15 +401,36 @@ module IslandjsRails
|
|
|
393
401
|
create_partial_file(package_name, umd_content, global_name)
|
|
394
402
|
end
|
|
395
403
|
|
|
396
|
-
def download_umd_content(url)
|
|
404
|
+
def download_umd_content(url, use_ssl_verification = true)
|
|
405
|
+
require 'openssl'
|
|
397
406
|
uri = URI(url)
|
|
398
|
-
response = Net::HTTP.get_response(uri)
|
|
399
407
|
|
|
400
|
-
|
|
408
|
+
http_options = {
|
|
409
|
+
use_ssl: uri.scheme == 'https',
|
|
410
|
+
verify_mode: use_ssl_verification ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
begin
|
|
414
|
+
body = Net::HTTP.start(uri.host, uri.port, **http_options) do |http|
|
|
415
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
416
|
+
response = http.request(request)
|
|
417
|
+
|
|
418
|
+
if response.code == '200'
|
|
419
|
+
response.body
|
|
420
|
+
else
|
|
421
|
+
raise IslandjsRails::Error, "Failed to download UMD from #{url}: #{response.code}"
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
401
425
|
# Force UTF-8 encoding to avoid encoding errors when writing to file
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
426
|
+
body.force_encoding('UTF-8')
|
|
427
|
+
rescue OpenSSL::SSL::SSLError => ssl_error
|
|
428
|
+
if use_ssl_verification
|
|
429
|
+
$stderr.puts "⚠️ SSL verification failed, retrying without verification: #{ssl_error.message}"
|
|
430
|
+
return download_umd_content(url, false)
|
|
431
|
+
else
|
|
432
|
+
raise IslandjsRails::Error, "Failed to download UMD from #{url}: #{ssl_error.message}"
|
|
433
|
+
end
|
|
405
434
|
end
|
|
406
435
|
end
|
|
407
436
|
|
|
@@ -536,10 +565,45 @@ module IslandjsRails
|
|
|
536
565
|
copy_template_file('webpack.config.js', configuration.webpack_config_path)
|
|
537
566
|
end
|
|
538
567
|
|
|
539
|
-
def url_accessible?(url)
|
|
568
|
+
def url_accessible?(url, limit = 5, use_ssl_verification = true)
|
|
569
|
+
require 'openssl'
|
|
570
|
+
return false if limit == 0
|
|
571
|
+
|
|
540
572
|
uri = URI(url)
|
|
541
|
-
|
|
542
|
-
|
|
573
|
+
|
|
574
|
+
# Use proper SSL verification by default, but allow fallback for certificate issues
|
|
575
|
+
http_options = {
|
|
576
|
+
use_ssl: uri.scheme == 'https',
|
|
577
|
+
verify_mode: use_ssl_verification ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
begin
|
|
581
|
+
Net::HTTP.start(uri.host, uri.port, **http_options) do |http|
|
|
582
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
583
|
+
response = http.request(request)
|
|
584
|
+
|
|
585
|
+
case response
|
|
586
|
+
when Net::HTTPSuccess
|
|
587
|
+
return true
|
|
588
|
+
when Net::HTTPRedirection
|
|
589
|
+
# Follow redirects
|
|
590
|
+
location = response['location']
|
|
591
|
+
return url_accessible?(location, limit - 1, use_ssl_verification)
|
|
592
|
+
else
|
|
593
|
+
return false
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
rescue OpenSSL::SSL::SSLError => ssl_error
|
|
597
|
+
# If SSL verification fails and we're using verification, try once without it
|
|
598
|
+
# This handles local certificate store issues while still attempting security first
|
|
599
|
+
if use_ssl_verification
|
|
600
|
+
$stderr.puts "⚠️ SSL verification failed, retrying without verification: #{ssl_error.message}"
|
|
601
|
+
return url_accessible?(url, limit, false)
|
|
602
|
+
else
|
|
603
|
+
# Already tried without verification, give up
|
|
604
|
+
false
|
|
605
|
+
end
|
|
606
|
+
end
|
|
543
607
|
rescue => e
|
|
544
608
|
false
|
|
545
609
|
end
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
module IslandjsRails
|
|
2
2
|
module RailsHelpers
|
|
3
|
+
# Script attributes that can be passed through options
|
|
4
|
+
SCRIPT_ATTRIBUTES = %i[nonce defer async crossorigin integrity].freeze
|
|
5
|
+
|
|
3
6
|
# Main helper method that combines all IslandJS functionality
|
|
4
|
-
def islands
|
|
7
|
+
def islands(**attributes)
|
|
5
8
|
output = []
|
|
6
9
|
output << island_partials # Now uses vendor UMD partial
|
|
7
|
-
output << island_bundle_script
|
|
10
|
+
output << island_bundle_script(**attributes)
|
|
8
11
|
output << umd_versions_debug if umd_debug_enabled?
|
|
9
12
|
output.compact.join("\n").html_safe
|
|
10
13
|
end
|
|
@@ -22,29 +25,32 @@ module IslandjsRails
|
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
# Render the main IslandJS bundle script tag
|
|
25
|
-
def island_bundle_script
|
|
28
|
+
def island_bundle_script(**attributes)
|
|
26
29
|
manifest_path = Rails.root.join('public', 'islands_manifest.json')
|
|
27
30
|
bundle_path = '/islands_bundle.js'
|
|
28
|
-
|
|
31
|
+
|
|
32
|
+
# Get formatted HTML attributes with defaults (including auto-nonce and defer)
|
|
33
|
+
html_attributes = script_html_attributes(defer: true, **attributes)
|
|
34
|
+
|
|
29
35
|
unless File.exist?(manifest_path)
|
|
30
36
|
# Fallback to direct bundle path when no manifest
|
|
31
|
-
return html_safe_string("<script src=\"#{bundle_path}\"
|
|
37
|
+
return html_safe_string("<script src=\"#{bundle_path}\"#{html_attributes}></script>")
|
|
32
38
|
end
|
|
33
|
-
|
|
39
|
+
|
|
34
40
|
begin
|
|
35
41
|
manifest = JSON.parse(File.read(manifest_path))
|
|
36
42
|
# Look for islands_bundle.js in manifest
|
|
37
43
|
bundle_file = manifest['islands_bundle.js']
|
|
38
|
-
|
|
44
|
+
|
|
39
45
|
if bundle_file
|
|
40
|
-
html_safe_string("<script src=\"#{bundle_file}\"
|
|
46
|
+
html_safe_string("<script src=\"#{bundle_file}\"#{html_attributes}></script>")
|
|
41
47
|
else
|
|
42
48
|
# Fallback to direct bundle path
|
|
43
|
-
html_safe_string("<script src=\"#{bundle_path}\"
|
|
49
|
+
html_safe_string("<script src=\"#{bundle_path}\"#{html_attributes}></script>")
|
|
44
50
|
end
|
|
45
51
|
rescue JSON::ParserError
|
|
46
52
|
# Fallback to direct bundle path on manifest parse error
|
|
47
|
-
html_safe_string("<script src=\"#{bundle_path}\"
|
|
53
|
+
html_safe_string("<script src=\"#{bundle_path}\"#{html_attributes}></script>")
|
|
48
54
|
end
|
|
49
55
|
end
|
|
50
56
|
|
|
@@ -66,7 +72,11 @@ module IslandjsRails
|
|
|
66
72
|
# Handle placeholder options
|
|
67
73
|
placeholder_class = options[:placeholder_class]
|
|
68
74
|
placeholder_style = options[:placeholder_style]
|
|
69
|
-
|
|
75
|
+
|
|
76
|
+
# Extract script attributes from options
|
|
77
|
+
script_attributes = options.slice(*SCRIPT_ATTRIBUTES)
|
|
78
|
+
script_attributes.compact!
|
|
79
|
+
|
|
70
80
|
# For turbo-cache compatibility, store initial state as JSON in data attribute
|
|
71
81
|
initial_state_json = props.to_json
|
|
72
82
|
|
|
@@ -91,7 +101,7 @@ module IslandjsRails
|
|
|
91
101
|
end
|
|
92
102
|
|
|
93
103
|
# Generate the mounting script - pass container_id as the only prop for turbo-cache pattern
|
|
94
|
-
mount_script = generate_react_mount_script(component_name, component_id, namespace, namespace_with_optional)
|
|
104
|
+
mount_script = generate_react_mount_script(component_name, component_id, namespace, namespace_with_optional, **script_attributes)
|
|
95
105
|
|
|
96
106
|
# Return the container div with data-initial-state and script
|
|
97
107
|
data_part = data_attrs.empty? ? '' : " #{data_attrs}"
|
|
@@ -133,9 +143,13 @@ module IslandjsRails
|
|
|
133
143
|
# Extract options
|
|
134
144
|
tag_name = options[:tag] || 'div'
|
|
135
145
|
css_class = options[:class] || ''
|
|
136
|
-
|
|
146
|
+
|
|
147
|
+
# Extract script attributes from options
|
|
148
|
+
script_attributes = options.slice(*SCRIPT_ATTRIBUTES)
|
|
149
|
+
script_attributes.compact!
|
|
150
|
+
|
|
137
151
|
# Generate the mounting script
|
|
138
|
-
mount_script = generate_vue_mount_script(component_name, component_id, props_json)
|
|
152
|
+
mount_script = generate_vue_mount_script(component_name, component_id, props_json, **script_attributes)
|
|
139
153
|
|
|
140
154
|
# Return the container div and script
|
|
141
155
|
container_html = "<#{tag_name} id=\"#{component_id}\" class=\"#{css_class}\"></#{tag_name}>"
|
|
@@ -237,6 +251,37 @@ module IslandjsRails
|
|
|
237
251
|
|
|
238
252
|
private
|
|
239
253
|
|
|
254
|
+
# Format HTML attributes into a string
|
|
255
|
+
# Returns a string like ' nonce="abc123" defer' or empty string if no attributes
|
|
256
|
+
def format_html_attributes(**attributes)
|
|
257
|
+
return '' if attributes.empty?
|
|
258
|
+
|
|
259
|
+
attributes.filter_map do |key, value|
|
|
260
|
+
next if value.nil? || value == false
|
|
261
|
+
key_str = key.to_s.tr('_', '-')
|
|
262
|
+
value == true ? " #{key_str}" : " #{key_str}=\"#{value}\""
|
|
263
|
+
end.join
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Get default script attributes with auto-nonce detection
|
|
267
|
+
def default_script_attributes(**user_attributes)
|
|
268
|
+
attributes = {}
|
|
269
|
+
|
|
270
|
+
# Auto-add nonce if CSP is enabled and not explicitly provided
|
|
271
|
+
if !user_attributes.key?(:nonce) && respond_to?(:content_security_policy_nonce)
|
|
272
|
+
nonce = content_security_policy_nonce
|
|
273
|
+
attributes[:nonce] = nonce if nonce
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
attributes.merge(user_attributes)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Get formatted HTML attributes string for script tags
|
|
280
|
+
def script_html_attributes(**attributes)
|
|
281
|
+
script_attributes = default_script_attributes(**attributes)
|
|
282
|
+
format_html_attributes(**script_attributes)
|
|
283
|
+
end
|
|
284
|
+
|
|
240
285
|
# Whether the floating UMD versions debug footer should render
|
|
241
286
|
def umd_debug_enabled?
|
|
242
287
|
return false unless Rails.env.development?
|
|
@@ -274,9 +319,11 @@ module IslandjsRails
|
|
|
274
319
|
end
|
|
275
320
|
|
|
276
321
|
# Generate React component mounting script with Turbo compatibility
|
|
277
|
-
def generate_react_mount_script(component_name, component_id, namespace, namespace_with_optional)
|
|
322
|
+
def generate_react_mount_script(component_name, component_id, namespace, namespace_with_optional, **attributes)
|
|
323
|
+
html_attributes = script_html_attributes(**attributes)
|
|
324
|
+
|
|
278
325
|
<<~JAVASCRIPT
|
|
279
|
-
<script>
|
|
326
|
+
<script#{html_attributes}>
|
|
280
327
|
(function() {
|
|
281
328
|
function mount#{component_name}() {
|
|
282
329
|
const container = document.getElementById('#{component_id}');
|
|
@@ -307,13 +354,13 @@ module IslandjsRails
|
|
|
307
354
|
const element = React.createElement(#{namespace_with_optional}.#{component_name}, props);
|
|
308
355
|
|
|
309
356
|
try {
|
|
310
|
-
// Use React
|
|
357
|
+
// Use React 19 createRoot if available, fallback to React 17 render
|
|
311
358
|
if (window.ReactDOM.createRoot) {
|
|
312
359
|
if (!container._reactRoot) {
|
|
313
360
|
container._reactRoot = window.ReactDOM.createRoot(container);
|
|
314
361
|
}
|
|
315
362
|
container._reactRoot.render(element);
|
|
316
|
-
// React
|
|
363
|
+
// React 19 automatically clears container - no manual cleanup needed
|
|
317
364
|
} else {
|
|
318
365
|
// React 17 - render is synchronous and clears container automatically
|
|
319
366
|
window.ReactDOM.render(element, container);
|
|
@@ -328,7 +375,7 @@ module IslandjsRails
|
|
|
328
375
|
const container = document.getElementById('#{component_id}');
|
|
329
376
|
if (!container) return;
|
|
330
377
|
|
|
331
|
-
// React
|
|
378
|
+
// React 19 unmount
|
|
332
379
|
if (container._reactRoot) {
|
|
333
380
|
container._reactRoot.unmount();
|
|
334
381
|
container._reactRoot = null;
|
|
@@ -360,9 +407,11 @@ module IslandjsRails
|
|
|
360
407
|
end
|
|
361
408
|
|
|
362
409
|
# Generate Vue component mounting script with Turbo compatibility
|
|
363
|
-
def generate_vue_mount_script(component_name, component_id, props_json)
|
|
410
|
+
def generate_vue_mount_script(component_name, component_id, props_json, **attributes)
|
|
411
|
+
html_attributes = script_html_attributes(**attributes)
|
|
412
|
+
|
|
364
413
|
<<~JAVASCRIPT
|
|
365
|
-
<script>
|
|
414
|
+
<script#{html_attributes}>
|
|
366
415
|
(function() {
|
|
367
416
|
let vueApp = null;
|
|
368
417
|
|
data/lib/islandjs_rails/tasks.rb
CHANGED
|
@@ -13,7 +13,7 @@ namespace :islandjs do
|
|
|
13
13
|
|
|
14
14
|
if package_name.nil?
|
|
15
15
|
puts "❌ Package name is required"
|
|
16
|
-
puts "Usage: rails \"islandjs:install[react,
|
|
16
|
+
puts "Usage: rails \"islandjs:install[react,19.2.0]\""
|
|
17
17
|
exit 1
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -27,7 +27,7 @@ namespace :islandjs do
|
|
|
27
27
|
|
|
28
28
|
if package_name.nil?
|
|
29
29
|
puts "❌ Package name is required"
|
|
30
|
-
puts "Usage: rails \"islandjs:update[react,
|
|
30
|
+
puts "Usage: rails \"islandjs:update[react,19.2.0]\""
|
|
31
31
|
exit 1
|
|
32
32
|
end
|
|
33
33
|
|
data/lib/islandjs_rails.rb
CHANGED