stimulus_reflex 3.5.0.pre8 → 3.5.0.pre10

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of stimulus_reflex might be problematic. Click here for more details.

Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -1218
  3. data/Gemfile +0 -1
  4. data/Gemfile.lock +145 -193
  5. data/README.md +48 -20
  6. data/Rakefile +0 -8
  7. data/app/assets/javascripts/stimulus_reflex.js +1174 -0
  8. data/app/assets/javascripts/stimulus_reflex.min.js +2 -0
  9. data/app/assets/javascripts/stimulus_reflex.min.js.map +1 -0
  10. data/app/assets/javascripts/stimulus_reflex.umd.js +1064 -0
  11. data/app/assets/javascripts/stimulus_reflex.umd.min.js +1065 -0
  12. data/app/assets/javascripts/stimulus_reflex.umd.min.js.map +1 -0
  13. data/app/channels/stimulus_reflex/channel.rb +28 -7
  14. data/bin/console +0 -2
  15. data/bin/standardize +2 -1
  16. data/lib/generators/stimulus_reflex/stimulus_reflex_generator.rb +72 -7
  17. data/lib/generators/stimulus_reflex/templates/app/controllers/examples_controller.rb.tt +9 -0
  18. data/lib/generators/stimulus_reflex/templates/app/javascript/channels/consumer.js.tt +6 -0
  19. data/lib/generators/stimulus_reflex/templates/app/javascript/channels/index.js.esbuild.tt +4 -0
  20. data/lib/generators/stimulus_reflex/templates/app/javascript/channels/index.js.importmap.tt +2 -0
  21. data/lib/generators/stimulus_reflex/templates/app/javascript/channels/index.js.shakapacker.tt +5 -0
  22. data/lib/generators/stimulus_reflex/templates/app/javascript/channels/index.js.vite.tt +1 -0
  23. data/lib/generators/stimulus_reflex/templates/app/javascript/channels/index.js.webpacker.tt +5 -0
  24. data/lib/generators/stimulus_reflex/templates/app/javascript/config/cable_ready.js.tt +4 -0
  25. data/lib/generators/stimulus_reflex/templates/app/javascript/config/index.js.tt +2 -0
  26. data/lib/generators/stimulus_reflex/templates/app/javascript/config/mrujs.js.tt +9 -0
  27. data/lib/generators/stimulus_reflex/templates/app/javascript/config/stimulus_reflex.js.tt +5 -0
  28. data/lib/generators/stimulus_reflex/templates/app/javascript/controllers/%file_name%_controller.js.tt +114 -74
  29. data/lib/generators/stimulus_reflex/templates/app/javascript/controllers/application.js.tt +11 -0
  30. data/lib/generators/stimulus_reflex/templates/app/javascript/controllers/application_controller.js.tt +49 -35
  31. data/lib/generators/stimulus_reflex/templates/app/javascript/controllers/index.js.esbuild.tt +7 -0
  32. data/lib/generators/stimulus_reflex/templates/app/javascript/controllers/index.js.importmap.tt +5 -0
  33. data/lib/generators/stimulus_reflex/templates/app/javascript/controllers/index.js.shakapacker.tt +5 -0
  34. data/lib/generators/stimulus_reflex/templates/app/javascript/controllers/index.js.vite.tt +5 -0
  35. data/lib/generators/stimulus_reflex/templates/app/javascript/controllers/index.js.webpacker.tt +5 -0
  36. data/lib/generators/stimulus_reflex/templates/app/reflexes/%file_name%_reflex.rb.tt +38 -7
  37. data/lib/generators/stimulus_reflex/templates/app/reflexes/application_reflex.rb.tt +10 -2
  38. data/lib/generators/stimulus_reflex/templates/app/views/examples/show.html.erb.tt +207 -0
  39. data/lib/generators/stimulus_reflex/templates/config/initializers/cable_ready.rb +22 -0
  40. data/lib/generators/stimulus_reflex/templates/config/initializers/stimulus_reflex.rb +18 -13
  41. data/lib/generators/stimulus_reflex/templates/esbuild.config.mjs.tt +94 -0
  42. data/lib/install/action_cable.rb +155 -0
  43. data/lib/install/broadcaster.rb +90 -0
  44. data/lib/install/bundle.rb +56 -0
  45. data/lib/install/compression.rb +41 -0
  46. data/lib/install/config.rb +87 -0
  47. data/lib/install/development.rb +110 -0
  48. data/lib/install/esbuild.rb +114 -0
  49. data/lib/install/example.rb +22 -0
  50. data/lib/install/importmap.rb +133 -0
  51. data/lib/install/initializers.rb +25 -0
  52. data/lib/install/mrujs.rb +133 -0
  53. data/lib/install/npm_packages.rb +25 -0
  54. data/lib/install/reflexes.rb +25 -0
  55. data/lib/install/shakapacker.rb +64 -0
  56. data/lib/install/spring.rb +54 -0
  57. data/lib/install/updatable.rb +34 -0
  58. data/lib/install/vite.rb +64 -0
  59. data/lib/install/webpacker.rb +90 -0
  60. data/lib/install/yarn.rb +55 -0
  61. data/lib/stimulus_reflex/broadcasters/broadcaster.rb +15 -8
  62. data/lib/stimulus_reflex/broadcasters/page_broadcaster.rb +7 -8
  63. data/lib/stimulus_reflex/broadcasters/selector_broadcaster.rb +10 -10
  64. data/lib/stimulus_reflex/broadcasters/update.rb +3 -0
  65. data/lib/stimulus_reflex/cable_readiness.rb +29 -0
  66. data/lib/stimulus_reflex/cable_ready_channels.rb +5 -5
  67. data/lib/stimulus_reflex/callbacks.rb +17 -1
  68. data/lib/stimulus_reflex/concern_enhancer.rb +6 -4
  69. data/lib/stimulus_reflex/configuration.rb +12 -2
  70. data/lib/stimulus_reflex/dataset.rb +11 -1
  71. data/lib/stimulus_reflex/engine.rb +40 -0
  72. data/lib/stimulus_reflex/html/document.rb +59 -0
  73. data/lib/stimulus_reflex/html/document_fragment.rb +13 -0
  74. data/lib/stimulus_reflex/importmap.rb +7 -0
  75. data/lib/stimulus_reflex/installer.rb +274 -0
  76. data/lib/stimulus_reflex/open_struct_fix.rb +31 -0
  77. data/lib/stimulus_reflex/reflex.rb +32 -25
  78. data/lib/stimulus_reflex/reflex_data.rb +18 -2
  79. data/lib/stimulus_reflex/reflex_factory.rb +6 -3
  80. data/lib/stimulus_reflex/request_parameters.rb +2 -0
  81. data/lib/stimulus_reflex/utils/logger.rb +12 -0
  82. data/lib/stimulus_reflex/utils/sanity_checker.rb +8 -106
  83. data/lib/stimulus_reflex/version.rb +1 -1
  84. data/lib/stimulus_reflex.rb +4 -6
  85. data/lib/tasks/stimulus_reflex/stimulus_reflex.rake +252 -0
  86. data/package.json +73 -0
  87. data/rollup.config.mjs +86 -0
  88. data/stimulus_reflex.gemspec +60 -0
  89. data/web-test-runner.config.mjs +12 -0
  90. data/yarn.lock +5098 -0
  91. metadata +191 -78
  92. data/LATEST +0 -1
  93. data/lib/generators/USAGE +0 -14
  94. data/lib/generators/stimulus_reflex/initializer_generator.rb +0 -14
  95. data/lib/tasks/stimulus_reflex/install.rake +0 -116
  96. data/test/broadcasters/broadcaster_test.rb +0 -11
  97. data/test/broadcasters/broadcaster_test_case.rb +0 -39
  98. data/test/broadcasters/nothing_broadcaster_test.rb +0 -30
  99. data/test/broadcasters/page_broadcaster_test.rb +0 -77
  100. data/test/broadcasters/selector_broadcaster_test.rb +0 -167
  101. data/test/callbacks_test.rb +0 -652
  102. data/test/concern_enhancer_test.rb +0 -54
  103. data/test/element_test.rb +0 -254
  104. data/test/generators/stimulus_reflex_generator_test.rb +0 -58
  105. data/test/reflex_test.rb +0 -43
  106. data/test/test_helper.rb +0 -71
  107. data/test/tmp/app/reflexes/application_reflex.rb +0 -19
  108. data/test/tmp/app/reflexes/demo_reflex.rb +0 -35
@@ -0,0 +1,207 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>StimulusReflex Demo</title>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width,initial-scale=1">
7
+ <%%= csrf_meta_tags %>
8
+ <%%= csp_meta_tag %>
9
+
10
+ <%% unless Rails.root.join("config/importmap.rb").exist? %>
11
+ <script type="importmap">
12
+ {
13
+ "imports": {
14
+ "fireworks-js": "https://ga.jspm.io/npm:fireworks-js@2.10.0/dist/index.es.js"
15
+ }
16
+ }
17
+ </script>
18
+ <%% end %>
19
+
20
+ <%% if respond_to?(:vite_javascript_tag) %>
21
+ <%%= vite_client_tag %>
22
+ <%%= vite_javascript_tag "application", defer: true %>
23
+ <%% elsif respond_to?(:javascript_pack_tag) %>
24
+ <%%= javascript_pack_tag "application", defer: true %>
25
+ <%% elsif respond_to?(:javascript_importmap_tags) %>
26
+ <%%= javascript_importmap_tags %>
27
+ <%% elsif respond_to?(:javascript_include_tag) %>
28
+ <%%= javascript_include_tag "application", defer: true %>
29
+ <%% end %>
30
+
31
+ <script async src="https://ga.jspm.io/npm:es-module-shims@1.5.1/dist/es-module-shims.js" crossorigin="anonymous"></script>
32
+ <script type="module">
33
+ import { Fireworks } from 'fireworks-js'
34
+
35
+ const fireworks = new Fireworks(document.querySelector('.fireworks'))
36
+ document.addEventListener('fireworks', () => fireworks.launch(12))
37
+ </script>
38
+
39
+ <link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.min.css">
40
+ </head>
41
+
42
+ <body style="cursor: auto;">
43
+ <main class="container">
44
+ <h1>StimulusReflex</h1>
45
+
46
+ <p>Actual demonstrations will show up in this space, soon. In the meantime, verify that your installation was successful:</p>
47
+
48
+ <button data-reflex="click->Example#increment">Increment</button>
49
+ <button data-reflex="click->Example#reset">Reset</button>
50
+
51
+ <br />
52
+
53
+ <div>
54
+ Last Refresh:
55
+ <b><mark id="time"><%%= Time.now %></mark></b>
56
+ </div>
57
+
58
+ <div>
59
+ Clicked
60
+ <b><mark id="count"><%%= session[:count] || 0 %></mark></b>
61
+ times
62
+ </div>
63
+
64
+ <br/>
65
+
66
+ <progress value="<%%= session[:count] %>" max="10"></progress>
67
+
68
+ <div id="reload"></div>
69
+
70
+ <h1>CableReady</h1>
71
+
72
+ <p>CableReady lets you control one or many clients from the server in real-time.</p>
73
+
74
+ <p>Everything in CableReady revolves around its <a href="https://cableready.stimulusreflex.com/reference/operations" target="_blank">38+</a> <b>operations</b>, which are commands that can update content, raise events, write cookies and even play audio. A group of one or more operations is called a <b>broadcast</b>. Broadcasts follow a <a href="https://dev.to/leastbad/the-cableready-language-implementation-project-4hjd">simple JSON format</a>.</p>
75
+
76
+ <p>We're going to go through the main ways developers use CableReady with some live demonstrations and code samples. We recommend that you open the controller class and ERB template for this page to follow along.</p>
77
+
78
+ <article>
79
+ <h3>Subscribe to a channel to receive broadcasts</h3>
80
+
81
+ WebSockets is the primary way most Rails developers use CableReady, via the <ins>cable_ready</ins> method.
82
+
83
+ <p>Use the <ins>cable_ready_stream_from</ins> helper to create a secure Action Cable subscription:</p>
84
+
85
+ <kbd>cable_ready_stream_from :example_page</kbd>
86
+
87
+ <%%= cable_ready_stream_from :example_page %>
88
+
89
+ <p style="margin-top: 1rem;">Every user looking at a page subscribed to the <ins>:example_page</ins> channel will receive the same broadcasts.</p>
90
+
91
+ <p>You can call <ins>cable_ready</ins> <a href="https://cableready.stimulusreflex.com/guide/cableready-everywhere" target="_blank">pretty much anywhere</a> in your application. Try it in your <kbd>rails console</kbd> now:</p>
92
+
93
+ <kbd>include CableReady::Broadcaster<br>cable_ready[:example_page].text_content("#cable_ready_stream_from_output", text: "Hello from the console!").broadcast</kbd>
94
+
95
+ <p style="margin-top: 1rem;">Any message you send will appear in the <ins>#cable_ready_stream_from_output</ins> DIV below &mdash; even if you <i>open multiple tabs</i>.</p>
96
+
97
+ <div id="cable_ready_stream_from_output" style="height: 2rem; font-weight: bolder; border: coral 2px dashed; padding: 0.15rem 0.4rem;"></div>
98
+
99
+ <p style="margin-top: 1rem;">While it's easy to <a href="https://cableready.stimulusreflex.com/guide/broadcasting-to-resources" target="_blank">create your own custom Action Cable channels</a>, <ins>cable_ready_stream_from</ins> will be the first tool you reach for, because it doesn't require any additional code.</p>
100
+
101
+ <p>Specify Active Record models or compound qualifiers to go full-ninja: 🥷</p>
102
+
103
+ <kbd>cable_ready_stream_from current_user</kbd><br>
104
+
105
+ <kbd style="margin-top: 0.3rem;">cable_ready_stream_from @post, :comments</kbd>
106
+
107
+ <p style="margin-top: 1rem;">These examples barely scrape the surface of what's possible. Be sure to check out the <a href="https://cableready.stimulusreflex.com/guide/identifiers" target="_blank">Stream Identifiers</a> chapter.</p>
108
+ </article>
109
+
110
+ <article>
111
+ <h3>Updatable: magically update the DOM when server-side data changes</h3>
112
+
113
+ <p>The <ins>updates_for</ins> helper allow you to designate sections of your page that will <a href="https://cableready.stimulusreflex.com/guide/updatable" target="_blank">update automatically</a> with new content when an Active Record model changes. 🤯</p>
114
+
115
+ <small>It's difficult to demonstrate this feature without creating a temporary model and a migration; a road to hell, paved with good intentions. However, you likely have these models (or similar) in your application. Uncomment, tweak if necessary and follow along!</small>
116
+
117
+ <p style="margin-top: 1rem;">First, call <ins>enable_updates</ins> in your model. You can use it on associations, too.</p>
118
+
119
+ <kbd> class User < ApplicationRecord<br>
120
+ &nbsp;&nbsp;enable_updates<br>
121
+ &nbsp;&nbsp;has_many :posts, enable_updates: true<br>
122
+ end<br>
123
+ <br>
124
+ class Post < ApplicationRecord<br>
125
+ &nbsp;&nbsp;belongs_to :user<br>
126
+ end
127
+ </kbd>
128
+
129
+ <p style="margin-top: 1rem;">By default, updates will be broadcast when any CRUD action is performed on the model. You can customize this behavior by passing options such as <ins>on: [:create, :update]</ins> or <ins>if: -> { id.even? }</ins>.</p>
130
+
131
+ <p>Next, use the <ins>updates_for</ins> helper to create one or more containers that will receive content updates.</p>
132
+
133
+ <kbd> &lt;%= cable_ready_updates_for current_user do %&gt;<br>
134
+ &nbsp;&nbsp;&lt;%= current_user.name %&gt;<br>
135
+ &lt;% end %&gt;
136
+ </kbd>
137
+
138
+ <!--
139
+ <%%#= cable_ready_updates_for current_user do %>
140
+ <p style="margin-top: 1rem;"><%%#= current_user.name %></p>
141
+ <%%# end %>
142
+ -->
143
+
144
+ <p style="margin-top: 1rem;">Update the current user in Rails console, and your page instantly reflects the new name. 🪄</p>
145
+
146
+ <p>Specify the class constant to get updates when records are created or deleted:</p>
147
+
148
+ <kbd> &lt;%= cable_ready_updates_for User do %&gt;<br>
149
+ &nbsp;&nbsp;&lt;ul&gt;<br>
150
+ &nbsp;&nbsp;&lt;% @users.each do |user| %&gt;<br>
151
+ &nbsp;&nbsp;&nbsp;&nbsp;&lt;li&gt;&lt;%= user.name %&gt;&lt;/li&gt;<br>
152
+ &nbsp;&nbsp;&lt;% end %&gt;<br>
153
+ &nbsp;&nbsp;&lt;/ul&gt;<br>
154
+ &lt;% end %&gt;
155
+ </kbd>
156
+
157
+ <!--
158
+ <%%#= cable_ready_updates_for User do %>
159
+ <ul style="margin-top: 1rem;">
160
+ <%%# @users.each do |user| %>
161
+ <li><%%#= user.name %></li>
162
+ <%%# end %>
163
+ </ul>
164
+ <%%# end %>
165
+ -->
166
+
167
+ <p style="margin-top: 1rem;">Update when new posts are created by the current user:</p>
168
+
169
+ <kbd> &lt;%= cable_ready_updates_for current_user, :posts do %&gt;<br>
170
+ &nbsp;&nbsp;&lt;ul&gt;<br>
171
+ &nbsp;&nbsp;&lt;% @posts.each do |post| %&gt;<br>
172
+ &nbsp;&nbsp;&nbsp;&nbsp;&lt;li&gt;&lt;%= post.title %&gt;&lt;/li&gt;<br>
173
+ &nbsp;&nbsp;&lt;% end %&gt;<br>
174
+ &nbsp;&nbsp;&lt;/ul&gt;<br>
175
+ &lt;% end %&gt;
176
+ </kbd>
177
+
178
+ <!--
179
+ <%%#= cable_ready_updates_for current_user, :posts do %>
180
+ <ul style="margin-top: 1rem;">
181
+ <%%# @posts.each do |post| %>
182
+ <li><%%#= post.title %></li>
183
+ <%%# end %>
184
+ </ul>
185
+ <%%# end %>
186
+ -->
187
+
188
+ <p style="margin-top: 1rem;">One major advantage of the Updatable approach is that each visitor sees <b>personalized content</b>. This is difficult with a WebSockets broadcast, where every subscriber receives the same data.</p>
189
+
190
+ <p>Instead, Updatable notifies all subscribers that an update is available, prompting each client to make a fetch request and refresh sections of the page.</p>
191
+
192
+ <p>There's more to <a href="https://cableready.stimulusreflex.com/guide/updatable" target="_blank">Updatable</a> than what's covered here... <i>but, not much more</i>. It really is that simple.</p>
193
+ </article>
194
+
195
+ <article>
196
+ <p>If you're finished with this example page and resource controller, you can destroy them:</p>
197
+
198
+ <kbd>rails destroy stimulus_reflex example</kbd>
199
+ </article>
200
+
201
+ <p>As always, please drop by the <a href="https://discord.gg/stimulus-reflex" target="_blank">StimulusReflex Discord server</a> if you have any questions or need support of any kind. We're incredibly proud of the community that has formed around these libraries, and we discuss everything from JavaScript/Ruby/CSS to View Component/Phlex to databases and CRDTs. <b>We'd love to hear what you're building with StimulusReflex and CableReady.</b></p>
202
+
203
+ <p>You can find the documentation for StimulusReflex <a href="https://docs.stimulusreflex.com" target="_blank">here</a> and CableReady <a href="https://cableready.stimulusreflex.com" target="_blank">here</a>.</p>
204
+ </main>
205
+ <div class="fireworks" style="position: fixed; bottom: 0; width: 100vw; height: 100vh; pointer-events: none;"></div>
206
+ </body>
207
+ </html>
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ CableReady.configure do |config|
4
+ # Enable/disable exiting / warning when the sanity checks fail options:
5
+ # `:exit` or `:warn` or `:ignore`
6
+ #
7
+ # config.on_failed_sanity_checks = :exit
8
+
9
+ # Enable/disable assets compilation
10
+ # `true` or `false`
11
+ #
12
+ # config.precompile_assets = true
13
+
14
+ # Define your own custom operations
15
+ # https://cableready.stimulusreflex.com/customization#custom-operations
16
+ #
17
+ # config.add_operation_name :jazz_hands
18
+
19
+ # Change the default Active Job queue used for broadcast_later and broadcast_later_to
20
+ #
21
+ # config.broadcast_job_queue = :default
22
+ end
@@ -6,44 +6,49 @@
6
6
  # ActionCable.server.config.logger = Logger.new(nil)
7
7
 
8
8
  StimulusReflex.configure do |config|
9
- # Enable/disable exiting / warning when the sanity checks fail options:
9
+ # Enable/disable exiting / warning when the sanity checks fail:
10
10
  # `:exit` or `:warn` or `:ignore`
11
-
11
+ #
12
12
  # config.on_failed_sanity_checks = :exit
13
13
 
14
- # Enable/disable exiting / warning when there's a new StimulusReflex release
15
- # `:exit` or `:warn` or `:ignore`
16
-
17
- # config.on_new_version_available = :ignore
18
-
19
14
  # Enable/disable exiting / warning when there is no default URLs specified in environment config
20
15
  # `:warn` or `:ignore`
21
-
16
+ #
22
17
  # config.on_missing_default_urls = :warn
23
18
 
24
- # Override the parent class that the StimulusReflex ActionCable channel inherits from
19
+ # Enable/disable assets compilation
20
+ # `true` or `false`
21
+ #
22
+ # config.precompile_assets = true
25
23
 
24
+ # Override the CableReady operation used for morphing and replacing content
25
+ #
26
+ # config.morph_operation = :morph
27
+ # config.replace_operation = :inner_html
28
+
29
+ # Override the parent class that the StimulusReflex ActionCable channel inherits from
30
+ #
26
31
  # config.parent_channel = "ApplicationCable::Channel"
27
32
 
28
33
  # Override the logger that the StimulusReflex uses; default is Rails' logger
29
34
  # eg. Logger.new(RAILS_ROOT + "/log/reflex.log")
30
-
35
+ #
31
36
  # config.logger = Rails.logger
32
37
 
33
38
  # Customize server-side Reflex logging format, with optional colorization:
34
- # Available tokens: session_id, session_id_full, reflex_info, operation, reflex_id, reflex_id_full, mode, selector, operation_counter, connection_id, connection_id_full, timestamp
39
+ # Available tokens: session_id, session_id_full, reflex_info, operation, id, id_full, mode, selector, operation_counter, connection_id, connection_id_full, timestamp
35
40
  # Available colors: red, green, yellow, blue, magenta, cyan, white
36
41
  # You can also use attributes from your ActionCable Connection's identifiers that resolve to valid ActiveRecord models
37
42
  # eg. if your connection is `identified_by :current_user` and your User model has an email attribute, you can access r.email (it will display `-` if the user isn't logged in)
38
43
  # Learn more at: https://docs.stimulusreflex.com/appendices/troubleshooting#stimulusreflex-logging
39
-
44
+ #
40
45
  # config.logging = proc { "[#{session_id}] #{operation_counter.magenta} #{reflex_info.green} -> #{selector.cyan} via #{mode} Morph (#{operation.yellow})" }
41
46
 
42
47
  # Optimized for speed, StimulusReflex doesn't enable Rack middleware by default.
43
48
  # If you are using Page Morphs and your app uses Rack middleware to rewrite part of the request path, you must enable those middleware modules in StimulusReflex.
44
49
  #
45
50
  # Learn more about registering Rack middleware in Rails here: https://guides.rubyonrails.org/rails_on_rack.html#configuring-middleware-stack
46
-
51
+ #
47
52
  # config.middleware.use FirstRackMiddleware
48
53
  # config.middleware.use SecondRackMiddleware
49
54
  end
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Esbuild is configured with 3 modes:
4
+ //
5
+ // `yarn build` - Build JavaScript and exit
6
+ // `yarn build --watch` - Rebuild JavaScript on change
7
+ // `yarn build --reload` - Reloads page when views, JavaScript, or stylesheets change
8
+ //
9
+ // Minify is enabled when "RAILS_ENV=production"
10
+ // Sourcemaps are enabled in non-production environments
11
+
12
+ import * as esbuild from "esbuild"
13
+ import path from "path"
14
+ import rails from "esbuild-rails"
15
+ import chokidar from "chokidar"
16
+ import http from "http"
17
+ import { setTimeout } from "timers/promises"
18
+
19
+ const clients = []
20
+
21
+ const entryPoints = [
22
+ "application.js"
23
+ ]
24
+
25
+ const watchDirectories = [
26
+ "./app/javascript/**/*.js",
27
+ "./app/views/**/*.html.erb",
28
+ "./app/assets/builds/**/*.css", // Wait for cssbundling changes
29
+ ]
30
+
31
+ const config = {
32
+ absWorkingDir: path.join(process.cwd(), "app/javascript"),
33
+ bundle: true,
34
+ entryPoints: entryPoints,
35
+ minify: process.env.RAILS_ENV == "production",
36
+ outdir: path.join(process.cwd(), "app/assets/builds"),
37
+ plugins: [rails()],
38
+ sourcemap: process.env.RAILS_ENV != "production"
39
+ }
40
+
41
+ async function buildAndReload() {
42
+ // Foreman & Overmind assign a separate PORT for each process
43
+ const port = parseInt(process.env.PORT)
44
+ const context = await esbuild.context({
45
+ ...config,
46
+ banner: {
47
+ js: ` (() => new EventSource("http://localhost:${port}").onmessage = () => location.reload())();`,
48
+ }
49
+ })
50
+
51
+ // Reload uses an HTTP server as an even stream to reload the browser
52
+ http.createServer((req, res) => {
53
+ return clients.push(
54
+ res.writeHead(200, {
55
+ "Content-Type": "text/event-stream",
56
+ "Cache-Control": "no-cache",
57
+ "Access-Control-Allow-Origin": "*",
58
+ Connection: "keep-alive",
59
+ })
60
+ )
61
+ }).listen(port)
62
+
63
+ await context.rebuild()
64
+ console.log("[reload] initial build succeeded")
65
+
66
+ let ready = false
67
+ chokidar.watch(watchDirectories).on("ready", () => {
68
+ console.log("[reload] ready")
69
+ ready = true
70
+ }).on("all", async (event, path) => {
71
+ if (ready === false) return
72
+
73
+ if (path.includes("javascript")) {
74
+ try {
75
+ await setTimeout(20)
76
+ await context.rebuild()
77
+ console.log("[reload] build succeeded")
78
+ } catch (error) {
79
+ console.error("[reload] build failed", error)
80
+ }
81
+ }
82
+ clients.forEach((res) => res.write("data: update\n\n"))
83
+ clients.length = 0
84
+ })
85
+ }
86
+
87
+ if (process.argv.includes("--reload")) {
88
+ buildAndReload()
89
+ } else if (process.argv.includes("--watch")) {
90
+ let context = await esbuild.context({...config, logLevel: 'info'})
91
+ context.watch()
92
+ } else {
93
+ esbuild.build(config)
94
+ }
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stimulus_reflex/installer"
4
+
5
+ # verify that Action Cable is installed
6
+ if defined?(ActionCable::Engine)
7
+ say "⏩ ActionCable::Engine is already loaded and in scope. Skipping"
8
+ else
9
+ halt "ActionCable::Engine is not loaded, please add or uncomment `require \"action_cable/engine\"` to your `config/application.rb`"
10
+ return
11
+ end
12
+
13
+ return if pack_path_missing?
14
+
15
+ # verify that the Action Cable pubsub config is created
16
+ cable_config = Rails.root.join("config/cable.yml")
17
+
18
+ if cable_config.exist?
19
+ say "⏩ config/cable.yml is already present. Skipping."
20
+ else
21
+ inside "config" do
22
+ template "cable.yml"
23
+ end
24
+
25
+ say "✅ Created config/cable.yml"
26
+ end
27
+
28
+ # verify that the Action Cable pubsub is set to use redis in development
29
+ yaml = YAML.safe_load(cable_config.read)
30
+ app_name = Rails.application.class.module_parent.name.underscore
31
+
32
+ if yaml["development"]["adapter"] == "redis"
33
+ say "⏩ config/cable.yml is already configured to use the redis adapter in development. Skipping."
34
+ elsif yaml["development"]["adapter"] == "async"
35
+ yaml["development"] = {
36
+ "adapter" => "redis",
37
+ "url" => "<%= ENV.fetch(\"REDIS_URL\") { \"redis://localhost:6379/1\" } %>",
38
+ "channel_prefix" => "#{app_name}_development"
39
+ }
40
+ backup(cable_config) do
41
+ cable_config.write(yaml.to_yaml)
42
+ end
43
+ say "✅ config/cable.yml was updated to use the redis adapter in development"
44
+ else
45
+ say "🤷 config/cable.yml should use the redis adapter - or something like it - in development. You have something else specified, and we trust that you know what you're doing."
46
+ end
47
+
48
+ if gemfile.match?(/gem ['"]redis['"]/)
49
+ say "⏩ redis gem is already present in Gemfile. Skipping."
50
+ elsif Rails::VERSION::MAJOR >= 7
51
+ add_gem "redis@~> 5"
52
+ else
53
+ add_gem "redis@~> 4"
54
+ end
55
+
56
+ # install action-cable-redis-backport gem if using Action Cable < 7.1
57
+ unless ActionCable::VERSION::MAJOR >= 7 && ActionCable::VERSION::MINOR >= 1
58
+ if gemfile.match?(/gem ['"]action-cable-redis-backport['"]/)
59
+ say "⏩ action-cable-redis-backport gem is already present in Gemfile. Skipping."
60
+ else
61
+ add_gem "action-cable-redis-backport@~> 1"
62
+ end
63
+ end
64
+
65
+ # verify that the Action Cable channels folder and consumer class is available
66
+ step_path = "/app/javascript/channels/"
67
+ channels_path = Rails.root.join(entrypoint, "channels")
68
+ consumer_src = fetch(step_path, "consumer.js.tt")
69
+ consumer_path = channels_path / "consumer.js"
70
+ index_src = fetch(step_path, "index.js.#{bundler}.tt")
71
+ index_path = channels_path / "index.js"
72
+ friendly_index_path = index_path.relative_path_from(Rails.root).to_s
73
+
74
+ empty_directory channels_path unless channels_path.exist?
75
+
76
+ copy_file(consumer_src, consumer_path) unless consumer_path.exist?
77
+
78
+ if index_path.exist?
79
+ if index_path.read == index_src.read
80
+ say "⏩ #{friendly_index_path} is already present. Skipping."
81
+ else
82
+ backup(index_path) do
83
+ copy_file(index_src, index_path, verbose: false)
84
+ end
85
+ say "✅ #{friendly_index_path} has been updated"
86
+ end
87
+ else
88
+ copy_file(index_src, index_path)
89
+ say "✅ #{friendly_index_path} has been created"
90
+ end
91
+
92
+ # import Action Cable channels into application pack
93
+ channels_pattern = /import ['"](\.\.\/|\.\/)?channels['"]/
94
+ channels_commented_pattern = /\s*\/\/\s*#{channels_pattern}/
95
+ channel_import = "import \"#{prefix}channels\"\n"
96
+
97
+ if pack.match?(channels_pattern)
98
+ if pack.match?(channels_commented_pattern)
99
+ proceed = if options.key? "uncomment"
100
+ options["uncomment"]
101
+ else
102
+ !no?("✨ Action Cable seems to be commented out in your application.js. Do you want to uncomment it? (Y/n)")
103
+ end
104
+
105
+ if proceed
106
+ # uncomment_lines only works with Ruby comments 🙄
107
+ lines = pack_path.readlines
108
+ matches = lines.select { |line| line =~ channels_commented_pattern }
109
+ lines[lines.index(matches.last).to_i] = channel_import
110
+ pack_path.write lines.join
111
+ say "✅ Uncommented channels import in #{friendly_pack_path}"
112
+ else
113
+ say "🤷 your Action Cable channels are not being imported in your application.js. We trust that you have a reason for this."
114
+ end
115
+ else
116
+ say "⏩ channels are already being imported in #{friendly_pack_path}. Skipping."
117
+ end
118
+ else
119
+ lines = pack_path.readlines
120
+ matches = lines.select { |line| line =~ /^import / }
121
+ lines.insert lines.index(matches.last).to_i + 1, channel_import
122
+ pack_path.write lines.join
123
+ say "✅ channels imported in #{friendly_pack_path}"
124
+ end
125
+
126
+ # create working copy of Action Cable initializer in tmp
127
+ if action_cable_initializer_path.exist?
128
+ FileUtils.cp(action_cable_initializer_path, action_cable_initializer_working_path)
129
+
130
+ say "⏩ Action Cable initializer already exists. Skipping"
131
+ else
132
+ # create Action Cable initializer if it doesn't already exist
133
+ create_file(action_cable_initializer_working_path, verbose: false) do
134
+ <<~RUBY
135
+ # frozen_string_literal: true
136
+
137
+ RUBY
138
+ end
139
+ say "✅ Action Cable initializer created"
140
+ end
141
+
142
+ # silence notoriously chatty Action Cable logs
143
+ if action_cable_initializer_working_path.read.match?(/^[^#]*ActionCable.server.config.logger/)
144
+ say "⏩ Action Cable logger is already being silenced. Skipping"
145
+ else
146
+ append_file(action_cable_initializer_working_path, verbose: false) do
147
+ <<~RUBY
148
+ ActionCable.server.config.logger = Logger.new(nil)
149
+
150
+ RUBY
151
+ end
152
+ say "✅ Action Cable logger silenced for performance and legibility"
153
+ end
154
+
155
+ complete_step :action_cable
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stimulus_reflex/installer"
4
+
5
+ def needs_broadcaster?(path)
6
+ return false unless path.exist?
7
+
8
+ !path.readlines.index { |line| line =~ /^\s*include CableReady::Broadcaster/ }
9
+ end
10
+
11
+ channel_path = Rails.root.join("app/channels/application_cable/channel.rb")
12
+ controller_path = Rails.root.join("app/controllers/application_controller.rb")
13
+ job_path = Rails.root.join("app/jobs/application_job.rb")
14
+ model_path = Rails.root.join(application_record_path)
15
+
16
+ include_in_channel = needs_broadcaster?(channel_path)
17
+ include_in_controller = needs_broadcaster?(controller_path)
18
+ include_in_job = needs_broadcaster?(job_path)
19
+ include_in_model = needs_broadcaster?(model_path)
20
+
21
+ proceed = [include_in_channel, include_in_controller, include_in_job, include_in_model].reduce(:|)
22
+
23
+ unless proceed
24
+ complete_step :broadcaster
25
+
26
+ puts "⏩ CableReady::Broadcaster already included in all files. Skipping."
27
+ return
28
+ end
29
+
30
+ proceed = if options.key? "broadcaster"
31
+ options["broadcaster"]
32
+ else
33
+ !no?("✨ Make CableReady::Broadcaster available to channels, controllers, jobs and models? (Y/n)")
34
+ end
35
+
36
+ unless proceed
37
+ complete_step :broadcaster
38
+
39
+ puts "⏩ Skipping."
40
+ return
41
+ end
42
+
43
+ broadcaster_include = "\n include CableReady::Broadcaster\n"
44
+
45
+ # include CableReady::Broadcaster in Action Cable Channel classes
46
+ if include_in_channel
47
+ backup(channel_path) do
48
+ inject_into_file channel_path, broadcaster_include, after: /class (ApplicationCable::)?Channel < ActionCable::Channel::Base/, verbose: false
49
+ end
50
+
51
+ puts "✅ include CableReady::Broadcaster in ApplicationCable::Channel"
52
+ else
53
+ puts "⏩ Not including CableReady::Broadcaster in ApplicationCable::Channel channels. Skipping."
54
+ end
55
+
56
+ # include CableReady::Broadcaster in Action Controller classes
57
+ if include_in_controller
58
+ backup(controller_path) do
59
+ inject_into_class controller_path, "ApplicationController", broadcaster_include, verbose: false
60
+ end
61
+
62
+ puts "✅ include CableReady::Broadcaster in ApplicationController"
63
+ else
64
+ puts "⏩ Not including CableReady::Broadcaster in ApplicationController. Skipping."
65
+ end
66
+
67
+ # include CableReady::Broadcaster in Active Job classes, if present
68
+
69
+ if include_in_job
70
+ backup(job_path) do
71
+ inject_into_class job_path, "ApplicationJob", broadcaster_include, verbose: false
72
+ end
73
+
74
+ puts "✅ include CableReady::Broadcaster in ApplicationJob"
75
+ else
76
+ puts "⏩ Not including CableReady::Broadcaster in ApplicationJob. Skipping."
77
+ end
78
+
79
+ # include CableReady::Broadcaster in Active Record model classes
80
+ if include_in_model
81
+ backup(application_record_path) do
82
+ inject_into_class application_record_path, "ApplicationRecord", broadcaster_include, verbose: false
83
+ end
84
+
85
+ puts "✅ include CableReady::Broadcaster in ApplicationRecord"
86
+ else
87
+ puts "⏩ Not including CableReady::Broadcaster in ApplicationRecord. Skipping"
88
+ end
89
+
90
+ complete_step :broadcaster