view_component_devtools 0.1.0
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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +9 -0
- data/lib/view_component_devtools/component.rb +77 -0
- data/lib/view_component_devtools/configuration.rb +38 -0
- data/lib/view_component_devtools/engine.rb +29 -0
- data/lib/view_component_devtools/helper.rb +23 -0
- data/lib/view_component_devtools/middleware.rb +31 -0
- data/lib/view_component_devtools/normalizer.rb +104 -0
- data/lib/view_component_devtools/state.rb +122 -0
- data/lib/view_component_devtools/subscriber.rb +34 -0
- data/lib/view_component_devtools/version.rb +5 -0
- data/lib/view_component_devtools.rb +36 -0
- metadata +133 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 21babb25c8660808d7564914dd2105566e39e9954a197643e7862815ea867583
|
|
4
|
+
data.tar.gz: c64e1042bef475216ff30b64d7fc8f5ac16d308524bb916d2460ef3a9d3e75a9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f88cbecd121cc209eb6dd9e2a82e792cf1f7636becbc183ab6ea490cf563af7b3318d86246bce203780f79465ad5f457047ad348704732185d3ce952109fded6
|
|
7
|
+
data.tar.gz: e9420245c4fdad743a6cec795c2e2c8846d247181436ec3bc0d41f4a1868dc0c793a745f0744be4f565e31976ff5a7a3b70d0ac69a49ebd6c97b271b2727d1d2
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ViewComponent DevTools contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# view_component_devtools gem
|
|
2
|
+
|
|
3
|
+
Development-only Rails instrumentation for the ViewComponent DevTools browser
|
|
4
|
+
panel. The gem subscribes to ViewComponent's native ActiveSupport notification,
|
|
5
|
+
stores a bounded render tree in request-local state, and exposes it through an
|
|
6
|
+
explicit layout helper.
|
|
7
|
+
|
|
8
|
+
See the repository [README](../../README.md) for installation, configuration,
|
|
9
|
+
security behavior, and the browser extension setup.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponentDevtools
|
|
4
|
+
module Component
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.extend(ClassMethods)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
def devtools_props(*names)
|
|
11
|
+
@view_component_devtools_props ||= []
|
|
12
|
+
@view_component_devtools_props.concat(names.map(&:to_sym)).uniq!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def view_component_devtools_prop_names
|
|
16
|
+
inherited = superclass.respond_to?(:view_component_devtools_prop_names) ?
|
|
17
|
+
superclass.view_component_devtools_prop_names : []
|
|
18
|
+
initializer_names = instance_method(:initialize).parameters.filter_map do |kind, name|
|
|
19
|
+
name if %i[req opt keyreq key].include?(kind)
|
|
20
|
+
end
|
|
21
|
+
declared = @view_component_devtools_props || []
|
|
22
|
+
trusted = inherited + declared
|
|
23
|
+
|
|
24
|
+
(trusted + initializer_names).uniq.select do |name|
|
|
25
|
+
prop_reader?(name, trusted: trusted.include?(name))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def prop_reader?(name, trusted:)
|
|
32
|
+
return false unless public_method_defined?(name)
|
|
33
|
+
|
|
34
|
+
method = instance_method(name)
|
|
35
|
+
callable_without_arguments = method.parameters.none? do |kind, _|
|
|
36
|
+
%i[req keyreq].include?(kind)
|
|
37
|
+
end
|
|
38
|
+
callable_without_arguments && (trusted || component_class_owner?(method.owner))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def component_class_owner?(owner)
|
|
42
|
+
return false unless owner.is_a?(Class)
|
|
43
|
+
|
|
44
|
+
if defined?(ViewComponent::Base) && self < ViewComponent::Base
|
|
45
|
+
owner < ViewComponent::Base
|
|
46
|
+
else
|
|
47
|
+
self <= owner
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def render_in(...)
|
|
53
|
+
return super unless State.active?
|
|
54
|
+
|
|
55
|
+
metadata = {
|
|
56
|
+
name: self.class.name,
|
|
57
|
+
source_location: component_source_location,
|
|
58
|
+
props: self.class.view_component_devtools_prop_names.to_h do |name|
|
|
59
|
+
[name, public_send(name)]
|
|
60
|
+
end
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
State.with_component(metadata) { super }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def component_source_location
|
|
69
|
+
if self.class.respond_to?(:identifier)
|
|
70
|
+
identifier = self.class.identifier
|
|
71
|
+
return identifier if identifier
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
self.class.instance_method(:initialize).source_location&.join(":")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponentDevtools
|
|
4
|
+
class Configuration
|
|
5
|
+
DEFAULT_REDACT_KEYS = [
|
|
6
|
+
/authorization/i,
|
|
7
|
+
/cookie/i,
|
|
8
|
+
/credential/i,
|
|
9
|
+
/passw(or)?d/i,
|
|
10
|
+
/private.?key/i,
|
|
11
|
+
/secret/i,
|
|
12
|
+
/session/i,
|
|
13
|
+
/token/i
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
attr_accessor :allow_non_development,
|
|
17
|
+
:enabled,
|
|
18
|
+
:max_collection_size,
|
|
19
|
+
:max_components,
|
|
20
|
+
:max_depth,
|
|
21
|
+
:max_string_length,
|
|
22
|
+
:redact_keys
|
|
23
|
+
|
|
24
|
+
def initialize
|
|
25
|
+
@enabled = false
|
|
26
|
+
@allow_non_development = false
|
|
27
|
+
@max_components = 500
|
|
28
|
+
@max_depth = 8
|
|
29
|
+
@max_collection_size = 50
|
|
30
|
+
@max_string_length = 2_000
|
|
31
|
+
@redact_keys = DEFAULT_REDACT_KEYS.dup
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def enabled_for?(environment)
|
|
35
|
+
enabled && (environment.to_s == "development" || allow_non_development)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/engine"
|
|
4
|
+
|
|
5
|
+
module ViewComponentDevtools
|
|
6
|
+
class Engine < ::Rails::Engine
|
|
7
|
+
initializer "view_component_devtools.middleware" do |application|
|
|
8
|
+
application.middleware.use Middleware
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
initializer "view_component_devtools.helper" do
|
|
12
|
+
ActiveSupport.on_load(:action_view) { include Helper }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
initializer "view_component_devtools.subscriber" do
|
|
16
|
+
Subscriber.install!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
config.after_initialize do
|
|
20
|
+
next unless ViewComponentDevtools.capture_enabled?
|
|
21
|
+
|
|
22
|
+
Rails.application.config.view_component.instrumentation_enabled = true
|
|
23
|
+
require "view_component/instrumentation"
|
|
24
|
+
unless ViewComponent::Base < ViewComponent::Instrumentation
|
|
25
|
+
ViewComponent::Base.prepend(ViewComponent::Instrumentation)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/string/output_safety"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module ViewComponentDevtools
|
|
7
|
+
module Helper
|
|
8
|
+
ELEMENT_ID = "view-component-devtools-data"
|
|
9
|
+
|
|
10
|
+
def view_component_devtools_payload
|
|
11
|
+
return "".html_safe unless State.active?
|
|
12
|
+
|
|
13
|
+
json = JSON.generate(State.current.payload)
|
|
14
|
+
.gsub("<", "\\u003c")
|
|
15
|
+
.gsub(">", "\\u003e")
|
|
16
|
+
.gsub("&", "\\u0026")
|
|
17
|
+
.gsub("\u2028", "\\u2028")
|
|
18
|
+
.gsub("\u2029", "\\u2029")
|
|
19
|
+
|
|
20
|
+
%(<script type="application/json" id="#{ELEMENT_ID}" data-version="1">#{json}</script>).html_safe
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack/body_proxy"
|
|
4
|
+
|
|
5
|
+
module ViewComponentDevtools
|
|
6
|
+
class Middleware
|
|
7
|
+
def initialize(app)
|
|
8
|
+
@app = app
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(environment)
|
|
12
|
+
return app.call(environment) unless ViewComponentDevtools.capture_enabled?
|
|
13
|
+
|
|
14
|
+
State.start!(request_id: environment["action_dispatch.request_id"] || environment["HTTP_X_REQUEST_ID"])
|
|
15
|
+
completed = false
|
|
16
|
+
|
|
17
|
+
begin
|
|
18
|
+
status, headers, body = app.call(environment)
|
|
19
|
+
response = [status, headers, Rack::BodyProxy.new(body) { State.reset! }]
|
|
20
|
+
completed = true
|
|
21
|
+
response
|
|
22
|
+
ensure
|
|
23
|
+
State.reset! unless completed
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :app
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module ViewComponentDevtools
|
|
7
|
+
class Normalizer
|
|
8
|
+
REDACTED = "[REDACTED]"
|
|
9
|
+
|
|
10
|
+
def initialize(configuration = ViewComponentDevtools.configuration)
|
|
11
|
+
@configuration = configuration
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(value)
|
|
15
|
+
normalize(value, depth: 0, ancestors: {})
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :configuration
|
|
21
|
+
|
|
22
|
+
def normalize(value, depth:, ancestors:)
|
|
23
|
+
return "[maximum depth exceeded]" if depth > configuration.max_depth
|
|
24
|
+
|
|
25
|
+
case value
|
|
26
|
+
when nil, true, false, Integer
|
|
27
|
+
value
|
|
28
|
+
when Float
|
|
29
|
+
value.finite? ? value : "[non-finite Float]"
|
|
30
|
+
when String
|
|
31
|
+
truncate(sanitize_string(value))
|
|
32
|
+
when Symbol
|
|
33
|
+
truncate(sanitize_string(value.to_s))
|
|
34
|
+
when Time, DateTime
|
|
35
|
+
value.iso8601(3)
|
|
36
|
+
when Date
|
|
37
|
+
value.iso8601
|
|
38
|
+
when Array
|
|
39
|
+
normalize_array(value, depth:, ancestors:)
|
|
40
|
+
when Hash
|
|
41
|
+
normalize_hash(value, depth:, ancestors:)
|
|
42
|
+
else
|
|
43
|
+
"[unsupported #{value.class.name}]"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def normalize_array(value, depth:, ancestors:)
|
|
48
|
+
with_cycle_guard(value, ancestors) do |next_ancestors|
|
|
49
|
+
entries = value.first(configuration.max_collection_size).map do |entry|
|
|
50
|
+
normalize(entry, depth: depth + 1, ancestors: next_ancestors)
|
|
51
|
+
end
|
|
52
|
+
entries << "[#{value.length - entries.length} more items]" if value.length > entries.length
|
|
53
|
+
entries
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def normalize_hash(value, depth:, ancestors:)
|
|
58
|
+
with_cycle_guard(value, ancestors) do |next_ancestors|
|
|
59
|
+
entries = value.first(configuration.max_collection_size).to_h
|
|
60
|
+
normalized = entries.each_with_object({}) do |(key, entry), result|
|
|
61
|
+
original_key = stringify_key(key)
|
|
62
|
+
next unless original_key
|
|
63
|
+
|
|
64
|
+
normalized_key = truncate(original_key)
|
|
65
|
+
result[normalized_key] = if redact?(original_key)
|
|
66
|
+
REDACTED
|
|
67
|
+
else
|
|
68
|
+
normalize(entry, depth: depth + 1, ancestors: next_ancestors)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
normalized["__truncated__"] = value.length - entries.length if value.length > entries.length
|
|
72
|
+
normalized
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def stringify_key(key)
|
|
77
|
+
return sanitize_string(key.to_s) if key.is_a?(String) || key.is_a?(Symbol)
|
|
78
|
+
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def redact?(key)
|
|
83
|
+
configuration.redact_keys.any? do |pattern|
|
|
84
|
+
pattern.is_a?(Regexp) ? pattern.match?(key) : key.downcase.include?(pattern.to_s.downcase)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def truncate(value)
|
|
89
|
+
return value if value.length <= configuration.max_string_length
|
|
90
|
+
|
|
91
|
+
"#{value[0, configuration.max_string_length]}…"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def sanitize_string(value)
|
|
95
|
+
value.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def with_cycle_guard(value, ancestors)
|
|
99
|
+
return "[circular reference]" if ancestors.key?(value.object_id)
|
|
100
|
+
|
|
101
|
+
yield ancestors.merge(value.object_id => true)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/isolated_execution_state"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module ViewComponentDevtools
|
|
8
|
+
module State
|
|
9
|
+
KEY = :view_component_devtools_session
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def start!(request_id: nil)
|
|
13
|
+
raise "ViewComponent DevTools capture already active" if active?
|
|
14
|
+
|
|
15
|
+
ActiveSupport::IsolatedExecutionState[KEY] = Session.new(
|
|
16
|
+
request_id: request_id || SecureRandom.uuid,
|
|
17
|
+
configuration: ViewComponentDevtools.configuration
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def current
|
|
22
|
+
ActiveSupport::IsolatedExecutionState[KEY]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def active?
|
|
26
|
+
!current.nil?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reset!
|
|
30
|
+
ActiveSupport::IsolatedExecutionState.delete(KEY)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def with_component(metadata)
|
|
34
|
+
return yield unless current
|
|
35
|
+
|
|
36
|
+
current.push_component(metadata)
|
|
37
|
+
yield
|
|
38
|
+
ensure
|
|
39
|
+
current&.pop_component(metadata)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class Session
|
|
44
|
+
attr_reader :request_id
|
|
45
|
+
|
|
46
|
+
def initialize(request_id:, configuration:)
|
|
47
|
+
@request_id = request_id
|
|
48
|
+
@configuration = configuration
|
|
49
|
+
@components = []
|
|
50
|
+
@component_stack = []
|
|
51
|
+
@event_stack = []
|
|
52
|
+
@component_count = 0
|
|
53
|
+
@truncated = false
|
|
54
|
+
@normalizer = Normalizer.new(configuration)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def push_component(metadata)
|
|
58
|
+
component_stack << metadata
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def pop_component(metadata)
|
|
62
|
+
actual = component_stack.pop
|
|
63
|
+
raise "ViewComponent DevTools component stack mismatch" unless actual.equal?(metadata)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def start_event(event_id, payload, start_time)
|
|
67
|
+
node = build_node(payload, start_time)
|
|
68
|
+
event_stack << {event_id:, node:}
|
|
69
|
+
return unless node
|
|
70
|
+
|
|
71
|
+
parent = event_stack.reverse_each.drop(1).find { |entry| entry[:node] }&.fetch(:node)
|
|
72
|
+
(parent ? parent[:children] : components) << node
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def finish_event(event_id, payload, finish_time)
|
|
76
|
+
entry = event_stack.pop
|
|
77
|
+
raise "ViewComponent DevTools event stack mismatch" unless entry&.fetch(:event_id) == event_id
|
|
78
|
+
|
|
79
|
+
node = entry[:node]
|
|
80
|
+
return unless node
|
|
81
|
+
|
|
82
|
+
node[:durationMs] = ((finish_time - node.delete(:startedAt)) * 1_000).round(3)
|
|
83
|
+
node[:templateLocation] = payload[:view_identifier] if payload[:view_identifier]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def payload
|
|
87
|
+
{
|
|
88
|
+
version: 1,
|
|
89
|
+
requestId: request_id,
|
|
90
|
+
generatedAt: Time.now.utc.iso8601(3),
|
|
91
|
+
truncated: @truncated,
|
|
92
|
+
components:
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
attr_reader :component_stack, :components, :configuration, :event_stack, :normalizer
|
|
99
|
+
|
|
100
|
+
def build_node(payload, start_time)
|
|
101
|
+
if @component_count >= configuration.max_components
|
|
102
|
+
@truncated = true
|
|
103
|
+
return nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
@component_count += 1
|
|
107
|
+
metadata = component_stack.last
|
|
108
|
+
metadata = nil unless metadata&.fetch(:name, nil) == payload[:name]
|
|
109
|
+
{
|
|
110
|
+
id: "#{request_id}:#{@component_count}",
|
|
111
|
+
name: payload[:name] || metadata&.fetch(:name, nil) || "AnonymousComponent",
|
|
112
|
+
sourceLocation: payload[:identifier] || metadata&.fetch(:source_location, nil),
|
|
113
|
+
templateLocation: payload[:view_identifier],
|
|
114
|
+
durationMs: nil,
|
|
115
|
+
props: normalizer.call(metadata&.fetch(:props, {}) || {}),
|
|
116
|
+
children: [],
|
|
117
|
+
startedAt: start_time
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/notifications"
|
|
4
|
+
|
|
5
|
+
module ViewComponentDevtools
|
|
6
|
+
class Subscriber
|
|
7
|
+
EVENT_NAMES = ["render.view_component", "!render.view_component"].freeze
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def install!
|
|
11
|
+
@install_mutex ||= Mutex.new
|
|
12
|
+
@install_mutex.synchronize do
|
|
13
|
+
@subscriptions ||= EVENT_NAMES.map do |event_name|
|
|
14
|
+
ActiveSupport::Notifications.subscribe(event_name, new)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def start(_name, event_id, payload)
|
|
21
|
+
State.current&.start_event(event_id, payload, monotonic_time)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def finish(_name, event_id, payload)
|
|
25
|
+
State.current&.finish_event(event_id, payload, monotonic_time)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def monotonic_time
|
|
31
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "view_component_devtools/version"
|
|
4
|
+
require_relative "view_component_devtools/configuration"
|
|
5
|
+
require "rails"
|
|
6
|
+
require "view_component"
|
|
7
|
+
|
|
8
|
+
module ViewComponentDevtools
|
|
9
|
+
class << self
|
|
10
|
+
def configuration
|
|
11
|
+
@configuration ||= Configuration.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def configure
|
|
15
|
+
yield configuration
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def capture_enabled?(environment = rails_environment)
|
|
19
|
+
configuration.enabled_for?(environment)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def rails_environment
|
|
25
|
+
defined?(Rails) && Rails.respond_to?(:env) ? Rails.env : ENV.fetch("RAILS_ENV", "development")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
require_relative "view_component_devtools/normalizer"
|
|
31
|
+
require_relative "view_component_devtools/state"
|
|
32
|
+
require_relative "view_component_devtools/component"
|
|
33
|
+
require_relative "view_component_devtools/subscriber"
|
|
34
|
+
require_relative "view_component_devtools/middleware"
|
|
35
|
+
require_relative "view_component_devtools/helper"
|
|
36
|
+
require_relative "view_component_devtools/engine"
|
metadata
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: view_component_devtools
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ViewComponent DevTools contributors
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-08-11 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '7.1'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: rack
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '2.2'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '4'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '2.2'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '4'
|
|
52
|
+
- !ruby/object:Gem::Dependency
|
|
53
|
+
name: railties
|
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '7.1'
|
|
59
|
+
- - "<"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '9'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '7.1'
|
|
69
|
+
- - "<"
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '9'
|
|
72
|
+
- !ruby/object:Gem::Dependency
|
|
73
|
+
name: view_component
|
|
74
|
+
requirement: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '3.0'
|
|
79
|
+
- - "<"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '5'
|
|
82
|
+
type: :runtime
|
|
83
|
+
prerelease: false
|
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.0'
|
|
89
|
+
- - "<"
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '5'
|
|
92
|
+
description: Captures a safe ViewComponent render tree for a browser DevTools panel.
|
|
93
|
+
executables: []
|
|
94
|
+
extensions: []
|
|
95
|
+
extra_rdoc_files: []
|
|
96
|
+
files:
|
|
97
|
+
- LICENSE.txt
|
|
98
|
+
- README.md
|
|
99
|
+
- lib/view_component_devtools.rb
|
|
100
|
+
- lib/view_component_devtools/component.rb
|
|
101
|
+
- lib/view_component_devtools/configuration.rb
|
|
102
|
+
- lib/view_component_devtools/engine.rb
|
|
103
|
+
- lib/view_component_devtools/helper.rb
|
|
104
|
+
- lib/view_component_devtools/middleware.rb
|
|
105
|
+
- lib/view_component_devtools/normalizer.rb
|
|
106
|
+
- lib/view_component_devtools/state.rb
|
|
107
|
+
- lib/view_component_devtools/subscriber.rb
|
|
108
|
+
- lib/view_component_devtools/version.rb
|
|
109
|
+
homepage: https://github.com/zaviermiller/view_component_devtools
|
|
110
|
+
licenses:
|
|
111
|
+
- MIT
|
|
112
|
+
metadata:
|
|
113
|
+
homepage_uri: https://github.com/zaviermiller/view_component_devtools
|
|
114
|
+
source_code_uri: https://github.com/zaviermiller/view_component_devtools/tree/main/packages/view_component_devtools
|
|
115
|
+
rubygems_mfa_required: 'true'
|
|
116
|
+
rdoc_options: []
|
|
117
|
+
require_paths:
|
|
118
|
+
- lib
|
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '3.2'
|
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
125
|
+
requirements:
|
|
126
|
+
- - ">="
|
|
127
|
+
- !ruby/object:Gem::Version
|
|
128
|
+
version: '0'
|
|
129
|
+
requirements: []
|
|
130
|
+
rubygems_version: 3.6.2
|
|
131
|
+
specification_version: 4
|
|
132
|
+
summary: Development-only component inspection for Rails ViewComponent
|
|
133
|
+
test_files: []
|