humid 0.2.0 → 0.6.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: 961eafadcfe1132cdd132c096a28aba48155c740b224c3cbc32b4fd9731c10a6
4
+ data.tar.gz: 8a3635e896a4915c508a096c3b9f60e7d51265b39c6c448150fb89a627a3ab06
5
5
  SHA512:
6
- metadata.gz: ce807ac8725f2054eaa15f00ecd2ec7532b5d720145f9078d211966fd3b5c81f06e282a9511778fa1ecfdcc4af028dd109ae950c14d6a9c4cb1f2c78d64bba14
7
- data.tar.gz: 903b52f1aebf08c3f90d606f5e1eca7656aa04bc988d1342f863cf9a915acdc52b94d440d2f6f50c68f3d8e34f58d400c669dd7afc6f75688aad9104203e592c
6
+ metadata.gz: 7f9f13d45e1b4023cccdc88300c151024dbb08a5bcc73c24b1a81e089f1ad1d2a4ec1faea12feb81e13949e2c423a8e8a392d73bf45b547ecdcd20a6462b74b6
7
+ data.tar.gz: fba2e9ea74fdeab8a90f5503a0b34cf977ddb0006dcaf234ceff8b1d06548079461e0e5d17466cace1c68937b7fa290a5823ef0fe01352886b545b0fe87c9cc2
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,43 +68,40 @@ 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?
72
75
  # Use single_threaded mode for Spring and other forked envs.
73
76
  MiniRacer::Platform.set_flags! :single_threaded
74
- Humid.create_context
77
+ ctx = MiniRacer::Context.new(timeout: 100, ensure_gc_after_idle: 2000)
78
+ MINI_RACER_CONTEXT = Humid.prepare(ctx)
75
79
  end
76
80
  ```
77
81
 
78
- Then add to your `config/puma.rb`
82
+ ## Usage
79
83
 
80
- ```
81
- workers ENV.fetch("WEB_CONCURRENCY") { 1 }
84
+ ### Set a renderer
82
85
 
83
- on_worker_boot do
84
- Humid.create_context
85
- end
86
+ In your entry file, e.g, `server_rendering.js` (specified in
87
+ `config.application_path`), pass your HTML render function to
88
+ `setHumidRenderer`. There is no need to require the function, its included in
89
+ the environment.
86
90
 
87
- on_worker_shutdown do
88
- Humid.dispose
89
- end
91
+ ```javascript
92
+ // Set a factory function that will create a new instance of our app
93
+ // for each request.
94
+ setHumidRenderer((json) => {
95
+ const initialState = JSON.parse(json)
96
+
97
+ return ReactDOMServer.renderToString(
98
+ <Application initialPage={initialState}/>
99
+ )
100
+ })
90
101
  ```
91
102
 
92
- If you'd like support for source map support, you will need to
93
- 1. Add the following to your entry file, e.g, `server_rendering.js`.
94
- 2. set `config.source_map_path`.
103
+ If you'd like support for source map support, you will need to add the following
104
+ to the same file and set `config.source_map_path` like the configuration above.
95
105
 
96
106
  ```javascript
97
107
  require("source-map-support").install({
@@ -103,9 +113,60 @@ require("source-map-support").install({
103
113
  }
104
114
  });
105
115
  ```
106
- A [sample] webpack.config is available for reference.
107
116
 
108
- ## The mini_racer environment.
117
+ ### Your webserver
118
+
119
+ On production, keep in mind that `mini_racer` is **thread safe, but not fork
120
+ safe**. When using with web servers that employ forking, create a
121
+ `MINI_RACER_CONTEXT` with options of your choosing on worker boot. **There
122
+ should be no context created on the master process.**
123
+
124
+ For example with puma:
125
+
126
+ ```ruby
127
+ # config/puma.rb
128
+ on_worker_boot do
129
+ ctx = MiniRacer::Context.new(timeout: 100, ensure_gc_after_idle: 2000)
130
+
131
+ MINI_RACER_CONTEXT = Humid.prepare(ctx)
132
+ end
133
+
134
+ on_worker_shutdown do
135
+ MINI_RACER_CONTEXT.dispose
136
+ end
137
+ ```
138
+
139
+ `Humid.prepare` will prepare the context's
140
+ [environment](#the-mini_racer-environment).
141
+
142
+ You can also override config options per-context:
143
+
144
+ ```ruby
145
+ MINI_RACER_CONTEXT = Humid.prepare(
146
+ MiniRacer::Context.new(timeout: 1000),
147
+ application_path: Rails.root.join("other_bundle.js"),
148
+ logger: nil
149
+ )
150
+ ```
151
+
152
+ See the [sample server_rendering.tsx](./sample/server_rendering.tsx) to see how
153
+ it is integrated.
154
+
155
+ ### Call `Humid.render`
156
+
157
+ And finally call `render` from ERB.
158
+
159
+ ```ruby
160
+ <%= Humid.render(MINI_RACER_CONTEXT, json).html_safe %>
161
+ ```
162
+
163
+ Instrumentation is included:
164
+
165
+ ```
166
+ Completed 200 OK in 14ms (Views: 0.2ms | Humid SSR: 11.0ms | ActiveRecord: 2.7ms)
167
+ ```
168
+
169
+ ## The `mini_racer` environment
109
170
 
110
171
  ### Functions not available
111
172
 
@@ -144,55 +205,10 @@ The formatter receives `(level, message, *rest)` where:
144
205
  - `message` — the first argument passed to `console.log/info/warn/error`
145
206
  - `rest` — any additional arguments (objects come through as Ruby hashes/arrays)
146
207
 
147
- The default formatter simply returns `message` unchanged.
148
-
149
- ## Usage
208
+ The default formatter returns `message` unchanged.
150
209
 
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.
210
+ ## Server-side libraries that detect node.js envs.
153
211
 
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
212
  You may need webpacker to create aliases for server friendly libraries that can
197
213
  not detect the `mini_racer` environment. For example, in `webpack.config.js`.
198
214
 
@@ -210,7 +226,7 @@ not detect the `mini_racer` environment. For example, in `webpack.config.js`.
210
226
  [Vue has a resource][vue_ssr] on how to write universal code. Below
211
227
  are a few highlights that are important to keep in mind.
212
228
 
213
- ### State
229
+ ## State
214
230
 
215
231
  Humid uses a single context across multiple request. To avoid state pollution, we
216
232
  provide a factory function to `setHumidRenderer` that builds a new app instance on
@@ -219,9 +235,9 @@ every call.
219
235
  This provides better isolation, but as it is still a shared context, polluting
220
236
  `global` is still possible. Be careful of modifying `global` in your code.
221
237
 
222
- ### Missing browser APIs
238
+ ## Missing browser APIs
223
239
 
224
- Polyfills and some libraries that depend on browser APIs will fail in the
240
+ Some libraries that depend on browser APIs will fail in the
225
241
  `mini_racer` environment because of missing browser APIs. Account for this by
226
242
  moving the `require` to `useEffect` in your component.
227
243
 
@@ -232,6 +248,71 @@ moving the `require` to `useEffect` in your component.
232
248
  }, [])
233
249
  ```
234
250
 
251
+ ## Polyfills
252
+
253
+ React SSR may import node.js dependencies that you need to polyfill for. See
254
+ a sample esbuild [build script](./sample/bulid_ssr.js) and a [shim.js](./sample/shim.js)
255
+ to get around these issues.
256
+
257
+ ## Testing
258
+
259
+ When running in test environments that also forks, you may need to set up new mini_racer
260
+ contexts for each parallel worker. For example:
261
+
262
+ ```ruby
263
+ ActiveSupport.on_load(:action_dispatch_integration_test) do
264
+ include ActionView::Helpers::TranslationHelper
265
+ include Devise::Test::IntegrationHelpers
266
+
267
+ parallelize_setup do
268
+ MINI_RACER_CONTEXT.dispose if defined?(MINI_RACER_CONTEXT)
269
+ ctx = MiniRacer::Context.new(timeout: 1000, ensure_gc_after_idle: 2000)
270
+ Object.send(:remove_const, :MINI_RACER_CONTEXT) if defined?(MINI_RACER_CONTEXT)
271
+ Object.const_set(:MINI_RACER_CONTEXT, Humid.prepare(ctx))
272
+ end
273
+
274
+ parallelize_teardown do
275
+ MINI_RACER_CONTEXT.dispose if defined?(MINI_RACER_CONTEXT)
276
+ end
277
+ end
278
+ ```
279
+
280
+ ## Telemetry
281
+
282
+ The `MiniRacer::Context` gives you access to V8 heap statistics for monitoring
283
+ memory usage over time.
284
+
285
+ ```ruby
286
+ MINI_RACER_CONTEXT.heap_stats
287
+ # {:total_heap_size=>3100672,
288
+ # :total_heap_size_executable=>4194304,
289
+ # :total_physical_size=>1280640,
290
+ # :total_available_size=>1501560832,
291
+ # :used_heap_size=>1205376,
292
+ # :heap_size_limit=>1501560832,
293
+ # ...}
294
+ ```
295
+
296
+ You can combine humid's instrumentation and OpenTelemetry to track heap growth
297
+ per worker:
298
+
299
+ ```ruby
300
+ meter = OpenTelemetry.meter_provider.meter("humid")
301
+ render_histogram = meter.create_histogram("humid.render.duration", unit: "ms", description: "SSR render duration")
302
+ heap_gauge = meter.create_gauge("humid.heap.used_bytes", unit: "By", description: "V8 heap used bytes")
303
+
304
+ ActiveSupport::Notifications.subscribe("render.humid") do |event|
305
+ stats = MINI_RACER_CONTEXT.heap_stats
306
+ attributes = { "worker.pid" => Process.pid.to_s }
307
+
308
+ render_histogram.record(event.duration, attributes: attributes)
309
+ heap_gauge.record(stats[:used_heap_size], attributes: attributes)
310
+ end
311
+ ```
312
+
313
+ A steadily climbing `used_heap_size` across requests indicates a memory leak in
314
+ your JavaScript bundle.
315
+
235
316
  ## Contributing
236
317
 
237
318
  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.6.0".freeze
3
3
  end
data/lib/humid.rb CHANGED
@@ -6,108 +6,101 @@ 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
+ class NotPrepared < StandardError
17
+ end
18
+
19
+ mattr_accessor :config
19
20
 
20
21
  self.config = ActiveSupport::OrderedOptions.new.merge({
21
22
  raise_render_errors: true,
22
- context_options: {},
23
23
  log_formatter: proc { |_level, message, *_rest| message },
24
24
  })
25
25
 
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
26
+ extend self
27
+
28
+ def configure
29
+ yield self.config
30
+ end
41
31
 
42
- def logger
43
- config.logger
32
+ def prepare(ctx, options = {})
33
+ effective_config = config.merge(options)
34
+ logger = effective_config.logger
35
+ log_formatter = effective_config.log_formatter
36
+
37
+ if logger
38
+ fmt = log_formatter || proc { |_level, message, *_rest| message }
39
+ ctx.attach("console.log", proc { |*args| logger.debug(fmt.call(:debug, *args)) })
40
+ ctx.attach("console.info", proc { |*args| logger.info(fmt.call(:info, *args)) })
41
+ ctx.attach("console.error", proc { |*args| logger.error(fmt.call(:error, *args)) })
42
+ ctx.attach("console.warn", proc { |*args| logger.warn(fmt.call(:warn, *args)) })
44
43
  end
45
44
 
46
- def renderer
47
- <<~JS
48
- var __renderer;
49
- function setHumidRenderer(fn) {
50
- __renderer = fn;
51
- }
52
- JS
53
- end
45
+ js = remove_functions + renderer
46
+ ctx.eval(js)
54
47
 
55
- def context
56
- @@context
57
- end
48
+ source_path = effective_config.application_path
49
+ map_path = effective_config.source_map_path
58
50
 
59
- def dispose
60
- if @@context
61
- @@context.dispose
62
- @@context = nil
63
- end
51
+ if map_path
52
+ ctx.attach("readSourceMap", proc { File.read(map_path) })
64
53
  end
65
54
 
66
- def create_context
67
- ctx = MiniRacer::Context.new(**config.context_options)
55
+ filename = File.basename(source_path.to_s)
56
+ ctx.eval(File.read(source_path), filename: filename)
68
57
 
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)
58
+ def ctx.humid_prepared?
59
+ true
60
+ end
81
61
 
82
- source_path = config.application_path
83
- map_path = config.source_map_path
62
+ ctx
63
+ end
84
64
 
85
- if map_path
86
- ctx.attach("readSourceMap", proc { File.read(map_path) })
65
+ def render(ctx, *args)
66
+ is_prepared = ctx.respond_to?(:humid_prepared?) && ctx.humid_prepared?
67
+ raise Humid::NotPrepared, "Context was not prepared with Humid.prepare" unless is_prepared
68
+
69
+ ActiveSupport::Notifications.instrument("render.humid") do
70
+ ctx.call("__renderer", *args)
71
+ rescue MiniRacer::RuntimeError => e
72
+ message = ([e.message] + e.backtrace.filter { |x| x.starts_with? "JavaScript" }).join("\n")
73
+ render_error = Humid::RenderError.new(message)
74
+
75
+ if config.raise_render_errors
76
+ raise render_error
77
+ else
78
+ config.logger.error(render_error.inspect)
79
+ ""
87
80
  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
81
  end
82
+ end
95
83
 
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
84
+ private
85
+
86
+ def remove_functions
87
+ <<~JS
88
+ delete this.setTimeout;
89
+ delete this.setInterval;
90
+ delete this.clearTimeout;
91
+ delete this.clearInterval;
92
+ delete this.setImmediate;
93
+ delete this.clearImmediate;
94
+ JS
95
+ end
96
+
97
+ def renderer
98
+ <<~JS
99
+ var __renderer;
100
+ function setHumidRenderer(fn) {
101
+ __renderer = fn;
102
+ }
103
+ JS
111
104
  end
112
105
  end
113
106
 
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.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johny Ho
@@ -37,7 +37,7 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '8.0'
40
- description: Javascript SSR rendering for Rails
40
+ description: Javascript Server Side Rendering (SSR) for Rails
41
41
  email: jho406@gmail.com
42
42
  executables: []
43
43
  extensions: []
@@ -68,5 +68,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
68
68
  requirements: []
69
69
  rubygems_version: 3.6.9
70
70
  specification_version: 4
71
- summary: Javascript SSR rendering for Rails
71
+ summary: Javascript Server Side Rendering (SSR) for Rails
72
72
  test_files: []