lib/phlex/rails/streaming.rb in phlex-rails-1.1.2 vs lib/phlex/rails/streaming.rb in phlex-rails-1.2.0

- old
+ new

@@ -1,26 +1,114 @@ # frozen_string_literal: true # @api private module Phlex::Rails::Streaming + include ActionController::Live + private - def stream(view) + def stream(view, last_modified: Time.now.httpdate, filename: nil) + set_stream_headers(last_modified: last_modified) + + case view + when Phlex::HTML + stream_html(view) + when Phlex::CSV + stream_csv(view, filename: filename) + end + end + + def set_stream_headers(last_modified:) headers.delete("Content-Length") headers["X-Accel-Buffering"] = "no" - headers["Cache-Control"] = "no-cache" + headers["Cache-Control"] = "no-transform" + headers["Last-Modified"] = last_modified + end + + def stream_csv(view, filename:) + headers["Content-Type"] = "text/csv; charset=utf-8" + headers["Content-Disposition"] = "attachment; filename=\"#{filename || view.filename}\"" + + self.response_body = Enumerator.new do |buffer| + view.call(buffer, view_context: view_context) + end + end + + def stream_html(view) headers["Content-Type"] = "text/html; charset=utf-8" - headers["Last-Modified"] = Time.zone.now.ctime.to_s - response.status = 200 + # Ensure we have a session id. + # See https://github.com/rails/rails/issues/51424 + if session.id.nil? + session[:phlex_init_session] = 1 + session.delete(:phlex_init_session) + end self.response_body = Enumerator.new do |buffer| view.call(buffer, view_context: view_context) rescue => e - buffer << %('">) - buffer << view_context.javascript_tag(nonce: true) { %(window.location = "/500.html").html_safe } + raise(e) if Rails.env.test? - raise e + debug_middleware = ActionDispatch::DebugExceptions.new( + proc { |_env| raise(e) }, + response_format: :html + ) + + _debug_status, _debug_headers, debug_body = debug_middleware.call(request.env) + + if Rails.env.development? + js = <<~JAVASCRIPT + document.documentElement.innerHTML = "#{view_context.j(debug_body.join)}"; + + // Re-evaluate all script tags + document.querySelectorAll("script").forEach((script) => { + const newScript = document.createElement("script"); + newScript.text = script.text; + newScript.nonce = "#{view_context.content_security_policy_nonce}"; + script.replaceWith(newScript); + }); + + // Re-evaluate all style tags + document.querySelectorAll("style").forEach((style) => { + const newStyle = document.createElement("style"); + newStyle.textContent = style.textContent; + newStyle.nonce = "#{view_context.content_security_policy_nonce}"; + style.replaceWith(newStyle); + }); + + // Map onclick events to event listeners + document.querySelectorAll("[onclick]").forEach((element) => { + const action = element.getAttribute("onclick"); + const newScript = document.createElement("script") + + element.dataset.onclick = action; + element.removeAttribute("onclick"); + + newScript.text = ` + (function() { + const action = () => { ${action} }; + document.addEventListener("click", (event) => { + const element = event.target; + + if (element.dataset.onclick === "${action}") { + action.bind(element)(); + }; + }); + })(); + `; + + newScript.nonce = "#{view_context.content_security_policy_nonce}"; + document.body.appendChild(newScript); + }); + JAVASCRIPT + else + js = <<~JAVASCRIPT + window.location = "/500.html"; + JAVASCRIPT + end + + buffer << %(-->"'>) + buffer << view_context.javascript_tag(js, nonce: true) end end end