view_component 3.0.0.rc1 → 3.0.0.rc3

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: 18fc4d884af4512c13d7b1f4184c59e57859a8d797451ec0b2063ff425a7d357
4
- data.tar.gz: 8da2e96b470f354b1d09f1b461506e64cca4345522f2b9ddfa4143ee5bf0627c
3
+ metadata.gz: f88375c08b20eb604992d315362fbdf6e0654fd87d7766d1ac404daecb1b57c9
4
+ data.tar.gz: 5f37ce63b609f5ff542a7e7c4629c952e3a02fc4e2ffc83df28aee86ff5d40cd
5
5
  SHA512:
6
- metadata.gz: 9750449d8d23a150a099e8c3fa6620eb7ba03adecfeefaa9e4f0c3ce463353d9918f1212971ae214557675eae367232ccd96308e89f11831259efa1000b8fa22
7
- data.tar.gz: 7b1a6852d060bea82ab51e69f3328d95dd6491d11fb2f88b84fa8b235fb17a83670a05764c8df116127017332fd3a6f9d76d9205fd5bcc31d925ec9e39a573b8
6
+ metadata.gz: 425107ab124527b5199b4174437c17a0f2dae4447b77a9393f68dd8c7a88517e25a3100fa0ed4cf6190d7e3b38390c6fd5cf48718753a53169f72dfe50ae5d12
7
+ data.tar.gz: b1642a727d8a586d8dec7a2e7075b570f5ccb20cee0b3c41037ac0b119adc6261ca1d1fb1dea0558f4bb40a0a45ba73b111ce4816a85be6798e3fb2bfaf76daf
@@ -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,82 @@ nav_order: 5
10
10
 
11
11
  ## main
12
12
 
13
+ ## v3.0.0.rc3
14
+
15
+ Run into an issue with this release candidate? [Let us know](https://github.com/ViewComponent/view_component/issues/1629).
16
+
17
+ * Fix typos in generator docs.
18
+
19
+ *Sascha Karnatz*
20
+
21
+ * Add `TestHelpers#vc_test_controller`.
22
+
23
+ *Joel Hawksley*
24
+
25
+ * Document `config.view_component.capture_compatibility_patch_enabled` as option for the known incompatibilities with Rails form helpers.
26
+
27
+ *Tobias L. Maier*
28
+
29
+ * Add support for experimental inline templates.
30
+
31
+ *Blake Williams*
32
+
33
+ * Expose `translate` and `t` I18n methods on component classes.
34
+
35
+ *Elia Schito*
36
+
37
+ * Protect against Arbitrary File Read edge case in `ViewComponentsSystemTestController`.
38
+
39
+ *Nick Malcolm*
40
+
41
+ ## v3.0.0.rc2
42
+
43
+ Run into an issue with this release? [Let us know](https://github.com/ViewComponent/view_component/issues/1629).
44
+
45
+ * BREAKING: Rename `SlotV2` to `Slot` and `SlotableV2` to `Slotable`.
46
+
47
+ *Joel Hawksley*
48
+
49
+ * BREAKING: Incorporate `PolymorphicSlots` into `Slotable`. To migrate, remove any references to `PolymorphicSlots` as they are no longer necessary.
50
+
51
+ *Joel Hawksley*
52
+
53
+ * BREAKING: Rename private TestHelpers#controller, #build_controller, #request, and #preview_class to avoid conflicts. Note: While these methods were undocumented and marked as private, they was easily accessible in tests. As such, we're cautiously considering this to be a breaking change.
54
+
55
+ *Joel Hawksley*
56
+
57
+ * Avoid loading ActionView::Base during Rails initialization. Originally submitted in #1528.
58
+
59
+ *Jonathan del Strother*
60
+
61
+ * Improve documentation of known incompatibilities with Rails form helpers.
62
+
63
+ *Tobias L. Maier*
64
+
65
+ * Remove dependency on environment task from `view_component:statsetup`.
66
+
67
+ *Svetlin Simonyan*
68
+
69
+ * Add experimental `config.view_component.capture_compatibility_patch_enabled` option resolving rendering issues related to forms, capture, turbo frames, etc.
70
+
71
+ *Blake Williams*
72
+
73
+ * Add `#content?` method that indicates if content has been passed to component.
74
+
75
+ *Joel Hawksley*
76
+
77
+ * Added example of a custom preview controller.
78
+
79
+ *Graham Rogers*
80
+
81
+ * Add Krystal to list of companies using ViewComponent.
82
+
83
+ *Matt Bearman*
84
+
85
+ * Add Mon Ami to list of companies using ViewComponent.
86
+
87
+ *Ethan Lee-Tyson*
88
+
13
89
  ## 3.0.0.rc1
14
90
 
15
91
  1,000+ days and 100+ releases later, the 200+ contributors to ViewComponent are proud to ship v3.0.0!
@@ -6,9 +6,8 @@ require "view_component/collection"
6
6
  require "view_component/compile_cache"
7
7
  require "view_component/compiler"
8
8
  require "view_component/config"
9
- require "view_component/polymorphic_slots"
10
9
  require "view_component/preview"
11
- require "view_component/slotable_v2"
10
+ require "view_component/slotable"
12
11
  require "view_component/translatable"
13
12
  require "view_component/with_content_helper"
14
13
 
@@ -21,7 +20,7 @@ module ViewComponent
21
20
  #
22
21
  # @return [ViewComponent::Config]
23
22
  def config
24
- @config ||= ViewComponent::Config.defaults
23
+ @config ||= ActiveSupport::OrderedOptions.new
25
24
  end
26
25
 
27
26
  # Replaces the entire config. You shouldn't need to use this directly
@@ -29,8 +28,7 @@ module ViewComponent
29
28
  attr_writer :config
30
29
  end
31
30
 
32
- include ViewComponent::PolymorphicSlots
33
- include ViewComponent::SlotableV2
31
+ include ViewComponent::Slotable
34
32
  include ViewComponent::Translatable
35
33
  include ViewComponent::WithContentHelper
36
34
 
@@ -245,22 +243,40 @@ module ViewComponent
245
243
  @request ||= controller.request if controller.respond_to?(:request)
246
244
  end
247
245
 
248
- private
249
-
250
- attr_reader :view_context
251
-
246
+ # The content passed to the component instance as a block.
247
+ #
248
+ # @return [String]
252
249
  def content
253
250
  @__vc_content_evaluated = true
254
251
  return @__vc_content if defined?(@__vc_content)
255
252
 
256
253
  @__vc_content =
257
- if @view_context && @__vc_render_in_block
254
+ if __vc_render_in_block_provided?
258
255
  view_context.capture(self, &@__vc_render_in_block)
259
- elsif defined?(@__vc_content_set_by_with_content)
256
+ elsif __vc_content_set_by_with_content_defined?
260
257
  @__vc_content_set_by_with_content
261
258
  end
262
259
  end
263
260
 
261
+ # Whether `content` has been passed to the component.
262
+ #
263
+ # @return [Boolean]
264
+ def content?
265
+ __vc_render_in_block_provided? || __vc_content_set_by_with_content_defined?
266
+ end
267
+
268
+ private
269
+
270
+ attr_reader :view_context
271
+
272
+ def __vc_render_in_block_provided?
273
+ @view_context && @__vc_render_in_block
274
+ end
275
+
276
+ def __vc_content_set_by_with_content_defined?
277
+ defined?(@__vc_content_set_by_with_content)
278
+ end
279
+
264
280
  def content_evaluated?
265
281
  @__vc_content_evaluated
266
282
  end
@@ -465,6 +481,11 @@ module ViewComponent
465
481
  compiler.compiled?
466
482
  end
467
483
 
484
+ # @private
485
+ def ensure_compiled
486
+ compile unless compiled?
487
+ end
488
+
468
489
  # Compile templates to instance methods, assuming they haven't been compiled already.
469
490
  #
470
491
  # Do as much work as possible in this step, as doing so reduces the amount
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ # CaptureCompatibility is a module that patches #capture to fix issues
5
+ # related to ViewComponent and functionality that relies on `capture`
6
+ # like forms, capture itself, turbo frames, etc.
7
+ #
8
+ # This underlying incompatibility with ViewComponent and capture is
9
+ # that several features like forms keep a reference to the primary
10
+ # `ActionView::Base` instance which has its own @output_buffer. When
11
+ # `#capture` is called on the original `ActionView::Base` instance while
12
+ # evaluating a block from a ViewComponent the @output_buffer is overridden
13
+ # in the ActionView::Base instance, and *not* the component. This results
14
+ # in a double render due to `#capture` implementation details.
15
+ #
16
+ # To resolve the issue, we override `#capture` so that we can delegate
17
+ # the `capture` logic to the ViewComponent that created the block.
18
+ module CaptureCompatibility
19
+ def self.included(base)
20
+ base.class_eval do
21
+ alias_method :original_capture, :capture
22
+ end
23
+
24
+ base.prepend(InstanceMethods)
25
+ end
26
+
27
+ module InstanceMethods
28
+ def capture(*args, &block)
29
+ # Handle blocks that originate from C code and raise, such as `&:method`
30
+ return original_capture(*args, &block) if block.source_location.nil?
31
+
32
+ block_context = block.binding.receiver
33
+
34
+ if block_context != self && block_context.class < ActionView::Base
35
+ block_context.original_capture(*args, &block)
36
+ else
37
+ original_capture(*args, &block)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -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
@@ -23,7 +23,8 @@ module ViewComponent
23
23
  show_previews: Rails.env.development? || Rails.env.test?,
24
24
  preview_paths: default_preview_paths,
25
25
  test_controller: "ApplicationController",
26
- default_preview_layout: nil
26
+ default_preview_layout: nil,
27
+ capture_compatibility_patch_enabled: false
27
28
  })
28
29
  end
29
30
 
@@ -137,6 +138,13 @@ module ViewComponent
137
138
  # A custom default layout used for the previews index page and individual
138
139
  # previews.
139
140
  # Defaults to `nil`. If this is falsy, `"component_preview"` is used.
141
+ #
142
+ # @!attribute capture_compatibility_patch_enabled
143
+ # @return [Boolean]
144
+ # Enables the experimental capture compatibility patch that makes ViewComponent
145
+ # compatible with forms, capture, and other built-ins.
146
+ # previews.
147
+ # Defaults to `false`.
140
148
 
141
149
  def default_preview_paths
142
150
  return [] unless defined?(Rails.root) && Dir.exist?("#{Rails.root}/test/components/previews")
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails"
4
- require "view_component/base"
4
+ require "view_component/config"
5
5
 
6
6
  module ViewComponent
7
7
  class Engine < Rails::Engine # :nodoc:
8
- config.view_component = ViewComponent::Base.config
8
+ config.view_component = ViewComponent::Config.defaults
9
9
 
10
10
  rake_tasks do
11
11
  load "view_component/rails/tasks/view_component.rake"
@@ -14,9 +14,6 @@ module ViewComponent
14
14
  initializer "view_component.set_configs" do |app|
15
15
  options = app.config.view_component
16
16
 
17
- %i[generate preview_controller preview_route show_previews_source].each do |config_option|
18
- options[config_option] ||= ViewComponent::Base.public_send(config_option)
19
- end
20
17
  options.instrumentation_enabled = false if options.instrumentation_enabled.nil?
21
18
  options.render_monkey_patch_enabled = true if options.render_monkey_patch_enabled.nil?
22
19
  options.show_previews = (Rails.env.development? || Rails.env.test?) if options.show_previews.nil?
@@ -39,6 +36,8 @@ module ViewComponent
39
36
 
40
37
  initializer "view_component.enable_instrumentation" do |app|
41
38
  ActiveSupport.on_load(:view_component) do
39
+ Base.config = app.config.view_component
40
+
42
41
  if app.config.view_component.instrumentation_enabled.present?
43
42
  # :nocov:
44
43
  ViewComponent::Base.prepend(ViewComponent::Instrumentation)
@@ -47,6 +46,14 @@ module ViewComponent
47
46
  end
48
47
  end
49
48
 
49
+ # :nocov:
50
+ initializer "view_component.enable_capture_patch" do |app|
51
+ ActiveSupport.on_load(:view_component) do
52
+ ActionView::Base.include(ViewComponent::CaptureCompatibility) if app.config.view_component.capture_compatibility_patch_enabled
53
+ end
54
+ end
55
+ # :nocov:
56
+
50
57
  initializer "view_component.set_autoload_paths" do |app|
51
58
  options = app.config.view_component
52
59
 
@@ -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
@@ -3,7 +3,7 @@
3
3
  task stats: "view_component:statsetup"
4
4
 
5
5
  namespace :view_component do
6
- task statsetup: :environment do
6
+ task :statsetup do
7
7
  require "rails/code_statistics"
8
8
 
9
9
  ::STATS_DIRECTORIES << ["ViewComponents", ViewComponent::Base.view_component_path]
@@ -3,7 +3,7 @@
3
3
  require "view_component/with_content_helper"
4
4
 
5
5
  module ViewComponent
6
- class SlotV2
6
+ class Slot
7
7
  include ViewComponent::WithContentHelper
8
8
 
9
9
  attr_writer :__vc_component_instance, :__vc_content_block, :__vc_content