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.
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/islandjs_rails'
4
+ require 'islandjs_rails/cli'
5
+
6
+ IslandjsRails::CLI.start(ARGV)
@@ -0,0 +1,55 @@
1
+ require_relative "lib/islandjs_rails/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "islandjs-rails"
5
+ spec.version = IslandjsRails::VERSION
6
+ spec.authors = ["Eric Arnold"]
7
+ spec.email = ["ericarnold00+praxisemergent@gmail.com"]
8
+
9
+ spec.summary = "Simple, modern JavaScript islands for Rails"
10
+ spec.description = "IslandJS Rails enables React and other JavaScript islands in Rails apps with zero webpack configuration. Load UMD libraries from CDNs, integrate with ERB partials, and render components with Turbo-compatible lifecycle management."
11
+ spec.homepage = "https://github.com/praxis-emergent/islandjs-rails"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = ">= 3.0.0"
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+ spec.metadata["source_code_uri"] = "https://github.com/praxis-emergent/islandjs-rails"
17
+ spec.metadata["changelog_uri"] = "https://github.com/praxis-emergent/islandjs-rails/blob/main/CHANGELOG.md"
18
+ spec.metadata["documentation_uri"] = "https://github.com/praxis-emergent/islandjs-rails/blob/main/README.md"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ spec.files = Dir.chdir(__dir__) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ (File.expand_path(f) == __FILE__) ||
24
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github .claude appveyor Gemfile]) ||
25
+ f.match?(%r{\A(\.rspec|Rakefile)\z}) ||
26
+ f.end_with?(".gem")
27
+ end
28
+ end
29
+
30
+ spec.bindir = "exe"
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+
34
+ # Post-install message
35
+ spec.post_install_message = <<~MSG
36
+
37
+ šŸļø IslandJS Rails installed successfully!
38
+
39
+ šŸ“‹ Next step: Initialize IslandJS in your Rails app
40
+
41
+ rails islandjs:init
42
+
43
+ MSG
44
+
45
+ # Rails integration
46
+ spec.add_dependency "rails", ">= 7.0", "< 9.0"
47
+ spec.add_dependency "thor", "~> 1.0"
48
+
49
+ # Development dependencies
50
+ spec.add_development_dependency "rake", "~> 13.0"
51
+ spec.add_development_dependency "rspec", "~> 3.0"
52
+ spec.add_development_dependency "webmock", "~> 3.0"
53
+ spec.add_development_dependency "vcr", "~> 6.0"
54
+ spec.add_development_dependency "simplecov", "~> 0.22"
55
+ end
@@ -0,0 +1,3 @@
1
+ # Bridge file for islandjs-rails gem
2
+ # This allows Bundler to auto-require the gem properly
3
+ require_relative 'islandjs_rails'
@@ -0,0 +1,57 @@
1
+ require 'thor'
2
+
3
+ module IslandjsRails
4
+ class CLI < Thor
5
+ desc "init", "Initialize IslandJS in this Rails project"
6
+ def init
7
+ IslandjsRails.init!
8
+ end
9
+
10
+ desc "install PACKAGE_NAME [VERSION]", "Install a JavaScript island package"
11
+ def install(package_name, version = nil)
12
+ IslandjsRails.install!(package_name, version)
13
+ end
14
+
15
+ desc "update PACKAGE_NAME [VERSION]", "Update a JavaScript island package"
16
+ def update(package_name, version = nil)
17
+ IslandjsRails.update!(package_name, version)
18
+ end
19
+
20
+ desc "remove PACKAGE_NAME", "Remove a JavaScript island package"
21
+ def remove(package_name)
22
+ IslandjsRails.remove!(package_name)
23
+ end
24
+
25
+ desc "sync", "Sync all JavaScript island packages with current package.json"
26
+ def sync
27
+ IslandjsRails.sync!
28
+ end
29
+
30
+ desc "status", "Show status of all JavaScript island packages"
31
+ def status
32
+ IslandjsRails.status!
33
+ end
34
+
35
+ desc "clean", "Clean all island partials and reset webpack externals"
36
+ def clean
37
+ IslandjsRails.clean!
38
+ end
39
+
40
+ desc "config", "Show IslandJS configuration"
41
+ def config
42
+ config = IslandjsRails.configuration
43
+ puts "šŸ“Š IslandjsRails Configuration"
44
+ puts "=" * 40
45
+ puts "Package.json path: #{config.package_json_path}"
46
+ puts "Partials directory: #{config.partials_dir}"
47
+ puts "Webpack config path: #{config.webpack_config_path}"
48
+ puts "Supported CDNs: #{config.supported_cdns.join(', ')}"
49
+ puts "Built-in global name overrides: #{IslandjsRails::BUILT_IN_GLOBAL_NAME_OVERRIDES.size} available"
50
+ end
51
+
52
+ desc "version", "Show IslandJS Rails version"
53
+ def version
54
+ puts "IslandjsRails #{IslandjsRails::VERSION}"
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,49 @@
1
+ require 'pathname'
2
+
3
+ module IslandjsRails
4
+ class Configuration
5
+ attr_accessor :package_json_path, :partials_dir, :webpack_config_path, :supported_cdns,
6
+ :vendor_script_mode, :vendor_order, :vendor_dir, :combined_basename
7
+
8
+ def initialize
9
+ @package_json_path = Rails.root.join('package.json')
10
+ @partials_dir = Rails.root.join('app', 'views', 'shared', 'islands')
11
+ @webpack_config_path = Rails.root.join('webpack.config.js')
12
+ @vendor_script_mode = :external_split # :external_split or :external_combined
13
+ @vendor_order = %w[react react-dom] # combine order for :external_combined
14
+ @vendor_dir = Rails.root.join('public', 'islands', 'vendor')
15
+ @combined_basename = 'islands-vendor'
16
+ @supported_cdns = [
17
+ 'https://unpkg.com',
18
+ 'https://cdn.jsdelivr.net/npm'
19
+ ]
20
+ end
21
+
22
+
23
+ # Scoped package name mappings
24
+ SCOPED_PACKAGE_MAPPINGS = {
25
+ '@solana/web3.js' => 'solana-web3.js',
26
+ '@babel/core' => 'babel-core',
27
+ '@babel/preset-env' => 'babel-preset-env',
28
+ '@babel/preset-react' => 'babel-preset-react'
29
+ }.freeze
30
+
31
+ # Vendor file helper methods
32
+ def vendor_manifest_path
33
+ @vendor_dir.join('manifest.json')
34
+ end
35
+
36
+ def vendor_partial_path
37
+ @partials_dir.join('_vendor_umd.html.erb')
38
+ end
39
+
40
+ def vendor_file_path(package_name, version)
41
+ safe_name = package_name.gsub(/[@\/]/, '_').gsub(/-/, '_')
42
+ @vendor_dir.join("#{safe_name}-#{version}.min.js")
43
+ end
44
+
45
+ def combined_vendor_path(hash)
46
+ @vendor_dir.join("#{@combined_basename}-#{hash}.js")
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,462 @@
1
+ require 'json'
2
+ require 'open3'
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'fileutils'
6
+
7
+ module IslandjsRails
8
+ class Core
9
+ attr_reader :configuration
10
+
11
+ def initialize
12
+ @configuration = IslandjsRails.configuration
13
+ end
14
+
15
+ # Essential dependencies for IslandJS webpack setup
16
+ ESSENTIAL_DEPENDENCIES = [
17
+ 'webpack@^5.88.2',
18
+ 'webpack-cli@^5.1.4',
19
+ 'terser-webpack-plugin@^5.3.14',
20
+ 'webpack-manifest-plugin@^5.0.1',
21
+ 'babel-loader@^9.1.3',
22
+ '@babel/core@^7.23.0',
23
+ '@babel/preset-env@^7.23.0',
24
+ '@babel/preset-react@^7.23.0'
25
+ ].freeze
26
+
27
+ # Initialize IslandJS in a Rails project
28
+ def init!
29
+ puts "šŸš€ Initializing IslandjsRails..."
30
+
31
+ # Step 1: Check for required tools
32
+ check_node_tools!
33
+
34
+ # Step 2: Ensure package.json exists
35
+ ensure_package_json!
36
+
37
+ # Step 3: Install essential webpack dependencies
38
+ install_essential_dependencies!
39
+
40
+ # Step 4: Create scaffolded structure
41
+ create_scaffolded_structure!
42
+
43
+ # Step 5: Create directories
44
+ FileUtils.mkdir_p(configuration.partials_dir)
45
+ FileUtils.mkdir_p(configuration.vendor_dir)
46
+ puts "āœ“ Created #{configuration.partials_dir}"
47
+ puts "āœ“ Created #{configuration.vendor_dir}"
48
+
49
+ # Step 6: Generate webpack config if it doesn't exist
50
+ unless File.exist?(configuration.webpack_config_path)
51
+ generate_webpack_config!
52
+ puts "āœ“ Generated webpack.config.js"
53
+ else
54
+ puts "āœ“ webpack.config.js already exists"
55
+ end
56
+
57
+ # Step 7: Set up vendor system and inject into layout
58
+ setup_vendor_system!
59
+ inject_islands_helper_into_layout!
60
+
61
+ # Step 8: Add node_modules to .gitignore
62
+ ensure_node_modules_gitignored!
63
+
64
+ puts "\nšŸŽ‰ IslandjsRails initialized successfully!"
65
+ puts "\nšŸ“‹ Next steps:"
66
+ puts "1. Install libraries: rails \"islandjs:install[react,18.3.1]\""
67
+ puts " rails \"islandjs:install[react-dom,18.3.1]\" "
68
+ puts "2. Start dev: yarn watch"
69
+ puts "3. Use components: <%= react_component('HelloWorld') %>"
70
+ puts "4. Build for prod: yarn build"
71
+ puts "5. Commit assets: git add public/islands_* && git add public/islands/*"
72
+
73
+ puts "\nšŸš€ Rails 8 Ready: Commit your built assets for bulletproof deploys!"
74
+ puts "šŸ’” IslandjsRails is framework-agnostic - use React, Vue, or any UMD library!"
75
+ puts "šŸŽ‰ Ready to build!"
76
+ end
77
+
78
+ # Install a new island package
79
+ def install!(package_name, version = nil)
80
+ puts "šŸ“¦ Installing UMD package: #{package_name}"
81
+
82
+ # Check if React ecosystem was incomplete before this install
83
+ was_react_ecosystem_incomplete = !react_ecosystem_complete?
84
+
85
+ # Add to package.json via yarn if not present
86
+ add_package_via_yarn(package_name, version) unless package_installed?(package_name)
87
+
88
+ # Install to vendor directory
89
+ vendor_manager = IslandjsRails.vendor_manager
90
+ success = vendor_manager.install_package!(package_name, version)
91
+
92
+ return false unless success
93
+
94
+ global_name = detect_global_name(package_name)
95
+ update_webpack_externals(package_name, global_name)
96
+
97
+ puts "āœ… Successfully installed #{package_name}!"
98
+
99
+ # Auto-scaffold React if ecosystem just became complete
100
+ if was_react_ecosystem_incomplete && react_ecosystem_complete? &&
101
+ (package_name == 'react' || package_name == 'react-dom')
102
+ activate_react_scaffolding!
103
+ end
104
+ end
105
+
106
+ # Update an existing package
107
+ def update!(package_name, version = nil)
108
+ puts "šŸ”„ Updating UMD package: #{package_name}"
109
+
110
+ unless package_installed?(package_name)
111
+ raise IslandjsRails::PackageNotFoundError, "#{package_name} is not installed. Use 'install' instead."
112
+ end
113
+
114
+ # Update package.json via yarn
115
+ yarn_update!(package_name, version)
116
+
117
+ # Re-install to vendor directory
118
+ vendor_manager = IslandjsRails.vendor_manager
119
+ vendor_manager.install_package!(package_name, version)
120
+
121
+ # Update webpack externals
122
+ global_name = detect_global_name(package_name)
123
+ update_webpack_externals(package_name, global_name)
124
+
125
+ puts "āœ… Successfully updated #{package_name}!"
126
+ end
127
+
128
+ # Remove a specific package
129
+ def remove!(package_name)
130
+ puts "šŸ—‘ļø Removing island package: #{package_name}"
131
+
132
+ unless package_installed?(package_name)
133
+ raise IslandjsRails::PackageNotFoundError, "Package #{package_name} is not installed"
134
+ end
135
+
136
+ remove_package_via_yarn(package_name)
137
+
138
+ # Remove from vendor directory
139
+ vendor_manager = IslandjsRails.vendor_manager
140
+ vendor_manager.remove_package!(package_name)
141
+
142
+ update_webpack_externals
143
+ puts "āœ… Successfully removed #{package_name}!"
144
+ end
145
+
146
+ # Sync all packages
147
+ def sync!
148
+ puts "šŸ”„ Syncing all UMD packages..."
149
+
150
+ packages = installed_packages
151
+ if packages.empty?
152
+ puts "šŸ“¦ No packages found in package.json"
153
+ return
154
+ end
155
+
156
+ vendor_manager = IslandjsRails.vendor_manager
157
+
158
+ packages.each do |package_name|
159
+ next unless supported_package?(package_name)
160
+ puts " šŸ“¦ Processing #{package_name}..."
161
+
162
+ # Get version from package.json
163
+ version = version_for(package_name)
164
+
165
+ # Install to vendor system
166
+ vendor_manager.install_package!(package_name, version)
167
+
168
+ # Update webpack externals
169
+ global_name = detect_global_name(package_name)
170
+ update_webpack_externals(package_name, global_name)
171
+ end
172
+
173
+ puts "āœ… Sync completed!"
174
+ end
175
+
176
+ # Show status of all packages
177
+ def status!
178
+ puts "šŸ“Š IslandjsRails Status"
179
+ puts "=" * 40
180
+
181
+ packages = installed_packages
182
+ if packages.empty?
183
+ puts "šŸ“¦ No packages found in package.json"
184
+ return
185
+ end
186
+
187
+ # Check vendor system instead of partials
188
+ vendor_manager = IslandjsRails.vendor_manager
189
+ manifest = vendor_manager.send(:read_manifest)
190
+ vendor_packages = manifest['libs'].map { |lib| lib['name'] }
191
+
192
+ packages.each do |package_name|
193
+ version = version_for(package_name)
194
+ has_vendor = vendor_packages.include?(package_name)
195
+ status_icon = has_vendor ? "āœ…" : "āŒ"
196
+ puts "#{status_icon} #{package_name}@#{version} #{has_vendor ? '(vendor ready)' : '(missing vendor)'}"
197
+ end
198
+ end
199
+
200
+ # Clean vendor files and rebuild
201
+ def clean!
202
+ puts "🧹 Cleaning vendor files..."
203
+
204
+ vendor_manager = IslandjsRails.vendor_manager
205
+
206
+ # Clean vendor files
207
+ if Dir.exist?(configuration.vendor_dir)
208
+ Dir.glob(File.join(configuration.vendor_dir, '*.js')).each do |file|
209
+ File.delete(file)
210
+ puts " āœ“ Removed #{File.basename(file)}"
211
+ end
212
+ end
213
+
214
+ # Reset vendor manifest by writing empty manifest
215
+ empty_manifest = { 'libs' => [] }
216
+ vendor_manager.send(:write_manifest, empty_manifest)
217
+ puts " āœ“ Reset vendor manifest"
218
+
219
+ # Regenerate vendor partial
220
+ vendor_manager.send(:regenerate_vendor_partial!)
221
+ puts " āœ“ Regenerated vendor partial"
222
+
223
+ # Reset webpack externals
224
+ reset_webpack_externals
225
+ puts " āœ“ Reset webpack externals"
226
+
227
+ # Reinstall all packages from package.json
228
+ installed_packages.each do |package_name, version|
229
+ puts " šŸ“¦ Reinstalling #{package_name}@#{version}..."
230
+ vendor_manager.install_package!(package_name, version)
231
+ global_name = detect_global_name(package_name)
232
+ update_webpack_externals(package_name, global_name)
233
+ end
234
+
235
+ puts "āœ… Clean completed!"
236
+ end
237
+
238
+ # Public methods for external access
239
+ def package_installed?(package_name)
240
+ return false unless File.exist?(configuration.package_json_path)
241
+
242
+ begin
243
+ package_data = JSON.parse(File.read(configuration.package_json_path))
244
+ dependencies = package_data.dig('dependencies') || {}
245
+ dev_dependencies = package_data.dig('devDependencies') || {}
246
+
247
+ dependencies.key?(package_name) || dev_dependencies.key?(package_name)
248
+ rescue JSON::ParserError, Errno::ENOENT
249
+ false
250
+ end
251
+ end
252
+
253
+ def detect_global_name(package_name, url = nil)
254
+ # Check built-in overrides first
255
+ override = IslandjsRails::BUILT_IN_GLOBAL_NAME_OVERRIDES[package_name]
256
+ return override if override
257
+
258
+ # For scoped packages, use the package name part
259
+ clean_name = package_name.include?('/') ? package_name.split('/').last : package_name
260
+
261
+ # Convert kebab-case to camelCase
262
+ clean_name.split('-').map.with_index { |part, i| i == 0 ? part : part.capitalize }.join
263
+ end
264
+
265
+ def version_for(library_name)
266
+ package_data = package_json
267
+ return nil unless package_data
268
+
269
+ dependencies = package_data.dig('dependencies') || {}
270
+ dev_dependencies = package_data.dig('devDependencies') || {}
271
+
272
+ version = dependencies[library_name] || dev_dependencies[library_name]
273
+ return nil unless version
274
+
275
+ version.gsub(/[\^~>=<]/, '')
276
+ end
277
+
278
+ def find_working_island_url(package_name, version)
279
+ puts "šŸ” Searching for island build..."
280
+
281
+ version ||= version_for(package_name)
282
+ return nil unless version
283
+
284
+ # Use original package name for URL, but get clean name for {name} substitution
285
+ clean_name = (Configuration::SCOPED_PACKAGE_MAPPINGS[package_name] || package_name).split('/').last
286
+
287
+ configuration.supported_cdns.each do |cdn_base|
288
+ IslandjsRails::UMD_PATH_PATTERNS.each do |pattern|
289
+ # Handle both {name} substitution patterns and fixed filename patterns
290
+ path = if pattern.include?('{name}')
291
+ pattern.gsub('{name}', clean_name)
292
+ else
293
+ pattern # Use pattern as-is for fixed filenames like IIFE
294
+ end
295
+ url = "#{cdn_base}/#{package_name}@#{version}/#{path}"
296
+
297
+
298
+ if url_accessible?(url)
299
+ puts "āœ“ Found island: #{url}"
300
+ return url
301
+ end
302
+ end
303
+ end
304
+
305
+ puts "āŒ No island build found for #{package_name}@#{version}"
306
+ nil
307
+ end
308
+
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
+ def find_working_umd_url(package_name, version)
324
+ puts " šŸ” Searching for UMD build..."
325
+
326
+ # Get package name without scope for path patterns
327
+ clean_name = package_name.split('/').last
328
+
329
+ IslandjsRails::CDN_BASES.each do |cdn_base|
330
+ IslandjsRails::UMD_PATH_PATTERNS.each do |pattern|
331
+ # Replace placeholders in pattern only if they exist
332
+ path = if pattern.include?('{name}')
333
+ pattern.gsub('{name}', clean_name)
334
+ else
335
+ pattern # Use pattern as-is for fixed filenames like IIFE
336
+ end
337
+ url = "#{cdn_base}/#{package_name}@#{version}/#{path}"
338
+
339
+
340
+ if url_accessible?(url)
341
+ puts " āœ“ Found UMD: #{url}"
342
+
343
+ # Try to detect global name from the UMD content
344
+ global_name = detect_global_name(package_name, url)
345
+
346
+ return [url, global_name]
347
+ end
348
+ end
349
+ end
350
+
351
+ puts " āŒ No UMD build found for #{package_name}@#{version}"
352
+ [nil, nil]
353
+ end
354
+
355
+ 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
+
369
+ # Check if a package has a partial file
370
+ def has_partial?(package_name)
371
+ File.exist?(partial_path_for(package_name))
372
+ end
373
+
374
+ # Get global name for a package (used by webpack externals)
375
+ def get_global_name_for_package(package_name)
376
+ detect_global_name(package_name)
377
+ end
378
+
379
+ def react_ecosystem_complete?
380
+ package_installed?('react') && package_installed?('react-dom')
381
+ end
382
+
383
+ def activate_react_scaffolding!
384
+ puts "\nšŸŽ‰ React ecosystem is now complete (React + React-DOM)!"
385
+
386
+ uncomment_react_imports!
387
+ create_hello_world_component!
388
+ build_bundle!
389
+ offer_demo_route!
390
+ end
391
+
392
+ def uncomment_react_imports!
393
+ index_js_path = File.join(Dir.pwd, 'app', 'javascript', 'islands', 'index.js')
394
+ return unless File.exist?(index_js_path)
395
+
396
+ content = File.read(index_js_path)
397
+
398
+ # Check if this looks like our commented template
399
+ if content.include?('// import HelloWorld from') && content.include?('// HelloWorld')
400
+ # Uncomment the import
401
+ updated_content = content.gsub('// import HelloWorld from', 'import HelloWorld from')
402
+ # Uncomment the export within the window.islandjsRails object
403
+ updated_content = updated_content.gsub(/(\s+)\/\/ HelloWorld/, '\1HelloWorld')
404
+
405
+ File.write(index_js_path, updated_content)
406
+ puts "āœ“ Activated React imports in index.js"
407
+ else
408
+ puts "āš ļø index.js has been modified - please add HelloWorld manually"
409
+ end
410
+ end
411
+
412
+ def create_hello_world_component!
413
+ components_dir = File.join(Dir.pwd, 'app', 'javascript', 'islands', 'components')
414
+ FileUtils.mkdir_p(components_dir)
415
+
416
+ # Create turbo.js utility first
417
+ create_turbo_utility!
418
+
419
+ hello_world_path = File.join(components_dir, 'HelloWorld.jsx')
420
+
421
+ if File.exist?(hello_world_path)
422
+ puts "āœ“ HelloWorld.jsx already exists"
423
+ return
424
+ end
425
+
426
+ # Copy from gem's template file instead of hardcoded string
427
+ gem_template_path = File.join(__dir__, '..', 'templates', 'app', 'javascript', 'islands', 'components', 'HelloWorld.jsx')
428
+
429
+ if File.exist?(gem_template_path)
430
+ FileUtils.cp(gem_template_path, hello_world_path)
431
+ puts "āœ“ Created HelloWorld.jsx component"
432
+ else
433
+ puts "āš ļø Template file not found: #{gem_template_path}"
434
+ end
435
+ end
436
+
437
+ def create_turbo_utility!
438
+ utils_dir = File.join(Dir.pwd, 'app', 'javascript', 'islands', 'utils')
439
+ FileUtils.mkdir_p(utils_dir)
440
+
441
+ turbo_path = File.join(utils_dir, 'turbo.js')
442
+
443
+ if File.exist?(turbo_path)
444
+ puts "āœ“ turbo.js utility already exists"
445
+ return
446
+ end
447
+
448
+ # Copy from gem's template file instead of hardcoded string
449
+ gem_template_path = File.join(__dir__, '..', 'templates', 'app', 'javascript', 'islands', 'utils', 'turbo.js')
450
+
451
+ if File.exist?(gem_template_path)
452
+ FileUtils.cp(gem_template_path, turbo_path)
453
+ puts "āœ“ Created turbo.js utility"
454
+ else
455
+ puts "āš ļø Template file not found: #{gem_template_path}"
456
+ end
457
+ end
458
+ end
459
+ end
460
+
461
+ # Load additional core methods
462
+ require_relative 'core_methods'