Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions javascript/packages/dev-tools/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { initHerbDevTools, HerbOverlay, type HerbDevToolsOptions } from "@herb-tools/dev-tools"
import { enhanceLabelsWithRenderTimes } from "./reactionview-overlay"

export interface ReActionViewDevToolsOptions extends HerbDevToolsOptions {
projectPath?: string
Expand All @@ -25,6 +26,8 @@ export class ReActionViewDevTools {
...this.options
})

enhanceLabelsWithRenderTimes()

return this.herbOverlay
}

Expand Down
94 changes: 94 additions & 0 deletions javascript/packages/dev-tools/src/reactionview-overlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { HerbOverlay } from "@herb-tools/dev-tools"

interface TimingData {
duration: number
template: string
path: string
identifier: string
}

interface TimingsMap {
[renderId: string]: TimingData
}

export function enhanceLabelsWithRenderTimes(): void {
const timings = loadTimingData()

const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as HTMLElement

if (element.classList?.contains('herb-overlay-label')) {
enhanceLabel(element, timings)
} else {
element.querySelectorAll?.('.herb-overlay-label').forEach((label) => {
enhanceLabel(label as HTMLElement, timings)
})
}
}
})
})
})

observer.observe(document.body, {
childList: true,
subtree: true
})

document.querySelectorAll('.herb-overlay-label').forEach((label) => {
enhanceLabel(label as HTMLElement, timings)
})
}

function loadTimingData(): TimingsMap {
const script = document.getElementById('reactionview-timings')
if (!script) return {}

try {
return JSON.parse(script.textContent || '{}')
} catch (e) {
console.error('Failed to parse ReactionView timing data:', e)
return {}
}
}

function enhanceLabel(label: HTMLElement, timings: TimingsMap): void {
let parent = label.parentElement
while (parent) {
const renderId = parent.getAttribute('data-reactionview-id')

if (renderId && timings[renderId]) {
const timing = timings[renderId]
const shortName = parent.getAttribute('data-herb-debug-file-name') || timing.template

if (!(label as any)._reactionviewEnhanced) {
label.textContent = `${shortName} (${timing.duration} ms)`

label.addEventListener('mouseenter', () => {
label.textContent = `${timing.path} (${timing.duration} ms)`

document.querySelectorAll('.herb-overlay-label').forEach(otherLabel => {
(otherLabel as HTMLElement).style.zIndex = '1000'
})

label.style.zIndex = '1002'
})

label.addEventListener('mouseleave', () => {
label.textContent = `${shortName} (${timing.duration} ms)`
label.style.zIndex = '1000'
})

;(label as any)._reactionviewEnhanced = true
}

break
}

parent = parent.parentElement
}
}

export { HerbOverlay }
3 changes: 3 additions & 0 deletions lib/reactionview.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
require_relative "reactionview/template/handlers/herb"
require_relative "reactionview/template/handlers/herb/herb"

require_relative "reactionview/timing_visitor"
require_relative "reactionview/middleware/timing_output"

require_relative "reactionview/railtie" if defined?(Rails::Railtie)

module ReActionView
Expand Down
8 changes: 8 additions & 0 deletions lib/reactionview/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ class Config
attr_accessor :intercept_erb
attr_accessor :debug_mode
attr_accessor :transform_visitors
attr_accessor :show_render_times

def initialize
@intercept_erb = false
@debug_mode = nil
@transform_visitors = []
@show_render_times = nil
end

def development?
Expand All @@ -29,6 +31,12 @@ def debug_mode_enabled?

development?
end

def show_render_times?
return @show_render_times unless @show_render_times.nil?

debug_mode_enabled?
end
end

def self.config
Expand Down
65 changes: 65 additions & 0 deletions lib/reactionview/middleware/timing_output.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

module ReActionView
module Middleware
class TimingOutput
def initialize(app)
@app = app
end

def call(env)
Thread.current[:reactionview_timings] = {}

status, headers, response = @app.call(env)

response = inject_timing_data(response) if ::ReActionView.config.show_render_times? && html_response?(headers)

Thread.current[:reactionview_timings] = nil

[status, headers, response]
end

private

def html_response?(headers)
content_type = headers["Content-Type"]
content_type&.include?("text/html")
end

def inject_timing_data(response)
body = response_body(response)
timings = Thread.current[:reactionview_timings] || {}

return [body] if timings.empty?

timing_script = build_timing_script(timings)

if body.include?("</body>")
body = body.sub("</body>", "#{timing_script}</body>")
elsif body.include?("</html>")
body = body.sub("</html>", "#{timing_script}</html>")
else
body << timing_script
end

[body]
end

def response_body(response)
body = +""
response.each { |part| body << part }
body
end

def build_timing_script(timings)
timings_json = timings.to_json.gsub("<", "\\u003c").gsub(">", "\\u003e")

<<~HTML
<script type="application/json" id="reactionview-timings">
#{timings_json}
</script>
HTML
end
end
end
end
4 changes: 4 additions & 0 deletions lib/reactionview/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ class Railtie < Rails::Railtie
end
end

initializer "reactionview.middleware" do |app|
app.middleware.use ReActionView::Middleware::TimingOutput if ReActionView.config.show_render_times?
end

config.after_initialize do
ActiveSupport.on_load(:action_view) do
ActionView::Template.register_template_handler :erb, ReActionView::Template::Handlers::ERB if ReActionView.config.intercept_erb
Expand Down
8 changes: 8 additions & 0 deletions lib/reactionview/template/handlers/herb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ def call(template, source)
)
end

if ::ReActionView.config.show_render_times? && local_template?(template)
visitors << ::ReActionView::TimingVisitor.new(
template_name: File.basename(template.identifier),
template_path: template.identifier.sub("#{Rails.root}/", ""),
template_identifier: template.identifier
)
end

config = {
filename: template.identifier,
project_path: Rails.root.to_s,
Expand Down
Loading