view_component 3.0.0.rc2 → 3.0.0.rc4

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.

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: f2171ee963d59844894858309db964f092fb423bc4828a3bccbc917608502278
4
- data.tar.gz: aaf7c734530bf46ea0fa5e7fdad4c6efaf7325eb45970fa2bea56d6c9f9a15c1
3
+ metadata.gz: 53d058ef489a0f7230b3a83fe92d36fab42b9b8ccf0cd2bba0d12f7320f2c261
4
+ data.tar.gz: de28dc5a7e3e9c34c3cf81bbd5d52f518c653950552547c86a547bb4a577bdfa
5
5
  SHA512:
6
- metadata.gz: 5f26a04b1af1b93be44db4436d57544c182c64a1d1036a2db62cecf0bc8b12da3f7ccf11e7cb8bf88c9c915495686ddcd774cca13a5adf7353dfc678d6c7ead1
7
- data.tar.gz: a982b51ed80dc7da05da996005b2e60ca7af576a91c5ef13cdd68698735635fb42834e7cc45d23e5cbf45326b4ab8d8b85a1e6ddb7b2967a6d1e45638c73d50a
6
+ metadata.gz: d6161e41e5b9b82ce20f9ffc624cfe2910e707d867e2ba0019bc960a02353e7d2b083700e3504ea1f84da83f9a31110eb581a07a16609d30c6a7737806f5986e
7
+ data.tar.gz: c137ce79675ec78c4f31dff419b42fdf31d2af696e2275047d74058c7e1402be00e0e44df125ea6c5803b59e6931d8a7619c9c266376b02881965a778e60d784
@@ -1,7 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ViewComponentsSystemTestController < ActionController::Base # :nodoc:
4
+ TEMP_DIR = FileUtils.mkdir_p("./tmp/view_components/").first
5
+
6
+ before_action :validate_test_env
7
+ before_action :validate_file_path
8
+
4
9
  def system_test_entrypoint
5
- render file: "./tmp/view_components/#{params.permit(:file)[:file]}"
10
+ render file: @path
11
+ end
12
+
13
+ private
14
+
15
+ def validate_test_env
16
+ raise "ViewComponentsSystemTestController must only be called in a test environment" unless Rails.env.test?
17
+ end
18
+
19
+ # Ensure that the file path is valid and doesn't target files outside
20
+ # the expected directory (e.g. via a path traversal or symlink attack)
21
+ def validate_file_path
22
+ base_path = ::File.realpath(TEMP_DIR)
23
+ @path = ::File.realpath(params.permit(:file)[:file], base_path)
24
+ unless @path.start_with?(base_path)
25
+ raise ArgumentError, "Invalid file path"
26
+ end
6
27
  end
7
28
  end
data/docs/CHANGELOG.md CHANGED
@@ -10,6 +10,42 @@ nav_order: 5
10
10
 
11
11
  ## main
12
12
 
13
+ ## v3.0.0.rc4
14
+
15
+ Run into an issue with this release candidate? [Let us know](https://github.com/ViewComponent/view_component/issues/1629).
16
+
17
+ * Add `TestHelpers#vc_test_request`.
18
+
19
+ *Joel Hawksley*
20
+
21
+ ## v3.0.0.rc3
22
+
23
+ Run into an issue with this release candidate? [Let us know](https://github.com/ViewComponent/view_component/issues/1629).
24
+
25
+ * Fix typos in generator docs.
26
+
27
+ *Sascha Karnatz*
28
+
29
+ * Add `TestHelpers#vc_test_controller`.
30
+
31
+ *Joel Hawksley*
32
+
33
+ * Document `config.view_component.capture_compatibility_patch_enabled` as option for the known incompatibilities with Rails form helpers.
34
+
35
+ *Tobias L. Maier*
36
+
37
+ * Add support for experimental inline templates.
38
+
39
+ *Blake Williams*
40
+
41
+ * Expose `translate` and `t` I18n methods on component classes.
42
+
43
+ *Elia Schito*
44
+
45
+ * Protect against Arbitrary File Read edge case in `ViewComponentsSystemTestController`.
46
+
47
+ *Nick Malcolm*
48
+
13
49
  ## v3.0.0.rc2
14
50
 
15
51
  Run into an issue with this release? [Let us know](https://github.com/ViewComponent/view_component/issues/1629).
@@ -481,6 +481,11 @@ module ViewComponent
481
481
  compiler.compiled?
482
482
  end
483
483
 
484
+ # @private
485
+ def ensure_compiled
486
+ compile unless compiled?
487
+ end
488
+
484
489
  # Compile templates to instance methods, assuming they haven't been compiled already.
485
490
  #
486
491
  # Do as much work as possible in this step, as doing so reduces the amount
@@ -51,24 +51,46 @@ module ViewComponent
51
51
  component_class.validate_collection_parameter!
52
52
  end
53
53
 
54
- templates.each do |template|
55
- # Remove existing compiled template methods,
56
- # as Ruby warns when redefining a method.
57
- method_name = call_method_name(template[:variant])
54
+ if has_inline_template?
55
+ template = component_class.inline_template
58
56
 
59
57
  redefinition_lock.synchronize do
60
- component_class.silence_redefinition_of_method(method_name)
58
+ component_class.silence_redefinition_of_method("call")
61
59
  # rubocop:disable Style/EvalWithLocation
62
- component_class.class_eval <<-RUBY, template[:path], 0
63
- def #{method_name}
64
- #{compiled_template(template[:path])}
60
+ component_class.class_eval <<-RUBY, template.path, template.lineno
61
+ def call
62
+ #{compiled_inline_template(template)}
65
63
  end
66
64
  RUBY
67
65
  # rubocop:enable Style/EvalWithLocation
66
+
67
+ component_class.silence_redefinition_of_method("render_template_for")
68
+ component_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
69
+ def render_template_for(variant = nil)
70
+ call
71
+ end
72
+ RUBY
73
+ end
74
+ else
75
+ templates.each do |template|
76
+ # Remove existing compiled template methods,
77
+ # as Ruby warns when redefining a method.
78
+ method_name = call_method_name(template[:variant])
79
+
80
+ redefinition_lock.synchronize do
81
+ component_class.silence_redefinition_of_method(method_name)
82
+ # rubocop:disable Style/EvalWithLocation
83
+ component_class.class_eval <<-RUBY, template[:path], 0
84
+ def #{method_name}
85
+ #{compiled_template(template[:path])}
86
+ end
87
+ RUBY
88
+ # rubocop:enable Style/EvalWithLocation
89
+ end
68
90
  end
69
- end
70
91
 
71
- define_render_template_for
92
+ define_render_template_for
93
+ end
72
94
 
73
95
  component_class.build_i18n_backend
74
96
 
@@ -103,12 +125,16 @@ module ViewComponent
103
125
  end
104
126
  end
105
127
 
128
+ def has_inline_template?
129
+ component_class.respond_to?(:inline_template) && component_class.inline_template.present?
130
+ end
131
+
106
132
  def template_errors
107
133
  @__vc_template_errors ||=
108
134
  begin
109
135
  errors = []
110
136
 
111
- if (templates + inline_calls).empty?
137
+ if (templates + inline_calls).empty? && !has_inline_template?
112
138
  errors << "Couldn't find a template file or inline render method for #{component_class}."
113
139
  end
114
140
 
@@ -216,9 +242,21 @@ module ViewComponent
216
242
  end
217
243
  end
218
244
 
245
+ def compiled_inline_template(template)
246
+ handler = ActionView::Template.handler_for_extension(template.language)
247
+ template.rstrip! if component_class.strip_trailing_whitespace?
248
+
249
+ compile_template(template.source, handler)
250
+ end
251
+
219
252
  def compiled_template(file_path)
220
253
  handler = ActionView::Template.handler_for_extension(File.extname(file_path).delete("."))
221
254
  template = File.read(file_path)
255
+
256
+ compile_template(template, handler)
257
+ end
258
+
259
+ def compile_template(template, handler)
222
260
  template.rstrip! if component_class.strip_trailing_whitespace?
223
261
 
224
262
  if handler.method(:call).parameters.length > 1
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent # :nodoc:
4
+ module InlineTemplate
5
+ extend ActiveSupport::Concern
6
+ Template = Struct.new(:source, :language, :path, :lineno)
7
+
8
+ class_methods do
9
+ def method_missing(method, *args)
10
+ return super if !method.end_with?("_template")
11
+
12
+ if defined?(@__vc_inline_template_defined) && @__vc_inline_template_defined
13
+ raise ViewComponent::ComponentError, "inline templates can only be defined once per-component"
14
+ end
15
+
16
+ if args.size != 1
17
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)"
18
+ end
19
+
20
+ ext = method.to_s.gsub("_template", "")
21
+ template = args.first
22
+
23
+ @__vc_inline_template_language = ext
24
+
25
+ caller = caller_locations(1..1)[0]
26
+ @__vc_inline_template = Template.new(
27
+ template,
28
+ ext,
29
+ caller.absolute_path || caller.path,
30
+ caller.lineno
31
+ )
32
+
33
+ @__vc_inline_template_defined = true
34
+ end
35
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
36
+
37
+ def respond_to_missing?(method, include_all = false)
38
+ method.end_with?("_template") || super
39
+ end
40
+
41
+ def inline_template
42
+ @__vc_inline_template
43
+ end
44
+
45
+ def inline_template_language
46
+ @__vc_inline_template_language if defined?(@__vc_inline_template_language)
47
+ end
48
+
49
+ def inherited(subclass)
50
+ super
51
+ subclass.instance_variable_set(:@__vc_inline_template_language, inline_template_language)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -10,12 +10,12 @@ module ViewComponent
10
10
  # @param layout [String] The (optional) layout to use.
11
11
  # @return [Proc] A block that can be used to visit the path of the inline rendered component.
12
12
  def with_rendered_component_path(fragment, layout: false, &block)
13
- # Add './tmp/view_components/' directory if it doesn't exist to store the rendered component HTML
14
- FileUtils.mkdir_p("./tmp/view_components/") unless Dir.exist?("./tmp/view_components/")
15
-
16
- file = Tempfile.new(["rendered_#{fragment.class.name}", ".html"], "tmp/view_components/")
13
+ file = Tempfile.new(
14
+ ["rendered_#{fragment.class.name}", ".html"],
15
+ ViewComponentsSystemTestController::TEMP_DIR
16
+ )
17
17
  begin
18
- file.write(__vc_test_helpers_controller.render_to_string(html: fragment.to_html.html_safe, layout: layout))
18
+ file.write(vc_test_controller.render_to_string(html: fragment.to_html.html_safe, layout: layout))
19
19
  file.rewind
20
20
 
21
21
  block.call("/_system_test_entrypoint?file=#{file.path.split("/").last}")
@@ -47,9 +47,9 @@ module ViewComponent
47
47
  @page = nil
48
48
  @rendered_content =
49
49
  if Rails.version.to_f >= 6.1
50
- __vc_test_helpers_controller.view_context.render(component, args, &block)
50
+ vc_test_controller.view_context.render(component, args, &block)
51
51
  else
52
- __vc_test_helpers_controller.view_context.render_component(component, &block)
52
+ vc_test_controller.view_context.render_component(component, &block)
53
53
  end
54
54
 
55
55
  Nokogiri::HTML.fragment(@rendered_content)
@@ -105,7 +105,7 @@ module ViewComponent
105
105
  # ```
106
106
  def render_in_view_context(*args, &block)
107
107
  @page = nil
108
- @rendered_content = __vc_test_helpers_controller.view_context.instance_exec(*args, &block)
108
+ @rendered_content = vc_test_controller.view_context.instance_exec(*args, &block)
109
109
  Nokogiri::HTML.fragment(@rendered_content)
110
110
  end
111
111
  ruby2_keywords(:render_in_view_context) if respond_to?(:ruby2_keywords, true)
@@ -120,12 +120,12 @@ module ViewComponent
120
120
  #
121
121
  # @param variant [Symbol] The variant to be set for the provided block.
122
122
  def with_variant(variant)
123
- old_variants = __vc_test_helpers_controller.view_context.lookup_context.variants
123
+ old_variants = vc_test_controller.view_context.lookup_context.variants
124
124
 
125
- __vc_test_helpers_controller.view_context.lookup_context.variants = variant
125
+ vc_test_controller.view_context.lookup_context.variants = variant
126
126
  yield
127
127
  ensure
128
- __vc_test_helpers_controller.view_context.lookup_context.variants = old_variants
128
+ vc_test_controller.view_context.lookup_context.variants = old_variants
129
129
  end
130
130
 
131
131
  # Set the controller to be used while executing the given block,
@@ -139,12 +139,12 @@ module ViewComponent
139
139
  #
140
140
  # @param klass [ActionController::Base] The controller to be used.
141
141
  def with_controller_class(klass)
142
- old_controller = defined?(@__vc_test_helpers_controller) && @__vc_test_helpers_controller
142
+ old_controller = defined?(@vc_test_controller) && @vc_test_controller
143
143
 
144
- @__vc_test_helpers_controller = __vc_test_helpers_build_controller(klass)
144
+ @vc_test_controller = __vc_test_helpers_build_controller(klass)
145
145
  yield
146
146
  ensure
147
- @__vc_test_helpers_controller = old_controller
147
+ @vc_test_controller = old_controller
148
148
  end
149
149
 
150
150
  # Set the URL of the current request (such as when using request-dependent path helpers):
@@ -157,35 +157,54 @@ module ViewComponent
157
157
  #
158
158
  # @param path [String] The path to set for the current request.
159
159
  def with_request_url(path)
160
- old_request_path_info = __vc_test_helpers_request.path_info
161
- old_request_path_parameters = __vc_test_helpers_request.path_parameters
162
- old_request_query_parameters = __vc_test_helpers_request.query_parameters
163
- old_request_query_string = __vc_test_helpers_request.query_string
164
- old_controller = defined?(@__vc_test_helpers_controller) && @__vc_test_helpers_controller
160
+ old_request_path_info = vc_test_request.path_info
161
+ old_request_path_parameters = vc_test_request.path_parameters
162
+ old_request_query_parameters = vc_test_request.query_parameters
163
+ old_request_query_string = vc_test_request.query_string
164
+ old_controller = defined?(@vc_test_controller) && @vc_test_controller
165
165
 
166
166
  path, query = path.split("?", 2)
167
- __vc_test_helpers_request.path_info = path
168
- __vc_test_helpers_request.path_parameters = Rails.application.routes.recognize_path_with_request(__vc_test_helpers_request, path, {})
169
- __vc_test_helpers_request.set_header("action_dispatch.request.query_parameters", Rack::Utils.parse_nested_query(query))
170
- __vc_test_helpers_request.set_header(Rack::QUERY_STRING, query)
167
+ vc_test_request.path_info = path
168
+ vc_test_request.path_parameters = Rails.application.routes.recognize_path_with_request(vc_test_request, path, {})
169
+ vc_test_request.set_header("action_dispatch.request.query_parameters", Rack::Utils.parse_nested_query(query))
170
+ vc_test_request.set_header(Rack::QUERY_STRING, query)
171
171
  yield
172
172
  ensure
173
- __vc_test_helpers_request.path_info = old_request_path_info
174
- __vc_test_helpers_request.path_parameters = old_request_path_parameters
175
- __vc_test_helpers_request.set_header("action_dispatch.request.query_parameters", old_request_query_parameters)
176
- __vc_test_helpers_request.set_header(Rack::QUERY_STRING, old_request_query_string)
177
- @__vc_test_helpers_controller = old_controller
173
+ vc_test_request.path_info = old_request_path_info
174
+ vc_test_request.path_parameters = old_request_path_parameters
175
+ vc_test_request.set_header("action_dispatch.request.query_parameters", old_request_query_parameters)
176
+ vc_test_request.set_header(Rack::QUERY_STRING, old_request_query_string)
177
+ @vc_test_controller = old_controller
178
178
  end
179
179
 
180
- # Note: We prefix private methods here to prevent collisions in consumer's tests.
181
- private
182
-
183
- def __vc_test_helpers_controller
184
- @__vc_test_helpers_controller ||= __vc_test_helpers_build_controller(Base.test_controller.constantize)
180
+ # Access the controller used by `render_inline`:
181
+ #
182
+ # ```ruby
183
+ # test "logged out user sees login link" do
184
+ # vc_test_controller.expects(:logged_in?).at_least_once.returns(false)
185
+ # render_inline(LoginComponent.new)
186
+ # assert_selector("[aria-label='You must be signed in']")
187
+ # end
188
+ # ```
189
+ #
190
+ # @return [ActionController::Base]
191
+ def vc_test_controller
192
+ @vc_test_controller ||= __vc_test_helpers_build_controller(Base.test_controller.constantize)
185
193
  end
186
194
 
187
- def __vc_test_helpers_request
188
- @__vc_test_helpers_request ||=
195
+ # Access the request used by `render_inline`:
196
+ #
197
+ # ```ruby
198
+ # test "component does not render in Firefox" do
199
+ # request.env["HTTP_USER_AGENT"] = "Mozilla/5.0"
200
+ # render_inline(NoFirefoxComponent.new)
201
+ # refute_component_rendered
202
+ # end
203
+ # ```
204
+ #
205
+ # @return [ActionDispatch::TestRequest]
206
+ def vc_test_request
207
+ @vc_test_request ||=
189
208
  begin
190
209
  out = ActionDispatch::TestRequest.create
191
210
  out.session = ActionController::TestSession.new
@@ -193,8 +212,11 @@ module ViewComponent
193
212
  end
194
213
  end
195
214
 
215
+ # Note: We prefix private methods here to prevent collisions in consumer's tests.
216
+ private
217
+
196
218
  def __vc_test_helpers_build_controller(klass)
197
- klass.new.tap { |c| c.request = __vc_test_helpers_request }.extend(Rails.application.routes.url_helpers)
219
+ klass.new.tap { |c| c.request = vc_test_request }.extend(Rails.application.routes.url_helpers)
198
220
  end
199
221
 
200
222
  def __vc_test_helpers_preview_class
@@ -21,7 +21,7 @@ module ViewComponent
21
21
  end
22
22
 
23
23
  def build_i18n_backend
24
- return if CompileCache.compiled? self
24
+ return if compiled?
25
25
 
26
26
  self.i18n_backend = if (translation_files = sidecar_files(%w[yml yaml])).any?
27
27
  # Returning nil cleans up if translations file has been removed since the last compilation
@@ -32,6 +32,27 @@ module ViewComponent
32
32
  )
33
33
  end
34
34
  end
35
+
36
+ def i18n_key(key, scope = nil)
37
+ scope = scope.join(".") if scope.is_a? Array
38
+ key = key&.to_s unless key.is_a?(String)
39
+ key = "#{scope}.#{key}" if scope
40
+ key = "#{i18n_scope}#{key}" if key.start_with?(".")
41
+ key
42
+ end
43
+
44
+ def translate(key = nil, **options)
45
+ return key.map { |k| translate(k, **options) } if key.is_a?(Array)
46
+
47
+ ensure_compiled
48
+
49
+ locale = options.delete(:locale) || ::I18n.locale
50
+ key = i18n_key(key, options.delete(:scope))
51
+
52
+ i18n_backend.translate(locale, key, options)
53
+ end
54
+
55
+ alias_method :t, :translate
35
56
  end
36
57
 
37
58
  class I18nBackend < ::I18n::Backend::Simple
@@ -64,15 +85,10 @@ module ViewComponent
64
85
  return key.map { |k| translate(k, **options) } if key.is_a?(Array)
65
86
 
66
87
  locale = options.delete(:locale) || ::I18n.locale
67
- scope = options.delete(:scope)
68
- scope = scope.join(".") if scope.is_a? Array
69
- key = key&.to_s unless key.is_a?(String)
70
- key = "#{scope}.#{key}" if scope
71
- key = "#{i18n_scope}#{key}" if key.start_with?(".")
72
-
73
- if HTML_SAFE_TRANSLATION_KEY.match?(key)
74
- html_escape_translation_options!(options)
75
- end
88
+ key = self.class.i18n_key(key, options.delete(:scope))
89
+ as_html = HTML_SAFE_TRANSLATION_KEY.match?(key)
90
+
91
+ html_escape_translation_options!(options) if as_html
76
92
 
77
93
  if key.start_with?(i18n_scope + ".")
78
94
  translated =
@@ -85,10 +101,7 @@ module ViewComponent
85
101
  return super(key, locale: locale, **options)
86
102
  end
87
103
 
88
- if HTML_SAFE_TRANSLATION_KEY.match?(key)
89
- translated = html_safe_translation(translated)
90
- end
91
-
104
+ translated = html_safe_translation(translated) if as_html
92
105
  translated
93
106
  else
94
107
  super(key, locale: locale, **options)
@@ -101,6 +114,8 @@ module ViewComponent
101
114
  self.class.i18n_scope
102
115
  end
103
116
 
117
+ private
118
+
104
119
  def html_safe_translation(translation)
105
120
  if translation.respond_to?(:map)
106
121
  translation.map { |element| html_safe_translation(element) }
@@ -112,18 +127,13 @@ module ViewComponent
112
127
  end
113
128
  end
114
129
 
115
- private
116
-
117
130
  def html_escape_translation_options!(options)
118
131
  options.each do |name, value|
119
- unless i18n_option?(name) || (name == :count && value.is_a?(Numeric))
120
- options[name] = ERB::Util.html_escape(value.to_s)
121
- end
122
- end
123
- end
132
+ next if ::I18n.reserved_keys_pattern.match?(name)
133
+ next if name == :count && value.is_a?(Numeric)
124
134
 
125
- def i18n_option?(name)
126
- (@i18n_option_names ||= I18n::RESERVED_KEYS.to_set).include?(name)
135
+ options[name] = ERB::Util.html_escape(value.to_s)
136
+ end
127
137
  end
128
138
  end
129
139
  end
@@ -5,7 +5,7 @@ module ViewComponent
5
5
  MAJOR = 3
6
6
  MINOR = 0
7
7
  PATCH = 0
8
- PRE = "rc2"
8
+ PRE = "rc4"
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
11
11
  end
@@ -13,6 +13,7 @@ module ViewComponent
13
13
  autoload :ComponentError
14
14
  autoload :Config
15
15
  autoload :Deprecation
16
+ autoload :InlineTemplate
16
17
  autoload :Instrumentation
17
18
  autoload :Preview
18
19
  autoload :PreviewTemplateError
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: 3.0.0.rc2
4
+ version: 3.0.0.rc4
5
5
  platform: ruby
6
6
  authors:
7
7
  - ViewComponent Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-02-17 00:00:00.000000000 Z
11
+ date: 2023-03-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -363,6 +363,7 @@ files:
363
363
  - lib/view_component/docs_builder_component.html.erb
364
364
  - lib/view_component/docs_builder_component.rb
365
365
  - lib/view_component/engine.rb
366
+ - lib/view_component/inline_template.rb
366
367
  - lib/view_component/instrumentation.rb
367
368
  - lib/view_component/preview.rb
368
369
  - lib/view_component/preview_template_error.rb