humid 0.1.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c4f5090d85ea4b1c4998f6c8533816a8875039f2ffe5b5785e3e571092445c6
4
- data.tar.gz: 844410054db5d2c89fa3fc1fd3ac4d64f83605cf4a157573ebd6d172b01d0a7f
3
+ metadata.gz: 1b62f6d6c3c7ffac0ba1c609965742a6a09dafebe0ebb9fc05cbae3c9200299e
4
+ data.tar.gz: 56557384d6768ada9b8fa9e9403720b5e75f03a579e39483892afcb631cb04a4
5
5
  SHA512:
6
- metadata.gz: 574e6c64e749180f964a3d9ffe4dd91f18179408901b4b26960b2ef3c9bbe12c601e0879fc97bb60b340ed0270be627aaf0cc92d18827deda5f45dd515d952c4
7
- data.tar.gz: b1511fc86f5655ac063bf6d927866bd00096c7ddca23e822e2cd3dbe03a97d1e4f62288f1242c9b436e67695402c2a61a59e4f096a0dc0f3dbf450ac0fbdc250
6
+ metadata.gz: 4465a746beab25d302782cc71b8a70cd7fe827dc6ce3058e6720056c121bb86357733a9d31a7c266ceee6a53f938b174fec1eef111dbc634aa6699ffccb30ac9
7
+ data.tar.gz: c5d8a1c61b84b49a14a78c7d198af09871bbdf9cebc8c9bbd1d4b32c2cdd95ac7de2bad06214f80ec12a8507158b8bb95a592eff313e51e454dcd768e5a2cdc1
data/README.md CHANGED
@@ -2,9 +2,20 @@
2
2
 
3
3
  ![Build Status](https://github.com/thoughtbot/humid/actions/workflows/build.yml/badge.svg?branch=main)
4
4
 
5
- Humid is a lightweight wrapper around [mini_racer] used to generate Server
6
- Side Rendered (SSR) pages from your js-bundling builds. While it was built
7
- for React, it can work with any JS function that returns a HTML string.
5
+ Humid is a set of helper functions for using `mini_racer` for Server Side
6
+ Rendering (SSR). **There are only 2 pure public functions and a `configure` to set
7
+ default args**. `mini_racer` does the heavy lifting, Humid just provides a few
8
+ conveniences.
9
+
10
+ While it was built with React in mind, it can work with any JS function that
11
+ returns an HTML string.
12
+
13
+ ## Design
14
+
15
+ Humid is designed for the common case where all data is gathered before
16
+ rendering. Your application fetches everything needed, passes it as props, and
17
+ Humid returns the rendered HTML in a single synchronous call. It does not
18
+ support streaming or async data fetching during render.
8
19
 
9
20
  ## Caution
10
21
 
@@ -26,12 +37,14 @@ For source-map support, also add
26
37
  yarn add source-map-support
27
38
  ```
28
39
 
29
-
30
40
  ## Configuration
31
41
 
32
- Add an initializer to configure
42
+ Add an initializer to configure the default options for `Humid.render`. These
43
+ are overridable on `Humid.render`.
33
44
 
34
45
  ```ruby
46
+ # app/initializers/humid.rb
47
+
35
48
  Humid.configure do |config|
36
49
  # Path to your build file located in `app/assets/builds/`. You should use a
37
50
  # separate build apart from your `application.js`.
@@ -55,41 +68,70 @@ Humid.configure do |config|
55
68
  # the respective logger levels on the ruby side.
56
69
  #
57
70
  # Defaults to `nil`
58
- config.logger = Rails.env.development? ? Rails.logger : nil
59
-
60
- # Options passed to mini_racer.
61
- #
62
- # Defaults to empty `{}`.
63
- config.context_options = {
64
- timeout: 1000,
65
- ensure_gc_after_idle: 2000
66
- }
71
+ config.logger = Rails.env.local? ? Rails.logger : nil
67
72
  end
68
73
 
69
- # Capybara defines its own puma config which is set up to run a single puma process
70
- # with a thread pool. This ensures that a context gets created on that process.
71
- if Rails.env.test?
74
+ # if Rails.env.local?
75
+ # # Use single_threaded mode for Spring and other forked envs.
76
+ # MiniRacer::Platform.set_flags! :single_threaded
77
+ # ctx = MiniRacer::Context.new(timeout: 100, ensure_gc_after_idle: 2000)
78
+ # MINI_RACER_CONTEXT = Humid.prepare(ctx)
79
+ # end
80
+ ```
81
+
82
+ ## Usage
83
+
84
+
85
+ ### Create the MiniRacer Context.
86
+
87
+ On local development or test environments, uncomment the below.
88
+
89
+ ```ruby
90
+ if Rails.env.local?
72
91
  # Use single_threaded mode for Spring and other forked envs.
73
92
  MiniRacer::Platform.set_flags! :single_threaded
74
- Humid.create_context
93
+ ctx = MiniRacer::Context.new(timeout: 100, ensure_gc_after_idle: 2000)
94
+ MINI_RACER_CONTEXT = Humid.prepare(ctx)
75
95
  end
76
96
  ```
77
97
 
78
- Then add to your `config/puma.rb`
98
+ On production, keep in mind that `mini_racer` is **thread safe, but not fork
99
+ safe**. When using with web servers that employ forking, create a
100
+ `MINI_RACER_CONTEXT` with options of your choosing **on worker boot. There
101
+ should be no context created on the master process.**
79
102
 
80
- ```
81
- workers ENV.fetch("WEB_CONCURRENCY") { 1 }
103
+ For example with puma:
82
104
 
105
+ ```ruby
106
+ # config/puma.rb
83
107
  on_worker_boot do
84
- Humid.create_context
108
+ ctx = MiniRacer::Context.new(timeout: 100, ensure_gc_after_idle: 2000)
109
+ MINI_RACER_CONTEXT = Humid.prepare(ctx)
85
110
  end
86
111
 
87
112
  on_worker_shutdown do
88
- Humid.dispose
113
+ MINI_RACER_CONTEXT.dispose
89
114
  end
90
115
  ```
91
116
 
117
+ ### Prepare the context with `Humid.prepare`
118
+
119
+ `Humid.prepare` will prepare the context's environment by [removing
120
+ functions](#functions-not-available), delegate `console.log` and friends to
121
+ your logger, load the SSR js bundle, and add the render function.
122
+
123
+ You can also override config options per-context:
124
+
125
+ ```ruby
126
+ MINI_RACER_CONTEXT = Humid.prepare(
127
+ MiniRacer::Context.new(timeout: 1000),
128
+ application_path: Rails.root.join("other_bundle.js"),
129
+ logger: nil
130
+ )
131
+ ```
132
+
92
133
  If you'd like support for source map support, you will need to
134
+
93
135
  1. Add the following to your entry file, e.g, `server_rendering.js`.
94
136
  2. set `config.source_map_path`.
95
137
 
@@ -103,27 +145,11 @@ require("source-map-support").install({
103
145
  }
104
146
  });
105
147
  ```
106
- A [sample] webpack.config is available for reference.
107
-
108
- ## The mini_racer environment.
109
-
110
- ### Functions not available
111
-
112
- The following functions are **not** available in the mini_racer environment
113
-
114
- - `setTimeout`
115
- - `clearTimeout`
116
- - `setInterval`
117
- - `clearInterval`
118
- - `setImmediate`
119
- - `clearImmediate`
120
148
 
121
- ### `console.log`
149
+ See the [sample server_rendering.tsx](./sample/server_rendering.tsx) to see how
150
+ it is integrated.
122
151
 
123
- `console.log` and friends (`info`, `error`, `warn`) are delegated to the
124
- respective methods on the configured logger.
125
-
126
- ## Usage
152
+ ### Add a renderer and call `Humid.render`
127
153
 
128
154
  In your entry file, e.g, `server_rendering.js`, pass your HTML render function
129
155
  to `setHumidRenderer`. There is no need to require the function.
@@ -143,7 +169,7 @@ setHumidRenderer((json) => {
143
169
  And finally call `render` from ERB.
144
170
 
145
171
  ```ruby
146
- <%= Humid.render(initial_state).html_safe %>
172
+ <%= Humid.render(MINI_RACER_CONTEXT, json).html_safe %>
147
173
  ```
148
174
 
149
175
  Instrumentation is included:
@@ -152,24 +178,49 @@ Instrumentation is included:
152
178
  Completed 200 OK in 14ms (Views: 0.2ms | Humid SSR: 11.0ms | ActiveRecord: 2.7ms)
153
179
  ```
154
180
 
155
- ### Puma
181
+ ## The prepared `mini_racer` environment.
156
182
 
157
- `mini_racer` is thread safe, but not fork safe. To use with web servers that
158
- employ forking, use `Humid.create_context` only on forked processes. On
159
- production, There should be no context created on the master process.
183
+ ### Functions not available
184
+
185
+ The following functions are **not** available in the mini_racer environment
186
+
187
+ - `setTimeout`
188
+ - `clearTimeout`
189
+ - `setInterval`
190
+ - `clearInterval`
191
+ - `setImmediate`
192
+ - `clearImmediate`
193
+
194
+ ### `console.log`
195
+
196
+ `console.log` and friends (`info`, `error`, `warn`) are delegated to the
197
+ respective methods on the configured logger.
198
+
199
+ All arguments are passed through — MiniRacer converts JS objects to Ruby
200
+ hashes and arrays automatically. A `log_formatter` proc controls how these
201
+ arguments are formatted into a single string for the logger:
160
202
 
161
203
  ```ruby
162
- # Puma
163
- on_worker_boot do
164
- Humid.create_context
165
- end
204
+ Humid.configure do |config|
205
+ config.logger = Rails.logger
166
206
 
167
- on_worker_shutdown do
168
- Humid.dispose
207
+ config.log_formatter = proc { |level, message, *rest|
208
+ parts = [message]
209
+ parts += rest.map { |a| a.is_a?(String) ? a : JSON.pretty_generate(a) }
210
+ parts.join("\n")
211
+ }
169
212
  end
170
213
  ```
171
214
 
172
- ### Server-side libraries that detect node.js envs.
215
+ The formatter receives `(level, message, *rest)` where:
216
+ - `level` — the log level as a symbol (`:debug`, `:info`, `:warn`, `:error`)
217
+ - `message` — the first argument passed to `console.log/info/warn/error`
218
+ - `rest` — any additional arguments (objects come through as Ruby hashes/arrays)
219
+
220
+ The default formatter returns `message` unchanged.
221
+
222
+ ## Server-side libraries that detect node.js envs.
223
+
173
224
  You may need webpacker to create aliases for server friendly libraries that can
174
225
  not detect the `mini_racer` environment. For example, in `webpack.config.js`.
175
226
 
@@ -187,7 +238,7 @@ not detect the `mini_racer` environment. For example, in `webpack.config.js`.
187
238
  [Vue has a resource][vue_ssr] on how to write universal code. Below
188
239
  are a few highlights that are important to keep in mind.
189
240
 
190
- ### State
241
+ ## State
191
242
 
192
243
  Humid uses a single context across multiple request. To avoid state pollution, we
193
244
  provide a factory function to `setHumidRenderer` that builds a new app instance on
@@ -196,9 +247,9 @@ every call.
196
247
  This provides better isolation, but as it is still a shared context, polluting
197
248
  `global` is still possible. Be careful of modifying `global` in your code.
198
249
 
199
- ### Missing browser APIs
250
+ ## Missing browser APIs
200
251
 
201
- Polyfills and some libraries that depend on browser APIs will fail in the
252
+ Some libraries that depend on browser APIs will fail in the
202
253
  `mini_racer` environment because of missing browser APIs. Account for this by
203
254
  moving the `require` to `useEffect` in your component.
204
255
 
@@ -209,6 +260,71 @@ moving the `require` to `useEffect` in your component.
209
260
  }, [])
210
261
  ```
211
262
 
263
+ ## Polyfills
264
+
265
+ React SSR may import node.js dependencies that you need to polyfill for. See
266
+ a sample esbuild [build script](./sample/bulid_ssr.js) and a [shim.js](./sample/shim.js)
267
+ to get around these issues.
268
+
269
+ ## Testing
270
+
271
+ When running in test environments that also forks, you may need to set up new mini_racer
272
+ contexts for each parallel worker. For example:
273
+
274
+ ```ruby
275
+ ActiveSupport.on_load(:action_dispatch_integration_test) do
276
+ include ActionView::Helpers::TranslationHelper
277
+ include Devise::Test::IntegrationHelpers
278
+
279
+ parallelize_setup do
280
+ MINI_RACER_CONTEXT.dispose if defined?(MINI_RACER_CONTEXT)
281
+ ctx = MiniRacer::Context.new(timeout: 1000, ensure_gc_after_idle: 2000)
282
+ Object.send(:remove_const, :MINI_RACER_CONTEXT) if defined?(MINI_RACER_CONTEXT)
283
+ Object.const_set(:MINI_RACER_CONTEXT, Humid.prepare(ctx))
284
+ end
285
+
286
+ parallelize_teardown do
287
+ MINI_RACER_CONTEXT.dispose if defined?(MINI_RACER_CONTEXT)
288
+ end
289
+ end
290
+ ```
291
+
292
+ ## Telemetry
293
+
294
+ The `MiniRacer::Context` gives you access to V8 heap statistics for monitoring
295
+ memory usage over time.
296
+
297
+ ```ruby
298
+ MINI_RACER_CONTEXT.heap_stats
299
+ # {:total_heap_size=>3100672,
300
+ # :total_heap_size_executable=>4194304,
301
+ # :total_physical_size=>1280640,
302
+ # :total_available_size=>1501560832,
303
+ # :used_heap_size=>1205376,
304
+ # :heap_size_limit=>1501560832,
305
+ # ...}
306
+ ```
307
+
308
+ You can combine humid's instrumentation and OpenTelemetry to track heap growth
309
+ per worker:
310
+
311
+ ```ruby
312
+ meter = OpenTelemetry.meter_provider.meter("humid")
313
+ render_histogram = meter.create_histogram("humid.render.duration", unit: "ms", description: "SSR render duration")
314
+ heap_gauge = meter.create_gauge("humid.heap.used_bytes", unit: "By", description: "V8 heap used bytes")
315
+
316
+ ActiveSupport::Notifications.subscribe("render.humid") do |event|
317
+ stats = MINI_RACER_CONTEXT.heap_stats
318
+ attributes = { "worker.pid" => Process.pid.to_s }
319
+
320
+ render_histogram.record(event.duration, attributes: attributes)
321
+ heap_gauge.record(stats[:used_heap_size], attributes: attributes)
322
+ end
323
+ ```
324
+
325
+ A steadily climbing `used_heap_size` across requests indicates a memory leak in
326
+ your JavaScript bundle.
327
+
212
328
  ## Contributing
213
329
 
214
330
  Please see [CONTRIBUTING.md](/CONTRIBUTING.md).
@@ -1,4 +1,4 @@
1
- class Humid
1
+ module Humid
2
2
  module ControllerRuntime
3
3
  extend ActiveSupport::Concern
4
4
 
@@ -1,4 +1,4 @@
1
- class Humid
1
+ module Humid
2
2
  class LogSubscriber < ActiveSupport::LogSubscriber
3
3
  thread_cattr_accessor :humid_runtime
4
4
 
data/lib/humid/version.rb CHANGED
@@ -1,3 +1,3 @@
1
- class Humid
2
- VERSION = "0.1.0".freeze
1
+ module Humid
2
+ VERSION = "0.5.0".freeze
3
3
  end
data/lib/humid.rb CHANGED
@@ -6,106 +6,93 @@ require "humid/log_subscriber"
6
6
  require "humid/controller_runtime"
7
7
  require "humid/version"
8
8
 
9
- class Humid
10
- @@context = nil
11
-
9
+ module Humid
12
10
  class RenderError < StandardError
13
11
  end
14
12
 
15
13
  class FileNotFound < StandardError
16
14
  end
17
-
18
- class_attribute :config
15
+
16
+ mattr_accessor :config
19
17
 
20
18
  self.config = ActiveSupport::OrderedOptions.new.merge({
21
19
  raise_render_errors: true,
22
- context_options: {},
20
+ log_formatter: proc { |_level, message, *_rest| message },
23
21
  })
24
22
 
25
- class << self
26
- def configure
27
- yield config
28
- end
29
-
30
- def remove_functions
31
- <<~JS
32
- delete this.setTimeout;
33
- delete this.setInterval;
34
- delete this.clearTimeout;
35
- delete this.clearInterval;
36
- delete this.setImmediate;
37
- delete this.clearImmediate;
38
- JS
39
- end
23
+ extend self
24
+
25
+ def configure
26
+ yield self.config
27
+ end
40
28
 
41
- def logger
42
- config.logger
29
+ def prepare(ctx, options = {})
30
+ effective_config = config.merge(options)
31
+ logger = effective_config.logger
32
+ log_formatter = effective_config.log_formatter
33
+
34
+ if logger
35
+ fmt = log_formatter || proc { |_level, message, *_rest| message }
36
+ ctx.attach("console.log", proc { |*args| logger.debug(fmt.call(:debug, *args)) })
37
+ ctx.attach("console.info", proc { |*args| logger.info(fmt.call(:info, *args)) })
38
+ ctx.attach("console.error", proc { |*args| logger.error(fmt.call(:error, *args)) })
39
+ ctx.attach("console.warn", proc { |*args| logger.warn(fmt.call(:warn, *args)) })
43
40
  end
44
41
 
45
- def renderer
46
- <<~JS
47
- var __renderer;
48
- function setHumidRenderer(fn) {
49
- __renderer = fn;
50
- }
51
- JS
52
- end
42
+ js = ""
43
+ js << remove_functions
44
+ js << renderer
45
+ ctx.eval(js)
53
46
 
54
- def context
55
- @@context
56
- end
47
+ source_path = effective_config.application_path
48
+ map_path = effective_config.source_map_path
57
49
 
58
- def dispose
59
- if @@context
60
- @@context.dispose
61
- @@context = nil
62
- end
50
+ if map_path
51
+ ctx.attach("readSourceMap", proc { File.read(map_path) })
63
52
  end
64
53
 
65
- def create_context
66
- ctx = MiniRacer::Context.new(**config.context_options)
54
+ filename = File.basename(source_path.to_s)
55
+ ctx.eval(File.read(source_path), filename: filename)
67
56
 
68
- if logger
69
- ctx.attach("console.log", proc { |err| logger.debug(err.to_s) })
70
- ctx.attach("console.info", proc { |err| logger.info(err.to_s) })
71
- ctx.attach("console.error", proc { |err| logger.error(err.to_s) })
72
- ctx.attach("console.warn", proc { |err| logger.warn(err.to_s) })
73
- end
74
-
75
- js = ""
76
- js << remove_functions
77
- js << renderer
78
- ctx.eval(js)
79
-
80
- source_path = config.application_path
81
- map_path = config.source_map_path
57
+ ctx
58
+ end
82
59
 
83
- if map_path
84
- ctx.attach("readSourceMap", proc { File.read(map_path) })
60
+ def render(ctx, *args)
61
+ ActiveSupport::Notifications.instrument("render.humid") do
62
+ ctx.call("__renderer", *args)
63
+ rescue MiniRacer::RuntimeError => e
64
+ message = ([e.message] + e.backtrace.filter { |x| x.starts_with? "JavaScript" }).join("\n")
65
+ render_error = Humid::RenderError.new(message)
66
+
67
+ if config.raise_render_errors
68
+ raise render_error
69
+ else
70
+ config.logger.error(render_error.inspect)
71
+ ""
85
72
  end
86
-
87
- filename = File.basename(source_path.to_s)
88
- @@current_filename = filename
89
- ctx.eval(File.read(source_path), filename: filename)
90
-
91
- @@context = ctx
92
73
  end
74
+ end
93
75
 
94
- def render(*args)
95
- ActiveSupport::Notifications.instrument("render.humid") do
96
- context.call("__renderer", *args)
97
- rescue MiniRacer::RuntimeError => e
98
- message = ([e.message] + e.backtrace.filter { |x| x.starts_with? "JavaScript" }).join("\n")
99
- render_error = Humid::RenderError.new(message)
100
-
101
- if config.raise_render_errors
102
- raise render_error
103
- else
104
- config.logger.error(render_error.inspect)
105
- ""
106
- end
107
- end
108
- end
76
+ private
77
+
78
+ def remove_functions
79
+ <<~JS
80
+ delete this.setTimeout;
81
+ delete this.setInterval;
82
+ delete this.clearTimeout;
83
+ delete this.clearInterval;
84
+ delete this.setImmediate;
85
+ delete this.clearImmediate;
86
+ JS
87
+ end
88
+
89
+ def renderer
90
+ <<~JS
91
+ var __renderer;
92
+ function setHumidRenderer(fn) {
93
+ __renderer = fn;
94
+ }
95
+ JS
109
96
  end
110
97
  end
111
98
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: humid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johny Ho