turbo-rails 1.5.0 → 2.0.11

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +126 -16
  3. data/app/assets/javascripts/turbo.js +2226 -953
  4. data/app/assets/javascripts/turbo.min.js +9 -5
  5. data/app/assets/javascripts/turbo.min.js.map +1 -1
  6. data/app/channels/turbo/streams/broadcasts.rb +47 -10
  7. data/app/channels/turbo/streams_channel.rb +15 -15
  8. data/app/controllers/concerns/turbo/request_id_tracking.rb +12 -0
  9. data/app/controllers/turbo/frames/frame_request.rb +2 -2
  10. data/app/controllers/turbo/native/navigation.rb +17 -11
  11. data/app/helpers/turbo/drive_helper.rb +72 -14
  12. data/app/helpers/turbo/frames_helper.rb +8 -8
  13. data/app/helpers/turbo/streams/action_helper.rb +12 -4
  14. data/app/helpers/turbo/streams_helper.rb +5 -0
  15. data/app/javascript/turbo/cable_stream_source_element.js +10 -0
  16. data/app/javascript/turbo/index.js +2 -0
  17. data/app/jobs/turbo/streams/action_broadcast_job.rb +2 -2
  18. data/app/jobs/turbo/streams/broadcast_job.rb +1 -1
  19. data/app/jobs/turbo/streams/broadcast_stream_job.rb +7 -0
  20. data/app/models/concerns/turbo/broadcastable.rb +201 -42
  21. data/app/models/turbo/debouncer.rb +24 -0
  22. data/app/models/turbo/streams/tag_builder.rb +50 -12
  23. data/app/models/turbo/thread_debouncer.rb +28 -0
  24. data/config/routes.rb +3 -4
  25. data/lib/install/turbo_with_importmap.rb +1 -1
  26. data/lib/tasks/turbo_tasks.rake +0 -22
  27. data/lib/turbo/broadcastable/test_helper.rb +5 -5
  28. data/lib/turbo/engine.rb +80 -9
  29. data/lib/turbo/system_test_helper.rb +128 -0
  30. data/lib/turbo/test_assertions/integration_test_assertions.rb +2 -2
  31. data/lib/turbo/test_assertions.rb +2 -2
  32. data/lib/turbo/version.rb +1 -1
  33. data/lib/turbo-rails.rb +10 -0
  34. metadata +10 -19
  35. data/lib/install/turbo_needs_redis.rb +0 -20
@@ -5,20 +5,6 @@ module Turbo
5
5
  system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../install/#{path}.rb", __dir__)}"
6
6
  end
7
7
 
8
- def redis_installed?
9
- Gem.win_platform? ?
10
- system('where redis-server > NUL 2>&1') :
11
- system('which redis-server > /dev/null')
12
- end
13
-
14
- def switch_on_redis_if_available
15
- if redis_installed?
16
- Rake::Task["turbo:install:redis"].invoke
17
- else
18
- puts "Run turbo:install:redis to switch on Redis and use it in development for turbo streams"
19
- end
20
- end
21
-
22
8
  def using_bun?
23
9
  Rails.root.join("bun.config.js").exist?
24
10
  end
@@ -43,24 +29,16 @@ namespace :turbo do
43
29
  desc "Install Turbo into the app with asset pipeline"
44
30
  task :importmap do
45
31
  Turbo::Tasks.run_turbo_install_template "turbo_with_importmap"
46
- Turbo::Tasks.switch_on_redis_if_available
47
32
  end
48
33
 
49
34
  desc "Install Turbo into the app with webpacker"
50
35
  task :node do
51
36
  Turbo::Tasks.run_turbo_install_template "turbo_with_node"
52
- Turbo::Tasks.switch_on_redis_if_available
53
37
  end
54
38
 
55
39
  desc "Install Turbo into the app with bun"
56
40
  task :bun do
57
41
  Turbo::Tasks.run_turbo_install_template "turbo_with_bun"
58
- Turbo::Tasks.switch_on_redis_if_available
59
- end
60
-
61
- desc "Switch on Redis and use it in development"
62
- task :redis do
63
- Turbo::Tasks.run_turbo_install_template "turbo_needs_redis"
64
42
  end
65
43
  end
66
44
  end
@@ -11,14 +11,14 @@ module Turbo
11
11
 
12
12
  # Asserts that `<turbo-stream>` elements were broadcast over Action Cable
13
13
  #
14
- # === Arguments
14
+ # ==== Arguments
15
15
  #
16
16
  # * <tt>stream_name_or_object</tt> the objects used to generate the
17
17
  # channel Action Cable name, or the name itself
18
18
  # * <tt>&block</tt> optional block executed before the
19
19
  # assertion
20
20
  #
21
- # === Options
21
+ # ==== Options
22
22
  #
23
23
  # * <tt>count:</tt> the number of `<turbo-stream>` elements that are
24
24
  # expected to be broadcast
@@ -64,13 +64,13 @@ module Turbo
64
64
  else
65
65
  broadcasts = "Turbo Stream broadcast".pluralize(count)
66
66
 
67
- assert count == payloads.count, "Expected #{count} #{broadcasts} on #{stream_name.inspect}, but there were none"
67
+ assert count == payloads.count, "Expected #{count} #{broadcasts} on #{stream_name.inspect}, but there were #{payloads.count}"
68
68
  end
69
69
  end
70
70
 
71
71
  # Asserts that no `<turbo-stream>` elements were broadcast over Action Cable
72
72
  #
73
- # === Arguments
73
+ # ==== Arguments
74
74
  #
75
75
  # * <tt>stream_name_or_object</tt> the objects used to generate the
76
76
  # channel Action Cable name, or the name itself
@@ -113,7 +113,7 @@ module Turbo
113
113
 
114
114
  # Captures any `<turbo-stream>` elements that were broadcast over Action Cable
115
115
  #
116
- # === Arguments
116
+ # ==== Arguments
117
117
  #
118
118
  # * <tt>stream_name_or_object</tt> the objects used to generate the
119
119
  # channel Action Cable name, or the name itself
data/lib/turbo/engine.rb CHANGED
@@ -5,6 +5,7 @@ module Turbo
5
5
  isolate_namespace Turbo
6
6
  config.eager_load_namespaces << Turbo
7
7
  config.turbo = ActiveSupport::OrderedOptions.new
8
+ config.turbo.test_connect_after_actions = %i[visit]
8
9
  config.autoload_once_paths = %W(
9
10
  #{root}/app/channels
10
11
  #{root}/app/controllers
@@ -15,8 +16,46 @@ module Turbo
15
16
  #{root}/app/jobs
16
17
  )
17
18
 
19
+ # If the parent application does not use Active Job, app/jobs cannot
20
+ # be eager loaded, because it references the ActiveJob constant.
21
+ #
22
+ # When turbo-rails depends on Rails 7 or above, the entire block can be
23
+ # reduced to
24
+ #
25
+ # unless defined?(ActiveJob)
26
+ # Rails.autoloaders.once.do_not_eager_load("#{root}/app/jobs")
27
+ # end
28
+ #
29
+ initializer "turbo.no_active_job", before: :set_eager_load_paths do
30
+ unless defined?(ActiveJob)
31
+ if Rails.autoloaders.zeitwerk_enabled?
32
+ Rails.autoloaders.once.do_not_eager_load("#{root}/app/jobs")
33
+ else
34
+ # This else branch only runs in Rails 6.x + classic mode.
35
+ config.eager_load_paths.delete("#{root}/app/jobs")
36
+ end
37
+ end
38
+ end
39
+
40
+ # If the parent application does not use Action Cable, app/channels cannot
41
+ # be eager loaded, because it references the ActionCable constant.
42
+ #
43
+ # When turbo-rails depends on Rails 7 or above, the entire block can be
44
+ # reduced to
45
+ #
46
+ # unless defined?(ActionCable)
47
+ # Rails.autoloaders.once.do_not_eager_load("#{root}/app/channels")
48
+ # end
49
+ #
18
50
  initializer "turbo.no_action_cable", before: :set_eager_load_paths do
19
- config.eager_load_paths.delete("#{root}/app/channels") unless defined?(ActionCable)
51
+ unless defined?(ActionCable)
52
+ if Rails.autoloaders.zeitwerk_enabled?
53
+ Rails.autoloaders.once.do_not_eager_load("#{root}/app/channels")
54
+ else
55
+ # This else branch only runs in Rails 6.x + classic mode.
56
+ config.eager_load_paths.delete("#{root}/app/channels")
57
+ end
58
+ end
20
59
  end
21
60
 
22
61
  # If you don't want to precompile Turbo's assets (eg. because you're using webpack),
@@ -46,9 +85,17 @@ module Turbo
46
85
  end
47
86
  end
48
87
 
88
+ initializer "turbo.request_id_tracking" do
89
+ ActiveSupport.on_load(:action_controller) do
90
+ include Turbo::RequestIdTracking
91
+ end
92
+ end
93
+
49
94
  initializer "turbo.broadcastable" do
50
95
  ActiveSupport.on_load(:active_record) do
51
- include Turbo::Broadcastable
96
+ if defined?(ActiveJob)
97
+ include Turbo::Broadcastable
98
+ end
52
99
  end
53
100
  end
54
101
 
@@ -57,11 +104,9 @@ module Turbo
57
104
  end
58
105
 
59
106
  initializer "turbo.renderer" do
60
- ActiveSupport.on_load(:action_controller) do
61
- ActionController::Renderers.add :turbo_stream do |turbo_streams_html, options|
62
- self.content_type = Mime[:turbo_stream] if media_type.nil?
63
- turbo_streams_html
64
- end
107
+ ActionController::Renderers.add :turbo_stream do |turbo_streams_html, options|
108
+ self.content_type = Mime[:turbo_stream] if media_type.nil?
109
+ turbo_streams_html
65
110
  end
66
111
  end
67
112
 
@@ -75,11 +120,18 @@ module Turbo
75
120
  initializer "turbo.test_assertions" do
76
121
  ActiveSupport.on_load(:active_support_test_case) do
77
122
  require "turbo/test_assertions"
78
- require "turbo/broadcastable/test_helper"
79
-
80
123
  include Turbo::TestAssertions
81
124
  end
82
125
 
126
+ ActiveSupport.on_load(:action_cable) do
127
+ ActiveSupport.on_load(:active_support_test_case) do
128
+ if defined?(ActiveJob)
129
+ require "turbo/broadcastable/test_helper"
130
+ include Turbo::Broadcastable::TestHelper
131
+ end
132
+ end
133
+ end
134
+
83
135
  ActiveSupport.on_load(:action_dispatch_integration_test) do
84
136
  require "turbo/test_assertions/integration_test_assertions"
85
137
 
@@ -100,5 +152,24 @@ module Turbo
100
152
  end
101
153
  end
102
154
  end
155
+
156
+ initializer "turbo.system_test_helper" do
157
+ ActiveSupport.on_load(:action_dispatch_system_test_case) do
158
+ require "turbo/system_test_helper"
159
+ include Turbo::SystemTestHelper
160
+ end
161
+ end
162
+
163
+ config.after_initialize do |app|
164
+ ActiveSupport.on_load(:action_dispatch_system_test_case) do
165
+ app.config.turbo.test_connect_after_actions.map do |method|
166
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
167
+ def #{method}(...) # def visit(...)
168
+ super.tap { connect_turbo_cable_stream_sources } # super.tap { connect_turbo_cable_stream_sources }
169
+ end # end
170
+ RUBY
171
+ end
172
+ end
173
+ end
103
174
  end
104
175
  end
@@ -0,0 +1,128 @@
1
+ module Turbo::SystemTestHelper
2
+ # Delay until every `<turbo-cable-stream-source>` element present in the page
3
+ # is ready to receive broadcasts
4
+ #
5
+ # test "renders broadcasted Messages" do
6
+ # message = Message.new content: "Hello, from Action Cable"
7
+ #
8
+ # visit "/"
9
+ # click_link "All Messages"
10
+ # message.save! # execute server-side code to broadcast a Message
11
+ #
12
+ # assert_text message.content
13
+ # end
14
+ #
15
+ # By default, calls to `#visit` will wait for all `<turbo-cable-stream-source>`
16
+ # elements to connect. You can control this by modifying the
17
+ # `config.turbo.test_connect_after_actions`. For example, to wait after calls to
18
+ # `#click_link`, add the following to `config/environments/test.rb`:
19
+ #
20
+ # # config/environments/test.rb
21
+ # config.turbo.test_connect_after_actions << :click_link
22
+ #
23
+ # To disable automatic connecting, set the configuration to `[]`:
24
+ #
25
+ # # config/environments/test.rb
26
+ # config.turbo.test_connect_after_actions = []
27
+ #
28
+ def connect_turbo_cable_stream_sources(**options, &block)
29
+ all(:turbo_cable_stream_source, **options, connected: false, wait: 0).each do |element|
30
+ element.assert_matches_selector(:turbo_cable_stream_source, **options, connected: true, &block)
31
+ end
32
+ end
33
+
34
+ # Asserts that a `<turbo-cable-stream-source>` element is present in the
35
+ # document
36
+ #
37
+ # ==== Arguments
38
+ #
39
+ # * <tt>locator</tt> optional locator to determine the element's
40
+ # `[signed-stream-name]` attribute. Can be of any type that is a valid
41
+ # argument to <tt>Turbo::Streams::StreamName#signed_stream_name</tt>.
42
+ #
43
+ # ==== Options
44
+ #
45
+ # * <tt>:connected</tt> matches the `[connected]` attribute
46
+ # * <tt>:channel</tt> matches the `[channel]` attribute. Can be a Class,
47
+ # String, Symbol, or Regexp
48
+ # * <tt>:signed_stream_name</tt> matches the element's `[signed-stream-name]`
49
+ # attribute. Can be of any type that is a valid
50
+ # argument to <tt>Turbo::Streams::StreamName#signed_stream_name</tt>.
51
+ #
52
+ # In addition to the filters listed above, accepts any valid Capybara global
53
+ # filter option.
54
+ def assert_turbo_cable_stream_source(...)
55
+ assert_selector(:turbo_cable_stream_source, ...)
56
+ end
57
+
58
+ # Asserts that a `<turbo-cable-stream-source>` element is absent from the
59
+ # document
60
+ #
61
+ # ==== Arguments
62
+ #
63
+ # * <tt>locator</tt> optional locator to determine the element's
64
+ # `[signed-stream-name]` attribute. Can be of any type that is a valid
65
+ # argument to <tt>Turbo::Streams::StreamName#signed_stream_name</tt>.
66
+ #
67
+ # ==== Options
68
+ #
69
+ # * <tt>:connected</tt> matches the `[connected]` attribute
70
+ # * <tt>:channel</tt> matches the `[channel]` attribute. Can be a Class,
71
+ # String, Symbol, or Regexp
72
+ # * <tt>:signed_stream_name</tt> matches the element's `[signed-stream-name]`
73
+ # attribute. Can be of any type that is a valid
74
+ # argument to <tt>Turbo::Streams::StreamName#signed_stream_name</tt>.
75
+ #
76
+ # In addition to the filters listed above, accepts any valid Capybara global
77
+ # filter option.
78
+ def assert_no_turbo_cable_stream_source(...)
79
+ assert_no_selector(:turbo_cable_stream_source, ...)
80
+ end
81
+
82
+ Capybara.add_selector :turbo_cable_stream_source do
83
+ xpath do |locator|
84
+ xpath = XPath.descendant.where(XPath.local_name == "turbo-cable-stream-source")
85
+ xpath.where(SignedStreamNameConditions.new(locator).reduce(:|))
86
+ end
87
+
88
+ expression_filter :connected do |xpath, value|
89
+ builder(xpath).add_attribute_conditions(connected: value)
90
+ end
91
+
92
+ expression_filter :channel do |xpath, value|
93
+ builder(xpath).add_attribute_conditions(channel: value.try(:name) || value)
94
+ end
95
+
96
+ expression_filter :signed_stream_name do |xpath, value|
97
+ case value
98
+ when TrueClass, FalseClass, NilClass, Regexp
99
+ builder(xpath).add_attribute_conditions("signed-stream-name": value)
100
+ else
101
+ xpath.where(SignedStreamNameConditions.new(value).reduce(:|))
102
+ end
103
+ end
104
+ end
105
+
106
+ class SignedStreamNameConditions # :nodoc:
107
+ include Turbo::Streams::StreamName, Enumerable
108
+
109
+ def initialize(value)
110
+ @value = value
111
+ end
112
+
113
+ def attribute
114
+ XPath.attr(:"signed-stream-name")
115
+ end
116
+
117
+ def each
118
+ if @value.is_a?(String)
119
+ yield attribute == @value
120
+ yield attribute == signed_stream_name(@value)
121
+ elsif @value.is_a?(Array) || @value.respond_to?(:to_key)
122
+ yield attribute == signed_stream_name(@value)
123
+ elsif @value.present?
124
+ yield attribute == @value
125
+ end
126
+ end
127
+ end
128
+ end
@@ -4,7 +4,7 @@ module Turbo
4
4
  # Assert that the Turbo Stream request's response body's HTML contains a
5
5
  # `<turbo-stream>` element.
6
6
  #
7
- # === Options
7
+ # ==== Options
8
8
  #
9
9
  # * <tt>:status</tt> [Integer, Symbol] the HTTP response status
10
10
  # * <tt>:action</tt> [String] matches the element's <tt>[action]</tt>
@@ -47,7 +47,7 @@ module Turbo
47
47
  # Assert that the Turbo Stream request's response body's HTML does not
48
48
  # contain a `<turbo-stream>` element.
49
49
  #
50
- # === Options
50
+ # ==== Options
51
51
  #
52
52
  # * <tt>:status</tt> [Integer, Symbol] the HTTP response status
53
53
  # * <tt>:action</tt> [String] matches the element's <tt>[action]</tt>
@@ -10,7 +10,7 @@ module Turbo
10
10
  # Assert that the rendered fragment of HTML contains a `<turbo-stream>`
11
11
  # element.
12
12
  #
13
- # === Options
13
+ # ==== Options
14
14
  #
15
15
  # * <tt>:action</tt> [String] matches the element's <tt>[action]</tt>
16
16
  # attribute
@@ -55,7 +55,7 @@ module Turbo
55
55
  # Assert that the rendered fragment of HTML does not contain a `<turbo-stream>`
56
56
  # element.
57
57
  #
58
- # === Options
58
+ # ==== Options
59
59
  #
60
60
  # * <tt>:action</tt> [String] matches the element's <tt>[action]</tt>
61
61
  # attribute
data/lib/turbo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Turbo
2
- VERSION = "1.5.0"
2
+ VERSION = "2.0.11"
3
3
  end
data/lib/turbo-rails.rb CHANGED
@@ -1,10 +1,13 @@
1
1
  require "turbo/engine"
2
+ require "active_support/core_ext/module/attribute_accessors_per_thread"
2
3
 
3
4
  module Turbo
4
5
  extend ActiveSupport::Autoload
5
6
 
6
7
  mattr_accessor :draw_routes, default: true
7
8
 
9
+ thread_mattr_accessor :current_request_id
10
+
8
11
  class << self
9
12
  attr_writer :signed_stream_verifier_key
10
13
 
@@ -15,5 +18,12 @@ module Turbo
15
18
  def signed_stream_verifier_key
16
19
  @signed_stream_verifier_key or raise ArgumentError, "Turbo requires a signed_stream_verifier_key"
17
20
  end
21
+
22
+ def with_request_id(request_id)
23
+ old_request_id, self.current_request_id = self.current_request_id, request_id
24
+ yield
25
+ ensure
26
+ self.current_request_id = old_request_id
27
+ end
18
28
  end
19
29
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbo-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 2.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Stephenson
@@ -10,22 +10,8 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2023-10-11 00:00:00.000000000 Z
13
+ date: 2024-10-15 00:00:00.000000000 Z
14
14
  dependencies:
15
- - !ruby/object:Gem::Dependency
16
- name: activejob
17
- requirement: !ruby/object:Gem::Requirement
18
- requirements:
19
- - - ">="
20
- - !ruby/object:Gem::Version
21
- version: 6.0.0
22
- type: :runtime
23
- prerelease: false
24
- version_requirements: !ruby/object:Gem::Requirement
25
- requirements:
26
- - - ">="
27
- - !ruby/object:Gem::Version
28
- version: 6.0.0
29
15
  - !ruby/object:Gem::Dependency
30
16
  name: actionpack
31
17
  requirement: !ruby/object:Gem::Requirement
@@ -69,6 +55,7 @@ files:
69
55
  - app/channels/turbo/streams/broadcasts.rb
70
56
  - app/channels/turbo/streams/stream_name.rb
71
57
  - app/channels/turbo/streams_channel.rb
58
+ - app/controllers/concerns/turbo/request_id_tracking.rb
72
59
  - app/controllers/turbo/frames/frame_request.rb
73
60
  - app/controllers/turbo/native/navigation.rb
74
61
  - app/controllers/turbo/native/navigation_controller.rb
@@ -85,11 +72,13 @@ files:
85
72
  - app/javascript/turbo/snakeize.js
86
73
  - app/jobs/turbo/streams/action_broadcast_job.rb
87
74
  - app/jobs/turbo/streams/broadcast_job.rb
75
+ - app/jobs/turbo/streams/broadcast_stream_job.rb
88
76
  - app/models/concerns/turbo/broadcastable.rb
77
+ - app/models/turbo/debouncer.rb
89
78
  - app/models/turbo/streams/tag_builder.rb
79
+ - app/models/turbo/thread_debouncer.rb
90
80
  - app/views/layouts/turbo_rails/frame.html.erb
91
81
  - config/routes.rb
92
- - lib/install/turbo_needs_redis.rb
93
82
  - lib/install/turbo_with_bun.rb
94
83
  - lib/install/turbo_with_importmap.rb
95
84
  - lib/install/turbo_with_node.rb
@@ -97,13 +86,15 @@ files:
97
86
  - lib/turbo-rails.rb
98
87
  - lib/turbo/broadcastable/test_helper.rb
99
88
  - lib/turbo/engine.rb
89
+ - lib/turbo/system_test_helper.rb
100
90
  - lib/turbo/test_assertions.rb
101
91
  - lib/turbo/test_assertions/integration_test_assertions.rb
102
92
  - lib/turbo/version.rb
103
93
  homepage: https://github.com/hotwired/turbo-rails
104
94
  licenses:
105
95
  - MIT
106
- metadata: {}
96
+ metadata:
97
+ changelog_uri: https://github.com/hotwired/turbo-rails/releases
107
98
  post_install_message:
108
99
  rdoc_options: []
109
100
  require_paths:
@@ -119,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
110
  - !ruby/object:Gem::Version
120
111
  version: '0'
121
112
  requirements: []
122
- rubygems_version: 3.4.15
113
+ rubygems_version: 3.5.11
123
114
  signing_key:
124
115
  specification_version: 4
125
116
  summary: The speed of a single-page web application without having to write any JavaScript.
@@ -1,20 +0,0 @@
1
- if (cable_config_path = Rails.root.join("config/cable.yml")).exist?
2
- say "Enable redis in bundle"
3
-
4
- gemfile_content = File.read(Rails.root.join("Gemfile"))
5
- pattern = /gem ['"]redis['"]/
6
-
7
- if gemfile_content.match?(pattern)
8
- uncomment_lines "Gemfile", pattern
9
- else
10
- append_file "Gemfile", "\n# Use Redis for Action Cable"
11
- gem 'redis', '~> 4.0'
12
- end
13
-
14
- run_bundle
15
-
16
- say "Switch development cable to use redis"
17
- gsub_file cable_config_path.to_s, /development:\n\s+adapter: async/, "development:\n adapter: redis\n url: redis://localhost:6379/1"
18
- else
19
- say 'ActionCable config file (config/cable.yml) is missing. Uncomment "gem \'redis\'" in your Gemfile and create config/cable.yml to use the Turbo Streams broadcast feature.'
20
- end