islandjs-rails 0.1.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 +7 -0
- data/CHANGELOG.md +16 -0
- data/LICENSE.md +22 -0
- data/README.md +754 -0
- data/exe/islandjs-rails +6 -0
- data/islandjs-rails.gemspec +55 -0
- data/lib/islandjs-rails.rb +3 -0
- data/lib/islandjs_rails/cli.rb +57 -0
- data/lib/islandjs_rails/configuration.rb +49 -0
- data/lib/islandjs_rails/core.rb +462 -0
- data/lib/islandjs_rails/core_methods.rb +609 -0
- data/lib/islandjs_rails/rails_helpers.rb +394 -0
- data/lib/islandjs_rails/railtie.rb +59 -0
- data/lib/islandjs_rails/tasks.rb +118 -0
- data/lib/islandjs_rails/vendor_manager.rb +271 -0
- data/lib/islandjs_rails/version.rb +3 -0
- data/lib/islandjs_rails.rb +142 -0
- data/lib/templates/app/controllers/islandjs_demo_controller.rb +9 -0
- data/lib/templates/app/javascript/islands/components/.gitkeep +0 -0
- data/lib/templates/app/javascript/islands/components/HelloWorld.jsx +117 -0
- data/lib/templates/app/javascript/islands/index.js +10 -0
- data/lib/templates/app/javascript/islands/utils/turbo.js +87 -0
- data/lib/templates/app/views/islandjs_demo/index.html.erb +98 -0
- data/lib/templates/app/views/islandjs_demo/react.html.erb +93 -0
- data/lib/templates/config/demo_routes.rb +3 -0
- data/lib/templates/package.json +21 -0
- data/lib/templates/webpack.config.js +49 -0
- data/package.json +12 -0
- data/yarn.lock +1890 -0
- metadata +181 -0
@@ -0,0 +1,271 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'digest'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module IslandjsRails
|
6
|
+
class VendorManager
|
7
|
+
attr_reader :configuration
|
8
|
+
|
9
|
+
def initialize(configuration)
|
10
|
+
@configuration = configuration
|
11
|
+
end
|
12
|
+
|
13
|
+
# Install a package to vendor directory
|
14
|
+
def install_package!(package_name, version = nil)
|
15
|
+
puts "📦 Installing #{package_name} to vendor directory..."
|
16
|
+
|
17
|
+
# Download UMD content
|
18
|
+
content, actual_version = download_umd_content(package_name, version)
|
19
|
+
return false unless content
|
20
|
+
|
21
|
+
# Ensure vendor directory exists
|
22
|
+
FileUtils.mkdir_p(configuration.vendor_dir)
|
23
|
+
|
24
|
+
# Save to vendor file
|
25
|
+
vendor_file = configuration.vendor_file_path(package_name, actual_version)
|
26
|
+
File.write(vendor_file, content)
|
27
|
+
|
28
|
+
# Update manifest
|
29
|
+
update_manifest!(package_name, actual_version, File.basename(vendor_file))
|
30
|
+
|
31
|
+
# Regenerate vendor partial
|
32
|
+
regenerate_vendor_partial!
|
33
|
+
|
34
|
+
puts "✅ #{package_name}@#{actual_version} installed to vendor"
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
# Remove a package from vendor directory
|
39
|
+
def remove_package!(package_name)
|
40
|
+
puts "🗑️ Removing #{package_name} from vendor..."
|
41
|
+
|
42
|
+
manifest = read_manifest
|
43
|
+
lib_entry = manifest['libs'].find { |lib| lib['name'] == package_name }
|
44
|
+
|
45
|
+
return false unless lib_entry
|
46
|
+
|
47
|
+
# Remove file
|
48
|
+
vendor_file = configuration.vendor_dir.join(lib_entry['file'])
|
49
|
+
File.delete(vendor_file) if File.exist?(vendor_file)
|
50
|
+
|
51
|
+
# Update manifest
|
52
|
+
manifest['libs'].reject! { |lib| lib['name'] == package_name }
|
53
|
+
write_manifest(manifest)
|
54
|
+
|
55
|
+
# Regenerate vendor partial
|
56
|
+
regenerate_vendor_partial!
|
57
|
+
|
58
|
+
puts "✅ #{package_name} removed from vendor"
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
# Rebuild combined bundle (for :external_combined mode)
|
63
|
+
def rebuild_combined_bundle!
|
64
|
+
return unless configuration.vendor_script_mode == :external_combined
|
65
|
+
|
66
|
+
puts "🔨 Building combined vendor bundle..."
|
67
|
+
|
68
|
+
manifest = read_manifest
|
69
|
+
return if manifest['libs'].empty?
|
70
|
+
|
71
|
+
# Order libraries according to vendor_order
|
72
|
+
ordered_libs = order_libraries(manifest['libs'])
|
73
|
+
|
74
|
+
# Combine all UMD content
|
75
|
+
combined_content = build_combined_content(ordered_libs)
|
76
|
+
|
77
|
+
# Generate hash for cache busting
|
78
|
+
content_hash = Digest::SHA256.hexdigest(combined_content)[0, 12]
|
79
|
+
|
80
|
+
# Write combined file
|
81
|
+
combined_file = configuration.combined_vendor_path(content_hash)
|
82
|
+
File.write(combined_file, combined_content)
|
83
|
+
|
84
|
+
# Update manifest with combined info
|
85
|
+
manifest['combined'] = {
|
86
|
+
'hash' => content_hash,
|
87
|
+
'file' => File.basename(combined_file),
|
88
|
+
'size_kb' => (combined_content.bytesize / 1024.0).round(1)
|
89
|
+
}
|
90
|
+
write_manifest(manifest)
|
91
|
+
|
92
|
+
# Warn if bundle is too large
|
93
|
+
size_mb = combined_content.bytesize / (1024.0 * 1024.0)
|
94
|
+
if size_mb > 1.0
|
95
|
+
puts "⚠️ Warning: Combined bundle is #{size_mb.round(1)}MB - consider splitting libraries"
|
96
|
+
end
|
97
|
+
|
98
|
+
# Clean up old combined files
|
99
|
+
cleanup_old_combined_files!
|
100
|
+
|
101
|
+
# Regenerate vendor partial
|
102
|
+
regenerate_vendor_partial!
|
103
|
+
|
104
|
+
puts "✅ Combined bundle built: #{content_hash}"
|
105
|
+
true
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def download_umd_content(package_name, version = nil)
|
111
|
+
# Use existing UMD download logic from core
|
112
|
+
core = IslandjsRails.core
|
113
|
+
|
114
|
+
# Try to find working UMD URL
|
115
|
+
version ||= core.version_for(package_name) || 'latest'
|
116
|
+
url = core.find_working_island_url(package_name, version)
|
117
|
+
|
118
|
+
return [nil, nil] unless url
|
119
|
+
|
120
|
+
content = core.download_umd_content(url)
|
121
|
+
return [nil, nil] unless content
|
122
|
+
|
123
|
+
# Extract actual version from URL if needed
|
124
|
+
actual_version = extract_version_from_url(url) || version
|
125
|
+
|
126
|
+
[content, actual_version]
|
127
|
+
end
|
128
|
+
|
129
|
+
def extract_version_from_url(url)
|
130
|
+
# Extract version from CDN URLs like unpkg.com/react@18.2.0/...
|
131
|
+
match = url.match(/@([^\/]+)\//)
|
132
|
+
match ? match[1] : nil
|
133
|
+
end
|
134
|
+
|
135
|
+
def read_manifest
|
136
|
+
manifest_path = configuration.vendor_manifest_path
|
137
|
+
|
138
|
+
if File.exist?(manifest_path)
|
139
|
+
begin
|
140
|
+
JSON.parse(File.read(manifest_path))
|
141
|
+
rescue JSON::ParserError
|
142
|
+
{ 'libs' => [] }
|
143
|
+
end
|
144
|
+
else
|
145
|
+
{ 'libs' => [] }
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def write_manifest(manifest)
|
150
|
+
FileUtils.mkdir_p(File.dirname(configuration.vendor_manifest_path))
|
151
|
+
File.write(configuration.vendor_manifest_path, JSON.pretty_generate(manifest))
|
152
|
+
end
|
153
|
+
|
154
|
+
def update_manifest!(package_name, version, filename)
|
155
|
+
manifest = read_manifest
|
156
|
+
|
157
|
+
# Remove existing entry for this package
|
158
|
+
manifest['libs'].reject! { |lib| lib['name'] == package_name }
|
159
|
+
|
160
|
+
# Add new entry
|
161
|
+
manifest['libs'] << {
|
162
|
+
'name' => package_name,
|
163
|
+
'version' => version,
|
164
|
+
'file' => filename
|
165
|
+
}
|
166
|
+
|
167
|
+
write_manifest(manifest)
|
168
|
+
end
|
169
|
+
|
170
|
+
def order_libraries(libs)
|
171
|
+
# Sort according to vendor_order, then alphabetically
|
172
|
+
ordered = []
|
173
|
+
remaining = libs.dup
|
174
|
+
|
175
|
+
# Add libraries in vendor_order first
|
176
|
+
configuration.vendor_order.each do |name|
|
177
|
+
lib = remaining.find { |l| l['name'] == name }
|
178
|
+
if lib
|
179
|
+
ordered << lib
|
180
|
+
remaining.delete(lib)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Add remaining libraries alphabetically
|
185
|
+
ordered + remaining.sort_by { |lib| lib['name'] }
|
186
|
+
end
|
187
|
+
|
188
|
+
def build_combined_content(ordered_libs)
|
189
|
+
content_parts = []
|
190
|
+
|
191
|
+
ordered_libs.each do |lib|
|
192
|
+
vendor_file = configuration.vendor_dir.join(lib['file'])
|
193
|
+
next unless File.exist?(vendor_file)
|
194
|
+
|
195
|
+
# Add header comment
|
196
|
+
content_parts << "// #{lib['name']}@#{lib['version']}"
|
197
|
+
|
198
|
+
# Add library content
|
199
|
+
content_parts << File.read(vendor_file)
|
200
|
+
|
201
|
+
# Add separator
|
202
|
+
content_parts << ""
|
203
|
+
end
|
204
|
+
|
205
|
+
content_parts.join("\n")
|
206
|
+
end
|
207
|
+
|
208
|
+
def cleanup_old_combined_files!
|
209
|
+
# Keep only the 2 most recent combined files
|
210
|
+
pattern = configuration.vendor_dir.join("#{configuration.combined_basename}-*.js")
|
211
|
+
combined_files = Dir.glob(pattern).sort_by { |f| File.mtime(f) }.reverse
|
212
|
+
|
213
|
+
# Delete all but the 2 most recent
|
214
|
+
combined_files[2..-1]&.each do |file|
|
215
|
+
File.delete(file)
|
216
|
+
puts " 🗑️ Cleaned up old combined file: #{File.basename(file)}"
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def regenerate_vendor_partial!
|
221
|
+
case configuration.vendor_script_mode
|
222
|
+
when :external_split
|
223
|
+
generate_split_partial!
|
224
|
+
when :external_combined
|
225
|
+
generate_combined_partial!
|
226
|
+
else
|
227
|
+
raise "Unknown vendor_script_mode: #{configuration.vendor_script_mode}"
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def generate_split_partial!
|
232
|
+
manifest = read_manifest
|
233
|
+
|
234
|
+
content = <<~ERB
|
235
|
+
<%# IslandJS Rails Vendor UMD Scripts (Split Mode) %>
|
236
|
+
<%# Generated automatically - do not edit manually %>
|
237
|
+
<% # Load each library separately for better caching %>
|
238
|
+
ERB
|
239
|
+
|
240
|
+
manifest['libs'].each do |lib|
|
241
|
+
content += <<~ERB
|
242
|
+
<script src="/islands/vendor/#{lib['file']}" data-turbo-track="reload"></script>
|
243
|
+
ERB
|
244
|
+
end
|
245
|
+
|
246
|
+
write_vendor_partial(content)
|
247
|
+
end
|
248
|
+
|
249
|
+
def generate_combined_partial!
|
250
|
+
manifest = read_manifest
|
251
|
+
combined_info = manifest['combined']
|
252
|
+
|
253
|
+
return generate_split_partial! unless combined_info
|
254
|
+
|
255
|
+
content = <<~ERB
|
256
|
+
<%# IslandJS Rails Vendor UMD Scripts (Combined Mode) %>
|
257
|
+
<%# Generated automatically - do not edit manually %>
|
258
|
+
<%# Combined bundle: #{combined_info['size_kb']}KB %>
|
259
|
+
<script src="/islands/vendor/#{combined_info['file']}" data-turbo-track="reload"></script>
|
260
|
+
ERB
|
261
|
+
|
262
|
+
write_vendor_partial(content)
|
263
|
+
end
|
264
|
+
|
265
|
+
def write_vendor_partial(content)
|
266
|
+
FileUtils.mkdir_p(File.dirname(configuration.vendor_partial_path))
|
267
|
+
File.write(configuration.vendor_partial_path, content)
|
268
|
+
puts " ✓ Generated vendor partial: #{configuration.vendor_partial_path}"
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require_relative "islandjs_rails/version"
|
2
|
+
require_relative "islandjs_rails/configuration"
|
3
|
+
require_relative "islandjs_rails/core"
|
4
|
+
require_relative "islandjs_rails/vendor_manager"
|
5
|
+
require_relative "islandjs_rails/cli"
|
6
|
+
|
7
|
+
# Conditionally require Rails-specific components
|
8
|
+
if defined?(Rails)
|
9
|
+
require_relative "islandjs_rails/railtie"
|
10
|
+
require_relative "islandjs_rails/rails_helpers"
|
11
|
+
end
|
12
|
+
|
13
|
+
module IslandjsRails
|
14
|
+
# Custom error classes
|
15
|
+
class Error < StandardError; end
|
16
|
+
class YarnError < Error; end
|
17
|
+
class PackageNotFoundError < Error; end
|
18
|
+
class UmdNotFoundError < Error; end
|
19
|
+
|
20
|
+
# Constants for compatibility with tests
|
21
|
+
UMD_PATH_PATTERNS = [
|
22
|
+
'umd/{name}.production.min.js',
|
23
|
+
'umd/{name}.development.js',
|
24
|
+
'umd/{name}.min.js',
|
25
|
+
'umd/{name}.js',
|
26
|
+
'dist/{name}.min.js',
|
27
|
+
'dist/{name}.js',
|
28
|
+
'dist/{name}.umd.min.js',
|
29
|
+
'dist/{name}.umd.js',
|
30
|
+
'lib/index.iife.min.js', # Solana Web3.js pattern
|
31
|
+
'lib/index.iife.js', # Solana Web3.js pattern
|
32
|
+
'lib/{name}.js',
|
33
|
+
'lib/{name}.min.js',
|
34
|
+
'{name}.min.js',
|
35
|
+
'{name}.js',
|
36
|
+
'build/{name}.min.js',
|
37
|
+
'build/{name}.js',
|
38
|
+
'bundles/{name}.min.js',
|
39
|
+
'bundles/{name}.js'
|
40
|
+
].freeze
|
41
|
+
|
42
|
+
CDN_BASES = [
|
43
|
+
'https://unpkg.com',
|
44
|
+
'https://cdn.jsdelivr.net/npm'
|
45
|
+
].freeze
|
46
|
+
|
47
|
+
BUILT_IN_GLOBAL_NAME_OVERRIDES = {
|
48
|
+
# React ecosystem
|
49
|
+
'react' => 'React',
|
50
|
+
'react-dom' => 'ReactDOM',
|
51
|
+
'react-router' => 'ReactRouter',
|
52
|
+
'react-router-dom' => 'ReactRouterDOM',
|
53
|
+
|
54
|
+
# Utility libraries
|
55
|
+
'lodash' => '_',
|
56
|
+
'underscore' => '_',
|
57
|
+
'jquery' => '$',
|
58
|
+
'zepto' => '$',
|
59
|
+
'date-fns' => 'dateFns',
|
60
|
+
|
61
|
+
# Frameworks
|
62
|
+
'vue' => 'Vue',
|
63
|
+
'angular' => 'ng',
|
64
|
+
|
65
|
+
# Blockchain
|
66
|
+
'@solana/web3.js' => 'solanaWeb3',
|
67
|
+
'web3' => 'Web3',
|
68
|
+
|
69
|
+
# Visualization
|
70
|
+
'chart.js' => 'Chart',
|
71
|
+
'plotly.js' => 'Plotly',
|
72
|
+
|
73
|
+
# State management
|
74
|
+
'redux' => 'Redux'
|
75
|
+
}.freeze
|
76
|
+
|
77
|
+
class << self
|
78
|
+
# Configuration management
|
79
|
+
def configuration
|
80
|
+
@configuration ||= Configuration.new
|
81
|
+
end
|
82
|
+
|
83
|
+
def configure
|
84
|
+
yield(configuration)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Core instance management
|
88
|
+
def core
|
89
|
+
@core ||= Core.new
|
90
|
+
end
|
91
|
+
|
92
|
+
# Vendor manager instance
|
93
|
+
def vendor_manager
|
94
|
+
@vendor_manager ||= VendorManager.new(configuration)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Delegate common methods to core
|
98
|
+
def init!
|
99
|
+
core.init!
|
100
|
+
end
|
101
|
+
|
102
|
+
def install!(package_name, version = nil)
|
103
|
+
core.install!(package_name, version)
|
104
|
+
end
|
105
|
+
|
106
|
+
def update!(package_name, version = nil)
|
107
|
+
core.update!(package_name, version)
|
108
|
+
end
|
109
|
+
|
110
|
+
def remove!(package_name)
|
111
|
+
core.remove!(package_name)
|
112
|
+
end
|
113
|
+
|
114
|
+
def sync!
|
115
|
+
core.sync!
|
116
|
+
end
|
117
|
+
|
118
|
+
def status!
|
119
|
+
core.status!
|
120
|
+
end
|
121
|
+
|
122
|
+
def clean!
|
123
|
+
core.clean!
|
124
|
+
end
|
125
|
+
|
126
|
+
def package_installed?(package_name)
|
127
|
+
core.package_installed?(package_name)
|
128
|
+
end
|
129
|
+
|
130
|
+
def version_for(library_name)
|
131
|
+
core.version_for(library_name)
|
132
|
+
end
|
133
|
+
|
134
|
+
def detect_global_name(package_name)
|
135
|
+
core.detect_global_name(package_name)
|
136
|
+
end
|
137
|
+
|
138
|
+
def find_working_island_url(package_name, version)
|
139
|
+
core.find_working_island_url(package_name, version)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
File without changes
|
@@ -0,0 +1,117 @@
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
2
|
+
import { useTurboProps, useTurboCache } from '../utils/turbo.js';
|
3
|
+
|
4
|
+
const HelloWorld = ({ containerId }) => {
|
5
|
+
// Get initial state from the div's data-initial-state attribute
|
6
|
+
const initialProps = useTurboProps(containerId);
|
7
|
+
|
8
|
+
// Component state with defaults, restored from turbo cache if available
|
9
|
+
const [count, setCount] = useState(initialProps.count || 0);
|
10
|
+
const [message, setMessage] = useState(initialProps.message || "Hello from IslandJS Rails!");
|
11
|
+
const [customMessage, setCustomMessage] = useState(initialProps.customMessage || '');
|
12
|
+
|
13
|
+
// Current state object for turbo caching
|
14
|
+
const currentState = {
|
15
|
+
count,
|
16
|
+
message,
|
17
|
+
customMessage
|
18
|
+
};
|
19
|
+
|
20
|
+
// Setup turbo cache persistence - this should run on mount and whenever state changes
|
21
|
+
useEffect(() => {
|
22
|
+
const cleanup = useTurboCache(containerId, currentState, true);
|
23
|
+
return cleanup;
|
24
|
+
}, [containerId, count, message, customMessage]);
|
25
|
+
|
26
|
+
const handleMessageChange = (e) => {
|
27
|
+
setCustomMessage(e.target.value);
|
28
|
+
};
|
29
|
+
|
30
|
+
const applyCustomMessage = () => {
|
31
|
+
if (customMessage.trim()) {
|
32
|
+
setMessage(customMessage.trim());
|
33
|
+
setCustomMessage('');
|
34
|
+
}
|
35
|
+
};
|
36
|
+
|
37
|
+
return (
|
38
|
+
<div style={{
|
39
|
+
padding: '20px',
|
40
|
+
border: '2px solid #4F46E5',
|
41
|
+
borderRadius: '8px',
|
42
|
+
backgroundColor: '#F8FAFC',
|
43
|
+
textAlign: 'center',
|
44
|
+
fontFamily: 'system-ui, sans-serif'
|
45
|
+
}}>
|
46
|
+
<h2 style={{ color: '#4F46E5', margin: '0 0 16px 0' }}>
|
47
|
+
🏝️ React + IslandjsRails (Turbo-Cache Compatible)
|
48
|
+
</h2>
|
49
|
+
<p style={{ margin: '0 0 16px 0', fontSize: '18px' }}>
|
50
|
+
{message}
|
51
|
+
</p>
|
52
|
+
|
53
|
+
<div style={{ margin: '16px 0' }}>
|
54
|
+
<input
|
55
|
+
type="text"
|
56
|
+
placeholder="Enter custom message"
|
57
|
+
value={customMessage}
|
58
|
+
onChange={handleMessageChange}
|
59
|
+
style={{
|
60
|
+
padding: '8px',
|
61
|
+
marginRight: '8px',
|
62
|
+
border: '1px solid #D1D5DB',
|
63
|
+
borderRadius: '4px',
|
64
|
+
fontSize: '14px'
|
65
|
+
}}
|
66
|
+
/>
|
67
|
+
<button
|
68
|
+
onClick={applyCustomMessage}
|
69
|
+
style={{
|
70
|
+
padding: '8px 16px',
|
71
|
+
backgroundColor: '#059669',
|
72
|
+
color: 'white',
|
73
|
+
border: 'none',
|
74
|
+
borderRadius: '4px',
|
75
|
+
cursor: 'pointer',
|
76
|
+
fontSize: '14px',
|
77
|
+
marginRight: '8px'
|
78
|
+
}}
|
79
|
+
>
|
80
|
+
Apply Message
|
81
|
+
</button>
|
82
|
+
</div>
|
83
|
+
|
84
|
+
<button
|
85
|
+
onClick={() => setCount(count + 1)}
|
86
|
+
style={{
|
87
|
+
padding: '8px 16px',
|
88
|
+
backgroundColor: '#4F46E5',
|
89
|
+
color: 'white',
|
90
|
+
border: 'none',
|
91
|
+
borderRadius: '4px',
|
92
|
+
cursor: 'pointer',
|
93
|
+
fontSize: '16px'
|
94
|
+
}}
|
95
|
+
>
|
96
|
+
Clicked {count} times
|
97
|
+
</button>
|
98
|
+
|
99
|
+
<div style={{
|
100
|
+
marginTop: '16px',
|
101
|
+
fontSize: '12px',
|
102
|
+
color: '#6B7280',
|
103
|
+
textAlign: 'left',
|
104
|
+
backgroundColor: '#F9FAFB',
|
105
|
+
padding: '12px',
|
106
|
+
borderRadius: '4px'
|
107
|
+
}}>
|
108
|
+
<strong>Turbo-Cache Demo:</strong>
|
109
|
+
<br />• Navigate away and back - your count and message persist!
|
110
|
+
<br />• Container ID: <code>{containerId}</code>
|
111
|
+
<br />• State: <code>{JSON.stringify(currentState)}</code>
|
112
|
+
</div>
|
113
|
+
</div>
|
114
|
+
);
|
115
|
+
};
|
116
|
+
|
117
|
+
export default HelloWorld;
|
@@ -0,0 +1,10 @@
|
|
1
|
+
// IslandJS Rails - Main entry point
|
2
|
+
// This file is the webpack entry point for your JavaScript islands
|
3
|
+
|
4
|
+
// React component imports
|
5
|
+
import HelloWorld from './components/HelloWorld.jsx';
|
6
|
+
|
7
|
+
// Mount components to the global islandjsRails namespace
|
8
|
+
window.islandjsRails = {
|
9
|
+
HelloWorld
|
10
|
+
};
|
@@ -0,0 +1,87 @@
|
|
1
|
+
// Turbo-compatible state management utilities for React components
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Get initial state from a container's data-initial-state attribute
|
5
|
+
* @param {string} containerId - The ID of the container element
|
6
|
+
* @returns {Object} - Parsed initial state object
|
7
|
+
*/
|
8
|
+
export function useTurboProps(containerId) {
|
9
|
+
const container = document.getElementById(containerId);
|
10
|
+
if (!container) {
|
11
|
+
console.warn(`IslandJS Turbo: Container ${containerId} not found`);
|
12
|
+
return {};
|
13
|
+
}
|
14
|
+
|
15
|
+
const initialStateJson = container.dataset.initialState;
|
16
|
+
if (!initialStateJson) {
|
17
|
+
return {};
|
18
|
+
}
|
19
|
+
|
20
|
+
try {
|
21
|
+
return JSON.parse(initialStateJson);
|
22
|
+
} catch (e) {
|
23
|
+
console.warn('IslandJS Turbo: Failed to parse initial state', e);
|
24
|
+
return {};
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
/**
|
29
|
+
* Set up Turbo cache persistence for React component state
|
30
|
+
* @param {string} containerId - The ID of the container element
|
31
|
+
* @param {Object} currentState - Current component state to persist
|
32
|
+
* @param {boolean} autoRestore - Whether to automatically restore state on turbo:load
|
33
|
+
* @returns {Function} - Cleanup function to remove event listeners
|
34
|
+
*/
|
35
|
+
export function useTurboCache(containerId, currentState, autoRestore = true) {
|
36
|
+
const container = document.getElementById(containerId);
|
37
|
+
if (!container) {
|
38
|
+
console.warn(`IslandJS Turbo: Container ${containerId} not found for caching`);
|
39
|
+
return () => {};
|
40
|
+
}
|
41
|
+
|
42
|
+
// Immediately persist the current state to the div (don't wait for turbo:before-cache)
|
43
|
+
try {
|
44
|
+
const stateJson = JSON.stringify(currentState);
|
45
|
+
container.dataset.initialState = stateJson;
|
46
|
+
} catch (e) {
|
47
|
+
console.warn('IslandJS Turbo: Failed to immediately serialize state', e);
|
48
|
+
}
|
49
|
+
}
|
50
|
+
|
51
|
+
/**
|
52
|
+
* Hook for React components to automatically manage Turbo cache persistence
|
53
|
+
* This is a React hook that should be called from within a React component
|
54
|
+
* @param {string} containerId - The ID of the container element
|
55
|
+
* @param {Object} state - Current component state to persist
|
56
|
+
* @param {Array} dependencies - Dependencies array for useEffect
|
57
|
+
*/
|
58
|
+
export function useTurboCacheEffect(containerId, state, dependencies = []) {
|
59
|
+
// This assumes React is available globally
|
60
|
+
if (typeof React !== 'undefined' && React.useEffect) {
|
61
|
+
React.useEffect(() => {
|
62
|
+
return useTurboCache(containerId, state, false);
|
63
|
+
}, [containerId, ...dependencies]);
|
64
|
+
} else {
|
65
|
+
console.warn('IslandJS Turbo: React.useEffect not available for useTurboCacheEffect');
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
/**
|
70
|
+
* Manually persist state to container for components that don't use the hook
|
71
|
+
* @param {string} containerId - The ID of the container element
|
72
|
+
* @param {Object} state - State object to persist
|
73
|
+
*/
|
74
|
+
export function persistState(containerId, state) {
|
75
|
+
const container = document.getElementById(containerId);
|
76
|
+
if (!container) {
|
77
|
+
console.warn(`IslandJS Turbo: Container ${containerId} not found for state persistence`);
|
78
|
+
return;
|
79
|
+
}
|
80
|
+
|
81
|
+
try {
|
82
|
+
const stateJson = JSON.stringify(state);
|
83
|
+
container.dataset.initialState = stateJson;
|
84
|
+
} catch (e) {
|
85
|
+
console.warn('IslandJS Turbo: Failed to serialize state', e);
|
86
|
+
}
|
87
|
+
}
|