islandjs-rails 0.6.0 → 1.0.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.
@@ -3,29 +3,37 @@ module IslandjsRails
3
3
  # Additional core methods (part 2)
4
4
 
5
5
  def build_bundle!
6
- puts "šŸ”Ø Building IslandJS webpack bundle..."
6
+ puts "šŸ”Ø Building IslandJS bundle with Vite..."
7
7
 
8
8
  unless system('which yarn > /dev/null 2>&1')
9
9
  puts "āŒ yarn not found, cannot build bundle"
10
10
  return false
11
11
  end
12
12
 
13
- unless system('yarn list webpack-cli > /dev/null 2>&1')
14
- puts "āš ļø webpack-cli not found, installing..."
15
- system('yarn add --dev webpack-cli@^5.1.4')
13
+ # Check if vite.config.islands.ts exists
14
+ unless File.exist?(Rails.root.join('vite.config.islands.ts'))
15
+ puts "āš ļø vite.config.islands.ts not found. Run: rails islandjs:init"
16
+ return false
16
17
  end
17
18
 
18
- if ENV['NODE_ENV'] == 'production' || ENV['RAILS_ENV'] == 'production'
19
- success = system('yarn build')
20
- else
21
- success = system('yarn build > /dev/null 2>&1')
19
+ # Determine which build script to use based on package.json
20
+ package_json_path = Rails.root.join('package.json')
21
+ build_cmd = 'yarn build'
22
+
23
+ if File.exist?(package_json_path)
24
+ package_json = JSON.parse(File.read(package_json_path))
25
+ scripts = package_json['scripts'] || {}
26
+ build_cmd = 'yarn build:islands' if scripts.key?('build:islands')
22
27
  end
23
28
 
29
+ # Run Vite build
30
+ success = system(build_cmd)
31
+
24
32
  if success
25
33
  puts "āœ… Bundle built successfully"
26
34
  return true
27
35
  else
28
- puts "āŒ Build failed. Check your webpack configuration."
36
+ puts "āŒ Build failed. Check your vite.config.islands.ts"
29
37
  return false
30
38
  end
31
39
  end
@@ -269,27 +277,27 @@ module IslandjsRails
269
277
  end
270
278
 
271
279
  def install_essential_dependencies!
272
- puts "šŸ“¦ Installing essential webpack dependencies..."
273
- puts " Installing: #{ESSENTIAL_DEPENDENCIES.join(', ')}"
274
-
275
- missing_deps = ESSENTIAL_DEPENDENCIES.select do |dep|
276
- package_name = dep.split('@').first
277
- !package_installed?(package_name)
278
- end
280
+ puts "šŸ“¦ Installing essential Vite dependencies..."
279
281
 
280
- if missing_deps.empty?
281
- puts "āœ“ All essential dependencies already installed"
282
- return
283
- end
282
+ deps_to_install = []
284
283
 
285
- success = system("yarn add --dev #{missing_deps.join(' ')}")
284
+ # Check for Vite and React plugin
285
+ deps_to_install << 'vite@^5.4.19' unless package_installed?('vite')
286
+ deps_to_install << '@vitejs/plugin-react@^5.0.0' unless package_installed?('@vitejs/plugin-react')
286
287
 
287
- unless success
288
- puts "āŒ Failed to install dependencies"
289
- exit 1
288
+ if deps_to_install.any?
289
+ puts " Installing: #{deps_to_install.join(', ')}"
290
+ success = system("yarn add --dev #{deps_to_install.join(' ')}")
291
+
292
+ unless success
293
+ puts "āŒ Failed to install dependencies"
294
+ exit 1
295
+ end
296
+ else
297
+ puts " āœ“ All essential dependencies already installed"
290
298
  end
291
299
 
292
- puts "āœ“ Installed essential webpack dependencies"
300
+ puts "āœ“ Installed essential Vite dependencies"
293
301
  end
294
302
 
295
303
  def create_scaffolded_structure!
@@ -561,9 +569,7 @@ module IslandjsRails
561
569
  puts " āœ“ Removed from package.json: #{package_name}"
562
570
  end
563
571
 
564
- def generate_webpack_config!
565
- copy_template_file('webpack.config.js', configuration.webpack_config_path)
566
- end
572
+ # No longer needed - vite.config.islands.ts created by ViteInstaller
567
573
 
568
574
  def url_accessible?(url, limit = 5, use_ssl_verification = true)
569
575
  require 'openssl'
@@ -616,59 +622,49 @@ module IslandjsRails
616
622
  detect_global_name(package_name)
617
623
  end
618
624
 
619
- def reset_webpack_externals
620
- webpack_config_path = configuration.webpack_config_path
621
- return unless File.exist?(webpack_config_path)
622
-
623
- content = File.read(webpack_config_path)
624
-
625
- externals_block = <<~JS
626
- externals: {
627
- // IslandjsRails managed externals - do not edit manually
628
- },
629
- JS
630
-
631
- updated_content = content.gsub(
632
- /externals:\s*\{[^}]*\}(?:,)?/m,
633
- externals_block.chomp
634
- )
635
-
636
- File.write(webpack_config_path, updated_content)
637
- puts " āœ“ Reset webpack externals"
638
- end
625
+ # No longer needed - Vite externals are in vite.config.islands.ts
639
626
 
640
- def update_webpack_externals(package_name = nil, global_name = nil)
641
- webpack_config_path = configuration.webpack_config_path
642
- return unless File.exist?(webpack_config_path)
627
+ def update_vite_externals(package_name = nil, global_name = nil)
628
+ vite_config_path = Rails.root.join('vite.config.islands.ts')
629
+ return unless File.exist?(vite_config_path)
643
630
 
644
- content = File.read(webpack_config_path)
631
+ content = File.read(vite_config_path)
645
632
 
646
633
  externals = {}
634
+ globals = {}
647
635
 
648
- # Get installed packages from vendor manifest instead of partials
636
+ # Get installed packages from vendor manifest
649
637
  vendor_manager = IslandjsRails.vendor_manager
650
638
  manifest = vendor_manager.send(:read_manifest)
651
639
 
652
640
  manifest['libs'].each do |lib|
653
641
  pkg = lib['name']
654
- externals[pkg] = get_global_name_for_package(pkg)
642
+ global = get_global_name_for_package(pkg)
643
+ externals[pkg] = true
644
+ globals[pkg] = global
655
645
  end
656
646
 
657
- externals_lines = externals.map { |pkg, global| " \"#{pkg}\": \"#{global}\"" }
658
- externals_block = <<~JS
659
- externals: {
660
- // IslandjsRails managed externals - do not edit manually
661
- #{externals_lines.join(",\n")}
662
- },
663
- JS
647
+ # Build external array for Vite
648
+ external_array = externals.keys.map { |pkg| "'#{pkg}'" }.join(', ')
649
+
650
+ # Build globals object for Vite
651
+ globals_lines = globals.map { |pkg, global| " '#{pkg}': '#{global}'" }
652
+ globals_block = globals_lines.join(",\n")
664
653
 
654
+ # Update external array
665
655
  updated_content = content.gsub(
666
- /externals:\s*\{[^}]*\}(?:,)?/m,
667
- externals_block.chomp
656
+ /external:\s*\[[^\]]*\]/m,
657
+ "external: [#{external_array}]"
658
+ )
659
+
660
+ # Update globals object
661
+ updated_content = updated_content.gsub(
662
+ /globals:\s*\{[^}]*\}/m,
663
+ "globals: {\n#{globals_block}\n }"
668
664
  )
669
665
 
670
- File.write(webpack_config_path, updated_content)
671
- puts " āœ“ Updated webpack externals"
666
+ File.write(vite_config_path, updated_content)
667
+ puts " āœ“ Updated Vite externals in vite.config.islands.ts"
672
668
  end
673
669
  end
674
670
  end
@@ -25,32 +25,54 @@ module IslandjsRails
25
25
  end
26
26
 
27
27
  # Render the main IslandJS bundle script tag
28
+ # Reads from Vite-generated manifest at public/islands/.vite/manifest.json
29
+ #
30
+ # IMPORTANT: Manifest is read on EVERY request (no caching) to ensure
31
+ # content-hashed filenames are always fresh after rebuilds.
32
+ # This enables hot-reload behavior: rebuild Islands → hard refresh → new bundle loads
28
33
  def island_bundle_script(**attributes)
29
- manifest_path = Rails.root.join('public', 'islands_manifest.json')
30
- bundle_path = '/islands_bundle.js'
31
-
34
+ # Vite manifest path (generated by vite.config.islands.ts)
35
+ manifest_path = Rails.root.join('public/islands/.vite/manifest.json')
36
+
32
37
  # Get formatted HTML attributes with defaults (including auto-nonce and defer)
33
38
  html_attributes = script_html_attributes(defer: true, **attributes)
34
39
 
35
40
  unless File.exist?(manifest_path)
36
- # Fallback to direct bundle path when no manifest
37
- return html_safe_string("<script src=\"#{bundle_path}\"#{html_attributes}></script>")
41
+ # Development hint
42
+ if Rails.env.development?
43
+ return html_safe_string("<!-- Islands bundle not built. Run: yarn build:islands -->")
44
+ else
45
+ return html_safe_string("<!-- Islands bundle missing -->")
46
+ end
38
47
  end
39
48
 
40
49
  begin
50
+ # ALWAYS read manifest fresh (no caching) to pick up new content hashes
51
+ # This is critical for development workflow and production cache-busting
41
52
  manifest = JSON.parse(File.read(manifest_path))
42
- # Look for islands_bundle.js in manifest
43
- bundle_file = manifest['islands_bundle.js']
53
+
54
+ # Look for islands entrypoint in Vite manifest
55
+ # Vite format: { "app/javascript/entrypoints/islands.js": { "file": "islands_bundle.abc123.js" } }
56
+ entry = manifest['app/javascript/entrypoints/islands.js']
44
57
 
45
- if bundle_file
46
- html_safe_string("<script src=\"#{bundle_file}\"#{html_attributes}></script>")
47
- else
48
- # Fallback to direct bundle path
58
+ if entry && entry['file']
59
+ # Bundle path includes content hash for automatic cache-busting
60
+ bundle_path = "/islands/#{entry['file']}"
49
61
  html_safe_string("<script src=\"#{bundle_path}\"#{html_attributes}></script>")
62
+ else
63
+ # Manifest exists but entry not found
64
+ if Rails.env.development?
65
+ html_safe_string("<!-- Islands entry not found in manifest. Available keys: #{manifest.keys.join(', ')} -->")
66
+ else
67
+ html_safe_string("<!-- Islands entry not found in manifest -->")
68
+ end
69
+ end
70
+ rescue JSON::ParserError => e
71
+ if Rails.env.development?
72
+ html_safe_string("<!-- Islands manifest parse error: #{e.message} -->")
73
+ else
74
+ html_safe_string("<!-- Islands manifest parse error -->")
50
75
  end
51
- rescue JSON::ParserError
52
- # Fallback to direct bundle path on manifest parse error
53
- html_safe_string("<script src=\"#{bundle_path}\"#{html_attributes}></script>")
54
76
  end
55
77
  end
56
78
 
@@ -176,7 +198,7 @@ module IslandjsRails
176
198
  debug_info = {
177
199
  bundle_path: find_bundle_path,
178
200
  partials_count: Dir.glob(File.join(IslandjsRails.configuration.partials_dir, '*.html.erb')).count,
179
- webpack_config_exists: File.exist?(IslandjsRails.configuration.webpack_config_path),
201
+ vite_islands_config_exists: File.exist?(Rails.root.join('vite.config.islands.ts')),
180
202
  package_json_exists: File.exist?(IslandjsRails.configuration.package_json_path)
181
203
  }
182
204
 
@@ -185,7 +207,7 @@ module IslandjsRails
185
207
  <strong>šŸļø IslandJS Debug Info:</strong><br>
186
208
  Bundle Path: #{debug_info[:bundle_path] || 'Not found'}<br>
187
209
  Partials: #{debug_info[:partials_count]} found<br>
188
- Webpack Config: #{debug_info[:webpack_config_exists] ? 'āœ“' : 'āœ—'}<br>
210
+ Vite Islands Config: #{debug_info[:vite_islands_config_exists] ? 'āœ“' : 'āœ—'}<br>
189
211
  Package.json: #{debug_info[:package_json_exists] ? 'āœ“' : 'āœ—'}
190
212
  </div>
191
213
  HTML
@@ -33,9 +33,9 @@ module IslandjsRails
33
33
  return
34
34
  end
35
35
 
36
- # Check if webpack config exists
37
- unless File.exist?(Rails.root.join('webpack.config.js'))
38
- Rails.logger.warn "IslandJS: webpack.config.js not found. Run 'rails islandjs:init' to set up."
36
+ # Check if Vite Islands config exists
37
+ unless File.exist?(Rails.root.join('vite.config.islands.ts'))
38
+ Rails.logger.warn "IslandJS: vite.config.islands.ts not found. Run 'rails islandjs:init' to set up."
39
39
  return
40
40
  end
41
41
 
@@ -45,14 +45,14 @@ module IslandjsRails
45
45
  return
46
46
  end
47
47
 
48
- # Check if essential webpack dependencies are installed
49
- essential_deps = ['webpack', 'webpack-cli', '@babel/core']
48
+ # Check if essential Vite dependencies are installed
49
+ essential_deps = ['vite', '@vitejs/plugin-react']
50
50
  missing_deps = essential_deps.select do |dep|
51
51
  !system("yarn list #{dep} > /dev/null 2>&1")
52
52
  end
53
53
 
54
54
  unless missing_deps.empty?
55
- Rails.logger.warn "IslandJS: Missing dependencies: #{missing_deps.join(', ')}. Run 'rails islandjs:init' to install."
55
+ Rails.logger.warn "IslandJS: Missing dependencies: #{missing_deps.join(', ')}. Run 'yarn install'."
56
56
  end
57
57
  end
58
58
  end
@@ -57,7 +57,7 @@ namespace :islandjs do
57
57
  IslandjsRails.status!
58
58
  end
59
59
 
60
- desc "Clean all island partials and reset webpack externals"
60
+ desc "Clean all island partials and reset Vite externals"
61
61
  task :clean => :environment do
62
62
  IslandjsRails.clean!
63
63
  end
@@ -69,7 +69,7 @@ namespace :islandjs do
69
69
  puts "=" * 40
70
70
  puts "Package.json path: #{config.package_json_path}"
71
71
  puts "Partials directory: #{config.partials_dir}"
72
- puts "Webpack config path: #{config.webpack_config_path}"
72
+ puts "Vite Islands config: vite.config.islands.ts"
73
73
  puts "Supported CDNs: #{config.supported_cdns.join(', ')}"
74
74
  puts "Built-in global name overrides: #{IslandjsRails::BUILT_IN_GLOBAL_NAME_OVERRIDES.size} available"
75
75
  end
@@ -237,9 +237,12 @@ module IslandjsRails
237
237
  <% # Load each library separately for better caching %>
238
238
  ERB
239
239
 
240
- manifest['libs'].each do |lib|
240
+ # CRITICAL: Order libraries correctly (react before react-dom)
241
+ ordered_libs = order_libraries(manifest['libs'])
242
+
243
+ ordered_libs.each do |lib|
241
244
  content += <<~ERB
242
- <script src="/islands/vendor/#{lib['file']}" data-turbo-track="reload"></script>
245
+ <script src="/vendor/islands/#{lib['file']}" data-turbo-track="reload"></script>
243
246
  ERB
244
247
  end
245
248
 
@@ -256,7 +259,7 @@ module IslandjsRails
256
259
  <%# IslandJS Rails Vendor UMD Scripts (Combined Mode) %>
257
260
  <%# Generated automatically - do not edit manually %>
258
261
  <%# Combined bundle: #{combined_info['size_kb']}KB %>
259
- <script src="/islands/vendor/#{combined_info['file']}" data-turbo-track="reload"></script>
262
+ <script src="/vendor/islands/#{combined_info['file']}" data-turbo-track="reload"></script>
260
263
  ERB
261
264
 
262
265
  write_vendor_partial(content)
@@ -1,3 +1,3 @@
1
1
  module IslandjsRails
2
- VERSION = "0.6.0"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require_relative 'vite_integration'
6
+
7
+ module IslandjsRails
8
+ # Idempotent installer for Islands + Vite integration
9
+ class ViteInstaller
10
+ attr_reader :vite_integration, :root_path
11
+
12
+ def initialize(root_path = Rails.root)
13
+ @root_path = Pathname.new(root_path)
14
+ @vite_integration = ViteIntegration.new(root_path)
15
+ end
16
+
17
+ # Main installation method - fully idempotent
18
+ def install!
19
+ puts "šŸļø Initializing IslandJS Rails with Vite..."
20
+
21
+ # Check current state
22
+ check_prerequisites!
23
+
24
+ # Install/configure based on current state
25
+ if vite_integration.vite_installed?
26
+ puts "āœ“ Vite detected, configuring Islands alongside existing setup"
27
+ configure_islands_alongside_vite!
28
+ else
29
+ puts "āœ“ Installing Vite for Islands"
30
+ install_vite_from_scratch!
31
+ end
32
+
33
+ # Always run these (idempotent)
34
+ create_islands_structure!
35
+ create_vendor_system!
36
+ inject_islands_helper!
37
+
38
+ puts "\nšŸŽ‰ IslandJS Rails initialized successfully!"
39
+ print_next_steps
40
+ end
41
+
42
+ private
43
+
44
+ def check_prerequisites!
45
+ unless command_exists?('node')
46
+ raise Error, "Node.js not found. Please install Node.js 16+ first."
47
+ end
48
+
49
+ unless command_exists?('yarn')
50
+ puts "āš ļø Yarn not found, installing..."
51
+ system('npm install -g yarn')
52
+ end
53
+ end
54
+
55
+ def configure_islands_alongside_vite!
56
+ # Vite already exists, just add Islands config
57
+ create_islands_vite_config! unless vite_integration.islands_vite_config_exists?
58
+ create_islands_entrypoint!
59
+ update_package_json_for_islands!
60
+ install_vite_dependencies!
61
+ end
62
+
63
+ def install_vite_from_scratch!
64
+ # No Vite yet, install everything
65
+ create_base_vite_config!
66
+ create_islands_vite_config!
67
+ create_islands_entrypoint!
68
+ setup_package_json!
69
+ install_vite_dependencies!
70
+ end
71
+
72
+ def create_base_vite_config!
73
+ return if vite_integration.vite_config_exists?
74
+
75
+ puts "āœ“ Creating base vite.config.ts"
76
+
77
+ template = <<~TYPESCRIPT
78
+ import { defineConfig } from 'vite'
79
+ import react from '@vitejs/plugin-react'
80
+ import path from 'path'
81
+
82
+ export default defineConfig({
83
+ plugins: [
84
+ react(),
85
+ ],
86
+ resolve: {
87
+ alias: {
88
+ '@': path.resolve(__dirname, 'app/javascript')
89
+ },
90
+ },
91
+ })
92
+ TYPESCRIPT
93
+
94
+ vite_integration.vite_config_path.write(template)
95
+ end
96
+
97
+ def create_islands_vite_config!
98
+ return if vite_integration.islands_vite_config_exists?
99
+
100
+ puts "āœ“ Creating vite.config.islands.ts"
101
+
102
+ template_path = File.expand_path('../templates/vite.config.islands.ts', __dir__)
103
+ FileUtils.cp(template_path, vite_integration.islands_vite_config_path)
104
+ end
105
+
106
+ def create_islands_entrypoint!
107
+ entrypoint_path = root_path.join('app/javascript/entrypoints/islands.js')
108
+ return if entrypoint_path.exist?
109
+
110
+ puts "āœ“ Creating Islands entrypoint"
111
+
112
+ FileUtils.mkdir_p(entrypoint_path.dirname)
113
+ template_path = File.expand_path('../templates/app/javascript/entrypoints/islands.js', __dir__)
114
+ FileUtils.cp(template_path, entrypoint_path)
115
+ end
116
+
117
+ def setup_package_json!
118
+ if vite_integration.package_json_exists?
119
+ update_package_json_for_islands!
120
+ else
121
+ create_package_json!
122
+ end
123
+ end
124
+
125
+ def create_package_json!
126
+ puts "āœ“ Creating package.json"
127
+
128
+ package_json = {
129
+ "private" => true,
130
+ "type" => "module",
131
+ "scripts" => {
132
+ "build:islands" => "vite build --config vite.config.islands.ts",
133
+ "watch:islands" => "vite build --config vite.config.islands.ts --watch"
134
+ },
135
+ "dependencies" => {},
136
+ "devDependencies" => {}
137
+ }
138
+
139
+ vite_integration.write_package_json(package_json)
140
+ end
141
+
142
+ def update_package_json_for_islands!
143
+ puts "āœ“ Updating package.json scripts"
144
+ vite_integration.update_package_json_scripts!
145
+ end
146
+
147
+ def install_vite_dependencies!
148
+ puts "šŸ“¦ Installing Vite dependencies..."
149
+
150
+ # Check what's already installed
151
+ package_json = vite_integration.read_package_json
152
+ dev_deps = package_json['devDependencies'] || {}
153
+
154
+ deps_to_install = []
155
+
156
+ # Required Vite dependencies
157
+ deps_to_install << 'vite@^5.4.19' unless dev_deps.key?('vite')
158
+ deps_to_install << '@vitejs/plugin-react@^5.0.0' unless dev_deps.key?('@vitejs/plugin-react')
159
+
160
+ if deps_to_install.any?
161
+ puts " Installing: #{deps_to_install.join(', ')}"
162
+ system("yarn add --dev #{deps_to_install.join(' ')}")
163
+ else
164
+ puts " āœ“ Vite dependencies already installed"
165
+ end
166
+ end
167
+
168
+ def create_islands_structure!
169
+ islands_dir = root_path.join('app/javascript/islands')
170
+
171
+ if islands_dir.exist?
172
+ puts "āœ“ Islands structure already exists"
173
+ return
174
+ end
175
+
176
+ puts "āœ“ Creating Islands directory structure"
177
+
178
+ # Create directories
179
+ FileUtils.mkdir_p(islands_dir.join('components'))
180
+ FileUtils.mkdir_p(islands_dir.join('utils'))
181
+
182
+ # Copy HelloWorld component template
183
+ hello_world_path = islands_dir.join('components/HelloWorld.jsx')
184
+ unless hello_world_path.exist?
185
+ template_path = File.expand_path('../templates/app/javascript/islands/components/HelloWorld.jsx', __dir__)
186
+ FileUtils.cp(template_path, hello_world_path) if File.exist?(template_path)
187
+ end
188
+
189
+ # Copy Turbo utilities
190
+ turbo_utils_path = islands_dir.join('utils/turbo.js')
191
+ unless turbo_utils_path.exist?
192
+ template_path = File.expand_path('../templates/app/javascript/islands/utils/turbo.js', __dir__)
193
+ FileUtils.cp(template_path, turbo_utils_path) if File.exist?(template_path)
194
+ end
195
+ end
196
+
197
+ def create_vendor_system!
198
+ vendor_dir = root_path.join('public/vendor/islands')
199
+
200
+ if vendor_dir.exist?
201
+ puts "āœ“ Vendor system already exists"
202
+ return
203
+ end
204
+
205
+ puts "āœ“ Creating vendor directory"
206
+ FileUtils.mkdir_p(vendor_dir)
207
+
208
+ # Create vendor UMD partial
209
+ create_vendor_partial!
210
+ end
211
+
212
+ def create_vendor_partial!
213
+ partials_dir = root_path.join('app/views/shared/islands')
214
+ FileUtils.mkdir_p(partials_dir)
215
+
216
+ partial_path = partials_dir.join('_vendor_umd.html.erb')
217
+ return if partial_path.exist?
218
+
219
+ puts "āœ“ Creating vendor UMD partial"
220
+
221
+ # Create empty partial - will be populated when packages are installed
222
+ partial_content = <<~ERB
223
+ <%# IslandJS Rails - UMD Vendor Scripts %>
224
+ <%# This partial is auto-generated. Run: rails islandjs:install[react] %>
225
+ ERB
226
+
227
+ partial_path.write(partial_content)
228
+ end
229
+
230
+ def inject_islands_helper!
231
+ layout_path = vite_integration.islands_layout_path
232
+
233
+ unless layout_path.exist?
234
+ puts "āš ļø application.html.erb not found, skipping helper injection"
235
+ return
236
+ end
237
+
238
+ if vite_integration.layout_has_islands_helper?
239
+ puts "āœ“ Islands helper already in layout"
240
+ return
241
+ end
242
+
243
+ puts "āœ“ Adding <%= islands %> to application.html.erb"
244
+
245
+ content = layout_path.read
246
+
247
+ # Try to inject before </head>
248
+ if content.include?('</head>')
249
+ updated = content.sub('</head>', " <%= islands %>\n </head>")
250
+ layout_path.write(updated)
251
+ else
252
+ puts " āš ļø Could not find </head> tag, please add <%= islands %> manually"
253
+ end
254
+ end
255
+
256
+ def print_next_steps
257
+ puts "\nšŸ“‹ Next steps:"
258
+ puts "1. Install React UMD libraries:"
259
+ puts " rails \"islandjs:install[react,19.1.0]\""
260
+ puts " rails \"islandjs:install[react-dom,19.1.0]\""
261
+ puts ""
262
+ puts "2. Build Islands bundle:"
263
+ puts " yarn build:islands"
264
+ puts ""
265
+ puts "3. Use in ERB templates:"
266
+ puts " <%= react_component('HelloWorld', { message: 'Hello!' }) %>"
267
+ puts ""
268
+
269
+ if vite_integration.vite_installed?
270
+ puts "šŸ’” Existing Vite setup detected! You now have:"
271
+ puts " - Islands dev: yarn watch:islands"
272
+ puts " - Build both: yarn build"
273
+ else
274
+ puts "šŸ’” Start development:"
275
+ puts " yarn watch:islands"
276
+ end
277
+
278
+ puts ""
279
+ puts "šŸš€ Ready to build Islands!"
280
+ end
281
+
282
+ def command_exists?(command)
283
+ system("which #{command} > /dev/null 2>&1")
284
+ end
285
+ end
286
+ end