view_component 3.0.0.rc2 → 3.0.0.rc4

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: 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