Skip to content

Commit

Permalink
Introduce use_context to inject mini_racer
Browse files Browse the repository at this point in the history
This allow devs to customize the mini_racer context, e.g, make use of
snapshots, additional options, `attach`es, etc.
  • Loading branch information
jho406 committed Feb 20, 2025
1 parent 638a192 commit 5b59361
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 127 deletions.
7 changes: 7 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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
44 changes: 27 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -27,7 +29,7 @@ yarn add source-map-support
```


## Configuration
## Setup

Add an initializer to configure

Expand Down Expand Up @@ -56,32 +58,34 @@ 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
# with a thread pool. This ensures that a context gets created on that process.
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
Expand Down Expand Up @@ -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
Expand Down
103 changes: 8 additions & 95 deletions lib/humid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion lib/humid/controller_runtime.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module Humid
class Humid
module ControllerRuntime
extend ActiveSupport::Concern

Expand Down
2 changes: 1 addition & 1 deletion lib/humid/log_subscriber.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module Humid
class Humid
class LogSubscriber < ActiveSupport::LogSubscriber
thread_cattr_accessor :humid_runtime

Expand Down
84 changes: 84 additions & 0 deletions lib/humid/renderer.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion lib/humid/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Humid
class Humid
VERSION = "0.0.6".freeze
end
2 changes: 1 addition & 1 deletion spec/log_subscriber_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 5b59361

Please sign in to comment.