view_component 3.0.0.rc1 → 3.0.0.rc3

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