cable_ready 4.5.0 → 5.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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -376
  3. data/Gemfile +4 -1
  4. data/Gemfile.lock +146 -144
  5. data/README.md +54 -20
  6. data/Rakefile +8 -8
  7. data/app/assets/javascripts/cable_ready.js +1269 -0
  8. data/app/assets/javascripts/cable_ready.umd.js +1190 -0
  9. data/app/channels/cable_ready/stream.rb +14 -0
  10. data/app/helpers/cable_ready/view_helper.rb +58 -0
  11. data/app/jobs/cable_ready/broadcast_job.rb +15 -0
  12. data/app/models/concerns/cable_ready/updatable/collection_updatable_callbacks.rb +21 -0
  13. data/app/models/concerns/cable_ready/updatable/collections_registry.rb +59 -0
  14. data/app/models/concerns/cable_ready/updatable/memory_cache_debounce_adapter.rb +24 -0
  15. data/app/models/concerns/cable_ready/updatable/model_updatable_callbacks.rb +33 -0
  16. data/app/models/concerns/cable_ready/updatable.rb +211 -0
  17. data/app/models/concerns/extend_has_many.rb +15 -0
  18. data/bin/standardize +1 -1
  19. data/cable_ready.gemspec +20 -6
  20. data/lib/cable_ready/broadcaster.rb +4 -3
  21. data/lib/cable_ready/cable_car.rb +19 -0
  22. data/lib/cable_ready/channel.rb +29 -31
  23. data/lib/cable_ready/channels.rb +4 -5
  24. data/lib/cable_ready/compoundable.rb +11 -0
  25. data/lib/cable_ready/config.rb +28 -1
  26. data/lib/cable_ready/engine.rb +59 -0
  27. data/lib/cable_ready/identifiable.rb +48 -0
  28. data/lib/cable_ready/importmap.rb +4 -0
  29. data/lib/cable_ready/installer.rb +224 -0
  30. data/lib/cable_ready/operation_builder.rb +80 -0
  31. data/lib/cable_ready/sanity_checker.rb +63 -0
  32. data/lib/cable_ready/stream_identifier.rb +13 -0
  33. data/lib/cable_ready/version.rb +1 -1
  34. data/lib/cable_ready.rb +23 -10
  35. data/lib/cable_ready_helper.rb +13 -0
  36. data/lib/generators/cable_ready/channel_generator.rb +110 -0
  37. data/lib/generators/cable_ready/templates/app/javascript/channels/consumer.js.tt +6 -0
  38. data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.esbuild.tt +4 -0
  39. data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.importmap.tt +2 -0
  40. data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.shakapacker.tt +5 -0
  41. data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.vite.tt +1 -0
  42. data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.webpacker.tt +5 -0
  43. data/lib/generators/cable_ready/templates/app/javascript/config/cable_ready.js.tt +4 -0
  44. data/lib/generators/cable_ready/templates/app/javascript/config/index.js.tt +1 -0
  45. data/lib/generators/cable_ready/templates/app/javascript/config/mrujs.js.tt +9 -0
  46. data/lib/generators/cable_ready/templates/app/javascript/controllers/%file_name%_controller.js.tt +38 -0
  47. data/lib/generators/cable_ready/templates/app/javascript/controllers/application.js.tt +11 -0
  48. data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.esbuild.tt +7 -0
  49. data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.importmap.tt +5 -0
  50. data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.shakapacker.tt +5 -0
  51. data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.vite.tt +5 -0
  52. data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.webpacker.tt +5 -0
  53. data/lib/generators/cable_ready/templates/config/initializers/cable_ready.rb +27 -0
  54. data/lib/generators/cable_ready/templates/esbuild.config.mjs.tt +94 -0
  55. data/lib/install/action_cable.rb +144 -0
  56. data/lib/install/broadcaster.rb +109 -0
  57. data/lib/install/bundle.rb +54 -0
  58. data/lib/install/compression.rb +51 -0
  59. data/lib/install/config.rb +39 -0
  60. data/lib/install/development.rb +34 -0
  61. data/lib/install/esbuild.rb +101 -0
  62. data/lib/install/importmap.rb +96 -0
  63. data/lib/install/initializers.rb +15 -0
  64. data/lib/install/mrujs.rb +121 -0
  65. data/lib/install/npm_packages.rb +13 -0
  66. data/lib/install/shakapacker.rb +65 -0
  67. data/lib/install/spring.rb +54 -0
  68. data/lib/install/updatable.rb +34 -0
  69. data/lib/install/vite.rb +66 -0
  70. data/lib/install/webpacker.rb +93 -0
  71. data/lib/install/yarn.rb +56 -0
  72. data/lib/tasks/cable_ready/cable_ready.rake +247 -0
  73. data/package.json +42 -13
  74. data/rollup.config.mjs +57 -0
  75. data/web-test-runner.config.mjs +12 -0
  76. data/yarn.lock +3252 -327
  77. metadata +138 -9
  78. data/tags +0 -62
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cable_ready/installer"
4
+
5
+ hash = gemfile_hash
6
+
7
+ # run bundle only when gems are waiting to be added or removed
8
+ add = add_gem_list.exist? ? add_gem_list.readlines.map(&:chomp) : []
9
+ remove = remove_gem_list.exist? ? remove_gem_list.readlines.map(&:chomp) : []
10
+
11
+ if add.present? || remove.present?
12
+ lines = gemfile_path.readlines
13
+
14
+ remove.each do |name|
15
+ index = lines.index { |line| line =~ /gem ['"]#{name}['"]/ }
16
+ if index
17
+ if /^[^#]*gem ['"]#{name}['"]/.match?(lines[index])
18
+ lines[index] = "# #{lines[index]}"
19
+ say "✅ #{name} gem has been disabled"
20
+ else
21
+ say "⏩ #{name} gem is already disabled. Skipping."
22
+ end
23
+ end
24
+ end
25
+
26
+ add.each do |package|
27
+ matches = package.match(/(.+)@(.+)/)
28
+ name, version = matches[1], matches[2]
29
+
30
+ index = lines.index { |line| line =~ /gem ['"]#{name}['"]/ }
31
+ if index
32
+ if !lines[index].match(/^[^#]*gem ['"]#{name}['"].*#{version}['"]/)
33
+ lines[index] = "\ngem \"#{name}\", \"#{version}\"\n"
34
+ say "✅ #{name} gem has been installed"
35
+ else
36
+ say "⏩ #{name} gem is already installed. Skipping."
37
+ end
38
+ else
39
+ lines << "\ngem \"#{name}\", \"#{version}\"\n"
40
+ end
41
+ end
42
+
43
+ gemfile_path.write lines.join
44
+
45
+ bundle_command("install --quiet", "BUNDLE_IGNORE_MESSAGES" => "1") if hash != gemfile_hash
46
+ end
47
+
48
+ FileUtils.cp(development_working_path, development_path)
49
+ say "✅ development environment configuration installed"
50
+
51
+ FileUtils.cp(action_cable_initializer_working_path, action_cable_initializer_path)
52
+ say "✅ Action Cable initializer installed"
53
+
54
+ complete_step :bundle
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cable_ready/installer"
4
+
5
+ initializer = action_cable_initializer_working_path.read
6
+
7
+ proceed = false
8
+
9
+ if initializer.exclude? "PermessageDeflate.configure"
10
+ proceed = if options.key? "compression"
11
+ options["compression"]
12
+ else
13
+ !no?("✨ Configure Action Cable to compress your WebSocket traffic with gzip? (Y/n)")
14
+ end
15
+ end
16
+
17
+ if proceed
18
+ if !gemfile.match?(/gem ['"]permessage_deflate['"]/)
19
+ add_gem "permessage_deflate@>= 0.1"
20
+ end
21
+
22
+ # add permessage_deflate config to Action Cable initializer
23
+ if initializer.exclude? "PermessageDeflate.configure"
24
+ create_or_append(action_cable_initializer_working_path, verbose: false) do
25
+ <<~RUBY
26
+ module ActionCable
27
+ module Connection
28
+ class ClientSocket
29
+ alias_method :old_initialize, :initialize
30
+ def initialize(env, event_target, event_loop, protocols)
31
+ old_initialize(env, event_target, event_loop, protocols)
32
+ @driver.add_extension(
33
+ PermessageDeflate.configure(
34
+ level: Zlib::BEST_COMPRESSION,
35
+ max_window_bits: 13
36
+ )
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
42
+ RUBY
43
+ end
44
+
45
+ say "✅ Action Cable initializer patched to deflate WS traffic"
46
+ else
47
+ say "⏩ Action Cable initializer is already patched to deflate WS traffic. Skipping."
48
+ end
49
+ end
50
+
51
+ complete_step :compression
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cable_ready/installer"
4
+
5
+ return if pack_path_missing?
6
+
7
+ step_path = "/app/javascript/config/"
8
+ index_src = fetch(step_path, "index.js.tt")
9
+ index_path = config_path / "index.js"
10
+ cable_ready_src = fetch(step_path, "cable_ready.js.tt")
11
+ cable_ready_path = config_path / "cable_ready.js"
12
+
13
+ empty_directory config_path unless config_path.exist?
14
+
15
+ copy_file(index_src, index_path) unless index_path.exist?
16
+
17
+ index_pattern = /import ['"](\.\.\/|\.\/)?config['"]/
18
+ index_commented_pattern = /\s*\/\/\s*#{index_pattern}/
19
+ index_import = "import \"#{prefix}config\"\n"
20
+
21
+ if pack.match?(index_pattern)
22
+ if pack.match?(index_commented_pattern)
23
+ lines = pack_path.readlines
24
+ matches = lines.select { |line| line =~ index_commented_pattern }
25
+ lines[lines.index(matches.last).to_i] = index_import
26
+ pack_path.write lines.join
27
+ end
28
+ else
29
+ lines = pack_path.readlines
30
+ matches = lines.select { |line| line =~ /^import / }
31
+ lines.insert lines.index(matches.last).to_i + 1, index_import
32
+ pack_path.write lines.join
33
+ end
34
+ say "✅ CableReady configs will be imported in #{friendly_pack_path}"
35
+
36
+ # create entrypoint/config/cable_ready.js and make sure it's imported in application.js
37
+ copy_file(cable_ready_src, cable_ready_path) unless cable_ready_path.exist?
38
+
39
+ complete_step :config
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cable_ready/installer"
4
+
5
+ # mutate working copy of development.rb to avoid bundle alerts
6
+ FileUtils.cp(development_path, development_working_path)
7
+
8
+ # add default_url_options to development.rb for Action Mailer
9
+ if defined?(ActionMailer)
10
+ lines = development_working_path.readlines
11
+ if lines.find { |line| line.include?("config.action_mailer.default_url_options") }
12
+ say "⏩ Action Mailer default_url_options already defined. Skipping."
13
+ else
14
+ index = lines.index { |line| line =~ /^Rails.application.configure do/ }
15
+ lines.insert index + 1, " config.action_mailer.default_url_options = {host: \"localhost\", port: 3000}\n\n"
16
+ development_working_path.write lines.join
17
+
18
+ say "✅ Action Mailer default_url_options defined"
19
+ end
20
+ end
21
+
22
+ # add default_url_options to development.rb for Action Controller
23
+ lines = development_working_path.readlines
24
+ if lines.find { |line| line.include?("config.action_controller.default_url_options") }
25
+ say "⏩ Action Controller default_url_options already defined. Skipping."
26
+ else
27
+ index = lines.index { |line| line =~ /^Rails.application.configure do/ }
28
+ lines.insert index + 1, " config.action_controller.default_url_options = {host: \"localhost\", port: 3000}\n"
29
+ development_working_path.write lines.join
30
+
31
+ say "✅ Action Controller default_url_options defined"
32
+ end
33
+
34
+ complete_step :development
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cable_ready/installer"
4
+
5
+ return if pack_path_missing?
6
+
7
+ # verify that all critical dependencies are up to date; if not, queue for later
8
+ lines = package_json.readlines
9
+
10
+ if !lines.index { |line| line =~ /^\s*["']esbuild-rails["']: ["']\^1.0.3["']/ }
11
+ add_package "esbuild-rails@^1.0.3"
12
+ end
13
+
14
+ if !lines.index { |line| line =~ /^\s*["']@hotwired\/stimulus["']:/ }
15
+ add_package "@hotwired/stimulus@^3.2"
16
+ end
17
+ # copy esbuild.config.mjs to app root
18
+ esbuild_src = fetch("/", "esbuild.config.mjs.tt")
19
+ esbuild_path = Rails.root.join("esbuild.config.mjs")
20
+ if esbuild_path.exist?
21
+ if esbuild_path.read == esbuild_src.read
22
+ say "✅ esbuild.config.mjs present in app root"
23
+ else
24
+ backup(esbuild_path) do
25
+ template(esbuild_src, esbuild_path, verbose: false, entrypoint: entrypoint)
26
+ end
27
+ end
28
+ else
29
+ template(esbuild_src, esbuild_path, entrypoint: entrypoint)
30
+ end
31
+
32
+ step_path = "/app/javascript/controllers/"
33
+ application_js_src = fetch(step_path, "application.js.tt")
34
+ application_js_path = controllers_path / "application.js"
35
+ index_src = fetch(step_path, "index.js.esbuild.tt")
36
+ index_path = controllers_path / "index.js"
37
+ friendly_index_path = index_path.relative_path_from(Rails.root).to_s
38
+
39
+ # create entrypoint/controllers, if necessary
40
+ empty_directory controllers_path unless controllers_path.exist?
41
+
42
+ # configure Stimulus application superclass to import Action Cable consumer
43
+ friendly_application_js_path = application_js_path.relative_path_from(Rails.root).to_s
44
+ if application_js_path.exist?
45
+ backup(application_js_path) do
46
+ if application_js_path.read.include?("import consumer")
47
+ say "✅ #{friendly_application_js_path} is present"
48
+ else
49
+ inject_into_file application_js_path, "import consumer from \"../channels/consumer\"\n", after: "import { Application } from \"@hotwired/stimulus\"\n", verbose: false
50
+ inject_into_file application_js_path, "application.consumer = consumer\n", after: "application.debug = false\n", verbose: false
51
+ say "✅ #{friendly_application_js_path} has been updated to import the Action Cable consumer"
52
+ end
53
+ end
54
+ else
55
+ copy_file(application_js_src, application_js_path)
56
+ end
57
+
58
+ if index_path.exist?
59
+ if index_path.read != index_src.read
60
+ backup(index_path, delete: true) do
61
+ copy_file(index_src, index_path, verbose: false)
62
+ end
63
+ end
64
+ else
65
+ copy_file(index_src, index_path)
66
+ end
67
+ say "✅ #{friendly_index_path} has been created"
68
+
69
+ controllers_pattern = /import ['"].\/controllers['"]/
70
+ controllers_commented_pattern = /\s*\/\/\s*#{controllers_pattern}/
71
+
72
+ if pack.match?(controllers_pattern)
73
+ if pack.match?(controllers_commented_pattern)
74
+ proceed = if options.key? "uncomment"
75
+ options["uncomment"]
76
+ else
77
+ !no?("✨ Stimulus seems to be commented out in your application.js. Do you want to import your controllers? (Y/n)")
78
+ end
79
+
80
+ if proceed
81
+ # uncomment_lines only works with Ruby comments 🙄
82
+ lines = pack_path.readlines
83
+ matches = lines.select { |line| line =~ controllers_commented_pattern }
84
+ lines[lines.index(matches.last).to_i] = "import \".\/controllers\"\n" # standard:disable Style/RedundantStringEscape
85
+ pack_path.write lines.join
86
+ say "✅ Stimulus controllers imported in #{friendly_pack_path}"
87
+ else
88
+ say "🤷 your Stimulus controllers are not being imported in your application.js. We trust that you have a reason for this."
89
+ end
90
+ else
91
+ say "✅ Stimulus controllers imported in #{friendly_pack_path}"
92
+ end
93
+ else
94
+ lines = pack_path.readlines
95
+ matches = lines.select { |line| line =~ /^import / }
96
+ lines.insert lines.index(matches.last).to_i + 1, "import \".\/controllers\"\n" # standard:disable Style/RedundantStringEscape
97
+ pack_path.write lines.join
98
+ say "✅ Stimulus controllers imported in #{friendly_pack_path}"
99
+ end
100
+
101
+ complete_step :esbuild
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cable_ready/installer"
4
+
5
+ return if pack_path_missing?
6
+
7
+ if !importmap_path.exist?
8
+ halt "#{friendly_importmap_path} is missing. You need a valid importmap config file to proceed."
9
+ return
10
+ end
11
+
12
+ importmap = importmap_path.read
13
+
14
+ backup(importmap_path) do
15
+ if !importmap.include?("pin_all_from \"#{entrypoint}/controllers\"")
16
+ append_file(importmap_path, <<~RUBY, verbose: false)
17
+ pin_all_from "#{entrypoint}/controllers", under: "controllers"
18
+ RUBY
19
+ say "✅ pin controllers folder"
20
+ end
21
+
22
+ if !importmap.include?("pin_all_from \"#{entrypoint}/channels\"")
23
+ append_file(importmap_path, <<~RUBY, verbose: false)
24
+ pin_all_from "#{entrypoint}/channels", under: "channels"
25
+ RUBY
26
+ say "✅ pin channels folder"
27
+ end
28
+
29
+ if !importmap.include?("pin_all_from \"#{entrypoint}/config\"")
30
+ append_file(importmap_path, <<~RUBY, verbose: false)
31
+ pin_all_from "#{entrypoint}/config", under: "config"
32
+ RUBY
33
+ say "✅ pin config folder"
34
+ end
35
+
36
+ if !importmap.include?("pin \"@rails/actioncable\"")
37
+ append_file(importmap_path, <<~RUBY, verbose: false)
38
+ pin "@rails/actioncable", to: "actioncable.esm.js", preload: true
39
+ RUBY
40
+ say "✅ pin Action Cable"
41
+ end
42
+
43
+ if !importmap.include?("pin \"cable_ready\"")
44
+ append_file(importmap_path, <<~RUBY, verbose: false)
45
+ pin "cable_ready", to: "cable_ready.js", preload: true
46
+ RUBY
47
+ say "✅ pin CableReady"
48
+ end
49
+
50
+ if !importmap.include?("pin \"morphdom\"")
51
+ append_file(importmap_path, <<~RUBY, verbose: false)
52
+ pin "morphdom", to: "https://ga.jspm.io/npm:morphdom@2.6.1/dist/morphdom.js", preload: true
53
+ RUBY
54
+ say "✅ pin morphdom"
55
+ end
56
+ end
57
+
58
+ application_js_src = fetch("/", "app/javascript/controllers/application.js.tt")
59
+ application_js_path = controllers_path / "application.js"
60
+ index_src = fetch("/", "app/javascript/controllers/index.js.importmap.tt")
61
+ index_path = controllers_path / "index.js"
62
+
63
+ # create entrypoint/controllers, as well as the index, application and application_controller
64
+ empty_directory controllers_path unless controllers_path.exist?
65
+
66
+ # configure Stimulus application superclass to import Action Cable consumer
67
+ backup(application_js_path) do
68
+ if application_js_path.exist?
69
+ friendly_application_js_path = application_js_path.relative_path_from(Rails.root).to_s
70
+ if application_js_path.read.include?("import consumer")
71
+ say "✅ #{friendly_application_js_path} is present"
72
+ else
73
+ inject_into_file application_js_path, "import consumer from \"../channels/consumer\"\n", after: "import { Application } from \"@hotwired/stimulus\"\n", verbose: false
74
+ inject_into_file application_js_path, "application.consumer = consumer\n", after: "application.debug = false\n", verbose: false
75
+ say "✅ #{friendly_application_js_path} has been updated to import the Action Cable consumer"
76
+ end
77
+ else
78
+ copy_file(application_js_src, application_js_path)
79
+ end
80
+ end
81
+
82
+ if index_path.exist?
83
+ friendly_index_path = index_path.relative_path_from(Rails.root).to_s
84
+ if index_path.read == index_src.read
85
+ say "✅ #{friendly_index_path} is present"
86
+ else
87
+ backup(index_path, delete: true) do
88
+ copy_file(index_src, index_path, verbose: false)
89
+ end
90
+ say "✅ #{friendly_index_path} has been created"
91
+ end
92
+ else
93
+ copy_file(index_src, index_path)
94
+ end
95
+
96
+ complete_step :importmap
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cable_ready/installer"
4
+
5
+ cr_initializer_src = fetch("/", "config/initializers/cable_ready.rb")
6
+ cr_initializer_path = Rails.root.join("config/initializers/cable_ready.rb")
7
+
8
+ if !cr_initializer_path.exist?
9
+ copy_file(cr_initializer_src, cr_initializer_path, verbose: false)
10
+ say "✅ CableReady initializer created at config/initializers/cable_ready.rb"
11
+ else
12
+ say "⏩ config/initializers/cable_ready.rb already exists. Skipping."
13
+ end
14
+
15
+ complete_step :initializers
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cable_ready/installer"
4
+
5
+ return if pack_path_missing?
6
+
7
+ mrujs_path = config_path / "mrujs.js"
8
+
9
+ proceed = false
10
+
11
+ if !File.exist?(mrujs_path)
12
+ proceed = if options.key? "mrujs"
13
+ options["mrujs"]
14
+ else
15
+ !no?("✨ Would you like to install and enable mrujs? It's a modern, drop-in replacement for rails-ujs (Y/n)")
16
+ end
17
+ end
18
+
19
+ if proceed
20
+ if bundler == "importmap"
21
+
22
+ if !importmap_path.exist?
23
+ halt "#{friendly_importmap_path} is missing. You need a valid importmap config file to proceed."
24
+ return
25
+ end
26
+
27
+ importmap = importmap_path.read
28
+
29
+ if !importmap.include?("pin \"mrujs\"")
30
+ append_file(importmap_path, <<~RUBY, verbose: false)
31
+ pin "mrujs", to: "https://ga.jspm.io/npm:mrujs@0.10.1/dist/index.module.js"
32
+ RUBY
33
+ say "✅ pin mrujs"
34
+ end
35
+
36
+ if !importmap.include?("pin \"mrujs/plugins\"")
37
+ append_file(importmap_path, <<~RUBY, verbose: false)
38
+ pin "mrujs/plugins", to: "https://ga.jspm.io/npm:mrujs@0.10.1/plugins/dist/plugins.module.js"
39
+ RUBY
40
+ say "✅ pin mrujs plugins"
41
+ end
42
+ else
43
+ # queue mrujs for installation
44
+ if !package_json.read.include?('"mrujs":')
45
+ add_package "mrujs@^0.10.1"
46
+ end
47
+
48
+ # queue @rails/ujs for removal
49
+ if package_json.read.include?('"@rails/ujs":')
50
+ drop_package "@rails/ujs"
51
+ end
52
+ end
53
+
54
+ step_path = "/app/javascript/config/"
55
+ mrujs_src = fetch(step_path, "mrujs.js.tt")
56
+
57
+ # create entrypoint/config/mrujs.js if necessary
58
+ copy_file(mrujs_src, mrujs_path) unless mrujs_path.exist?
59
+
60
+ # import mrujs config in entrypoint/config/index.js
61
+ index_path = config_path / "index.js"
62
+ index = index_path.read
63
+ friendly_index_path = index_path.relative_path_from(Rails.root).to_s
64
+ mrujs_pattern = /import ['"].\/mrujs['"]/
65
+ mrujs_import = "import '.\/mrujs'\n" # standard:disable Style/RedundantStringEscape
66
+
67
+ if !index.match?(mrujs_pattern)
68
+ append_file(index_path, mrujs_import, verbose: false)
69
+ end
70
+ say "✅ mrujs imported in #{friendly_index_path}"
71
+
72
+ # remove @rails/ujs from application.js
73
+ rails_ujs_pattern = /import Rails from ['"]@rails\/ujs['"]/
74
+
75
+ lines = pack_path.readlines
76
+ if lines.index { |line| line =~ rails_ujs_pattern }
77
+ gsub_file pack_path, rails_ujs_pattern, "", verbose: false
78
+ say "✅ @rails/ujs removed from #{friendly_pack_path}"
79
+ end
80
+
81
+ # set Action View to generate remote forms when using form_with
82
+ application_path = Rails.root.join("config/application.rb")
83
+ application_pattern = /^[^#]*config\.action_view\.form_with_generates_remote_forms = true/
84
+ defaults_pattern = /config\.load_defaults \d\.\d/
85
+
86
+ lines = application_path.readlines
87
+ backup(application_path) do
88
+ if !lines.index { |line| line =~ application_pattern }
89
+ if (index = lines.index { |line| line =~ /^[^#]*#{defaults_pattern}/ })
90
+ gsub_file application_path, /\s*#{defaults_pattern}\n/, verbose: false do
91
+ <<-RUBY
92
+ \n#{lines[index]}
93
+ # form_with helper will generate remote forms by default (mrujs)
94
+ config.action_view.form_with_generates_remote_forms = true
95
+ RUBY
96
+ end
97
+ else
98
+ insert_into_file application_path, after: "class Application < Rails::Application" do
99
+ <<-RUBY
100
+
101
+ # form_with helper will generate remote forms by default (mrujs)
102
+ config.action_view.form_with_generates_remote_forms = true
103
+ RUBY
104
+ end
105
+ end
106
+ end
107
+ say "✅ form_with_generates_remote_forms set to true in config/application.rb"
108
+ end
109
+
110
+ # remove turbolinks from Gemfile because it's incompatible with mrujs (and unnecessary)
111
+ turbolinks_pattern = /^[^#]*gem ["']turbolinks["']/
112
+
113
+ lines = gemfile_path.readlines
114
+ if lines.index { |line| line =~ turbolinks_pattern }
115
+ remove_gem :turbolinks
116
+ else
117
+ say "✅ turbolinks is not present in Gemfile"
118
+ end
119
+ end
120
+
121
+ complete_step :mrujs
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cable_ready/installer"
4
+
5
+ lines = package_json.readlines
6
+
7
+ if !lines.index { |line| line =~ /^\s*["']cable_ready["']: ["'].*#{cr_npm_version}["']/ }
8
+ add_package "cable_ready@#{cr_npm_version}"
9
+ else
10
+ say "⏩ cable_ready npm package is already present"
11
+ end
12
+
13
+ complete_step :npm_packages
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cable_ready/installer"
4
+
5
+ return if pack_path_missing?
6
+
7
+ # verify that all critical dependencies are up to date; if not, queue for later
8
+ lines = package_json.readlines
9
+ if !lines.index { |line| line =~ /^\s*["']@hotwired\/stimulus["']:/ }
10
+ add_package "@hotwired/stimulus@^3.2"
11
+ else
12
+ say "⏩ @hotwired/stimulus npm package is already present. Skipping."
13
+ end
14
+
15
+ if !lines.index { |line| line =~ /^\s*["']@hotwired\/stimulus-webpack-helpers["']: ["']\^1.0.1["']/ }
16
+ add_package "@hotwired/stimulus-webpack-helpers@^1.0.1"
17
+ else
18
+ say "⏩ @hotwired/stimulus-webpack-helpers npm package is already present. Skipping."
19
+ end
20
+
21
+ step_path = "/app/javascript/controllers/"
22
+ application_js_src = fetch(step_path, "application.js.tt")
23
+ application_js_path = controllers_path / "application.js"
24
+ index_src = fetch(step_path, "index.js.shakapacker.tt")
25
+ index_path = controllers_path / "index.js"
26
+
27
+ # create entrypoint/controllers, as well as the index, application and application_controller
28
+ empty_directory controllers_path unless controllers_path.exist?
29
+
30
+ copy_file(application_js_src, application_js_path) unless application_js_path.exist?
31
+ copy_file(index_src, index_path) unless index_path.exist?
32
+
33
+ controllers_pattern = /import ['"]controllers['"]/
34
+ controllers_commented_pattern = /\s*\/\/\s*#{controllers_pattern}/
35
+
36
+ if pack.match?(controllers_pattern)
37
+ if pack.match?(controllers_commented_pattern)
38
+ proceed = if options.key? "uncomment"
39
+ options["uncomment"]
40
+ else
41
+ !no?("✨ Do you want to import your Stimulus controllers in application.js? (Y/n)")
42
+ end
43
+
44
+ if proceed
45
+ # uncomment_lines only works with Ruby comments 🙄
46
+ lines = pack_path.readlines
47
+ matches = lines.select { |line| line =~ controllers_commented_pattern }
48
+ lines[lines.index(matches.last).to_i] = "import \"controllers\"\n"
49
+ pack_path.write lines.join
50
+ say "✅ Stimulus controllers imported in #{friendly_pack_path}"
51
+ else
52
+ say "🤷 your Stimulus controllers are not being imported in your application.js. We trust that you have a reason for this."
53
+ end
54
+ else
55
+ say "✅ Stimulus controllers imported in #{friendly_pack_path}"
56
+ end
57
+ else
58
+ lines = pack_path.readlines
59
+ matches = lines.select { |line| line =~ /^import / }
60
+ lines.insert lines.index(matches.last).to_i + 1, "import \"controllers\"\n"
61
+ pack_path.write lines.join
62
+ say "✅ Stimulus controllers imported in #{friendly_pack_path}"
63
+ end
64
+
65
+ complete_step :shakapacker
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cable_ready/installer"
4
+
5
+ spring_pattern = /^[^#]*gem ["']spring["']/
6
+
7
+ proceed = false
8
+ lines = gemfile_path.readlines
9
+
10
+ if lines.index { |line| line =~ spring_pattern }
11
+ proceed = if options.key? "spring"
12
+ options["spring"]
13
+ else
14
+ !no?("✨ Would you like to disable the spring gem? \nIt's been removed from Rails 7, and is the frequent culprit behind countless mystery bugs. (Y/n)")
15
+ end
16
+ else
17
+ say "⏩ Spring is not installed."
18
+ end
19
+
20
+ if proceed
21
+ spring_watcher_pattern = /^[^#]*gem ["']spring-watcher-listen["']/
22
+ bin_rails_pattern = /^[^#]*load File.expand_path\("spring", __dir__\)/
23
+
24
+ if (index = lines.index { |line| line =~ spring_pattern })
25
+ remove_gem :spring
26
+
27
+ bin_spring = Rails.root.join("bin/spring")
28
+ if bin_spring.exist?
29
+ run "bin/spring binstub --remove --all"
30
+ say "✅ Removed spring binstubs"
31
+ end
32
+
33
+ bin_rails = Rails.root.join("bin/rails")
34
+ bin_rails_content = bin_rails.readlines
35
+ if (index = bin_rails_content.index { |line| line =~ bin_rails_pattern })
36
+ backup(bin_rails) do
37
+ bin_rails_content[index] = "# #{bin_rails_content[index]}"
38
+ bin_rails.write bin_rails_content.join
39
+ end
40
+ say "✅ Removed spring from bin/rails"
41
+ end
42
+ create_file "tmp/cable_ready_installer/kill_spring", verbose: false
43
+ else
44
+ say "✅ spring has been successfully removed"
45
+ end
46
+
47
+ if lines.index { |line| line =~ spring_watcher_pattern }
48
+ remove_gem "spring-watcher-listen"
49
+ end
50
+ else
51
+ say "⏩ Skipping."
52
+ end
53
+
54
+ complete_step :spring
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cable_ready/installer"
4
+
5
+ if application_record_path.exist?
6
+ lines = application_record_path.readlines
7
+
8
+ if !lines.index { |line| line =~ /^\s*include CableReady::Updatable/ }
9
+ proceed = if options.key? "updatable"
10
+ options["updatable"]
11
+ else
12
+ !no?("✨ Include CableReady::Updatable in Active Record model classes? (Y/n)")
13
+ end
14
+
15
+ unless proceed
16
+ complete_step :updatable
17
+
18
+ puts "⏩ Skipping."
19
+ return
20
+ end
21
+
22
+ index = lines.index { |line| line.include?("class ApplicationRecord < ActiveRecord::Base") }
23
+ lines.insert index + 1, " include CableReady::Updatable\n"
24
+ application_record_path.write lines.join
25
+
26
+ say "✅ included CableReady::Updatable in ApplicationRecord"
27
+ else
28
+ say "⏩ CableReady::Updatable has already been included in Active Record model classes. Skipping."
29
+ end
30
+ else
31
+ say "⏩ ApplicationRecord doesn't exist. Skipping."
32
+ end
33
+
34
+ complete_step :updatable