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,609 @@
1
+ module IslandjsRails
2
+ class Core
3
+ # Additional core methods (part 2)
4
+
5
+ def build_bundle!
6
+ puts "šŸ”Ø Building IslandJS webpack bundle..."
7
+
8
+ unless system('which yarn > /dev/null 2>&1')
9
+ puts "āŒ yarn not found, cannot build bundle"
10
+ return false
11
+ end
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')
16
+ end
17
+
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')
22
+ end
23
+
24
+ if success
25
+ puts "āœ… Bundle built successfully"
26
+ return true
27
+ else
28
+ puts "āŒ Build failed. Check your webpack configuration."
29
+ return false
30
+ end
31
+ end
32
+
33
+ def offer_demo_route!
34
+ # Check if demo route already exists
35
+ if demo_route_exists?
36
+ puts "āœ“ Demo route already exists at /islandjs/react"
37
+ return
38
+ end
39
+
40
+ print "\nā“ Would you like to create a demo route at /islandjs/react to showcase your HelloWorld component? (y/n): "
41
+ answer = STDIN.gets.chomp.downcase
42
+
43
+ if answer == 'y' || answer == 'yes'
44
+ create_demo_route!
45
+ puts "\nšŸŽ‰ Demo route created! Visit http://localhost:3000/islandjs/react to see your React component in action."
46
+ puts "šŸ’” You can remove it later by deleting the route, controller, and view manually."
47
+ else
48
+ puts "\nšŸ’” No problem! Here's how to render your HelloWorld component manually:"
49
+ puts " In any view: <%= react_component('HelloWorld') %>"
50
+ puts " Don't forget to: yarn build && rails server"
51
+ end
52
+ end
53
+
54
+ def demo_route_exists?
55
+ routes_file = File.join(Dir.pwd, 'config', 'routes.rb')
56
+ return false unless File.exist?(routes_file)
57
+
58
+ content = File.read(routes_file)
59
+ content.include?('islandjs_demo') || content.include?('islandjs/react') || content.include?("get 'islandjs'")
60
+ end
61
+
62
+ def create_demo_route!
63
+ create_demo_controller!
64
+ create_demo_view!
65
+ add_demo_route!
66
+ end
67
+
68
+ def create_demo_controller!
69
+ controller_dir = File.join(Dir.pwd, 'app', 'controllers')
70
+ FileUtils.mkdir_p(controller_dir)
71
+
72
+ controller_file = File.join(controller_dir, 'islandjs_demo_controller.rb')
73
+ copy_template_file('app/controllers/islandjs_demo_controller.rb', controller_file)
74
+ end
75
+
76
+ def create_demo_view!
77
+ view_dir = File.join(Dir.pwd, 'app', 'views', 'islandjs_demo')
78
+ FileUtils.mkdir_p(view_dir)
79
+
80
+ # Copy demo view templates from gem
81
+ copy_demo_template('index.html.erb', view_dir)
82
+ copy_demo_template('react.html.erb', view_dir)
83
+ end
84
+
85
+ def copy_demo_template(template_name, destination_dir)
86
+ gem_root = File.expand_path('../../..', __FILE__)
87
+ template_path = File.join(gem_root, 'lib', 'templates', 'app', 'views', 'islandjs_demo', template_name)
88
+ destination_path = File.join(destination_dir, template_name)
89
+
90
+ if File.exist?(template_path)
91
+ FileUtils.cp(template_path, destination_path)
92
+ puts " āœ“ Created #{template_name} at app/views/islandjs_demo/#{template_name}"
93
+ else
94
+ puts " āš ļø Template not found: #{template_path}"
95
+ end
96
+ end
97
+
98
+ def copy_template_file(template_name, destination_path)
99
+ gem_root = File.expand_path('../../..', __FILE__)
100
+ template_path = File.join(gem_root, 'lib', 'templates', template_name)
101
+
102
+ if File.exist?(template_path)
103
+ FileUtils.cp(template_path, destination_path)
104
+ puts " āœ“ Created #{File.basename(template_name)} from template"
105
+ else
106
+ puts " āš ļø Template not found: #{template_path}"
107
+ end
108
+ end
109
+
110
+ def get_demo_routes_content(indent, has_root_route)
111
+ gem_root = File.expand_path('../../..', __FILE__)
112
+ template_path = File.join(gem_root, 'lib', 'templates', 'config', 'demo_routes.rb')
113
+
114
+ if File.exist?(template_path)
115
+ routes_content = File.read(template_path)
116
+ # Apply indentation to each line
117
+ route_lines = routes_content.lines.map { |line| "#{indent}#{line}" }.join
118
+
119
+ # Add root route if none exists
120
+ unless has_root_route
121
+ root_route = "#{indent}root 'islandjs_demo#index'\n"
122
+ route_lines = root_route + route_lines
123
+ end
124
+
125
+ route_lines
126
+ else
127
+ # Fallback to hardcoded routes if template not found
128
+ route_lines = "#{indent}# IslandJS demo routes (you can remove these)\n"
129
+ unless has_root_route
130
+ route_lines += "#{indent}root 'islandjs_demo#index'\n"
131
+ end
132
+ route_lines += "#{indent}get 'islandjs', to: 'islandjs_demo#index'\n"
133
+ route_lines += "#{indent}get 'islandjs/react', to: 'islandjs_demo#react'\n"
134
+ route_lines
135
+ end
136
+ end
137
+
138
+ def add_demo_route!
139
+ routes_file = File.join(Dir.pwd, 'config', 'routes.rb')
140
+ return unless File.exist?(routes_file)
141
+
142
+ content = File.read(routes_file)
143
+
144
+ # Check if root route already exists
145
+ has_root_route = content.include?('root ') || content.match(/^\s*root\s/)
146
+
147
+ # Find the Rails.application.routes.draw block
148
+ if content.match(/Rails\.application\.routes\.draw do\s*$/)
149
+ # Determine indentation
150
+ indent = content.match(/^(\s*)Rails\.application\.routes\.draw do\s*$/)[1]
151
+
152
+ # Build route lines from template
153
+ route_lines = get_demo_routes_content(indent, has_root_route)
154
+
155
+ # Add the routes after the draw line
156
+ updated_content = content.sub(
157
+ /(Rails\.application\.routes\.draw do\s*$)/,
158
+ "\\1\n#{route_lines}"
159
+ )
160
+
161
+ File.write(routes_file, updated_content)
162
+ puts " āœ“ Added demo routes to config/routes.rb:"
163
+ unless has_root_route
164
+ puts " root 'islandjs_demo#index' (set as homepage)"
165
+ end
166
+ puts " get 'islandjs', to: 'islandjs_demo#index'"
167
+ puts " get 'islandjs/react', to: 'islandjs_demo#react'"
168
+ end
169
+ end
170
+
171
+ def setup_vendor_system!
172
+ # Initialize empty vendor manifest
173
+ manifest_path = configuration.vendor_manifest_path
174
+ unless File.exist?(manifest_path)
175
+ require 'json'
176
+ initial_manifest = { 'libs' => [] }
177
+ FileUtils.mkdir_p(File.dirname(manifest_path))
178
+ File.write(manifest_path, JSON.pretty_generate(initial_manifest))
179
+ puts " āœ“ Created vendor manifest"
180
+ end
181
+
182
+ # Generate initial empty vendor partial
183
+ vendor_manager = IslandjsRails.vendor_manager
184
+ vendor_manager.send(:regenerate_vendor_partial!)
185
+ puts " āœ“ Generated vendor UMD partial"
186
+ end
187
+
188
+ def inject_islands_helper_into_layout!
189
+ layout_path = find_application_layout
190
+ return unless layout_path
191
+
192
+ content = File.read(layout_path)
193
+ islands_helper_line = '<%= islands %>'
194
+ vendor_render_line = '<%= render "shared/islands/vendor_umd" %>'
195
+
196
+ # Check if islands helper or vendor partial is already included
197
+ if content.include?(islands_helper_line) || content.include?('islands %>') ||
198
+ content.include?(vendor_render_line) || content.include?('render "shared/islands/vendor_umd"')
199
+ puts " āœ“ Islands helper already included in layout"
200
+ return
201
+ end
202
+
203
+ # Try to inject after existing head content or before </head>
204
+ if content.include?('</head>')
205
+ updated_content = content.sub(
206
+ /\s*<\/head>/,
207
+ "\n #{islands_helper_line}\n </head>"
208
+ )
209
+
210
+ File.write(layout_path, updated_content)
211
+ puts " āœ“ Added islands helper to #{File.basename(layout_path)}"
212
+ else
213
+ puts " āš ļø Could not automatically inject islands helper. Please add manually:"
214
+ puts " #{islands_helper_line}"
215
+ end
216
+ end
217
+
218
+ def find_application_layout
219
+ # Look for application layout in common locations
220
+ layout_paths = [
221
+ File.join(Dir.pwd, 'app', 'views', 'layouts', 'application.html.erb'),
222
+ File.join(Dir.pwd, 'app', 'views', 'layouts', 'application.html.haml'),
223
+ File.join(Dir.pwd, 'app', 'views', 'layouts', 'application.html.slim')
224
+ ]
225
+
226
+ layout_paths.find { |path| File.exist?(path) }
227
+ end
228
+
229
+ def check_node_tools!
230
+ unless system('which npm > /dev/null 2>&1')
231
+ puts "āŒ npm not found. Please install Node.js and npm first."
232
+ exit 1
233
+ end
234
+
235
+ unless system('which yarn > /dev/null 2>&1')
236
+ puts "āŒ yarn not found. Please install yarn first: npm install -g yarn"
237
+ exit 1
238
+ end
239
+
240
+ puts "āœ“ npm and yarn are available"
241
+ end
242
+
243
+ def ensure_package_json!
244
+ if File.exist?(configuration.package_json_path)
245
+ puts "āœ“ package.json already exists"
246
+ return
247
+ end
248
+
249
+ puts "šŸ“ Creating package.json..."
250
+
251
+ # Use template and customize with current directory name
252
+ template_path = File.join(__dir__, '..', 'templates', 'package.json')
253
+ template_content = File.read(template_path)
254
+ package_json = JSON.parse(template_content)
255
+
256
+ # Customize with current directory name
257
+ package_json["name"] = File.basename(Dir.pwd)
258
+
259
+ File.write(configuration.package_json_path, JSON.pretty_generate(package_json))
260
+ puts "āœ“ Created package.json"
261
+ end
262
+
263
+ def install_essential_dependencies!
264
+ puts "šŸ“¦ Installing essential webpack dependencies..."
265
+ puts " Installing: #{ESSENTIAL_DEPENDENCIES.join(', ')}"
266
+
267
+ missing_deps = ESSENTIAL_DEPENDENCIES.select do |dep|
268
+ package_name = dep.split('@').first
269
+ !package_installed?(package_name)
270
+ end
271
+
272
+ if missing_deps.empty?
273
+ puts "āœ“ All essential dependencies already installed"
274
+ return
275
+ end
276
+
277
+ success = system("yarn add --dev #{missing_deps.join(' ')}")
278
+
279
+ unless success
280
+ puts "āŒ Failed to install dependencies"
281
+ exit 1
282
+ end
283
+
284
+ puts "āœ“ Installed essential webpack dependencies"
285
+ end
286
+
287
+ def create_scaffolded_structure!
288
+ puts "šŸ—ļø Creating scaffolded structure..."
289
+
290
+ # Copy entire JavaScript islands structure from templates
291
+ gem_root = File.expand_path('../../..', __FILE__)
292
+ template_js_dir = File.join(gem_root, 'lib', 'templates', 'app', 'javascript', 'islands')
293
+ target_js_dir = File.join(Dir.pwd, 'app', 'javascript', 'islands')
294
+
295
+ if Dir.exist?(template_js_dir)
296
+ FileUtils.mkdir_p(File.dirname(target_js_dir))
297
+ FileUtils.cp_r(template_js_dir, File.dirname(target_js_dir))
298
+ puts "āœ“ Created JavaScript islands structure from templates"
299
+ else
300
+ puts "āš ļø Template JavaScript directory not found: #{template_js_dir}"
301
+ # Fallback: create minimal structure
302
+ FileUtils.mkdir_p(File.join(target_js_dir, 'components'))
303
+ File.write(File.join(target_js_dir, 'components', '.gitkeep'), '')
304
+ puts "āœ“ Created minimal JavaScript structure"
305
+ end
306
+
307
+ FileUtils.mkdir_p(configuration.partials_dir)
308
+ puts "āœ“ Created #{configuration.partials_dir}"
309
+ end
310
+
311
+ # Automatically inject islands helper into Rails layout
312
+ def inject_umd_partials_into_layout!
313
+ layout_path = File.join(Dir.pwd, 'app', 'views', 'layouts', 'application.html.erb')
314
+
315
+ unless File.exist?(layout_path)
316
+ puts "āš ļø Layout file not found: #{layout_path}"
317
+ puts " Please add manually to your layout:"
318
+ puts " <%= islands %>"
319
+ return
320
+ end
321
+
322
+ content = File.read(layout_path)
323
+
324
+ # Check if already injected (idempotent)
325
+ if content.include?('island_partials') && content.include?('island_bundle_script') || content.include?('islands')
326
+ puts "āœ“ Islands helper already present in layout"
327
+ return
328
+ end
329
+
330
+ # Find the closing </head> tag and inject before it with proper indentation
331
+ if match = content.match(/^(\s*)<\/head>/i)
332
+ indent = match[1] # Capture the existing indentation
333
+ islands_injection = "#{indent}<!-- IslandjsRails: Auto-injected -->\n#{indent}<%= islands %>"
334
+
335
+ # Inject before </head> with proper indentation
336
+ updated_content = content.gsub(/^(\s*)<\/head>/i, "#{islands_injection}\n\\1</head>")
337
+ File.write(layout_path, updated_content)
338
+ puts "āœ“ Auto-injected UMD helper into app/views/layouts/application.html.erb"
339
+ else
340
+ puts "āš ļø Could not find </head> tag in layout"
341
+ puts " Please add manually to your layout:"
342
+ puts " <%= islands %>"
343
+ end
344
+ end
345
+
346
+ # Ensure node_modules is in .gitignore
347
+ def ensure_node_modules_gitignored!
348
+ gitignore_path = File.join(Dir.pwd, '.gitignore')
349
+
350
+ unless File.exist?(gitignore_path)
351
+ puts "āš ļø .gitignore not found, creating one..."
352
+ gitignore_content = <<~GITIGNORE
353
+ /node_modules
354
+ GITIGNORE
355
+ File.write(gitignore_path, gitignore_content)
356
+ puts "āœ“ Created .gitignore with /node_modules"
357
+ return
358
+ end
359
+
360
+ content = File.read(gitignore_path)
361
+
362
+ # Check if node_modules is already ignored (various patterns)
363
+ unless content.match?(/^\/node_modules\s*$/m) ||
364
+ content.match?(/^node_modules\/?\s*$/m) ||
365
+ content.match?(/^\*\*\/node_modules\/?\s*$/m)
366
+ content += "\n# IslandJS: Node.js dependencies\n/node_modules\n"
367
+ File.write(gitignore_path, content)
368
+ puts "āœ“ Added /node_modules to .gitignore"
369
+ else
370
+ puts "āœ“ .gitignore already configured for IslandjsRails"
371
+ end
372
+ end
373
+
374
+ def install_package!(package_name, version = nil)
375
+ # Get version from package.json
376
+ actual_version = version_for(package_name)
377
+
378
+ unless actual_version
379
+ raise IslandjsRails::PackageNotFoundError, "#{package_name} not found in package.json"
380
+ end
381
+
382
+ # Try to find working UMD URL
383
+ umd_url, global_name = find_working_umd_url(package_name, actual_version)
384
+
385
+ unless umd_url
386
+ raise IslandjsRails::UmdNotFoundError, "No UMD build found for #{package_name}@#{actual_version}. This package may not provide a UMD build."
387
+ end
388
+
389
+ # Download UMD content
390
+ umd_content = download_umd_content(umd_url)
391
+
392
+ # Create partial
393
+ create_partial_file(package_name, umd_content, global_name)
394
+ end
395
+
396
+ def download_umd_content(url)
397
+ uri = URI(url)
398
+ response = Net::HTTP.get_response(uri)
399
+
400
+ if response.code == '200'
401
+ response.body
402
+ else
403
+ raise IslandjsRails::Error, "Failed to download UMD from #{url}: #{response.code}"
404
+ end
405
+ end
406
+
407
+ def create_partial_file(package_name, island_content, global_name = nil)
408
+ partial_path = partial_path_for(package_name)
409
+
410
+ FileUtils.mkdir_p(File.dirname(partial_path))
411
+
412
+ partial_content = generate_partial_content(package_name, island_content, global_name)
413
+
414
+ File.write(partial_path, partial_content)
415
+ puts " āœ“ Created partial: #{File.basename(partial_path)}"
416
+ end
417
+
418
+ def generate_partial_content(package_name, island_content, global_name = nil)
419
+ safe_name = package_name.gsub(/[@\/]/, '_').gsub(/-/, '_')
420
+ global_name ||= detect_global_name(package_name)
421
+
422
+ # Base64 encode the content to completely avoid ERB parsing issues
423
+ require 'base64'
424
+ encoded_content = Base64.strict_encode64(island_content)
425
+
426
+ <<~ERB
427
+ <%# #{global_name} UMD Library %>
428
+ <%# Global: #{global_name} %>
429
+ <%# Generated by IslandjsRails %>
430
+ <script type="text/javascript">
431
+ (function() {
432
+ var script = document.createElement('script');
433
+ script.text = atob('<%= "#{encoded_content}" %>');
434
+ document.head.appendChild(script);
435
+ document.head.removeChild(script);
436
+ })();
437
+ </script>
438
+ ERB
439
+ end
440
+
441
+ def package_json
442
+ return @package_json if @package_json
443
+ return nil unless File.exist?(configuration.package_json_path)
444
+
445
+ begin
446
+ @package_json = JSON.parse(File.read(configuration.package_json_path))
447
+ rescue JSON::ParserError
448
+ nil
449
+ end
450
+ end
451
+
452
+ def installed_packages
453
+ package_data = package_json
454
+ return [] unless package_data
455
+
456
+ dependencies = package_data.dig('dependencies') || {}
457
+ dev_dependencies = package_data.dig('devDependencies') || {}
458
+
459
+ (dependencies.keys + dev_dependencies.keys).uniq
460
+ end
461
+
462
+ def supported_package?(package_name)
463
+ true
464
+ end
465
+
466
+ def partial_path_for(package_name)
467
+ partial_name = package_name.gsub(/[@\/]/, '_').gsub(/-/, '_')
468
+ configuration.partials_dir.join("_#{partial_name}.html.erb")
469
+ end
470
+
471
+ def download_and_create_partial!(package_name)
472
+ version = version_for(package_name)
473
+
474
+ # Try to find working UMD URL
475
+ umd_url, global_name = find_working_umd_url(package_name, version)
476
+
477
+ unless umd_url
478
+ puts " āŒ No UMD build found for #{package_name}@#{version}"
479
+ return
480
+ end
481
+
482
+ # Download UMD content
483
+ umd_content = download_umd_content(umd_url)
484
+
485
+ # Create partial
486
+ create_partial_file(package_name, umd_content, global_name)
487
+
488
+ puts " āœ“ Created partial: #{partial_path_for(package_name)}"
489
+ end
490
+
491
+ def add_package_via_yarn(package_name, version = nil)
492
+ package_spec = version ? "#{package_name}@#{version}" : package_name
493
+ command = "yarn add #{package_spec}"
494
+
495
+ stdout, stderr, status = Open3.capture3(command, chdir: defined?(Rails) ? Rails.root : Dir.pwd)
496
+
497
+ unless status.success?
498
+ raise YarnError, "Failed to add #{package_spec}: #{stderr}"
499
+ end
500
+
501
+ @package_json = nil
502
+ puts " āœ“ Added to package.json: #{package_spec}"
503
+ end
504
+
505
+ def yarn_update!(package_name, version = nil)
506
+ if version
507
+ add_package_via_yarn(package_name, version)
508
+ else
509
+ command = "yarn upgrade #{package_name}"
510
+ stdout, stderr, status = Open3.capture3(command, chdir: defined?(Rails) ? Rails.root : Dir.pwd)
511
+
512
+ unless status.success?
513
+ raise YarnError, "Failed to update #{package_name}: #{stderr}"
514
+ end
515
+
516
+ @package_json = nil
517
+ puts " āœ“ Updated in package.json: #{package_name}"
518
+ end
519
+ end
520
+
521
+ def remove_package_via_yarn(package_name)
522
+ command = "yarn remove #{package_name}"
523
+
524
+ stdout, stderr, status = Open3.capture3(command, chdir: defined?(Rails) ? Rails.root : Dir.pwd)
525
+
526
+ unless status.success?
527
+ raise YarnError, "Failed to remove #{package_name}: #{stderr}"
528
+ end
529
+
530
+ @package_json = nil
531
+ puts " āœ“ Removed from package.json: #{package_name}"
532
+ end
533
+
534
+ def generate_webpack_config!
535
+ copy_template_file('webpack.config.js', configuration.webpack_config_path)
536
+ end
537
+
538
+ def url_accessible?(url)
539
+ uri = URI(url)
540
+ response = Net::HTTP.get_response(uri)
541
+ response.code == '200'
542
+ rescue => e
543
+ false
544
+ end
545
+
546
+ def has_partial?(package_name)
547
+ File.exist?(partial_path_for(package_name))
548
+ end
549
+
550
+ def get_global_name_for_package(package_name)
551
+ detect_global_name(package_name)
552
+ end
553
+
554
+ def reset_webpack_externals
555
+ webpack_config_path = configuration.webpack_config_path
556
+ return unless File.exist?(webpack_config_path)
557
+
558
+ content = File.read(webpack_config_path)
559
+
560
+ externals_block = <<~JS
561
+ externals: {
562
+ // IslandjsRails managed externals - do not edit manually
563
+ },
564
+ JS
565
+
566
+ updated_content = content.gsub(
567
+ /externals:\s*\{[^}]*\}(?:,)?/m,
568
+ externals_block.chomp
569
+ )
570
+
571
+ File.write(webpack_config_path, updated_content)
572
+ puts " āœ“ Reset webpack externals"
573
+ end
574
+
575
+ def update_webpack_externals(package_name = nil, global_name = nil)
576
+ webpack_config_path = configuration.webpack_config_path
577
+ return unless File.exist?(webpack_config_path)
578
+
579
+ content = File.read(webpack_config_path)
580
+
581
+ externals = {}
582
+
583
+ # Get installed packages from vendor manifest instead of partials
584
+ vendor_manager = IslandjsRails.vendor_manager
585
+ manifest = vendor_manager.send(:read_manifest)
586
+
587
+ manifest['libs'].each do |lib|
588
+ pkg = lib['name']
589
+ externals[pkg] = get_global_name_for_package(pkg)
590
+ end
591
+
592
+ externals_lines = externals.map { |pkg, global| " \"#{pkg}\": \"#{global}\"" }
593
+ externals_block = <<~JS
594
+ externals: {
595
+ // IslandjsRails managed externals - do not edit manually
596
+ #{externals_lines.join(",\n")}
597
+ },
598
+ JS
599
+
600
+ updated_content = content.gsub(
601
+ /externals:\s*\{[^}]*\}(?:,)?/m,
602
+ externals_block.chomp
603
+ )
604
+
605
+ File.write(webpack_config_path, updated_content)
606
+ puts " āœ“ Updated webpack externals"
607
+ end
608
+ end
609
+ end