diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..c6302d6 --- /dev/null +++ b/NEWS.md @@ -0,0 +1,7 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## Upcoming + +Prefer injection of the custom mini_racer context diff --git a/README.md b/README.md index d11548d..ae50029 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,11 @@ ![Build Status](https://github.com/thoughtbot/humid/actions/workflows/build.yml/badge.svg?branch=main) -Humid is a lightweight wrapper around [mini_racer] used to generate Server -Side Rendered (SSR) pages from your js-bundling builds. While it was built -for React, it can work with any JS function that returns a HTML string. +Humid is a lightweight wrapper for [mini_racer] to generate Server Side +Rendered (SSR) pages from your js-bundling builds. Inject a +`MiniRacer::Context` instance into Humid and get rendering, instrumentation, +logging defaults, and error handling. While it was built with React in mind, it +can work with any JS function that returns a HTML string. ## Caution @@ -27,7 +29,7 @@ yarn add source-map-support ``` -## Configuration +## Setup Add an initializer to configure @@ -56,14 +58,6 @@ Humid.configure do |config| # # Defaults to `Logger.new(STDOUT)` config.logger = Rails.logger - - # Options passed to mini_racer. - # - # Defaults to empty `{}`. - config.context_options = { - timeout: 1000, - ensure_gc_after_idle: 2000 - } end # Capybara defines its own puma config which is set up to run a single puma process @@ -71,17 +65,27 @@ end if Rails.env.test? # Use single_threaded mode for Spring and other forked envs. MiniRacer::Platform.set_flags! :single_threaded - Humid.create_context + context = MiniRacer::Context.create( + timeout: 1000, + ensure_gc_after_idle: 2000 + ) + + Humid.use_context(context) end ``` -Then add to your `config/puma.rb` +Then add to your `config/puma.rb`. ``` workers ENV.fetch("WEB_CONCURRENCY") { 1 } on_worker_boot do - Humid.create_context + context = MiniRacer::Context.create( + timeout: 1000, + ensure_gc_after_idle: 2000 + ) + + Humid.use_context(context) end on_worker_shutdown do @@ -156,12 +160,18 @@ Completed 200 OK in 14ms (Views: 0.2ms | Humid SSR: 11.0ms | ActiveRecord: 2.7ms `mini_racer` is thread safe, but not fork safe. To use with web servers that employ forking, use `Humid.create_context` only on forked processes. On -production, There should be no context created on the master process. +production, there **should NOT BE** any mini_racer context created on the +master process. ```ruby # Puma on_worker_boot do - Humid.create_context + context = MiniRacer::Context.create( + timeout: 1000, + ensure_gc_after_idle: 2000 + ) + + Humid.use_context(context) end on_worker_shutdown do diff --git a/lib/humid.rb b/lib/humid.rb index 5a61476..63865c6 100644 --- a/lib/humid.rb +++ b/lib/humid.rb @@ -5,109 +5,22 @@ require "humid/log_subscriber" require "humid/controller_runtime" require "humid/version" +require "humid/renderer" -module Humid - extend self +class Humid include ActiveSupport::Configurable - class RenderError < StandardError - end - - class FileNotFound < StandardError - end + class RenderError < StandardError; end - @@context = nil + class FileNotFound < StandardError; end config_accessor :application_path config_accessor :source_map_path + config_accessor :raise_render_errors, default: true + config_accessor :logger, default: Logger.new($stdout) - config_accessor :raise_render_errors do - true - end - - config_accessor :logger do - Logger.new($stdout) - end - - config_accessor :context_options do - {} - end - - def remove_functions - <<~JS - delete this.setTimeout; - delete this.setInterval; - delete this.clearTimeout; - delete this.clearInterval; - delete this.setImmediate; - delete this.clearImmediate; - JS - end - - def logger - config.logger - end - - def renderer - <<~JS - var __renderer; - function setHumidRenderer(fn) { - __renderer = fn; - } - JS - end - - def context - @@context - end - - def dispose - if @@context - @@context.dispose - @@context = nil - end - end - - def create_context - ctx = MiniRacer::Context.new(**config.context_options) - ctx.attach("console.log", proc { |err| logger.debug(err.to_s) }) - ctx.attach("console.info", proc { |err| logger.info(err.to_s) }) - ctx.attach("console.error", proc { |err| logger.error(err.to_s) }) - ctx.attach("console.warn", proc { |err| logger.warn(err.to_s) }) - - js = "" - js << remove_functions - js << renderer - ctx.eval(js) - - source_path = config.application_path - map_path = config.source_map_path - - if map_path - ctx.attach("readSourceMap", proc { File.read(map_path) }) - end - - filename = File.basename(source_path.to_s) - @@current_filename = filename - ctx.eval(File.read(source_path), filename: filename) - - @@context = ctx - end - - def render(*args) - ActiveSupport::Notifications.instrument("render.humid") do - context.call("__renderer", *args) - rescue MiniRacer::RuntimeError => e - message = ([e.message] + e.backtrace.filter { |x| x.starts_with? "JavaScript" }).join("\n") - render_error = Humid::RenderError.new(message) - - if config.raise_render_errors - raise render_error - else - config.logger.error(render_error.inspect) - "" - end - end + class << self + delegate :use_context, :render, :context, :dispose, to: Renderer end end diff --git a/lib/humid/controller_runtime.rb b/lib/humid/controller_runtime.rb index faad720..fab118b 100644 --- a/lib/humid/controller_runtime.rb +++ b/lib/humid/controller_runtime.rb @@ -1,4 +1,4 @@ -module Humid +class Humid module ControllerRuntime extend ActiveSupport::Concern diff --git a/lib/humid/log_subscriber.rb b/lib/humid/log_subscriber.rb index 413e466..b121f3d 100644 --- a/lib/humid/log_subscriber.rb +++ b/lib/humid/log_subscriber.rb @@ -1,4 +1,4 @@ -module Humid +class Humid class LogSubscriber < ActiveSupport::LogSubscriber thread_cattr_accessor :humid_runtime diff --git a/lib/humid/renderer.rb b/lib/humid/renderer.rb new file mode 100644 index 0000000..cf7f716 --- /dev/null +++ b/lib/humid/renderer.rb @@ -0,0 +1,84 @@ +class Humid + class Renderer + cattr_accessor :context, instance_writer: false, instance_reader: false + + class << self + def use_context(ctx) + self.context = ctx + + ctx.attach("console.log", proc { |err| logger.debug(err.to_s) }) + ctx.attach("console.info", proc { |err| logger.info(err.to_s) }) + ctx.attach("console.error", proc { |err| logger.error(err.to_s) }) + ctx.attach("console.warn", proc { |err| logger.warn(err.to_s) }) + + js = "" + js << remove_functions + js << renderer + ctx.eval(js) + + source_path = config.application_path + map_path = config.source_map_path + + if map_path + ctx.attach("readSourceMap", proc { File.read(map_path) }) + end + + filename = File.basename(source_path.to_s) + ctx.eval(File.read(source_path), filename: filename) + end + + def dispose + if context + context.dispose + self.context = nil + end + end + + def render(*args) + ActiveSupport::Notifications.instrument("render.humid") do + context.call("__renderer", *args) + rescue MiniRacer::RuntimeError => e + message = ([e.message] + e.backtrace.filter { |x| x.starts_with? "JavaScript" }).join("\n") + render_error = Humid::RenderError.new(message) + + if config.raise_render_errors + raise render_error + else + logger.error(render_error.inspect) + "" + end + end + end + + def logger + config.logger + end + + private + + def remove_functions + <<~JS + delete this.setTimeout; + delete this.setInterval; + delete this.clearTimeout; + delete this.clearInterval; + delete this.setImmediate; + delete this.clearImmediate; + JS + end + + def config + Humid.config + end + + def renderer + <<~JS + var __renderer; + function setHumidRenderer(fn) { + __renderer = fn; + } + JS + end + end + end +end diff --git a/lib/humid/version.rb b/lib/humid/version.rb index e3da19f..8a109c9 100644 --- a/lib/humid/version.rb +++ b/lib/humid/version.rb @@ -1,3 +1,3 @@ -module Humid +class Humid VERSION = "0.0.6".freeze end diff --git a/spec/log_subscriber_spec.rb b/spec/log_subscriber_spec.rb index 9147b51..c098f1c 100644 --- a/spec/log_subscriber_spec.rb +++ b/spec/log_subscriber_spec.rb @@ -56,7 +56,7 @@ def key allow(Humid.config).to receive("application_path") { js_path "simple.js" } - Humid.create_context + Humid.use_context(MiniRacer::Context.new) expect(Humid::LogSubscriber.runtime).to eql(0) Humid.render expect(Humid::LogSubscriber.runtime).to be > 0 diff --git a/spec/render_spec.rb b/spec/render_spec.rb index ec23ea5..805000a 100644 --- a/spec/render_spec.rb +++ b/spec/render_spec.rb @@ -1,14 +1,18 @@ require_relative "./support/helper" RSpec.describe "Humid" do - describe "create_context" do + def miniracer_context + MiniRacer::Context.new + end + + describe "use_context" do after(:each) do Humid.dispose end - it "creates a context with initial js" do + it "sets a context with initial js" do allow(Humid.config).to receive("application_path") { js_path "simple.js" } - Humid.create_context + Humid.use_context(miniracer_context) expect(Humid.context).to be_kind_of(MiniRacer::Context) end @@ -18,7 +22,7 @@ allow(Humid.config).to receive("application_path") { js_path "does_not_exist.js" } expect { - Humid.create_context + Humid.use_context(miniracer_context) }.to raise_error(Errno::ENOENT) end end @@ -26,7 +30,7 @@ it "does not have timeouts, immediates, and intervals" do allow(Humid.config).to receive("application_path") { js_path "simple.js" } - Humid.create_context + Humid.use_context(miniracer_context) expect { Humid.context.eval("setTimeout()") @@ -47,7 +51,7 @@ it "proxies to Rails logger" do allow(Humid.config).to receive("application_path") { js_path "simple.js" } - Humid.create_context + Humid.use_context(miniracer_context) expect(Humid.logger).to receive(:info).with("hello") Humid.context.eval("console.info('hello')") @@ -58,7 +62,7 @@ it "returns the created context" do allow(Humid.config).to receive("application_path") { js_path "simple.js" } - Humid.create_context + Humid.use_context(miniracer_context) expect(Humid.context).to be_kind_of(MiniRacer::Context) end @@ -67,14 +71,14 @@ describe "render" do it "returns a js output" do allow(Humid.config).to receive("application_path") { js_path "simple.js" } - Humid.create_context + Humid.use_context(miniracer_context) expect(Humid.render).to eql("hello") end it "applys args to the func" do allow(Humid.config).to receive("application_path") { js_path "args.js" } - Humid.create_context + Humid.use_context(miniracer_context) args = ["a", 1, 2, [], {}] @@ -86,7 +90,7 @@ allow(Humid.config).to receive("application_path") { build_path "reporting.js" } allow(Humid.config).to receive("source_map_path") { build_path "reporting.js.map" } - Humid.create_context + Humid.use_context(miniracer_context) expect { Humid.render @@ -107,7 +111,7 @@ allow(Humid.config).to receive("source_map_path") { build_path "reporting.js.map" } allow(Humid.config).to receive("raise_render_errors") { false } - Humid.create_context + Humid.use_context(miniracer_context) expect(Humid.logger).to receive(:error) output = Humid.render