humid 0.2.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: 73079cacc72320a7c7d439d5cc443a42221a9e177dda5f25712c9898212f533a
4
- data.tar.gz: f06abbe52e2e566a415d7982cac6c6313d158e1f47f2ec142179226f55d7cd63
3
+ metadata.gz: 1b62f6d6c3c7ffac0ba1c609965742a6a09dafebe0ebb9fc05cbae3c9200299e
4
+ data.tar.gz: 56557384d6768ada9b8fa9e9403720b5e75f03a579e39483892afcb631cb04a4
5
5
  SHA512:
6
- metadata.gz: ce807ac8725f2054eaa15f00ecd2ec7532b5d720145f9078d211966fd3b5c81f06e282a9511778fa1ecfdcc4af028dd109ae950c14d6a9c4cb1f2c78d64bba14
7
- data.tar.gz: 903b52f1aebf08c3f90d606f5e1eca7656aa04bc988d1342f863cf9a915acdc52b94d440d2f6f50c68f3d8e34f58d400c669dd7afc6f75688aad9104203e592c
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,9 +145,40 @@ require("source-map-support").install({
103
145
  }
104
146
  });
105
147
  ```
106
- A [sample] webpack.config is available for reference.
107
148
 
108
- ## The mini_racer environment.
149
+ See the [sample server_rendering.tsx](./sample/server_rendering.tsx) to see how
150
+ it is integrated.
151
+
152
+ ### Add a renderer and call `Humid.render`
153
+
154
+ In your entry file, e.g, `server_rendering.js`, pass your HTML render function
155
+ to `setHumidRenderer`. There is no need to require the function.
156
+
157
+ ```javascript
158
+ // Set a factory function that will create a new instance of our app
159
+ // for each request.
160
+ setHumidRenderer((json) => {
161
+ const initialState = JSON.parse(json)
162
+
163
+ return ReactDOMServer.renderToString(
164
+ <Application initialPage={initialState}/>
165
+ )
166
+ })
167
+ ```
168
+
169
+ And finally call `render` from ERB.
170
+
171
+ ```ruby
172
+ <%= Humid.render(MINI_RACER_CONTEXT, json).html_safe %>
173
+ ```
174
+
175
+ Instrumentation is included:
176
+
177
+ ```
178
+ Completed 200 OK in 14ms (Views: 0.2ms | Humid SSR: 11.0ms | ActiveRecord: 2.7ms)
179
+ ```
180
+
181
+ ## The prepared `mini_racer` environment.
109
182
 
110
183
  ### Functions not available
111
184
 
@@ -144,55 +217,10 @@ The formatter receives `(level, message, *rest)` where:
144
217
  - `message` — the first argument passed to `console.log/info/warn/error`
145
218
  - `rest` — any additional arguments (objects come through as Ruby hashes/arrays)
146
219
 
147
- The default formatter simply returns `message` unchanged.
220
+ The default formatter returns `message` unchanged.
148
221
 
149
- ## Usage
222
+ ## Server-side libraries that detect node.js envs.
150
223
 
151
- In your entry file, e.g, `server_rendering.js`, pass your HTML render function
152
- to `setHumidRenderer`. There is no need to require the function.
153
-
154
- ```javascript
155
- // Set a factory function that will create a new instance of our app
156
- // for each request.
157
- setHumidRenderer((json) => {
158
- const initialState = JSON.parse(json)
159
-
160
- return ReactDOMServer.renderToString(
161
- <Application initialPage={initialState}/>
162
- )
163
- })
164
- ```
165
-
166
- And finally call `render` from ERB.
167
-
168
- ```ruby
169
- <%= Humid.render(initial_state).html_safe %>
170
- ```
171
-
172
- Instrumentation is included:
173
-
174
- ```
175
- Completed 200 OK in 14ms (Views: 0.2ms | Humid SSR: 11.0ms | ActiveRecord: 2.7ms)
176
- ```
177
-
178
- ### Puma
179
-
180
- `mini_racer` is thread safe, but not fork safe. To use with web servers that
181
- employ forking, use `Humid.create_context` only on forked processes. On
182
- production, There should be no context created on the master process.
183
-
184
- ```ruby
185
- # Puma
186
- on_worker_boot do
187
- Humid.create_context
188
- end
189
-
190
- on_worker_shutdown do
191
- Humid.dispose
192
- end
193
- ```
194
-
195
- ### Server-side libraries that detect node.js envs.
196
224
  You may need webpacker to create aliases for server friendly libraries that can
197
225
  not detect the `mini_racer` environment. For example, in `webpack.config.js`.
198
226
 
@@ -210,7 +238,7 @@ not detect the `mini_racer` environment. For example, in `webpack.config.js`.
210
238
  [Vue has a resource][vue_ssr] on how to write universal code. Below
211
239
  are a few highlights that are important to keep in mind.
212
240
 
213
- ### State
241
+ ## State
214
242
 
215
243
  Humid uses a single context across multiple request. To avoid state pollution, we
216
244
  provide a factory function to `setHumidRenderer` that builds a new app instance on
@@ -219,9 +247,9 @@ every call.
219
247
  This provides better isolation, but as it is still a shared context, polluting
220
248
  `global` is still possible. Be careful of modifying `global` in your code.
221
249
 
222
- ### Missing browser APIs
250
+ ## Missing browser APIs
223
251
 
224
- 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
225
253
  `mini_racer` environment because of missing browser APIs. Account for this by
226
254
  moving the `require` to `useEffect` in your component.
227
255
 
@@ -232,6 +260,71 @@ moving the `require` to `useEffect` in your component.
232
260
  }, [])
233
261
  ```
234
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
+
235
328
  ## Contributing
236
329
 
237
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.2.0".freeze
1
+ module Humid
2
+ VERSION = "0.5.0".freeze
3
3
  end
data/lib/humid.rb CHANGED
@@ -6,108 +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: {},
23
20
  log_formatter: proc { |_level, message, *_rest| message },
24
21
  })
25
22
 
26
- class << self
27
- def configure
28
- yield config
29
- end
30
-
31
- def remove_functions
32
- <<~JS
33
- delete this.setTimeout;
34
- delete this.setInterval;
35
- delete this.clearTimeout;
36
- delete this.clearInterval;
37
- delete this.setImmediate;
38
- delete this.clearImmediate;
39
- JS
40
- end
23
+ extend self
24
+
25
+ def configure
26
+ yield self.config
27
+ end
41
28
 
42
- def logger
43
- 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)) })
44
40
  end
45
41
 
46
- def renderer
47
- <<~JS
48
- var __renderer;
49
- function setHumidRenderer(fn) {
50
- __renderer = fn;
51
- }
52
- JS
53
- end
42
+ js = ""
43
+ js << remove_functions
44
+ js << renderer
45
+ ctx.eval(js)
54
46
 
55
- def context
56
- @@context
57
- end
47
+ source_path = effective_config.application_path
48
+ map_path = effective_config.source_map_path
58
49
 
59
- def dispose
60
- if @@context
61
- @@context.dispose
62
- @@context = nil
63
- end
50
+ if map_path
51
+ ctx.attach("readSourceMap", proc { File.read(map_path) })
64
52
  end
65
53
 
66
- def create_context
67
- ctx = MiniRacer::Context.new(**config.context_options)
54
+ filename = File.basename(source_path.to_s)
55
+ ctx.eval(File.read(source_path), filename: filename)
68
56
 
69
- if logger
70
- fmt = config.log_formatter || proc { |_level, message, *_rest| message }
71
- ctx.attach("console.log", proc { |*args| logger.debug(fmt.call(:debug, *args)) })
72
- ctx.attach("console.info", proc { |*args| logger.info(fmt.call(:info, *args)) })
73
- ctx.attach("console.error", proc { |*args| logger.error(fmt.call(:error, *args)) })
74
- ctx.attach("console.warn", proc { |*args| logger.warn(fmt.call(:warn, *args)) })
75
- end
76
-
77
- js = ""
78
- js << remove_functions
79
- js << renderer
80
- ctx.eval(js)
81
-
82
- source_path = config.application_path
83
- map_path = config.source_map_path
57
+ ctx
58
+ end
84
59
 
85
- if map_path
86
- 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
+ ""
87
72
  end
88
-
89
- filename = File.basename(source_path.to_s)
90
- @@current_filename = filename
91
- ctx.eval(File.read(source_path), filename: filename)
92
-
93
- @@context = ctx
94
73
  end
74
+ end
95
75
 
96
- def render(*args)
97
- ActiveSupport::Notifications.instrument("render.humid") do
98
- context.call("__renderer", *args)
99
- rescue MiniRacer::RuntimeError => e
100
- message = ([e.message] + e.backtrace.filter { |x| x.starts_with? "JavaScript" }).join("\n")
101
- render_error = Humid::RenderError.new(message)
102
-
103
- if config.raise_render_errors
104
- raise render_error
105
- else
106
- config.logger.error(render_error.inspect)
107
- ""
108
- end
109
- end
110
- 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
111
96
  end
112
97
  end
113
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.2.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johny Ho