view_component 2.47.0 → 2.49.1

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

Potentially problematic release.


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

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2c94e6fdeccccb7180494d100eef8c78a8dc8e3e9bc30e5647017b440788c6eb
4
- data.tar.gz: c3836df9c15e8039ba673895ad5a9544aebe55325f9fbd418b96f4bb4238b8d6
3
+ metadata.gz: ef9372866a072103dd38b8da27bfcf57ad66012524b6a80f6caf311a97533435
4
+ data.tar.gz: 654c9561c09c8cae7ca8720d126060e8196d38b0cd832d6a5a6e1f7e8c8ad133
5
5
  SHA512:
6
- metadata.gz: 644f964985c8418371fdcac45e1e7b1771d89ee65ed8a692ec51c50cbed8a578937ba1bb7b9bf1d3b3e3729bd418df5fb2e9d1aee3bbb8215bfbed4504457074
7
- data.tar.gz: c4df9e14be4dc20e529beb794c96baf424087f7e36423b301402325e1b1ce0ae8ab575fe1b69334b3e27b5fa9e2fee227e9bd20e0e99fe5f384f2f535b43f80c
6
+ metadata.gz: fbd9bf43a06132fa0d26fba5155382be904bba9aaf181c9bb0f715a3caf103f5ae7bff74640e62c31e3ea8600a61f5405e364553b5f56f3cbc5af7a22a77f5f8
7
+ data.tar.gz: 946639d557fd6dd76889c6a645187fc1235bd7e76817944429f306be1d7798ac9fe9b8005301f3ea106c564d6502d3e1bc21fc1ea5b9902784d99296979c8d93
data/docs/CHANGELOG.md CHANGED
@@ -7,6 +7,96 @@ title: Changelog
7
7
 
8
8
  ## main
9
9
 
10
+ ## 2.49.1
11
+
12
+ * Patch XSS vulnerability in `Translatable` module caused by improperly escaped interpolation arguments.
13
+
14
+ *Cameron Dutro*
15
+
16
+ ## 2.49.0
17
+
18
+ * Fix path handling for evaluated view components that broke in Ruby 3.1.
19
+
20
+ *Adam Hess*
21
+
22
+ * Fix support for the `default:` option for a global translation.
23
+
24
+ *Elia Schito*
25
+
26
+ * Ensure i18n scope is a symbol to protect lookups.
27
+
28
+ *Simon Fish*
29
+
30
+ * Small update to preview docs to include rspec mention.
31
+
32
+ *Leigh Halliday*
33
+
34
+ * Small improvements to collection iteration docs.
35
+
36
+ *Brian O'Rourke*
37
+
38
+ * Add good and bad examples to `ViewComponents in practice`.
39
+
40
+ *Joel Hawksley*
41
+
42
+ * Add Ruby 3.1 and Rails 7.0 to CI
43
+
44
+ *Peter Goldstein*
45
+
46
+ ## 2.48.0
47
+
48
+ * Correct path in example test command in Contributing docs.
49
+
50
+ *Mark Wilkinson*
51
+
52
+ * Update link to GOV.UK Components library in the resources list.
53
+
54
+ *Peter Yates*
55
+
56
+ * Add Lookbook to Resources docs page.
57
+
58
+ *Mark Perkins*
59
+
60
+ * Add blocking compiler mode for use in Rails development and testing modes, improving thread safety.
61
+
62
+ *Horia Radu*
63
+
64
+ * Add generators to support `tailwindcss-rails`.
65
+
66
+ *Dino Maric*, *Hans Lemuet*
67
+
68
+ * Add a namespaced component example to docs.
69
+
70
+ *Hans Lemuet*
71
+
72
+ * Setup `Appraisal` to add flexibility when testing ViewComponent against multiple Rails versions.
73
+
74
+ *Hans Lemuet*
75
+
76
+ * Return correct values for `request.path` and `request.query_string` methods when using the `with_request_url` test helper.
77
+
78
+ *Vasiliy Matyushin*
79
+
80
+ * Improve style in generators docs.
81
+
82
+ *Hans Lemuet*
83
+
84
+ * Correctly type Ruby version strings and update Rails versions used in CI configuration.
85
+
86
+ *Hans Lemuet*
87
+
88
+ * Make `ViewComponent::Collection` act like a collection of view components.
89
+
90
+ *Sammy Henningsson*
91
+
92
+ * Update `@param` of `#render_inline` to include `ViewComponent::Collection`.
93
+
94
+ *Yutaka Kamei*
95
+
96
+ * Add Wecasa to users list.
97
+
98
+ *Mohamed Ziata*
99
+
10
100
  ## 2.47.0
11
101
 
12
102
  * Display preview source on previews that exclusively use templates.
@@ -17,7 +107,7 @@ title: Changelog
17
107
 
18
108
  *Simon Fish*
19
109
 
20
- * Add WEBrick as a depenency to the docs application.
110
+ * Add WEBrick as a depenency to the application.
21
111
 
22
112
  *Connor McQuillan*
23
113
 
@@ -79,6 +169,10 @@ title: Changelog
79
169
 
80
170
  *Bob Maerten*
81
171
 
172
+ * Add config option `config.view_component.generate_sidecar` to always generate in the sidecar directory.
173
+
174
+ *Gleydson Tavares*
175
+
82
176
  ## 2.46.0
83
177
 
84
178
  * Add thread safety to the compiler.
@@ -569,6 +663,12 @@ _Note: This release includes an underlying change to Slots that may affect incor
569
663
 
570
664
  *Joel Hawksley*
571
665
 
666
+ ## 2.29.1
667
+
668
+ * Patch XSS vulnerability in `ViewComponent::Translatable` module caused by improperly escaped interpolation arguments.
669
+
670
+ *Cameron Dutro*
671
+
572
672
  ## 2.29.0
573
673
 
574
674
  * Allow Slot lambdas to share data from the parent component and allow chaining on the returned component.
@@ -15,7 +15,7 @@ module ViewComponent
15
15
  end
16
16
 
17
17
  def destination_directory
18
- if options["sidecar"]
18
+ if sidecar?
19
19
  File.join(component_path, class_path, destination_file_name)
20
20
  else
21
21
  File.join(component_path, class_path)
@@ -42,5 +42,9 @@ module ViewComponent
42
42
  gsub("/", "--")
43
43
  end
44
44
  end
45
+
46
+ def sidecar?
47
+ options["sidecar"] || ViewComponent::Base.generate_sidecar
48
+ end
45
49
  end
46
50
  end
@@ -35,7 +35,7 @@ module Locale
35
35
 
36
36
  def destination(locale = nil)
37
37
  extention = ".#{locale}" if locale
38
- if options["sidecar"]
38
+ if sidecar?
39
39
  File.join(component_path, class_path, "#{file_name}_component", "#{file_name}_component#{extention}.yml")
40
40
  else
41
41
  File.join(component_path, class_path, "#{file_name}_component#{extention}.yml")
@@ -23,7 +23,7 @@ module Stimulus
23
23
  private
24
24
 
25
25
  def destination
26
- if options["sidecar"]
26
+ if sidecar?
27
27
  File.join(component_path, class_path, "#{file_name}_component", "#{file_name}_component_controller.js")
28
28
  else
29
29
  File.join(component_path, class_path, "#{file_name}_component_controller.js")
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/erb/component_generator"
4
+
5
+ module Tailwindcss
6
+ module Generators
7
+ class ComponentGenerator < Erb::Generators::ComponentGenerator
8
+ source_root File.expand_path("templates", __dir__)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1 @@
1
+ <div<%= data_attributes %>>Add <%= class_name %> template here</div>
@@ -303,6 +303,7 @@ module ViewComponent
303
303
  # config.view_component.view_component_path = "app/my_components"
304
304
  #
305
305
  # Defaults to `app/components`.
306
+ #
306
307
  mattr_accessor :view_component_path, instance_writer: false, default: "app/components"
307
308
 
308
309
  # Parent class for generated components
@@ -310,8 +311,16 @@ module ViewComponent
310
311
  # config.view_component.component_parent_class = "MyBaseComponent"
311
312
  #
312
313
  # Defaults to "ApplicationComponent" if defined, "ViewComponent::Base" otherwise.
313
- mattr_accessor :component_parent_class,
314
- instance_writer: false
314
+ #
315
+ mattr_accessor :component_parent_class, instance_writer: false
316
+
317
+ # Always generate a component with a sidecar directory:
318
+ #
319
+ # config.view_component.generate_sidecar = true
320
+ #
321
+ # Defaults to `false`.
322
+ #
323
+ mattr_accessor :generate_sidecar, instance_writer: false, default: false
315
324
 
316
325
  class << self
317
326
  # @private
@@ -392,7 +401,7 @@ module ViewComponent
392
401
  # Derive the source location of the component Ruby file from the call stack.
393
402
  # We need to ignore `inherited` frames here as they indicate that `inherited`
394
403
  # has been re-defined by the consuming application, likely in ApplicationComponent.
395
- child.source_location = caller_locations(1, 10).reject { |l| l.label == "inherited" }[0].absolute_path
404
+ child.source_location = caller_locations(1, 10).reject { |l| l.label == "inherited" }[0].path
396
405
 
397
406
  # Removes the first part of the path and the extension.
398
407
  child.virtual_path = child.source_location.gsub(
@@ -4,20 +4,34 @@ require "action_view/renderer/collection_renderer" if Rails.version.to_f >= 6.1
4
4
 
5
5
  module ViewComponent
6
6
  class Collection
7
+ include Enumerable
7
8
  attr_reader :component
8
9
 
9
10
  delegate :format, to: :component
11
+ delegate :size, to: :@collection
10
12
 
11
13
  def render_in(view_context, &block)
14
+ components.map do |component|
15
+ component.render_in(view_context, &block)
16
+ end.join.html_safe # rubocop:disable Rails/OutputSafety
17
+ end
18
+
19
+ def components
20
+ return @components if defined? @components
21
+
12
22
  iterator = ActionView::PartialIteration.new(@collection.size)
13
23
 
14
24
  component.validate_collection_parameter!(validate_default: true)
15
25
 
16
- @collection.map do |item|
17
- content = component.new(**component_options(item, iterator)).render_in(view_context, &block)
18
- iterator.iterate!
19
- content
20
- end.join.html_safe # rubocop:disable Rails/OutputSafety
26
+ @components = @collection.map do |item|
27
+ component.new(**component_options(item, iterator)).tap do |component|
28
+ iterator.iterate!
29
+ end
30
+ end
31
+ end
32
+
33
+ def each(&block)
34
+ components.each(&block)
21
35
  end
22
36
 
23
37
  private
@@ -42,7 +56,7 @@ module ViewComponent
42
56
  def component_options(item, iterator)
43
57
  item_options = { component.collection_parameter => item }
44
58
  item_options[component.collection_counter_parameter] = iterator.index + 1 if component.counter_argument_present?
45
- item_options[component.collection_iteration_parameter] = iterator if component.iteration_argument_present?
59
+ item_options[component.collection_iteration_parameter] = iterator.dup if component.iteration_argument_present?
46
60
 
47
61
  @options.merge(item_options)
48
62
  end
@@ -5,6 +5,15 @@ module ViewComponent
5
5
  # Lock required to be obtained before compiling the component
6
6
  attr_reader :__vc_compiler_lock
7
7
 
8
+ # Compiler mode. Can be either:
9
+ # * development (a blocking mode which ensures thread safety when redefining the `call` method for components,
10
+ # default in Rails development and test mode)
11
+ # * production (a non-blocking mode, default in Rails production mode)
12
+ DEVELOPMENT_MODE = :development
13
+ PRODUCTION_MODE = :production
14
+
15
+ class_attribute :mode, default: PRODUCTION_MODE
16
+
8
17
  def initialize(component_class)
9
18
  @component_class = component_class
10
19
  @__vc_compiler_lock = Monitor.new
@@ -14,10 +23,14 @@ module ViewComponent
14
23
  CompileCache.compiled?(component_class)
15
24
  end
16
25
 
26
+ def development?
27
+ self.class.mode == DEVELOPMENT_MODE
28
+ end
29
+
17
30
  def compile(raise_errors: false)
18
31
  return if compiled?
19
32
 
20
- __vc_compiler_lock.synchronize do
33
+ with_lock do
21
34
  CompileCache.invalidate_class!(component_class)
22
35
 
23
36
  subclass_instance_methods = component_class.instance_methods(false)
@@ -57,10 +70,10 @@ module ViewComponent
57
70
  end
58
71
 
59
72
  component_class.class_eval <<-RUBY, template[:path], -1
60
- def #{method_name}
61
- @output_buffer = ActionView::OutputBuffer.new
62
- #{compiled_template(template[:path])}
63
- end
73
+ def #{method_name}
74
+ @output_buffer = ActionView::OutputBuffer.new
75
+ #{compiled_template(template[:path])}
76
+ end
64
77
  RUBY
65
78
  end
66
79
 
@@ -72,6 +85,14 @@ module ViewComponent
72
85
  end
73
86
  end
74
87
 
88
+ def with_lock(&block)
89
+ if development?
90
+ __vc_compiler_lock.synchronize(&block)
91
+ else
92
+ block.call
93
+ end
94
+ end
95
+
75
96
  private
76
97
 
77
98
  attr_reader :component_class
@@ -85,16 +106,30 @@ module ViewComponent
85
106
  "elsif variant.to_sym == :#{variant}\n #{call_method_name(variant)}"
86
107
  end.join("\n")
87
108
 
88
- component_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
109
+ body = <<-RUBY
110
+ if variant.nil?
111
+ call
112
+ #{variant_elsifs}
113
+ else
114
+ call
115
+ end
116
+ RUBY
117
+
118
+ if development?
119
+ component_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
89
120
  def render_template_for(variant = nil)
90
- if variant.nil?
91
- call
92
- #{variant_elsifs}
93
- else
94
- call
121
+ self.class.compiler.with_lock do
122
+ #{body}
95
123
  end
96
124
  end
97
- RUBY
125
+ RUBY
126
+ else
127
+ component_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
128
+ def render_template_for(variant = nil)
129
+ #{body}
130
+ end
131
+ RUBY
132
+ end
98
133
  end
99
134
 
100
135
  def template_errors
@@ -115,6 +115,14 @@ module ViewComponent
115
115
  end
116
116
  end
117
117
 
118
+ initializer "compiler mode" do |app|
119
+ ViewComponent::Compiler.mode = if Rails.env.development? || Rails.env.test?
120
+ ViewComponent::Compiler::DEVELOPMENT_MODE
121
+ else
122
+ ViewComponent::Compiler::PRODUCTION_MODE
123
+ end
124
+ end
125
+
118
126
  config.after_initialize do |app|
119
127
  options = app.config.view_component
120
128
 
@@ -5,14 +5,16 @@ require "active_support/notifications"
5
5
  module ViewComponent # :nodoc:
6
6
  module Instrumentation
7
7
  def self.included(mod)
8
- mod.prepend(self)
8
+ mod.prepend(self) unless ancestors.include?(ViewComponent::Instrumentation)
9
9
  end
10
10
 
11
11
  def render_in(view_context, &block)
12
12
  ActiveSupport::Notifications.instrument(
13
13
  "!render.view_component",
14
- name: self.class.name,
15
- identifier: self.class.identifier
14
+ {
15
+ name: self.class.name,
16
+ identifier: self.class.identifier
17
+ }
16
18
  ) do
17
19
  super(view_context, &block)
18
20
  end
@@ -38,7 +38,7 @@ module ViewComponent
38
38
  # assert_text("Hello, World!")
39
39
  # ```
40
40
  #
41
- # @param component [ViewComponent::Base] The instance of the component to be rendered.
41
+ # @param component [ViewComponent::Base, ViewComponent::Collection] The instance of the component to be rendered.
42
42
  # @return [Nokogiri::HTML]
43
43
  def render_inline(component, **args, &block)
44
44
  @rendered_component =
@@ -113,16 +113,22 @@ module ViewComponent
113
113
  #
114
114
  # @param path [String] The path to set for the current request.
115
115
  def with_request_url(path)
116
+ old_request_path_info = request.path_info
116
117
  old_request_path_parameters = request.path_parameters
117
118
  old_request_query_parameters = request.query_parameters
119
+ old_request_query_string = request.query_string
118
120
  old_controller = defined?(@controller) && @controller
119
121
 
122
+ request.path_info = path
120
123
  request.path_parameters = Rails.application.routes.recognize_path(path)
121
124
  request.set_header("action_dispatch.request.query_parameters", Rack::Utils.parse_query(path.split("?")[1]))
125
+ request.set_header(Rack::QUERY_STRING, path.split("?")[1])
122
126
  yield
123
127
  ensure
128
+ request.path_info = old_request_path_info
124
129
  request.path_parameters = old_request_path_parameters
125
130
  request.set_header("action_dispatch.request.query_parameters", old_request_query_parameters)
131
+ request.set_header(Rack::QUERY_STRING, old_request_query_string)
126
132
  @controller = old_controller
127
133
  end
128
134
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "erb"
3
4
  require "set"
4
5
  require "i18n"
5
6
  require "action_view/helpers/translation_helper"
@@ -41,7 +42,7 @@ module ViewComponent
41
42
  EMPTY_HASH = {}.freeze
42
43
 
43
44
  def initialize(i18n_scope:, load_paths:)
44
- @i18n_scope = i18n_scope.split(".")
45
+ @i18n_scope = i18n_scope.split(".").map(&:to_sym)
45
46
  @load_paths = load_paths
46
47
  end
47
48
 
@@ -70,21 +71,29 @@ module ViewComponent
70
71
  key = key&.to_s unless key.is_a?(String)
71
72
  key = "#{i18n_scope}#{key}" if key.start_with?(".")
72
73
 
73
- translated =
74
- catch(:exception) do
75
- i18n_backend.translate(locale, key, options)
74
+ if HTML_SAFE_TRANSLATION_KEY.match?(key)
75
+ html_escape_translation_options!(options)
76
+ end
77
+
78
+ if key.start_with?(i18n_scope + ".")
79
+ translated =
80
+ catch(:exception) do
81
+ i18n_backend.translate(locale, key, options)
82
+ end
83
+
84
+ # Fallback to the global translations
85
+ if translated.is_a? ::I18n::MissingTranslation
86
+ return super(key, locale: locale, **options)
76
87
  end
77
88
 
78
- # Fallback to the global translations
79
- if translated.is_a? ::I18n::MissingTranslation
80
- return super(key, locale: locale, **options)
81
- end
89
+ if HTML_SAFE_TRANSLATION_KEY.match?(key)
90
+ translated = translated.html_safe # rubocop:disable Rails/OutputSafety
91
+ end
82
92
 
83
- if HTML_SAFE_TRANSLATION_KEY.match?(key)
84
- translated = translated.html_safe # rubocop:disable Rails/OutputSafety
93
+ translated
94
+ else
95
+ super(key, locale: locale, **options)
85
96
  end
86
-
87
- translated
88
97
  end
89
98
  alias :t :translate
90
99
 
@@ -92,5 +101,30 @@ module ViewComponent
92
101
  def i18n_scope
93
102
  self.class.i18n_scope
94
103
  end
104
+
105
+ def html_safe_translation(translation)
106
+ if translation.respond_to?(:map)
107
+ translation.map { |element| html_safe_translation(element) }
108
+ else
109
+ # It's assumed here that objects loaded by the i18n backend will respond to `#html_safe?`.
110
+ # It's reasonable that if we're in Rails, `active_support/core_ext/string/output_safety.rb`
111
+ # will provide this to `Object`.
112
+ translation.html_safe # rubocop:disable Rails/OutputSafety
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def html_escape_translation_options!(options)
119
+ options.each do |name, value|
120
+ unless i18n_option?(name) || (name == :count && value.is_a?(Numeric))
121
+ options[name] = ERB::Util.html_escape(value.to_s)
122
+ end
123
+ end
124
+ end
125
+
126
+ def i18n_option?(name)
127
+ (@i18n_option_names ||= I18n::RESERVED_KEYS.to_set).include?(name)
128
+ end
95
129
  end
96
130
  end
@@ -3,8 +3,8 @@
3
3
  module ViewComponent
4
4
  module VERSION
5
5
  MAJOR = 2
6
- MINOR = 47
7
- PATCH = 0
6
+ MINOR = 49
7
+ PATCH = 1
8
8
 
9
9
  STRING = [MAJOR, MINOR, PATCH].join(".")
10
10
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: view_component
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.47.0
4
+ version: 2.49.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub Open Source
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-20 00:00:00.000000000 Z
11
+ date: 2022-03-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -44,6 +44,20 @@ dependencies:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
46
  version: '1.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: appraisal
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.4'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.4'
47
61
  - !ruby/object:Gem::Dependency
48
62
  name: benchmark-ips
49
63
  requirement: !ruby/object:Gem::Requirement
@@ -268,7 +282,7 @@ dependencies:
268
282
  - - ">="
269
283
  - !ruby/object:Gem::Version
270
284
  version: '0'
271
- description:
285
+ description:
272
286
  email:
273
287
  - opensource+view_component@github.com
274
288
  executables: []
@@ -304,6 +318,8 @@ files:
304
318
  - lib/rails/generators/slim/templates/component.html.slim.tt
305
319
  - lib/rails/generators/stimulus/component_generator.rb
306
320
  - lib/rails/generators/stimulus/templates/component_controller.js.tt
321
+ - lib/rails/generators/tailwindcss/component_generator.rb
322
+ - lib/rails/generators/tailwindcss/templates/component.html.erb.tt
307
323
  - lib/rails/generators/test_unit/component_generator.rb
308
324
  - lib/rails/generators/test_unit/templates/component_test.rb.tt
309
325
  - lib/view_component.rb
@@ -342,7 +358,7 @@ licenses:
342
358
  - MIT
343
359
  metadata:
344
360
  allowed_push_host: https://rubygems.org
345
- post_install_message:
361
+ post_install_message:
346
362
  rdoc_options: []
347
363
  require_paths:
348
364
  - lib
@@ -357,8 +373,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
357
373
  - !ruby/object:Gem::Version
358
374
  version: '0'
359
375
  requirements: []
360
- rubygems_version: 3.1.2
361
- signing_key:
376
+ rubygems_version: 3.2.32
377
+ signing_key:
362
378
  specification_version: 4
363
379
  summary: View components for Rails
364
380
  test_files: []